use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use crate::{EmbeddingError, Embeddings};
const DEFAULT_CAPACITY: usize = 4096;
pub struct CachingEmbeddings<M> {
inner: M,
cache: Mutex<HashMap<String, Arc<Vec<f32>>>>,
capacity: usize,
}
impl<M: Embeddings> CachingEmbeddings<M> {
pub fn new(inner: M, capacity: usize) -> Self {
Self {
inner,
cache: Mutex::new(HashMap::new()),
capacity: if capacity == 0 {
DEFAULT_CAPACITY
} else {
capacity
},
}
}
pub fn wrap(inner: M) -> Self {
Self::new(inner, DEFAULT_CAPACITY)
}
pub fn inner(&self) -> &M {
&self.inner
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn cached_len(&self) -> usize {
self.cache
.lock()
.map(|c| c.len())
.unwrap_or_else(|e| e.into_inner().len())
}
pub fn clear_cache(&self) {
self.cache.lock().unwrap_or_else(|e| e.into_inner()).clear();
}
fn cache_insert(&self, key: String, vector: Arc<Vec<f32>>) {
let mut cache = self.cache.lock().unwrap_or_else(|e| e.into_inner());
if cache.len() >= self.capacity && !cache.contains_key(&key) {
cache.clear();
}
cache.insert(key, vector);
}
async fn cached_vector(&self, text: &str) -> Result<Arc<Vec<f32>>, EmbeddingError> {
if let Some(v) = self
.cache
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.get(text)
{
return Ok(Arc::clone(v));
}
let vector = Arc::new(self.inner.embed_query(text).await?);
self.cache_insert(text.to_string(), Arc::clone(&vector));
Ok(vector)
}
}
#[async_trait]
impl<M: Embeddings> Embeddings for CachingEmbeddings<M> {
async fn embed_query(&self, text: &str) -> Result<Vec<f32>, EmbeddingError> {
Ok(self.cached_vector(text).await?.as_ref().clone())
}
async fn embed_documents(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, EmbeddingError> {
if texts.iter().any(|t| t.trim().is_empty()) {
return Err(EmbeddingError::EmptyInput);
}
if texts.is_empty() {
return Ok(Vec::new());
}
let mut unique: Vec<&str> = Vec::with_capacity(texts.len());
let mut index_of: HashMap<&str, usize> = HashMap::with_capacity(texts.len());
let mut positions: Vec<usize> = Vec::with_capacity(texts.len());
for t in texts {
let index = *index_of.entry(t).or_insert_with(|| {
unique.push(t);
unique.len() - 1
});
positions.push(index);
}
let mut vectors: Vec<Arc<Vec<f32>>> = Vec::with_capacity(unique.len());
for t in &unique {
vectors.push(self.cached_vector(t).await?);
}
Ok(positions
.into_iter()
.map(|i| vectors[i].as_ref().clone())
.collect())
}
fn dimension(&self) -> usize {
self.inner.dimension()
}
fn model_name(&self) -> &str {
self.inner.model_name()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mock::MockEmbeddings;
use std::sync::atomic::{AtomicUsize, Ordering};
struct CountingEmbeddings {
inner: MockEmbeddings,
calls: AtomicUsize,
}
impl CountingEmbeddings {
fn new(dim: usize) -> Self {
Self {
inner: MockEmbeddings::new(dim),
calls: AtomicUsize::new(0),
}
}
fn calls(&self) -> usize {
self.calls.load(Ordering::Relaxed)
}
}
#[async_trait]
impl Embeddings for CountingEmbeddings {
async fn embed_query(&self, text: &str) -> Result<Vec<f32>, EmbeddingError> {
self.calls.fetch_add(1, Ordering::Relaxed);
self.inner.embed_query(text).await
}
fn dimension(&self) -> usize {
self.inner.dimension()
}
fn model_name(&self) -> &str {
self.inner.model_name()
}
}
#[tokio::test]
async fn batch_computes_each_unique_text_once_in_order() {
let cached = CachingEmbeddings::wrap(CountingEmbeddings::new(8));
let results = cached
.embed_documents(&["a", "b", "a", "c", "b", "a"])
.await
.unwrap();
assert_eq!(results.len(), 6, "one row per input, order preserved");
assert_eq!(results[0], results[2]);
assert_eq!(results[0], results[5]);
assert_eq!(results[1], results[4]);
assert_ne!(results[0], results[1]);
assert_eq!(
cached.inner().calls(),
3,
"only the three distinct texts were embedded"
);
assert_eq!(cached.cached_len(), 3);
}
#[tokio::test]
async fn batch_populates_cache_for_later_queries() {
let cached = CachingEmbeddings::wrap(CountingEmbeddings::new(4));
let batch = cached.embed_documents(&["x", "y"]).await.unwrap();
assert_eq!(cached.inner().calls(), 2);
let single = cached.embed_query("x").await.unwrap();
assert_eq!(single, batch[0], "cache hit returns the same vector");
assert_eq!(cached.inner().calls(), 2, "no extra delegate call");
}
#[tokio::test]
async fn warm_batch_hits_cache_entirely() {
let cached = CachingEmbeddings::wrap(CountingEmbeddings::new(4));
cached.embed_documents(&["a", "b"]).await.unwrap();
let again = cached.embed_documents(&["a", "b", "a"]).await.unwrap();
assert_eq!(cached.inner().calls(), 2, "warm batch recomputed nothing");
assert_eq!(again[0], again[2]);
assert_ne!(again[0], again[1]);
}
#[tokio::test]
async fn caches_query_by_text() {
let inner = MockEmbeddings::new(8);
let cached = CachingEmbeddings::wrap(inner);
let first = cached.embed_query("same text").await.unwrap();
let second = cached.embed_query("same text").await.unwrap();
assert_eq!(first, second);
assert_eq!(cached.cached_len(), 1, "one distinct text cached");
assert_eq!(cached.embed_query("another").await.unwrap().len(), 8);
assert_eq!(cached.cached_len(), 2);
}
#[tokio::test]
async fn dedups_batch_keeping_order() {
let cached = CachingEmbeddings::wrap(MockEmbeddings::new(4));
let results = cached
.embed_documents(&["a", "b", "a", "c", "b"])
.await
.unwrap();
assert_eq!(results.len(), 5);
assert_eq!(results[0], results[2], "a repeats map to same vector");
assert_eq!(results[1], results[4], "b repeats map to same vector");
assert_ne!(results[0], results[1]);
assert_eq!(cached.cached_len(), 3, "three distinct texts computed");
}
#[tokio::test]
async fn preserves_empty_contract() {
let cached = CachingEmbeddings::wrap(MockEmbeddings::new(4));
assert!(cached.embed_documents(&[""]).await.is_err());
assert!(cached.embed_documents(&["ok", " "]).await.is_err());
assert_eq!(cached.embed_documents(&[]).await.unwrap().len(), 0);
}
#[tokio::test]
async fn capacity_bounds_cache() {
let cached = CachingEmbeddings::new(MockEmbeddings::new(2), 3);
for i in 0..3 {
cached.embed_query(&format!("t{i}")).await.unwrap();
}
assert_eq!(cached.cached_len(), 3);
cached.embed_query("t3").await.unwrap();
assert!(cached.cached_len() <= 2);
cached.clear_cache();
assert_eq!(cached.cached_len(), 0);
}
#[tokio::test]
async fn delegates_metadata() {
let cached = CachingEmbeddings::wrap(MockEmbeddings::new(128));
assert_eq!(cached.dimension(), 128);
assert_eq!(cached.model_name(), "mock-embeddings");
assert_eq!(
CachingEmbeddings::new(MockEmbeddings::new(1), 0).capacity(),
4096
);
}
}