use crate::error::Result;
use std::collections::HashMap;
use super::documents::Document;
pub struct RetrievalCache {
cache: HashMap<String, CachedResult>,
max_size: usize,
current_size: usize,
next_seq: u64,
}
#[derive(Debug, Clone)]
struct CachedResult {
results: Vec<Document>,
timestamp: std::time::SystemTime,
access_count: usize,
seq: u64,
}
impl RetrievalCache {
pub fn new(max_size: usize) -> Self {
Self {
cache: HashMap::new(),
max_size,
current_size: 0,
next_seq: 0,
}
}
fn hash_query(&self, query: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(query.as_bytes());
hex::encode(hasher.finalize())
}
pub fn cache_results(&mut self, query: &str, results: Vec<Document>) -> Result<()> {
let query_hash = self.hash_query(query);
let result_size = results.iter().map(|d| d.content.len()).sum::<usize>();
while self.current_size + result_size > self.max_size && !self.cache.is_empty() {
self.evict_lru();
}
if result_size <= self.max_size {
let seq = self.next_seq;
self.next_seq += 1;
self.cache.insert(
query_hash,
CachedResult {
results,
timestamp: std::time::SystemTime::now(),
access_count: 0,
seq,
},
);
self.current_size += result_size;
}
Ok(())
}
pub fn get_cached(&mut self, query: &str) -> Option<Vec<Document>> {
let query_hash = self.hash_query(query);
if let Some(cached) = self.cache.get_mut(&query_hash) {
cached.access_count += 1;
Some(cached.results.clone())
} else {
None
}
}
fn evict_lru(&mut self) {
if let Some((key_to_remove, size)) = self
.cache
.iter()
.min_by(|(_, a), (_, b)| {
a.access_count
.cmp(&b.access_count)
.then_with(|| a.timestamp.cmp(&b.timestamp))
.then_with(|| a.seq.cmp(&b.seq))
})
.map(|(k, v)| {
let size = v.results.iter().map(|d| d.content.len()).sum::<usize>();
(k.clone(), size)
})
{
self.cache.remove(&key_to_remove);
self.current_size = self.current_size.saturating_sub(size);
}
}
pub fn clear(&mut self) {
self.cache.clear();
self.current_size = 0;
}
pub fn stats(&self) -> CacheStats {
CacheStats {
entries: self.cache.len(),
size_bytes: self.current_size,
max_size_bytes: self.max_size,
hit_rate: 0.0, }
}
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub entries: usize,
pub size_bytes: usize,
pub max_size_bytes: usize,
pub hit_rate: f64,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rag::documents::Document;
fn make_doc(id: &str, content: &str) -> Document {
Document {
id: id.to_string(),
content: content.to_string(),
metadata: HashMap::new(),
embedding: None,
chunk_info: None,
}
}
#[test]
fn test_cache_store_and_retrieve() {
let mut cache = RetrievalCache::new(10_000);
let docs = vec![make_doc("d1", "hello world")];
cache.cache_results("query1", docs.clone()).unwrap();
let cached = cache.get_cached("query1");
assert!(cached.is_some());
assert_eq!(cached.unwrap().len(), 1);
assert!(cache.get_cached("nonexistent").is_none());
}
#[test]
fn test_cache_eviction() {
let mut cache = RetrievalCache::new(20);
let doc1 = make_doc("d1", "twelve chars"); cache.cache_results("q1", vec![doc1]).unwrap();
let doc2 = make_doc("d2", "another twelve"); cache.cache_results("q2", vec![doc2]).unwrap();
assert!(cache.get_cached("q1").is_none());
assert!(cache.get_cached("q2").is_some());
}
#[test]
fn test_cache_clear() {
let mut cache = RetrievalCache::new(10_000);
cache
.cache_results("q1", vec![make_doc("d1", "x")])
.unwrap();
cache.clear();
assert!(cache.get_cached("q1").is_none());
}
#[test]
fn test_cache_stats() {
let mut cache = RetrievalCache::new(1000);
cache
.cache_results("q1", vec![make_doc("d1", "data")])
.unwrap();
let stats = cache.stats();
assert_eq!(stats.entries, 1);
assert_eq!(stats.max_size_bytes, 1000);
assert!(stats.size_bytes > 0);
}
#[test]
fn test_cache_oversized_item_rejected() {
let mut cache = RetrievalCache::new(5);
cache
.cache_results("q", vec![make_doc("d", "0123456789")])
.unwrap();
assert!(cache.get_cached("q").is_none());
}
#[test]
fn test_evict_lru_tiebreaker_by_timestamp() {
use crate::rag::documents::Document;
use std::collections::HashMap;
let mut cache = RetrievalCache::new(8);
let doc1 = Document {
id: "doc1".to_string(),
content: "aaaa".to_string(), metadata: HashMap::new(),
embedding: None,
chunk_info: None,
};
let doc2 = Document {
id: "doc2".to_string(),
content: "bbbb".to_string(), metadata: HashMap::new(),
embedding: None,
chunk_info: None,
};
let doc3 = Document {
id: "doc3".to_string(),
content: "cccc".to_string(), metadata: HashMap::new(),
embedding: None,
chunk_info: None,
};
let _ = cache.cache_results("query1", vec![doc1]);
std::thread::sleep(std::time::Duration::from_millis(10));
let _ = cache.cache_results("query2", vec![doc2]);
std::thread::sleep(std::time::Duration::from_millis(10));
let _ = cache.cache_results("query3", vec![doc3]);
assert!(
cache.get_cached("query1").is_none(),
"Oldest entry should be evicted"
);
assert!(cache.get_cached("query3").is_some());
}
}