FinRAG / src /generation /semantic_cache.py
Harshavard21's picture
Initial commit
454f118
Raw History Blame Contribute Delete
3.39 kB
"""
src/generation/semantic_cache.py
=================================
In-memory Semantic Cache for RAG query results.
Uses cosine similarity over NumPy matrices to find semantically identical questions.
"""
import numpy as np
from typing import Tuple, List, Dict, Optional
from src.utils.logger import logger
class SemanticCache:
"""
Singleton Semantic Cache.
Max 500 overall queries. FIFO eviction.
"""
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self, max_size: int = 500, threshold: float = 0.95):
if self._initialized:
return
self.max_size = max_size
self.threshold = threshold
# We store queries in a parallel array structure
# embeddings: np.ndarray of shape (N, 768)
self.embeddings: np.ndarray = np.empty((0, 768), dtype=np.float32)
# payloads parallel to embeddings rows
# [{"company": str, "fy": str, "text": str, "sources": list}, ...]
self.payloads: List[Dict] = []
self._initialized = True
logger.info(f"Semantic Cache initialized (max_size={max_size}, threshold={threshold})")
def find_match(self, query_emb: np.ndarray, company: str, fiscal_year: Optional[str]) -> Optional[Tuple[str, list]]:
"""
Finds a cached response using cosine similarity.
Query embedding must be L2 normalized (BGE embedder does this).
"""
if len(self.payloads) == 0:
return None
# query_emb shape: (768,)
# self.embeddings shape: (N, 768)
# Cosine similarity is just the dot product since vectors are L2 normalized
similarities = np.dot(self.embeddings, query_emb)
# Get the index of the highest similarity
best_idx = int(np.argmax(similarities))
best_score = similarities[best_idx]
if best_score >= self.threshold:
# Check hard filters (company and FY must match exactly)
p = self.payloads[best_idx]
if p["company"] == company and p["fy"] == fiscal_year:
logger.info(f"Semantic Cache HIT (score={best_score:.4f})")
return p["text"], p["sources"]
return None
def add(self, query_emb: np.ndarray, company: str, fiscal_year: Optional[str], text: str, sources: list):
"""Adds a new response to the cache, evicting the oldest if full."""
# Check size and apply FIFO eviction
if len(self.payloads) >= self.max_size:
# Remove oldest (index 0)
self.embeddings = self.embeddings[1:]
self.payloads.pop(0)
# Append new embedding
query_emb_2d = query_emb.reshape(1, -1)
if self.embeddings.shape[0] == 0:
self.embeddings = query_emb_2d
else:
self.embeddings = np.vstack([self.embeddings, query_emb_2d])
# Append payload
self.payloads.append({
"company": company,
"fy": fiscal_year,
"text": text,
"sources": sources
})
logger.info(f"Added to Semantic Cache (size={len(self.payloads)}/{self.max_size})")