-
Notifications
You must be signed in to change notification settings - Fork 2
systems caching
Active contributors: Magnus Hedemark
Valkey-backed response cache that stores merged result sets at two levels: a precise key (query + language + safesearch) and a broader answer-level key (query only). Category-aware TTL (300 seconds for news, 3600 seconds for general). Supports negative caching to short-circuit failed queries. Graceful degradation when Valkey is unavailable.
| Type | File | Description |
|---|---|---|
SearchCache |
slopsearx/cache.py |
Valkey-backed cache client. Provides get(), set(), get_answer(), set_answer(), set_error(), clear() methods. Gracefully degrades to no-op if Valkey is unreachable. |
normalize_query |
slopsearx/cache.py |
Normalizes a raw query string for deterministic cache key construction: URL-decodes, strips punctuation and whitespace, collapses whitespace, lowercases. |
cache_key |
slopsearx/cache.py |
Builds a deterministic SHA-256 cache key from the normalized query tuple (query string, language, safesearch level). Prefix: search:. |
_answer_cache_key |
slopsearx/cache.py |
Builds a broader cache key from query only (no language or safesearch). Prefix: answer:. |
_ttl_for_query |
slopsearx/cache.py |
Selects TTL based on query categories. News queries get 300 seconds; all other queries get 3600 seconds. |
Before building a cache key, raw queries are normalized via normalize_query():
- URL-decode the query (
unquote_plus) - Strip leading and trailing whitespace
- Strip trailing punctuation (
. ! ? , ; :) - Collapse multiple whitespace characters into one
- Lowercase and strip again
This ensures that "Python 3.12", "python 3.12.", and "Python%203.12" all produce the same cache key.
SlopSearX uses two cache levels:
-
Search cache (
search:{sha256}) — keyed on(normalized_query, language, safesearch). Precise matching: different languages or safesearch levels get different cache entries. -
Answer cache (
answer:{sha256}) — keyed onnormalized_queryonly. Broader matching: the same cached response is returned regardless of language or safesearch variation.
At request time, the server checks both caches in order: search cache first, then answer cache. On a fresh result, both caches are populated.
- On each search request, the server computes the search cache key and calls
_cache.get(key) - If a cached result exists at the search level, it is returned immediately with
meta.cached = True - If not, the server checks the answer cache via
_cache.get_answer(q) - If a cached result exists at the answer level, it is returned immediately
-
Negative cache check: If either cached entry has an
_errorsentinel, the server returns HTTP 503 without dispatching to engines. This prevents thundering-herd retries on failing queries. - If no cached result exists, the search executes normally
- The merged result set is stored in both the search cache and answer cache via
_cache.set()and_cache.set_answer() - Cache write is skipped for completely failed searches (all engines unresponsive)
When a search fails (all engines unresponsive or other systemic failure), the server stores a negative cache entry via _cache.set_error(key):
payload = json.dumps({"_error": True, "timestamp": int(time.time())})Negative entries have a short TTL (default 60 seconds, configurable via SEARCH_CACHE_NEGATIVE_TTL_SECONDS). When the server encounters a negative entry on a subsequent request, it returns HTTP 503 immediately rather than dispatching again to already-failing engines.
TTL is determined by _ttl_for_query(). The function checks if any requested category contains the string "news":
- News queries: 300 seconds (fast turnover for time-sensitive content)
- All other queries: 3600 seconds (1 hour default, configurable via
SEARCH_CACHE_TTL_SECONDS) - Answer cache TTL: same as search cache TTL, configurable via
SEARCH_CACHE_TTL_SECONDS
If Valkey is unreachable at startup, SearchCache logs a warning and sets _connected = False. All subsequent cache operations become no-ops:
-
get()returnsNone(cache miss) -
set()returns without storing - The server proceeds with a normal search
This ensures that a Valkey outage does not break search availability. The only impact is increased latency from cache misses and no negative-cache protection.
- Individual engine responses are not cached independently. Only merged result sets are cached.
- API keys, secrets, and config are not cached (read directly from env or file).
- Rate-limit state is ephemeral with short TTLs in a separate Valkey keyspace.
-
Server lifecycle:
SearchCacheis instantiated in thestartup()event handler and stored in the module-level_cacheglobal - Search flow: The server checks both cache levels before dispatching to engines and stores results after merging
-
Answer cache:
get_answer()andset_answer()are used alongside the primary search cache -
Negative cache:
set_error()is called by the server when a systemic failure occurs -
Category awareness: The server passes the requested categories to
_ttl_for_query()after determining them from search params
- Changing key format: modify
cache_key()or_answer_cache_key()functions - Adjusting normalization: modify
normalize_query()function - Adjusting TTL rules: modify
_ttl_for_query()function or env defaults - Changing negative cache behavior: modify
set_error()orSEARCH_CACHE_NEGATIVE_TTL_SECONDS - Adding cache invalidation: implement
clear()with more selective patterns
| File | Description |
|---|---|
slopsearx/cache.py |
SearchCache class, cache_key(), normalize_query(), _ttl_for_query(), _answer_cache_key()
|