SearXNG for metasearch, a local Hermes model for reasoning, a local Qwen model for code generation -- this stack gives you a fully private search assistant that never sends queries to third-party APIs. The tradeoff is result quality: self-hosted metasearch returns noisier data than commercial search APIs, so you need aggressive filtering.
A naming note, because two different things are called Hermes. In this stack, Hermes means the Nous Research open-weight model family served by Ollama. Hermes Agent -- the self-hosted agent runtime that ships every one to three days and is on the 0.20 line as of this writing -- is a separate project. The routing pattern at the end of this post applies to both, but the model tags below are the Ollama ones, not agent versions.
The Private Search Stack
SearXNG is a self-hosted metasearch engine that aggregates results from dozens of upstream engines without tracking. Run it on your own server and it queries Google, Bing, DuckDuckGo and others on your behalf. A local Hermes model handles reasoning and answer synthesis. A local Qwen model handles code generation, in whichever parameter size fits the VRAM you have.
Pin the model tag you actually pulled, not the marketing version number. Open-weight families re-release under the same brand every few months, and a stack that says "the latest Hermes" in a runbook is a stack that changed behaviour without anyone deciding to change it.
Cost: one VPS large enough to hold both models in memory alongside SearXNG -- 32GB RAM is a reasonable starting point for mid-size models, though this scales entirely with what you load. No per-query API fees. Full data sovereignty.
Setting Up SearXNG
# Docker Compose for SearXNG
# docker-compose.yml
# services:
# searxng:
# image: searxng/searxng:latest
# ports:
# - "8080:8080"
# volumes:
# - ./searxng:/etc/searxng
# environment:
# - SEARXNG_BASE_URL=http://localhost:8080
# Start SearXNG
docker compose up -d
# The JSON API is off by default. Add "json" to the `formats` list in
# settings.yml and restart, or this returns a 403.
curl "http://localhost:8080/search?q=test&format=json" | python -m json.toolConnecting SearXNG to a Local Model
import requests
SEARXNG_URL = "http://localhost:8080"
OLLAMA_URL = "http://localhost:11434"
MODEL = "hermes3" # whatever tag you pulled -- pin it, do not track "latest"
def private_search(query):
"""Search via SearXNG, synthesize with a local model."""
# Step 1: search
r = requests.get(
f"{SEARXNG_URL}/search",
params={"q": query, "format": "json"},
timeout=15,
).json()
results = r.get("results", [])[:5]
context = "\n".join(
f"- {item['title']}: {item.get('content', '')[:200]}"
for item in results
)
# Step 2: synthesize locally
answer = requests.post(
f"{OLLAMA_URL}/api/generate",
json={
"model": MODEL,
"prompt": f"Based on these search results:\n{context}\n\nAnswer: {query}",
"stream": False,
},
timeout=120,
).json()
return answer.get("response", "")
print(private_search("self-hosted metasearch setup"))Two things break this in practice. First, SearXNG's JSON format is disabled in the default config, so the first call 403s until you enable it. Second, the snippet in content is a couple of hundred characters -- you are synthesizing from search previews, not page text. If the answer needs what is actually on the page, you have to fetch and extract each URL yourself, which is a second pipeline with its own blocking and rendering problems.
The Quality Problem
SearXNG aggregates from multiple engines, but the results are noisier than commercial search APIs. Google rate-limits SearXNG instances aggressively, so you often fall back to secondary engines with smaller indexes. Result quality drops noticeably on niche queries, and it drops silently -- the response shape is identical whether you got Google's index or a fallback engine's, so a degraded day looks exactly like a good one until you read the answers.
If you need consistent result quality without managing SearXNG uptime and engine rotation, a commercial search API returns cleaner data with less operational overhead.
import os, requests
# Commercial alternative: consistent quality, no self-hosting.
# Every endpoint is a POST with a JSON body and a bearer token.
H = {"Authorization": f"Bearer {os.environ['SCAVIO_API_KEY']}"}
def reliable_search(query):
r = requests.post(
"https://api.scavio.dev/api/v2/google",
headers=H,
json={"query": query, "gl": "us", "hl": "en"},
timeout=30,
).json()
return r.get("organic_results", [])
for hit in reliable_search("mcp server setup guide"):
print(hit["position"], hit["title"], hit["link"])The response carries organic_results with position, title, link and snippet, plus credits_used and credits_remaining so a long-running agent can meter itself. It is the same SERP the private stack is trying to reach through a proxy, minus the rate-limit roulette.
When to Use Each Approach
Use the private stack when queries contain sensitive data (medical, legal, financial), when compliance forbids sending queries to third parties, or when you need a full local audit trail.
Use a commercial API when result quality matters more than sovereignty, when you do not want to own SearXNG's uptime, or when you need data that metasearch structurally cannot give you. That last one is the underrated case: SearXNG returns links and snippets. It has no concept of a product listing, a review corpus, a job posting or a video transcript. Scavio covers 50 platforms -- Google's full family (SERP, News, Maps, Shopping, Trends, Flights, Hotels, AI Mode), YouTube, TikTok, Instagram, Threads, X, LinkedIn, Reddit, Amazon, Walmart, eBay, Target, Booking, Airbnb, Zillow, Indeed, Glassdoor, the app stores, G2, Capterra, SEC EDGAR, UK Companies House and more -- plus an Extract endpoint that turns any URL into markdown, text or HTML, which is the piece that fills the snippet-versus-page-text gap above.
Credit cost varies by platform, so price the endpoints your workload actually calls. Google, Amazon, Walmart, eBay, Target, Airbnb, Zillow, Redfin, SEC EDGAR, Companies House and Meta Ads are 1 credit. Threads, Yelp, Tripadvisor, Indeed, Capterra, Google Play and Home Depot are 2. G2 is 5. New accounts get 50 credits on signup, one time, no card required, which is enough to A/B the two paths on your own queries before committing. Get a key.
Hybrid Architecture
Run SearXNG for sensitive queries and route non-sensitive queries to a commercial API. Tag each query with a sensitivity level and route accordingly. You get sovereignty where it matters and result quality where it matters.
SENSITIVE = ("patient", "diagnosis", "salary", "settlement", "acquisition")
def route(query):
"""Sovereignty where it matters, quality where it does not."""
if any(term in query.lower() for term in SENSITIVE):
return private_search(query) # never leaves your network
return reliable_search(query) # clean SERP, third-partyKeyword matching is a starting heuristic, not a compliance control -- if your policy is real, classify with the local model and log the routing decision.
If Hermes Agent is the runtime driving this, both halves plug in without custom glue. Scavio publishes 50 ClawHub skills covering all 50 platforms, Extract included, installable directly:
hermes skills install @scavio-ai/scavio-googleThe hosted MCP server at https://mcp.scavio.dev/mcp is the alternative when one agent needs many platforms in a session -- streamable HTTP, x-api-key header, 106 tools across 11 platforms by default, widened per-agent with the x-scavio-platforms header. Keep your SearXNG tool registered alongside it and let the router, not the model, decide which one a query is allowed to touch.