Connecting OpenWebUI to a Search API for Grounded Responses
OpenWebUI defaults to SearXNG for web search. Connecting it to a commercial search API improves reliability and adds structured result types — shopping, news, Reddit, YouTube — that SearXNG cannot provide. The tradeoff is a small per-query cost vs. free but higher-maintenance self-hosted search.
The SearXNG Problem
SearXNG is OpenWebUI's recommended self-hosted search backend. It aggregates results from Google, Bing, DuckDuckGo, and others. Problems in production:
- Google blocks SearXNG aggressively when queries come from a single server IP
- Results degrade silently when upstream sources block the instance
- Maintaining a reliable SearXNG instance requires proxy configuration and updates
- No structured result types beyond basic web search
For a personal OpenWebUI instance with low query volume, SearXNG works acceptably. For a team or production deployment, the maintenance burden exceeds the cost of a commercial API.
OpenWebUI Search API Configuration
OpenWebUI supports custom search endpoints via the admin settings. The endpoint must accept a query and return a specific JSON format.
Create a lightweight adapter that translates between OpenWebUI's expected format and your search API:
from flask import Flask, request, jsonify
import requests
app = Flask(__name__)
@app.route('/search', methods=['GET'])
def search():
query = request.args.get('q', '')
if not query:
return jsonify({"results": []})
# Call Scavio search API
resp = requests.post(
"https://api.scavio.dev/api/v1/search",
headers={"x-api-key": SCAVIO_KEY},
json={"query": query, "num": 5}
)
data = resp.json()
# Transform to OpenWebUI expected format
results = []
for r in data.get("organic_results", []):
results.append({
"title": r.get("title"),
"url": r.get("link"),
"content": r.get("snippet")
})
return jsonify({"results": results})
if __name__ == '__main__':
app.run(port=8080)Deploy this adapter on your OpenWebUI server. In OpenWebUI admin settings, set:
- Web Search Engine: custom
- Search API URL:
http://localhost:8080/search?q={query}
Adding Brave Search as Alternative
Brave Search's API response format is close to OpenWebUI's expected format with minimal transformation:
@app.route('/brave-search', methods=['GET'])
def brave_search():
query = request.args.get('q', '')
resp = requests.get(
"https://api.search.brave.com/res/v1/web/search",
headers={"X-Subscription-Token": BRAVE_KEY},
params={"q": query, "count": 5}
)
data = resp.json()
results = [{
"title": r["title"],
"url": r["url"],
"content": r.get("description", "")
} for r in data.get("web", {}).get("results", [])]
return jsonify({"results": results})Brave's $5/month free credit gives ~1,000 searches. For a personal OpenWebUI instance averaging 30 searches/day, that is roughly 900 searches/month — staying within the free tier.
Cost for a Team Deployment
For a team of 10 using OpenWebUI with 20 web searches per person per day:
- 200 searches/day
- 6,000 searches/month
- At Scavio $0.005/credit: $30/month
- At Brave PAYG $5/1k: $30/month
Both are comparable at this volume. Scavio adds multi-platform coverage (Amazon, YouTube, Reddit results) which Brave does not have. For a team that needs platform-specific search results from within OpenWebUI, multi-platform is worth the equivalent cost.
Production Considerations
For a production OpenWebUI deployment:
- Add API key rotation if using provider rate limits (unlikely to need for team-scale)
- Cache frequent searches (common queries in an internal tool repeat often)
- Add query logging to understand which searches are triggering web lookup — this tells you what to add to your LLM system prompt to avoid unnecessary searches
- Set a per-user daily search limit if cost control is important
The adapter pattern above also lets you switch providers without changing OpenWebUI configuration — just update the adapter's target API.