From 3a232f5e9a4d31a6b74cb34d007b581feeb2f005 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sun, 28 Jun 2026 23:20:54 -0500 Subject: [PATCH] refac --- backend/open_webui/retrieval/web/serphouse.py | 44 +++++++++++++++++++ backend/open_webui/routers/retrieval.py | 22 ++++++++++ .../admin/Settings/WebSearch.svelte | 33 ++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 backend/open_webui/retrieval/web/serphouse.py diff --git a/backend/open_webui/retrieval/web/serphouse.py b/backend/open_webui/retrieval/web/serphouse.py new file mode 100644 index 0000000000..7b8ae59ac0 --- /dev/null +++ b/backend/open_webui/retrieval/web/serphouse.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from open_webui.retrieval.web.main import SearchResult, get_filtered_results +from open_webui.utils.session_pool import get_session + + +async def search_serphouse( + api_key: str, + domain: str, + query: str, + count: int, + filter_list: list[str | None] | None = None, +) -> list[SearchResult]: + """Query SERPHouse and return normalised organic results.""" + session = await get_session() + async with session.get( + 'https://api.serphouse.com/serp/live', + params={ + 'q': query, + 'domain': (domain or 'google.com').strip() or 'google.com', + 'device': 'desktop', + 'serp_type': 'web', + 'page': 1, + 'num_result': count, + }, + headers={'Authorization': f'Bearer {api_key}', 'Accept': 'application/json'}, + ) as response: + response.raise_for_status() + payload = await response.json() + + organic = payload.get('results', {}).get('results', {}).get('organic', []) + organic = sorted(organic, key=lambda item: item.get('position', 0)) + if filter_list: + organic = get_filtered_results(organic, filter_list) + + return [ + SearchResult( + link=item.get('link', ''), + title=item.get('title'), + snippet=item.get('snippet'), + ) + for item in organic[:count] + if item.get('link') + ] diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index ebba3ff575..72bad9b02f 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -103,6 +103,7 @@ from open_webui.retrieval.web.searchapi import search_searchapi from open_webui.retrieval.web.searxng import search_searxng from open_webui.retrieval.web.serpapi import search_serpapi from open_webui.retrieval.web.serper import search_serper +from open_webui.retrieval.web.serphouse import search_serphouse from open_webui.retrieval.web.serply import search_serply from open_webui.retrieval.web.serpstack import search_serpstack from open_webui.retrieval.web.sougou import search_sougou @@ -357,6 +358,8 @@ RETRIEVAL_CONFIG_KEYS = { 'SERPAPI_API_KEY': 'rag.web.search.serpapi_api_key', 'SERPAPI_ENGINE': 'rag.web.search.serpapi_engine', 'SERPER_API_KEY': 'rag.web.search.serper_api_key', + 'SERPHOUSE_API_KEY': 'rag.web.search.serphouse_api_key', + 'SERPHOUSE_DOMAIN': 'rag.web.search.serphouse_domain', 'SERPLY_API_KEY': 'rag.web.search.serply_api_key', 'SERPSTACK_API_KEY': 'rag.web.search.serpstack_api_key', 'SERPSTACK_HTTPS': 'rag.web.search.serpstack_https', @@ -703,6 +706,8 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): 'SERPSTACK_API_KEY': config.SERPSTACK_API_KEY, 'SERPSTACK_HTTPS': config.SERPSTACK_HTTPS, 'SERPER_API_KEY': config.SERPER_API_KEY, + 'SERPHOUSE_API_KEY': config.SERPHOUSE_API_KEY, + 'SERPHOUSE_DOMAIN': config.SERPHOUSE_DOMAIN, 'SERPLY_API_KEY': config.SERPLY_API_KEY, 'DDGS_BACKEND': config.DDGS_BACKEND, 'TAVILY_API_KEY': config.TAVILY_API_KEY, @@ -774,6 +779,8 @@ class WebConfig(BaseModel): SERPSTACK_API_KEY: str | None = None SERPSTACK_HTTPS: bool | None = None SERPER_API_KEY: str | None = None + SERPHOUSE_API_KEY: str | None = None + SERPHOUSE_DOMAIN: str | None = None SERPLY_API_KEY: str | None = None DDGS_BACKEND: str | None = None TAVILY_API_KEY: str | None = None @@ -1273,6 +1280,8 @@ async def update_rag_config(request: Request, form_data: ConfigForm, user=Depend config.SERPSTACK_API_KEY = form_data.web.SERPSTACK_API_KEY config.SERPSTACK_HTTPS = form_data.web.SERPSTACK_HTTPS config.SERPER_API_KEY = form_data.web.SERPER_API_KEY + config.SERPHOUSE_API_KEY = form_data.web.SERPHOUSE_API_KEY + config.SERPHOUSE_DOMAIN = form_data.web.SERPHOUSE_DOMAIN config.SERPLY_API_KEY = form_data.web.SERPLY_API_KEY config.DDGS_BACKEND = form_data.web.DDGS_BACKEND config.TAVILY_API_KEY = form_data.web.TAVILY_API_KEY @@ -1412,6 +1421,8 @@ async def update_rag_config(request: Request, form_data: ConfigForm, user=Depend 'SERPSTACK_API_KEY': config.SERPSTACK_API_KEY, 'SERPSTACK_HTTPS': config.SERPSTACK_HTTPS, 'SERPER_API_KEY': config.SERPER_API_KEY, + 'SERPHOUSE_API_KEY': config.SERPHOUSE_API_KEY, + 'SERPHOUSE_DOMAIN': config.SERPHOUSE_DOMAIN, 'SERPLY_API_KEY': config.SERPLY_API_KEY, 'TAVILY_API_KEY': config.TAVILY_API_KEY, 'SEARCHAPI_API_KEY': config.SEARCHAPI_API_KEY, @@ -2259,6 +2270,17 @@ async def search_web(request: Request, engine: str, query: str, user=None) -> li ) else: raise Exception('No SERPER_API_KEY found in environment variables') + elif engine == 'serphouse': + if config.SERPHOUSE_API_KEY: + return await search_serphouse( + config.SERPHOUSE_API_KEY, + config.SERPHOUSE_DOMAIN, + query, + config.WEB_SEARCH_RESULT_COUNT, + config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception('No SERPHOUSE_API_KEY found in environment variables') elif engine == 'serply': if config.SERPLY_API_KEY: return await asyncio.to_thread( diff --git a/src/lib/components/admin/Settings/WebSearch.svelte b/src/lib/components/admin/Settings/WebSearch.svelte index 4f9929dc60..38938d6a72 100644 --- a/src/lib/components/admin/Settings/WebSearch.svelte +++ b/src/lib/components/admin/Settings/WebSearch.svelte @@ -26,6 +26,7 @@ 'bocha', 'serpstack', 'serper', + 'serphouse', 'serply', 'searchapi', 'serpapi', @@ -180,6 +181,8 @@ {#each webSearchEngines as engine} {#if engine === 'duckduckgo' || engine === 'ddgs'} + {:else if engine === 'serphouse'} + {:else} {/if} @@ -470,6 +473,36 @@ /> + {:else if webConfig.WEB_SEARCH_ENGINE === 'serphouse'} +
+
+
+ {$i18n.t('SERPHouse API Key')} +
+ + +
+
+
+ {$i18n.t('SERPHouse Domain')} +
+ +
+
+ +
+
+
+
{:else if webConfig.WEB_SEARCH_ENGINE === 'serply'}