-
Notifications
You must be signed in to change notification settings - Fork 2
overview architecture
SlopSearX runs as a single replica type behind a load balancer. Every replica loads all configured engines and communicates with Valkey for shared state. All replicas are interchangeable.
graph TD
LB["Load Balancer<br/>(Traefik / Nginx)"]
R1["Replica 1<br/>(all engines)"]
R2["Replica 2<br/>(all engines)"]
RN["Replica N<br/>(all engines)"]
V["Valkey<br/>(cluster)"]
LB --> R1
LB --> R2
LB --> RN
R1 --> V
R2 --> V
RN --> V
When a client sends GET /search?q=..., the following happens inside one replica:
sequenceDiagram
participant C as Client
participant S as Server (FastAPI)
participant RL as Client RL
participant CACHE as Valkey Cache
participant QR as QueryRouter
participant CB as Circuit Breaker
participant SM as Semaphore
participant E1 as Engine 1 (Brave)
participant E2 as Engine 2 (Wikipedia)
participant STATS as Stats Tracker
participant AUDIT as Audit Logger
participant R as Ranker
participant F as Formatter
C->>S: GET /search?q=python
S->>RL: Check per-client rate limit
alt Rate limited
RL-->>S: Deny
S-->>C: 429
end
S->>CACHE: Check search cache
alt Cache HIT
CACHE-->>S: Cached response
S-->>C: 200 (cached)
else Search cache MISS
S->>CACHE: Check answer cache
alt Answer cache HIT
CACHE-->>S: Cached response
S-->>C: 200 (cached)
else Cache MISS
S->>QR: route(query, categories)
QR-->>S: matched engines list
par Engine dispatch
S->>CB: circuit_allowed() per engine
CB-->>S: allow / deny
S->>SM: acquire semaphore slot
S->>E1: BraveAdapter.search(q)
S->>E2: WikipediaAdapter.search(q)
E1-->>S: AdapterResponse (10 results)
E2-->>S: AdapterResponse (3 results)
S->>CB: record_success / record_failure
end
S->>STATS: record(engine, status, latency)
S->>R: rank(engine_results)
R-->>S: ranked SearchResult list
S->>F: format(results)
S->>CACHE: store in cache
S->>AUDIT: record query (fire-and-forget)
S-->>C: 200 (fresh)
end
end
-
Query normalization — the raw query string is normalized into a tuple of
(query, language, pageno, categories, safesearch) - Per-client rate limiting — the client IP is checked against its request budget. Exceeded budgets return HTTP 429 before any engine work
- Search cache lookup — Valkey is checked with a SHA-256 key derived from the normalized tuple. A cache hit returns immediately
- Answer cache lookup — if the search cache misses, the broader answer-level cache is checked (query only, no language/safesearch)
-
Negative cache check — if either cached entry has an
_errorsentinel, HTTP 503 is returned without dispatching - Query routing — the QueryRouter analyzes the query and selects relevant engines based on topic matching and category filters. Unscoped queries without topic matches are restricted to Tier 1 (broad) engines
- Circuit breaker check — each target engine's circuit breaker is checked. Open circuits skip dispatch
-
Concurrent dispatch — allowed engines run concurrently via
asyncio.gather(), bounded by a semaphore (MAX_CONCURRENT_ENGINES, default 10). Each engine has a 3-second timeout -
Result collection — each adapter returns an
AdapterResponsewith results and a status. Circuit breaker state is updated (record_success/record_failure). The EngineStatsTracker records per-engine quality telemetry -
Merging and ranking — the
PresenceRankernormalizes URLs, deduplicates by normalized URL, and scores results by how many engines returned them - Caching — the merged result set is stored in both the search cache and answer cache
- Audit trail — the query is recorded in a Valkey stream with dispatch statistics (fire-and-forget)
- Formatting — the response is serialized as JSON (SearXNG-compatible) or YAML+Markdown
SlopSearX has two tiers of reliability:
- Tier 1 (always-available) — API-based engines like Brave and Wikipedia. These use structured JSON APIs, have SLAs, and are reliable
- Tier 2 (quality multipliers) — scrape-based engines like DuckDuckGo and Google. These send HTTP requests with stealth headers and parse HTML. They break frequently due to CAPTCHA walls, IP bans, and HTML structure changes
A CAPTCHA block or IP ban on a scrape engine never blocks the response. The failing engine is reported in unresponsive_engines and omitted from results. The response always returns HTTP 200 with whatever results are available.
| Subsystem |
slopsearx/ file |
Purpose |
|---|---|---|
| Adapter interface | adapter.py |
Base classes EngineAdapter and ScrapeAdapter, @register_engine decorator, engine registry, circuit breaker, URL sanitization |
| Merging and ranking | merger.py |
PresenceRanker, URL normalization, deduplication, metadata helpers |
| Configuration | config.py |
Three-layer config (defaults + YAML file + env vars), Config dataclass |
| Caching | cache.py |
Valkey-backed two-level response cache (search + answer), negative caching, query normalization |
| Rate limiting | ratelimit.py |
Distributed rate limiting with Valkey sliding window, fail-closed mode with local fallback, backpressure propagation |
| Metrics | metrics.py |
OpenMetrics-compatible counters, gauges, and histograms (stdlib only) |
| Server | server.py |
FastAPI application, /search, /health, /metrics, /config endpoints, per-client rate limiting, semaphore-bounded concurrency |
| Formatters | formatter.py |
SearXNG JSON formatter and YAML+Markdown agent-native formatter |
| QueryRouter | router.py |
Topic-based query routing that dispatches to relevant engines only |
| EngineStatsTracker | stats.py |
Per-engine quality telemetry stored in Valkey (success/failure counts, latency) |
| SuggestionService | suggest.py |
Background search query suggestions from engine suggest APIs |
| Query audit trail | audit.py |
Durable audit log of every search query in Valkey streams |
| ProxyPool | proxypool.py |
Configurable proxy rotation infrastructure for scrape-based adapters |
The project is 100% Python 3.12+ with supporting YAML, JSON, and Markdown configuration files.
| Language | Files | Lines |
|---|---|---|
| Python | ~100 | ~15,654 |
| YAML | 8 | ~230 |
| Markdown | 40 | ~3,600 |
| JSON | 2 | ~145 |
| Dockerfile | 1 | ~30 |
The slopsearx/ core library is ~2,870 lines across 13 files, the engines/ directory is ~6,670 lines across 49 files (48 adapters + __init__.py), and tests/ is ~6,100 lines across 37 files.
- FastAPI — HTTP framework for the REST API
- httpx — async HTTP client for engine API calls and HTML scraping
- lxml — HTML parsing for scrape-based engines (DuckDuckGo, Google)
- Valkey — in-memory data store for caching, rate limiting, and engine stats (Redis-compatible)
- uvicorn — ASGI server
- PyYAML — YAML config parsing and YAML+Markdown output formatting