ai_model_vault/rag/
cache.rs1use crate::error::Result;
4use std::collections::HashMap;
5
6use super::documents::Document;
7
8pub struct RetrievalCache {
10 cache: HashMap<String, CachedResult>,
11 max_size: usize,
12 current_size: usize,
13 next_seq: u64,
15}
16
17#[derive(Debug, Clone)]
18struct CachedResult {
19 results: Vec<Document>,
20 timestamp: std::time::SystemTime,
21 access_count: usize,
22 seq: u64,
28}
29
30impl RetrievalCache {
31 pub fn new(max_size: usize) -> Self {
33 Self {
34 cache: HashMap::new(),
35 max_size,
36 current_size: 0,
37 next_seq: 0,
38 }
39 }
40
41 fn hash_query(&self, query: &str) -> String {
43 use sha2::{Digest, Sha256};
44 let mut hasher = Sha256::new();
45 hasher.update(query.as_bytes());
46 hex::encode(hasher.finalize())
47 }
48
49 pub fn cache_results(&mut self, query: &str, results: Vec<Document>) -> Result<()> {
51 let query_hash = self.hash_query(query);
52
53 let result_size = results.iter().map(|d| d.content.len()).sum::<usize>();
54
55 while self.current_size + result_size > self.max_size && !self.cache.is_empty() {
57 self.evict_lru();
58 }
59
60 if result_size <= self.max_size {
61 let seq = self.next_seq;
62 self.next_seq += 1;
63 self.cache.insert(
64 query_hash,
65 CachedResult {
66 results,
67 timestamp: std::time::SystemTime::now(),
68 access_count: 0,
69 seq,
70 },
71 );
72 self.current_size += result_size;
73 }
74
75 Ok(())
76 }
77
78 pub fn get_cached(&mut self, query: &str) -> Option<Vec<Document>> {
80 let query_hash = self.hash_query(query);
81
82 if let Some(cached) = self.cache.get_mut(&query_hash) {
83 cached.access_count += 1;
84 Some(cached.results.clone())
85 } else {
86 None
87 }
88 }
89
90 fn evict_lru(&mut self) {
93 if let Some((key_to_remove, size)) = self
94 .cache
95 .iter()
96 .min_by(|(_, a), (_, b)| {
97 a.access_count
98 .cmp(&b.access_count)
99 .then_with(|| a.timestamp.cmp(&b.timestamp))
100 .then_with(|| a.seq.cmp(&b.seq))
101 })
102 .map(|(k, v)| {
103 let size = v.results.iter().map(|d| d.content.len()).sum::<usize>();
104 (k.clone(), size)
105 })
106 {
107 self.cache.remove(&key_to_remove);
108 self.current_size = self.current_size.saturating_sub(size);
109 }
110 }
111
112 pub fn clear(&mut self) {
114 self.cache.clear();
115 self.current_size = 0;
116 }
117
118 pub fn stats(&self) -> CacheStats {
120 CacheStats {
121 entries: self.cache.len(),
122 size_bytes: self.current_size,
123 max_size_bytes: self.max_size,
124 hit_rate: 0.0, }
126 }
127}
128
129#[derive(Debug, Clone)]
131pub struct CacheStats {
132 pub entries: usize,
133 pub size_bytes: usize,
134 pub max_size_bytes: usize,
135 pub hit_rate: f64,
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::rag::documents::Document;
142
143 fn make_doc(id: &str, content: &str) -> Document {
144 Document {
145 id: id.to_string(),
146 content: content.to_string(),
147 metadata: HashMap::new(),
148 embedding: None,
149 chunk_info: None,
150 }
151 }
152
153 #[test]
154 fn test_cache_store_and_retrieve() {
155 let mut cache = RetrievalCache::new(10_000);
156 let docs = vec![make_doc("d1", "hello world")];
157 cache.cache_results("query1", docs.clone()).unwrap();
158
159 let cached = cache.get_cached("query1");
160 assert!(cached.is_some());
161 assert_eq!(cached.unwrap().len(), 1);
162
163 assert!(cache.get_cached("nonexistent").is_none());
165 }
166
167 #[test]
168 fn test_cache_eviction() {
169 let mut cache = RetrievalCache::new(20);
171 let doc1 = make_doc("d1", "twelve chars"); cache.cache_results("q1", vec![doc1]).unwrap();
173
174 let doc2 = make_doc("d2", "another twelve"); cache.cache_results("q2", vec![doc2]).unwrap();
177
178 assert!(cache.get_cached("q1").is_none());
180 assert!(cache.get_cached("q2").is_some());
181 }
182
183 #[test]
184 fn test_cache_clear() {
185 let mut cache = RetrievalCache::new(10_000);
186 cache
187 .cache_results("q1", vec![make_doc("d1", "x")])
188 .unwrap();
189 cache.clear();
190 assert!(cache.get_cached("q1").is_none());
191 }
192
193 #[test]
194 fn test_cache_stats() {
195 let mut cache = RetrievalCache::new(1000);
196 cache
197 .cache_results("q1", vec![make_doc("d1", "data")])
198 .unwrap();
199 let stats = cache.stats();
200 assert_eq!(stats.entries, 1);
201 assert_eq!(stats.max_size_bytes, 1000);
202 assert!(stats.size_bytes > 0);
203 }
204
205 #[test]
206 fn test_cache_oversized_item_rejected() {
207 let mut cache = RetrievalCache::new(5);
209 cache
210 .cache_results("q", vec![make_doc("d", "0123456789")])
211 .unwrap();
212 assert!(cache.get_cached("q").is_none());
214 }
215
216 #[test]
217 fn test_evict_lru_tiebreaker_by_timestamp() {
218 use crate::rag::documents::Document;
220 use std::collections::HashMap;
221
222 let mut cache = RetrievalCache::new(8);
224
225 let doc1 = Document {
227 id: "doc1".to_string(),
228 content: "aaaa".to_string(), metadata: HashMap::new(),
230 embedding: None,
231 chunk_info: None,
232 };
233 let doc2 = Document {
234 id: "doc2".to_string(),
235 content: "bbbb".to_string(), metadata: HashMap::new(),
237 embedding: None,
238 chunk_info: None,
239 };
240 let doc3 = Document {
241 id: "doc3".to_string(),
242 content: "cccc".to_string(), metadata: HashMap::new(),
244 embedding: None,
245 chunk_info: None,
246 };
247
248 let _ = cache.cache_results("query1", vec![doc1]);
250 std::thread::sleep(std::time::Duration::from_millis(10));
252 let _ = cache.cache_results("query2", vec![doc2]);
253 std::thread::sleep(std::time::Duration::from_millis(10));
254
255 let _ = cache.cache_results("query3", vec![doc3]);
257
258 assert!(
260 cache.get_cached("query1").is_none(),
261 "Oldest entry should be evicted"
262 );
263 assert!(cache.get_cached("query3").is_some());
265 }
266}