RetrievalCache
On this page
core.retrieval_cache.RetrievalCache
Overview
A thread-safe, TTL-aware LRU cache for retrieval results, keyed by a normalized (query, top_k) pair. It exists to avoid repeating expensive query expansion + vector search + reranking work for repeated or near-duplicate queries within a short time window.
Exact signature
class RetrievalCache:
def __init__(self, maxsize: int = 256, ttl: Optional[float] = 300.0) -> None: ...
Constructor
Parameters
Initialization flow
Sets up an RLock, an empty OrderedDict store (key → (results, inserted_at)), and _hits/_misses counters at 0. Logs an INFO line with maxsize and ttl (formatted as "∞" if ttl is None).
Attributes
Key construction
_normalize_query (module-level)
def _normalize_query(query: str) -> str
re.sub(r"\s+", " ", query.strip().lower()) — collapses internal whitespace, strips leading/trailing whitespace, lowercases. Note this is not Unicode-normalization-aware (e.g. it won't treat Arabic diacritics or different Unicode representations of the same character as equal).
_make_key (module-level)
def _make_key(query: str, top_k: int) -> str
hashlib.md5(f"{normalized_query}|topk={top_k}".encode()).hexdigest() — an MD5 hex digest used as the cache dict key.
get
def get(self, query: str, top_k: int) -> Optional[List[Tuple[Any, float]]]
Parameters
Returns
Optional[List[Tuple[Any, float]]] — cached results, or None if absent or expired.
Algorithm
- Compute the key.
- Under the lock: if the key is absent, increment
_missesand returnNone. - If
ttlis set andtime.monotonic() - inserted_at > ttl, delete the entry, increment_misses, log a DEBUG "TTL expired" line, and returnNone. - Otherwise: move the entry to the end of the
OrderedDict(LRU touch), increment_hits, log a DEBUG "HIT" line (with a 40-char query preview), and return the cached results.
Thread safety
Fully thread-safe — every operation is performed under self._lock (an RLock).
Complexity
O(1) amortized (dict lookup + OrderedDict.move_to_end).
set
def set(self, query: str, top_k: int, results: List[Tuple[Any, float]]) -> None
Algorithm
- Compute the key.
- Under the lock: if the key already exists, move it to the end first.
- Store
(results, time.monotonic())under the key. - If the store now exceeds
maxsize, evict the single oldest entry (next(iter(self._store))) and log a DEBUG eviction line.
Side effects
Mutates the internal store; may evict an unrelated entry if at capacity.
Complexity
O(1) amortized.
invalidate
def invalidate(self, query: str, top_k: int) -> bool
Removes a specific (query, top_k) entry if present. Returns True if something was removed, False otherwise.
clear
def clear(self) -> None
Empties the entire store and resets _hits/_misses to 0. Logs RetrievalCache cleared at INFO. Called automatically by RAGSystem.add_documents() whenever new documents are successfully indexed.
stats
def stats(self) -> Dict[str, Any]
Returns, under the lock:
{
"size": len(self._store),
"maxsize": self.maxsize,
"ttl": self.ttl,
"hits": self._hits,
"misses": self._misses,
"hit_rate": round(self._hits / total, 3) if total else 0.0, # total = hits + misses
}
hit_rate is 0.0 (not None or NaN) when no lookups have occurred yet.
__repr__
RetrievalCache(size={size}/{maxsize}, hit_rate={hit_rate:.1%})
Examples
Basic:
from zerda.rag.core import RetrievalCache
cache = RetrievalCache(maxsize=512, ttl=600)
cached = cache.get("رقم هاتف محمد", top_k=5)
if cached is None:
result = vector_db.search("رقم هاتف محمد", top_k=5)
cache.set("رقم هاتف محمد", top_k=5, results=result)
Disabling expiry:
cache = RetrievalCache(maxsize=1000, ttl=None) # entries never expire by time
Manual invalidation after an external update:
cache.invalidate("outdated query", top_k=5)
Monitoring hit rate:
print(cache.stats())
# {'size': 12, 'maxsize': 256, 'ttl': 300.0, 'hits': 40, 'misses': 15, 'hit_rate': 0.727}
Production-ready usage — periodic cache health logging:
import time
while True:
time.sleep(60)
s = cache.stats()
logger.info("Cache: %d/%d entries, %.1f%% hit rate",
s["size"], s["maxsize"], s["hit_rate"] * 100)
Best practices
- Set
ttl=Noneonly for genuinely static corpora — otherwise stale results can persist indefinitely between explicitclear()/invalidate()calls. - Remember
RAGSystem.add_documents()clears the entire cache on every successful ingestion — for high-frequency ingestion pipelines, consider batching document adds to preserve cache warmth.
Common mistakes
- Assuming cache keys are case- or whitespace-sensitive — they are normalized (
_normalize_query), so"Refund Policy"and"refund policy"hit the same cache entry. - Forgetting that
top_kis part of the cache key —get("query", top_k=5)andget("query", top_k=10)are different entries entirely.
Related APIs
RAGSystem.retrieve— the primary consumer.RAGConfig—cache_maxsize/cache_ttl/enable_retrieval_cachefields configure this class.