SearXNG gives you free self-hosted search results but returns unstructured HTML without consistent JSON. A hybrid approach uses SearXNG for low-priority queries and fails over to Scavio at $0.005/request when you need structured data, SERP features, or guaranteed uptime. This tutorial builds a fallback pipeline that tries SearXNG first and promotes to Scavio when SearXNG returns incomplete results.
Prerequisites
- Python 3.8+
- requests library
- A Scavio API key from scavio.dev
- SearXNG instance (local or remote) with JSON format enabled
Walkthrough
Step 1: Configure SearXNG and Scavio clients
Set up both search backends with a unified interface.
import os, requests, time
SCAVIO_KEY = os.environ['SCAVIO_API_KEY']
SCAVIO_H = {'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json'}
SEARXNG_URL = os.environ.get('SEARXNG_URL', 'http://localhost:8080')
def search_searxng(query):
try:
r = requests.get(f'{SEARXNG_URL}/search',
params={'q': query, 'format': 'json', 'engines': 'google'},
timeout=5)
r.raise_for_status()
data = r.json()
return [{'title': r['title'], 'link': r['url'], 'snippet': r.get('content', '')}
for r in data.get('results', [])[:10]]
except Exception as e:
print(f'SearXNG failed: {e}')
return None
def search_scavio(query):
r = requests.post('https://api.scavio.dev/api/v1/search',
headers=SCAVIO_H, json={'query': query, 'country_code': 'us'})
return r.json()
print('Clients configured.')Step 2: Build the fallback router
Try SearXNG first. If results are missing or incomplete, promote to Scavio.
def search_with_fallback(query, need_structured=False, min_results=5):
cost = 0.0
source = 'searxng'
if not need_structured:
results = search_searxng(query)
if results and len(results) >= min_results:
return {'results': results, 'source': source, 'cost': cost}
print(f'SearXNG returned {len(results) if results else 0} results, falling back to Scavio')
source = 'scavio'
cost = 0.005
data = search_scavio(query)
return {
'results': data.get('organic_results', []),
'source': source,
'cost': cost,
'serp_features': {
'ai_overview': bool(data.get('ai_overview')),
'answer_box': bool(data.get('answer_box')),
'paa': len(data.get('related_questions', []))
}
}
r = search_with_fallback('best python web framework 2026')
print(f'Source: {r["source"]}, Results: {len(r["results"])}, Cost: ${r["cost"]:.3f}')Step 3: Add health checks and metrics
Track SearXNG availability and fallback rate to estimate costs.
from collections import Counter
stats = Counter()
def tracked_search(query, **kwargs):
r = search_with_fallback(query, **kwargs)
stats[r['source']] += 1
stats['total_cost'] += r['cost']
return r
queries = ['python web framework', 'best serp api', 'tiktok analytics tool',
'react vs vue 2026', 'llm agent search']
for q in queries:
r = tracked_search(q)
print(f' {q[:35]:35} -> {r["source"]:8} ({len(r["results"])} results)')
print(f'\nSearXNG hits: {stats["searxng"]}, Scavio fallbacks: {stats["scavio"]}')
print(f'Total cost: ${stats["total_cost"]:.3f}')
print(f'Savings vs all-Scavio: ${len(queries) * 0.005 - stats["total_cost"]:.3f}')Step 4: Force Scavio for structured queries
Some queries always need structured SERP data. Route those directly.
STRUCTURED_PATTERNS = ['price', 'cost', 'pricing', 'buy', 'vs ', 'compare']
def smart_search(query):
need_structured = any(p in query.lower() for p in STRUCTURED_PATTERNS)
r = tracked_search(query, need_structured=need_structured)
if need_structured:
print(f' [STRUCTURED] {query[:40]} -> Scavio (SERP features: {r.get("serp_features", {})})')
else:
print(f' [BASIC] {query[:40]} -> {r["source"]}')
return r
smart_search('python tutorial')
smart_search('scavio vs serpapi pricing')
smart_search('buy noise canceling headphones')
print(f'Total cost: ${stats["total_cost"]:.3f}')Python Example
import os, requests
SCAVIO_H = {'x-api-key': os.environ['SCAVIO_API_KEY'], 'Content-Type': 'application/json'}
SEARXNG = os.environ.get('SEARXNG_URL', 'http://localhost:8080')
def search(query, force_scavio=False):
if not force_scavio:
try:
r = requests.get(f'{SEARXNG}/search', params={'q': query, 'format': 'json'}, timeout=5)
results = r.json().get('results', [])[:10]
if len(results) >= 5:
print(f'{query}: {len(results)} results via SearXNG (free)')
return results
except: pass
data = requests.post('https://api.scavio.dev/api/v1/search',
headers=SCAVIO_H, json={'query': query, 'country_code': 'us'}).json()
print(f'{query}: {len(data.get("organic_results", []))} results via Scavio ($0.005)')
return data.get('organic_results', [])
search('python api tutorial')
search('serp api pricing comparison', force_scavio=True)JavaScript Example
const SH = { 'x-api-key': process.env.SCAVIO_API_KEY, 'Content-Type': 'application/json' };
const SEARXNG = process.env.SEARXNG_URL || 'http://localhost:8080';
async function search(query, forceScavio = false) {
if (!forceScavio) {
try {
const r = await fetch(`${SEARXNG}/search?q=${encodeURIComponent(query)}&format=json`,
{ signal: AbortSignal.timeout(5000) });
const data = await r.json();
if (data.results?.length >= 5) {
console.log(`${query}: ${data.results.length} via SearXNG (free)`);
return data.results.slice(0, 10);
}
} catch {}
}
const data = await fetch('https://api.scavio.dev/api/v1/search', {
method: 'POST', headers: SH,
body: JSON.stringify({ query, country_code: 'us' })
}).then(r => r.json());
console.log(`${query}: ${(data.organic_results||[]).length} via Scavio ($0.005)`);
return data.organic_results || [];
}
search('python tutorial').then(() => search('serp api pricing', true)).catch(console.error);Expected Output
python web framework -> searxng (8 results)
best serp api -> scavio (10 results)
tiktok analytics tool -> searxng (7 results)
react vs vue 2026 -> searxng (9 results)
llm agent search -> scavio (10 results)
SearXNG hits: 3, Scavio fallbacks: 2
Total cost: $0.010
Savings vs all-Scavio: $0.015