Spaces:
Sleeping
Sleeping
| """ | |
| Try Docs cache (Redis-only): | |
| 1. Exact-match cache in Redis | |
| 2. Semantic cache using Redis Vector Search (RediSearch) | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import json | |
| import logging | |
| import uuid | |
| from typing import Optional | |
| import numpy as np | |
| from .config import get_settings | |
| logger = logging.getLogger(__name__) | |
| settings = get_settings() | |
| CACHE_EMBEDDING_MODE = "openai-small" | |
| EXACT_PREFIX = "rag:try:exact:" | |
| SEMANTIC_INDEX = "rag:try:semantic" | |
| SEMANTIC_PREFIX = "rag:try:sem:" | |
| VECTOR_FIELD = "vector" | |
| PAYLOAD_FIELD = "payload" | |
| COLLECTION_FIELD = "collection_key" | |
| PARAMS_FIELD = "params_key" | |
| def _build_redis_client(): | |
| try: | |
| import redis | |
| client = redis.from_url(settings.redis_url, decode_responses=False) | |
| client.ping() | |
| logger.info("Redis cache connected") | |
| return client | |
| except Exception as exc: | |
| logger.warning("Redis unavailable (%s); caching disabled", exc) | |
| return None | |
| _client = _build_redis_client() | |
| _semantic_ready = False | |
| _semantic_failed = False | |
| def _ensure_semantic_index() -> bool: | |
| global _semantic_ready, _semantic_failed | |
| if _semantic_ready: | |
| return True | |
| if _semantic_failed or _client is None: | |
| return False | |
| try: | |
| _client.execute_command("FT.INFO", SEMANTIC_INDEX) | |
| _semantic_ready = True | |
| return True | |
| except Exception: | |
| pass | |
| try: | |
| dim = str(int(settings.embedding_dimensions_openai)) | |
| _client.execute_command( | |
| "FT.CREATE", | |
| SEMANTIC_INDEX, | |
| "ON", | |
| "HASH", | |
| "PREFIX", | |
| 1, | |
| SEMANTIC_PREFIX, | |
| "SCHEMA", | |
| "query", | |
| "TEXT", | |
| COLLECTION_FIELD, | |
| "TAG", | |
| PARAMS_FIELD, | |
| "TAG", | |
| VECTOR_FIELD, | |
| "VECTOR", | |
| "HNSW", | |
| 6, | |
| "TYPE", | |
| "FLOAT32", | |
| "DIM", | |
| dim, | |
| "DISTANCE_METRIC", | |
| "COSINE", | |
| ) | |
| _semantic_ready = True | |
| return True | |
| except Exception as exc: | |
| logger.warning("Failed to create Redis vector index: %s", exc) | |
| _semantic_failed = True | |
| return False | |
| def _exact_key(query: str, collection_key: str, params_key: str) -> str: | |
| payload = f"{query}::{collection_key}::{params_key}" | |
| return EXACT_PREFIX + hashlib.sha256(payload.encode()).hexdigest()[:32] | |
| def _tag_hash(value: str) -> str: | |
| return hashlib.sha1(value.encode()).hexdigest()[:16] | |
| def _vector_bytes(vec: list[float]) -> bytes: | |
| arr = np.array(vec, dtype=np.float32) | |
| norm = np.linalg.norm(arr) | |
| if norm > 0: | |
| arr = arr / norm | |
| return arr.tobytes() | |
| def _decode(value: bytes | str | None) -> Optional[str]: | |
| if value is None: | |
| return None | |
| if isinstance(value, bytes): | |
| return value.decode("utf-8") | |
| return value | |
| # Exact cache | |
| def get_exact(query: str, collection_key: str, params_key: str) -> Optional[dict]: | |
| if _client is None: | |
| return None | |
| key = _exact_key(query, collection_key, params_key) | |
| raw = _client.get(key) | |
| if not raw: | |
| return None | |
| payload = _decode(raw) | |
| if payload is None: | |
| return None | |
| return json.loads(payload) | |
| def set_exact(query: str, collection_key: str, params_key: str, value: dict) -> None: | |
| if _client is None: | |
| return | |
| key = _exact_key(query, collection_key, params_key) | |
| serialized = json.dumps(value) | |
| _client.set(key, serialized.encode("utf-8"), ex=settings.cache_ttl_seconds) | |
| # Semantic cache | |
| def get_semantic(query_vec: list[float], collection_key: str, params_key: str) -> Optional[dict]: | |
| if _client is None: | |
| return None | |
| if not _ensure_semantic_index(): | |
| return None | |
| vec = _vector_bytes(query_vec) | |
| k = 4 | |
| params_tag = _tag_hash(params_key) | |
| query = ( | |
| f"(@{COLLECTION_FIELD}:{{{collection_key}}} " | |
| f"@{PARAMS_FIELD}:{{{params_tag}}})" | |
| f"=>[KNN {k} @{VECTOR_FIELD} $vec AS score]" | |
| ) | |
| try: | |
| res = _client.execute_command( | |
| "FT.SEARCH", | |
| SEMANTIC_INDEX, | |
| query, | |
| "PARAMS", | |
| 2, | |
| "vec", | |
| vec, | |
| "RETURN", | |
| 2, | |
| PAYLOAD_FIELD, | |
| "score", | |
| "DIALECT", | |
| 2, | |
| ) | |
| except Exception as exc: | |
| logger.warning("Redis semantic search failed: %s", exc) | |
| return None | |
| if not res or res[0] == 0: | |
| return None | |
| best_similarity = 0.0 | |
| best_payload = None | |
| for i in range(1, len(res), 2): | |
| fields = res[i + 1] | |
| payload = None | |
| distance = None | |
| for j in range(0, len(fields), 2): | |
| name = _decode(fields[j]) or "" | |
| value = fields[j + 1] | |
| if name == PAYLOAD_FIELD: | |
| payload = _decode(value) | |
| elif name == "score": | |
| distance = float(_decode(value) or 0) | |
| if payload is None or distance is None: | |
| continue | |
| similarity = 1.0 - distance | |
| if similarity > best_similarity: | |
| best_similarity = similarity | |
| best_payload = payload | |
| if best_payload and best_similarity >= settings.semantic_cache_threshold: | |
| logger.info("Semantic cache hit (score=%.3f)", best_similarity) | |
| return json.loads(best_payload) | |
| return None | |
| def set_semantic( | |
| query_vec: list[float], | |
| query: str, | |
| collection_key: str, | |
| params_key: str, | |
| response: dict, | |
| ) -> None: | |
| if _client is None: | |
| return | |
| if not _ensure_semantic_index(): | |
| return | |
| key = f"{SEMANTIC_PREFIX}{uuid.uuid4().hex}" | |
| payload = json.dumps(response) | |
| _client.hset( | |
| key, | |
| mapping={ | |
| "query": query, | |
| COLLECTION_FIELD: collection_key, | |
| PARAMS_FIELD: _tag_hash(params_key), | |
| VECTOR_FIELD: _vector_bytes(query_vec), | |
| PAYLOAD_FIELD: payload, | |
| }, | |
| ) | |
| _client.expire(key, settings.cache_ttl_seconds) | |
| def cache_connected() -> bool: | |
| if _client is None: | |
| return False | |
| try: | |
| return bool(_client.ping()) | |
| except Exception: | |
| return False | |
| def _info_to_dict(raw: list) -> dict: | |
| info = {} | |
| if not raw: | |
| return info | |
| for i in range(0, len(raw), 2): | |
| key = _decode(raw[i]) or "" | |
| info[key] = raw[i + 1] | |
| return info | |
| def get_cache_stats() -> dict: | |
| if _client is None: | |
| return {"system": "disabled", "exact_matches_cached": 0, "semantic_matches_cached": 0} | |
| stats = {"system": "redis"} | |
| try: | |
| stats["exact_matches_cached"] = _client.dbsize() | |
| except Exception: | |
| stats["exact_matches_cached"] = "unknown" | |
| try: | |
| info = _client.execute_command("FT.INFO", SEMANTIC_INDEX) | |
| info_map = _info_to_dict(info) | |
| num_docs = info_map.get("num_docs", 0) | |
| stats["semantic_matches_cached"] = int(num_docs) if num_docs is not None else 0 | |
| except Exception: | |
| stats["semantic_matches_cached"] = "unknown" | |
| return stats | |
| print("[cache] Module ready") | |