DocMind-AI / cache.py
Ak47-model-ml's picture
Upload 10 files
406ba5c verified
Raw
History Blame Contribute Delete
4.57 kB
"""
Simple LRU disk-based cache for embeddings and query results.
Falls back to in-memory dict when disk writes fail.
"""
import hashlib
import json
import logging
import os
import pickle
import time
from collections import OrderedDict
from typing import Any, Optional
from config import config
logger = logging.getLogger(__name__)
os.makedirs(config.CACHE_DIR, exist_ok=True)
# =============================================================================
# IN-MEMORY LRU CACHE
# =============================================================================
class LRUCache:
"""Thread-unsafe but fast in-memory LRU cache."""
def __init__(self, max_size: int = 256):
self._cache: OrderedDict = OrderedDict()
self._max_size = max_size
def get(self, key: str) -> Optional[Any]:
if key not in self._cache:
return None
self._cache.move_to_end(key)
return self._cache[key]
def set(self, key: str, value: Any):
if key in self._cache:
self._cache.move_to_end(key)
self._cache[key] = value
if len(self._cache) > self._max_size:
self._cache.popitem(last=False)
def clear(self):
self._cache.clear()
def __len__(self):
return len(self._cache)
# =============================================================================
# DISK CACHE (pickle-based)
# =============================================================================
class DiskCache:
"""Key-value store backed by individual pickle files on disk."""
def __init__(self, directory: str = config.CACHE_DIR, ttl_seconds: int = 86400):
self._dir = directory
self._ttl = ttl_seconds
os.makedirs(self._dir, exist_ok=True)
def _path(self, key: str) -> str:
hashed = hashlib.sha256(key.encode()).hexdigest()
return os.path.join(self._dir, f"{hashed}.pkl")
def get(self, key: str) -> Optional[Any]:
path = self._path(key)
if not os.path.exists(path):
return None
try:
with open(path, "rb") as fh:
entry = pickle.load(fh)
if time.time() - entry["ts"] > self._ttl:
os.remove(path)
return None
return entry["value"]
except Exception as e:
logger.warning(f"DiskCache read error for key {key!r}: {e}")
return None
def set(self, key: str, value: Any):
path = self._path(key)
try:
with open(path, "wb") as fh:
pickle.dump({"ts": time.time(), "value": value}, fh)
except Exception as e:
logger.warning(f"DiskCache write error for key {key!r}: {e}")
def delete(self, key: str):
path = self._path(key)
if os.path.exists(path):
os.remove(path)
def clear(self):
for fname in os.listdir(self._dir):
if fname.endswith(".pkl"):
try:
os.remove(os.path.join(self._dir, fname))
except Exception:
pass
# =============================================================================
# TWO-LAYER CACHE
# =============================================================================
class TwoLayerCache:
"""
Checks in-memory LRU first, then falls through to disk.
On a disk hit the value is promoted to LRU.
"""
def __init__(self, lru_size: int = 128):
self._lru = LRUCache(max_size=lru_size)
self._disk = DiskCache()
def get(self, key: str) -> Optional[Any]:
value = self._lru.get(key)
if value is not None:
return value
value = self._disk.get(key)
if value is not None:
self._lru.set(key, value)
return value
def set(self, key: str, value: Any):
self._lru.set(key, value)
self._disk.set(key, value)
def clear(self):
self._lru.clear()
self._disk.clear()
# Singleton
_cache_instance: Optional[TwoLayerCache] = None
def get_cache() -> TwoLayerCache:
global _cache_instance
if _cache_instance is None:
_cache_instance = TwoLayerCache()
return _cache_instance
def make_cache_key(*parts) -> str:
"""Create a deterministic cache key from arbitrary string parts."""
raw = "|".join(str(p) for p in parts)
return hashlib.sha256(raw.encode()).hexdigest()