use crate::cache::{CacheMetrics, CacheStats, CacheStore};
use crate::config::{NormalizationMode, PoolingStrategy};
use lru::LruCache;
use moka::sync::Cache;
use sha2::{Digest, Sha256};
use std::cell::RefCell;
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::time::Duration;
use tracing::debug;
thread_local! {
static LOCAL_EMBEDDING_CACHE: RefCell<LruCache<String, Vec<f32>>> =
RefCell::new(LruCache::new(NonZeroUsize::new(100).unwrap()));
}
pub struct EmbeddingCache {
cache: Arc<Cache<String, Vec<f32>>>,
metrics: Arc<CacheMetrics>,
}
impl EmbeddingCache {
pub fn new(max_capacity: u64, ttl_seconds: u64) -> Self {
let cache = Cache::builder()
.max_capacity(max_capacity)
.time_to_live(Duration::from_secs(ttl_seconds))
.build();
Self {
cache: Arc::new(cache),
metrics: Arc::new(CacheMetrics::new()),
}
}
pub fn compute_key(
text: &str,
model_name: &str,
pooling: PoolingStrategy,
normalization: NormalizationMode,
) -> String {
let mut hasher = Sha256::new();
hasher.update(text.as_bytes());
hasher.update(model_name.as_bytes());
hasher.update([pooling as u8]);
match normalization {
NormalizationMode::None => hasher.update([0u8]),
NormalizationMode::MaxAbs => hasher.update([1u8]),
NormalizationMode::L2 => hasher.update([2u8]),
NormalizationMode::PNorm(p) => {
hasher.update([3u8]);
hasher.update(p.to_le_bytes());
}
}
format!("{:x}", hasher.finalize())
}
pub fn metrics(&self) -> &CacheMetrics {
&self.metrics
}
pub fn warm_cache(&self, entries: Vec<(String, Vec<f32>)>) {
for (key, value) in entries {
self.insert(key, value);
}
}
pub fn inner_cache(&self) -> &Arc<Cache<String, Vec<f32>>> {
&self.cache
}
}
impl CacheStore<String, Vec<f32>> for EmbeddingCache {
fn get(&self, key: &String) -> Option<Vec<f32>> {
let local_result = LOCAL_EMBEDDING_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
if let Some(value) = cache.get(key) {
return Some(value.clone());
}
None
});
if let Some(value) = local_result {
self.metrics.record_hit();
return Some(value);
}
let result = self.cache.get(key);
if let Some(ref value) = result {
LOCAL_EMBEDDING_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
cache.put(key.clone(), value.clone());
});
self.metrics.record_hit();
} else {
self.metrics.record_miss();
}
result
}
fn insert(&self, key: String, value: Vec<f32>) {
let memory_bytes = value.len() * 4 + key.len() + 32;
LOCAL_EMBEDDING_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
cache.put(key.clone(), value.clone());
});
let current_entry_count = self.cache.entry_count();
self.cache.insert(key.clone(), value);
self.cache.run_pending_tasks();
let new_entry_count = self.cache.entry_count();
if new_entry_count <= current_entry_count && current_entry_count > 0 {
self.metrics.record_eviction(1);
}
self.metrics.add_memory(memory_bytes as u64);
debug!("Inserted into the cache: {key}");
}
fn clear(&self) {
LOCAL_EMBEDDING_CACHE.with(|cache| {
cache.borrow_mut().clear();
});
let entry_count = self.cache.entry_count();
if entry_count > 0 {
self.metrics.record_eviction(entry_count);
}
self.cache.invalidate_all();
self.metrics.reset();
}
fn stats(&self) -> CacheStats {
self.cache.run_pending_tasks();
CacheStats::from_metrics(&self.metrics, self.cache.entry_count())
}
fn len(&self) -> usize {
self.cache.run_pending_tasks();
self.cache.entry_count().try_into().unwrap_or(usize::MAX)
}
}
impl EmbeddingCache {
pub fn evict_oldest(&self, count: usize) {
LOCAL_EMBEDDING_CACHE.with(|cache| {
cache.borrow_mut().clear();
});
let current_count = self.cache.entry_count();
if current_count == 0 {
return;
}
if count as u64 >= current_count / 2 {
self.clear();
} else {
self.metrics.record_eviction(count as u64);
self.cache.run_pending_tasks();
}
}
}