mhungtr211
Deploy rag
9c90345
Raw
History Blame Contribute Delete
5.06 kB
import hashlib
import json
import logging
import redis
from langchain_community.utils.math import cosine_similarity
from langchain_huggingface import HuggingFaceEmbeddings
from langfuse import observe
from config.settings import SETTINGS
logger = logging.getLogger(__name__)
class SemanticCache:
"""
Redis-backed semantic cache placed before the retrieval step.
Flow:
query → embed → search cache (cosine similarity) → HIT: return cached result
→ MISS: retrieve → store in cache
"""
KEY_PREFIX = "rag:retrieval:"
DEFAULT_TTL = 60 * 60 * 24 # 24 hours
def __init__(
self,
embeddings: HuggingFaceEmbeddings,
threshold: float = SETTINGS.REDIS_CACHE_THRESHOLD,
key_prefix: str = "rag:retrieval:",
):
self.embeddings = embeddings
self.threshold = threshold
self.KEY_PREFIX = key_prefix
# Ưu tiên REDIS_URL (cloud API) nếu có, fallback sang host:port (local Docker)
if SETTINGS.REDIS_URL:
self._client = redis.from_url(
SETTINGS.REDIS_URL,
decode_responses=True,
)
else:
self._client = redis.Redis(
host=SETTINGS.REDIS_HOST,
port=SETTINGS.REDIS_PORT,
password=SETTINGS.REDIS_PASSWORD.get_secret_value() if SETTINGS.REDIS_PASSWORD else None,
decode_responses=True,
)
self._ping()
# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------
def _ping(self) -> None:
try:
self._client.ping()
logger.info("Redis connection established (%s:%s)", SETTINGS.REDIS_HOST, SETTINGS.REDIS_PORT)
except redis.ConnectionError as exc:
logger.warning("Cannot connect to Redis – cache will be skipped: %s", exc)
def _embed(self, text: str) -> list[float]:
return self.embeddings.embed_query(text)
def _build_key(self, query: str) -> str:
digest = hashlib.md5(query.encode()).hexdigest()
return f"{self.KEY_PREFIX}{digest}"
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
@observe(name="SemanticCache.get")
def get(self, query: str) -> str | None:
"""
Search cache using semantic similarity.
Returns the cached result if score >= threshold, otherwise None.
"""
try:
query_vector = self._embed(query)
best_score = 0
best_response = ""
best_query = ""
for key in self._client.scan_iter(match=f"{self.KEY_PREFIX}*"):
embedding_cache = json.loads(self._client.hget(key, "embedding"))
similarity_scores = cosine_similarity([query_vector], [embedding_cache])[0][0]
if similarity_scores > best_score:
best_score = similarity_scores
best_response = self._client.hget(key, "result")
best_query = self._client.hget(key, "query")
print(f"best query: {best_query}")
print(f"best score: {best_score}")
if best_score >= self.threshold:
logger.info(f"Cache HIT (score={best_score}, threshold={self.threshold})")
return best_response
logger.info(f"Cache MISS (score={best_score}, threshold={self.threshold})")
return None
except Exception as exc:
logger.warning("Cache lookup failed – falling through to retriever: %s", exc)
return None
@observe(name="SemanticCache.set")
def set(self, query: str, result: str) -> None:
"""Store a retrieval result in cache."""
try:
print(f"DEBUG query type: {type(query)}")
print(f"DEBUG query value: {repr(query)}")
vector = self._embed(query)
key = self._build_key(query)
pipe = self._client.pipeline(transaction=False)
pipe.hset(
key,
mapping={
"query": query,
"embedding": json.dumps(vector, ensure_ascii=False),
"result": result,
},
)
pipe.expire(key, self.DEFAULT_TTL)
pipe.execute()
logger.info("Cached retrieval result for query: %r", query[:60])
except Exception as exc:
logger.warning("Cache write failed: %s", exc)
def invalidate_all(self) -> int:
"""Delete all retrieval cache keys. Call when S3 / vector store is updated."""
keys = list(self._client.scan_iter(match=f"{self.KEY_PREFIX}*"))
if keys:
self._client.delete(*keys)
logger.info("Cache invalidated: %d key(s) removed", len(keys))
return len(keys)