diff --git a/docs/tutorials/web-search/external.md b/docs/tutorials/web-search/external.md new file mode 100644 index 0000000..7d450c3 --- /dev/null +++ b/docs/tutorials/web-search/external.md @@ -0,0 +1,132 @@ +--- +sidebar_position: 17 +title: "External" +--- + +:::warning +This tutorial is a community contribution and is not supported by the Open WebUI team. It serves only as a demonstration on how to customize Open WebUI for your specific use case. Want to contribute? Check out the contributing tutorial. +::: + +## External Web Search API + +This option allows you to connect Open WebUI to your own self-hosted web search API endpoint. This is useful if you want to: + +* Integrate a search engine not natively supported by Open WebUI. +* Implement custom search logic, filtering, or result processing. +* Use a private or internal search index. + +### Open WebUI Setup + +1. Navigate to the Open WebUI `Admin Panel`. +2. Go to the `Settings` tab and then select `Web Search`. +3. Toggle `Enable Web Search` to the on position. +4. Set `Web Search Engine` from the dropdown menu to `external`. +5. Fill `External Search URL` with the full URL of your custom search API endpoint (e.g., `http://localhost:8000/search` or `https://my-search-api.example.com/api/search`). +6. Fill `External Search API Key` with the secret API key required to authenticate with your custom search endpoint. Leave blank if your endpoint doesn't require authentication (not recommended for public endpoints). +7. Click `Save`. + +![Open WebUI Admin panel showing External Search config](/images/tutorial_external_search.png) + +### API Specification + +Open WebUI will interact with your `External Search URL` as follows: + +* **Method:** `POST` +* **Headers:** + * `Content-Type: application/json` + * `Authorization: Bearer ` +* **Request Body (JSON):** + ```json + { + "query": "The user's search query string", + "count": 5 // The maximum number of search results requested + } + ``` + * `query` (string): The search term entered by the user. + * `count` (integer): The suggested maximum number of results Open WebUI expects. Your API can return fewer results if necessary. + +* **Expected Response Body (JSON):** + Your API endpoint *must* return a JSON array of search result objects. Each object should have the following structure: + ```json + [ + { + "link": "URL of the search result", + "title": "Title of the search result page", + "snippet": "A brief description or snippet from the search result page" + }, + { + "link": "...", + "title": "...", + "snippet": "..." + } + // ... potentially more results up to the requested count + ] + ``` + * `link` (string): The direct URL to the search result. + * `title` (string): The title of the web page. + * `snippet` (string): A descriptive text snippet from the page content relevant to the query. + + If an error occurs or no results are found, your endpoint should ideally return an empty JSON array `[]`. + +### Example Implementation (Python/FastAPI) + +Here is a simple example of a self-hosted search API using Python with FastAPI and the `duckduckgo-search` library. + +```python +import uvicorn +from fastapi import FastAPI, Header, Body, HTTPException +from pydantic import BaseModel +from duckduckgo_search import DDGS + +EXPECTED_BEARER_TOKEN = "your_secret_token_here" + +app = FastAPI() + + +class SearchRequest(BaseModel): + query: str + count: int + + +class SearchResult(BaseModel): + link: str + title: str | None + snippet: str | None + + +@app.post("/search") +async def external_search( + search_request: SearchRequest = Body(...), + authorization: str | None = Header(None), +): + expected_auth_header = f"Bearer {EXPECTED_BEARER_TOKEN}" + if authorization != expected_auth_header: + raise HTTPException(status_code=401, detail="Unauthorized") + + query, count = search_request.query, search_request.count + + results = [] + try: + with DDGS() as ddgs: + search_results = ddgs.text( + query, safesearch="moderate", max_results=count, backend="lite" + ) + + results = [ + SearchResult( + link=result["href"], + title=result.get("title"), + snippet=result.get("body"), + ) + for result in search_results + ] + + except Exception as e: + print(f"Error during DuckDuckGo search: {e}") + + return results + + +if __name__ == "__main__": + uvicorn.run("main:app", host="0.0.0.0", port=8888) +``` \ No newline at end of file diff --git a/static/images/tutorial_external_search.png b/static/images/tutorial_external_search.png new file mode 100644 index 0000000..e932b31 Binary files /dev/null and b/static/images/tutorial_external_search.png differ