Skip to content

Commit 7b37cdc

Browse files
authored
Merge pull request open-webui#9980 from xring/web_search_serpapi
feat: add web search via SerpApi
2 parents d9ee53b + 27d395b commit 7b37cdc

File tree

5 files changed

+119
-0
lines changed

5 files changed

+119
-0
lines changed

backend/open_webui/config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1809,6 +1809,18 @@ class BannerModel(BaseModel):
18091809
os.getenv("SEARCHAPI_ENGINE", ""),
18101810
)
18111811

1812+
SERPAPI_API_KEY = PersistentConfig(
1813+
"SERPAPI_API_KEY",
1814+
"rag.web.search.serpapi_api_key",
1815+
os.getenv("SERPAPI_API_KEY", ""),
1816+
)
1817+
1818+
SERPAPI_ENGINE = PersistentConfig(
1819+
"SERPAPI_ENGINE",
1820+
"rag.web.search.serpapi_engine",
1821+
os.getenv("SERPAPI_ENGINE", ""),
1822+
)
1823+
18121824
BING_SEARCH_V7_ENDPOINT = PersistentConfig(
18131825
"BING_SEARCH_V7_ENDPOINT",
18141826
"rag.web.search.bing_search_v7_endpoint",

backend/open_webui/main.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@
179179
JINA_API_KEY,
180180
SEARCHAPI_API_KEY,
181181
SEARCHAPI_ENGINE,
182+
SERPAPI_API_KEY,
183+
SERPAPI_ENGINE,
182184
SEARXNG_QUERY_URL,
183185
SERPER_API_KEY,
184186
SERPLY_API_KEY,
@@ -547,6 +549,8 @@ async def lifespan(app: FastAPI):
547549
app.state.config.TAVILY_API_KEY = TAVILY_API_KEY
548550
app.state.config.SEARCHAPI_API_KEY = SEARCHAPI_API_KEY
549551
app.state.config.SEARCHAPI_ENGINE = SEARCHAPI_ENGINE
552+
app.state.config.SERPAPI_API_KEY = SERPAPI_API_KEY
553+
app.state.config.SERPAPI_ENGINE = SERPAPI_ENGINE
550554
app.state.config.JINA_API_KEY = JINA_API_KEY
551555
app.state.config.BING_SEARCH_V7_ENDPOINT = BING_SEARCH_V7_ENDPOINT
552556
app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = BING_SEARCH_V7_SUBSCRIPTION_KEY
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import logging
2+
from typing import Optional
3+
from urllib.parse import urlencode
4+
5+
import requests
6+
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
7+
from open_webui.env import SRC_LOG_LEVELS
8+
9+
log = logging.getLogger(__name__)
10+
log.setLevel(SRC_LOG_LEVELS["RAG"])
11+
12+
13+
def search_serpapi(
14+
api_key: str,
15+
engine: str,
16+
query: str,
17+
count: int,
18+
filter_list: Optional[list[str]] = None,
19+
) -> list[SearchResult]:
20+
"""Search using serpapi.com's API and return the results as a list of SearchResult objects.
21+
22+
Args:
23+
api_key (str): A serpapi.com API key
24+
query (str): The query to search for
25+
"""
26+
url = "https://serpapi.com/search"
27+
28+
engine = engine or "google"
29+
30+
payload = {"engine": engine, "q": query, "api_key": api_key}
31+
32+
url = f"{url}?{urlencode(payload)}"
33+
response = requests.request("GET", url)
34+
35+
json_response = response.json()
36+
log.info(f"results from serpapi search: {json_response}")
37+
38+
results = sorted(
39+
json_response.get("organic_results", []), key=lambda x: x.get("position", 0)
40+
)
41+
if filter_list:
42+
results = get_filtered_results(results, filter_list)
43+
return [
44+
SearchResult(
45+
link=result["link"], title=result["title"], snippet=result["snippet"]
46+
)
47+
for result in results[:count]
48+
]

backend/open_webui/routers/retrieval.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from open_webui.retrieval.web.google_pse import search_google_pse
5151
from open_webui.retrieval.web.jina_search import search_jina
5252
from open_webui.retrieval.web.searchapi import search_searchapi
53+
from open_webui.retrieval.web.serpapi import search_serpapi
5354
from open_webui.retrieval.web.searxng import search_searxng
5455
from open_webui.retrieval.web.serper import search_serper
5556
from open_webui.retrieval.web.serply import search_serply
@@ -388,6 +389,8 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
388389
"tavily_api_key": request.app.state.config.TAVILY_API_KEY,
389390
"searchapi_api_key": request.app.state.config.SEARCHAPI_API_KEY,
390391
"searchapi_engine": request.app.state.config.SEARCHAPI_ENGINE,
392+
"serpapi_api_key": request.app.state.config.SERPAPI_API_KEY,
393+
"serpapi_engine": request.app.state.config.SERPAPI_ENGINE,
391394
"jina_api_key": request.app.state.config.JINA_API_KEY,
392395
"bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
393396
"bing_search_v7_subscription_key": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY,
@@ -439,6 +442,8 @@ class WebSearchConfig(BaseModel):
439442
tavily_api_key: Optional[str] = None
440443
searchapi_api_key: Optional[str] = None
441444
searchapi_engine: Optional[str] = None
445+
serpapi_api_key: Optional[str] = None
446+
serpapi_engine: Optional[str] = None
442447
jina_api_key: Optional[str] = None
443448
bing_search_v7_endpoint: Optional[str] = None
444449
bing_search_v7_subscription_key: Optional[str] = None
@@ -545,6 +550,13 @@ async def update_rag_config(
545550
form_data.web.search.searchapi_engine
546551
)
547552

553+
request.app.state.config.SERPAPI_API_KEY = (
554+
form_data.web.search.serpapi_api_key
555+
)
556+
request.app.state.config.SERPAPI_ENGINE = (
557+
form_data.web.search.serpapi_engine
558+
)
559+
548560
request.app.state.config.JINA_API_KEY = form_data.web.search.jina_api_key
549561
request.app.state.config.BING_SEARCH_V7_ENDPOINT = (
550562
form_data.web.search.bing_search_v7_endpoint
@@ -604,6 +616,8 @@ async def update_rag_config(
604616
"serply_api_key": request.app.state.config.SERPLY_API_KEY,
605617
"serachapi_api_key": request.app.state.config.SEARCHAPI_API_KEY,
606618
"searchapi_engine": request.app.state.config.SEARCHAPI_ENGINE,
619+
"serpapi_api_key": request.app.state.config.SERPAPI_API_KEY,
620+
"serpapi_engine": request.app.state.config.SERPAPI_ENGINE,
607621
"tavily_api_key": request.app.state.config.TAVILY_API_KEY,
608622
"jina_api_key": request.app.state.config.JINA_API_KEY,
609623
"bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
@@ -1131,6 +1145,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
11311145
- TAVILY_API_KEY
11321146
- EXA_API_KEY
11331147
- SEARCHAPI_API_KEY + SEARCHAPI_ENGINE (by default `google`)
1148+
- SERPAPI_API_KEY + SERPAPI_ENGINE (by default `google`)
11341149
Args:
11351150
query (str): The query to search for
11361151
"""
@@ -1259,6 +1274,17 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
12591274
)
12601275
else:
12611276
raise Exception("No SEARCHAPI_API_KEY found in environment variables")
1277+
elif engine == "serpapi":
1278+
if request.app.state.config.SERPAPI_API_KEY:
1279+
return search_serpapi(
1280+
request.app.state.config.SERPAPI_API_KEY,
1281+
request.app.state.config.SERPAPI_ENGINE,
1282+
query,
1283+
request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
1284+
request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
1285+
)
1286+
else:
1287+
raise Exception("No SERPAPI_API_KEY found in environment variables")
12621288
elif engine == "jina":
12631289
return search_jina(
12641290
request.app.state.config.JINA_API_KEY,

src/lib/components/admin/Settings/WebSearch.svelte

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
'serper',
2424
'serply',
2525
'searchapi',
26+
'serpapi',
2627
'duckduckgo',
2728
'tavily',
2829
'jina',
@@ -268,6 +269,34 @@
268269
</div>
269270
</div>
270271
</div>
272+
{:else if webConfig.search.engine === 'serpapi'}
273+
<div>
274+
<div class=" self-center text-xs font-medium mb-1">
275+
{$i18n.t('SerpApi API Key')}
276+
</div>
277+
278+
<SensitiveInput
279+
placeholder={$i18n.t('Enter SerpApi API Key')}
280+
bind:value={webConfig.search.serpapi_api_key}
281+
/>
282+
</div>
283+
<div class="mt-1.5">
284+
<div class=" self-center text-xs font-medium mb-1">
285+
{$i18n.t('SerpApi Engine')}
286+
</div>
287+
288+
<div class="flex w-full">
289+
<div class="flex-1">
290+
<input
291+
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
292+
type="text"
293+
placeholder={$i18n.t('Enter SerpApi Engine')}
294+
bind:value={webConfig.search.serpapi_engine}
295+
autocomplete="off"
296+
/>
297+
</div>
298+
</div>
299+
</div>
271300
{:else if webConfig.search.engine === 'tavily'}
272301
<div>
273302
<div class=" self-center text-xs font-medium mb-1">

0 commit comments

Comments
 (0)