1use std::collections::{HashMap, VecDeque};
6use std::time::{Duration, Instant};
7
8pub struct QueryPlanCache {
10 cache: HashMap<u64, CachedPlan>,
12 order: VecDeque<u64>,
14 capacity: usize,
16 hits: u64,
18 misses: u64,
20 ttl: Duration,
22}
23
24#[derive(Debug, Clone)]
26pub struct CachedPlan {
27 pub terms: Vec<String>,
29 pub term_weights: Vec<f32>,
31 pub candidate_docs: Vec<u32>,
33 pub component_boosts: Vec<(String, f32)>,
35 pub created_at: Instant,
37}
38
39impl QueryPlanCache {
40 pub fn new(capacity: usize) -> Self {
42 let cap = if capacity == 0 { 1000 } else { capacity };
43 Self {
44 cache: HashMap::with_capacity(cap),
45 order: VecDeque::with_capacity(cap),
46 capacity: cap,
47 hits: 0,
48 misses: 0,
49 ttl: Duration::from_secs(300), }
51 }
52
53 pub fn with_ttl(capacity: usize, ttl: Duration) -> Self {
55 let mut cache = Self::new(capacity);
56 cache.ttl = ttl;
57 cache
58 }
59
60 fn hash_query(&self, query: &str) -> u64 {
62 use std::collections::hash_map::DefaultHasher;
63 use std::hash::{Hash, Hasher};
64
65 let mut hasher = DefaultHasher::new();
66 query.to_lowercase().hash(&mut hasher);
67 hasher.finish()
68 }
69
70 fn touch(&mut self, hash: u64) {
72 self.order.retain(|&h| h != hash);
74 self.order.push_front(hash);
76 }
77
78 fn evict_if_needed(&mut self) {
80 while self.order.len() > self.capacity {
81 if let Some(old_hash) = self.order.pop_back() {
82 self.cache.remove(&old_hash);
83 }
84 }
85 }
86
87 pub fn get(&mut self, query: &str) -> Option<&CachedPlan> {
89 let hash = self.hash_query(query);
90
91 if let Some(plan) = self.cache.get(&hash) {
92 if plan.created_at.elapsed() < self.ttl {
94 self.hits += 1;
95 self.touch(hash);
96 return self.cache.get(&hash);
98 }
99 self.misses += 1;
101 return None;
102 }
103
104 self.misses += 1;
105 None
106 }
107
108 pub fn get_clone(&mut self, query: &str) -> Option<CachedPlan> {
110 let hash = self.hash_query(query);
111
112 if let Some(plan) = self.cache.get(&hash) {
113 if plan.created_at.elapsed() < self.ttl {
115 self.hits += 1;
116 self.touch(hash);
117 return self.cache.get(&hash).cloned();
118 }
119 self.misses += 1;
120 return None;
121 }
122
123 self.misses += 1;
124 None
125 }
126
127 pub fn put(&mut self, query: &str, plan: CachedPlan) {
129 let hash = self.hash_query(query);
130 self.cache.insert(hash, plan);
131 self.touch(hash);
132 self.evict_if_needed();
133 }
134
135 pub fn create_plan(
137 &mut self,
138 query: &str,
139 terms: Vec<String>,
140 term_weights: Vec<f32>,
141 candidate_docs: Vec<u32>,
142 component_boosts: Vec<(String, f32)>,
143 ) -> &CachedPlan {
144 let plan = CachedPlan {
145 terms,
146 term_weights,
147 candidate_docs,
148 component_boosts,
149 created_at: crate::timing::start_timer(),
150 };
151
152 let hash = self.hash_query(query);
153 self.cache.insert(hash, plan);
154 self.touch(hash);
155 self.evict_if_needed();
156 self.cache.get(&hash).expect("freshly inserted cache entry must exist after LRU eviction")
159 }
160
161 pub fn clear(&mut self) {
163 self.cache.clear();
164 self.order.clear();
165 }
166
167 pub fn stats(&self) -> CacheStats {
169 let total = self.hits + self.misses;
170 CacheStats {
171 hits: self.hits,
172 misses: self.misses,
173 hit_rate: if total > 0 { self.hits as f64 / total as f64 } else { 0.0 },
174 size: self.cache.len(),
175 capacity: self.capacity,
176 }
177 }
178
179 pub fn reset_stats(&mut self) {
181 self.hits = 0;
182 self.misses = 0;
183 }
184}
185
186impl Default for QueryPlanCache {
187 fn default() -> Self {
188 Self::new(1000)
189 }
190}
191
192#[derive(Debug, Clone, Copy)]
194pub struct CacheStats {
195 pub hits: u64,
196 pub misses: u64,
197 pub hit_rate: f64,
198 pub size: usize,
199 pub capacity: usize,
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 fn test_plan(
208 terms: Vec<&str>,
209 weights: Vec<f32>,
210 docs: Vec<u32>,
211 boosts: Vec<(&str, f32)>,
212 ) -> CachedPlan {
213 CachedPlan {
214 terms: terms.into_iter().map(String::from).collect(),
215 term_weights: weights,
216 candidate_docs: docs,
217 component_boosts: boosts.into_iter().map(|(s, v)| (s.to_string(), v)).collect(),
218 created_at: crate::timing::start_timer(),
219 }
220 }
221
222 #[test]
223 fn test_cache_creation() {
224 let cache = QueryPlanCache::new(100);
225 assert_eq!(cache.stats().capacity, 100);
226 }
227
228 #[test]
229 fn test_cache_put_get() {
230 let mut cache = QueryPlanCache::new(100);
231
232 let plan =
233 test_plan(vec!["hello", "world"], vec![1.0, 1.0], vec![1, 2, 3], vec![("trueno", 1.5)]);
234
235 cache.put("hello world", plan);
236
237 let retrieved = cache.get("hello world");
238 assert!(retrieved.is_some());
239 assert_eq!(retrieved.expect("unexpected failure").terms.len(), 2);
240 }
241
242 #[test]
243 fn test_cache_hit_miss() {
244 let mut cache = QueryPlanCache::new(100);
245
246 let _ = cache.get("query1");
248 assert_eq!(cache.stats().misses, 1);
249
250 cache.create_plan("query1", vec![], vec![], vec![], vec![]);
252 let _ = cache.get("query1");
253 assert_eq!(cache.stats().hits, 1);
254 }
255
256 #[test]
257 fn test_cache_case_insensitive() {
258 let mut cache = QueryPlanCache::new(100);
259
260 cache.create_plan("Hello World", vec![], vec![], vec![], vec![]);
261
262 assert!(cache.get("hello world").is_some());
263 assert!(cache.get("HELLO WORLD").is_some());
264 }
265
266 #[test]
267 fn test_cache_ttl() {
268 let mut cache = QueryPlanCache::with_ttl(100, Duration::from_millis(1));
269
270 cache.create_plan("query", vec![], vec![], vec![], vec![]);
271
272 assert!(cache.get("query").is_some());
274
275 std::thread::sleep(Duration::from_millis(10));
277 assert!(cache.get("query").is_none());
278 }
279
280 #[test]
281 fn test_cache_lru_eviction() {
282 let mut cache = QueryPlanCache::new(3);
283
284 cache.create_plan("query1", vec![], vec![], vec![], vec![]);
286 cache.create_plan("query2", vec![], vec![], vec![], vec![]);
287 cache.create_plan("query3", vec![], vec![], vec![], vec![]);
288
289 assert_eq!(cache.stats().size, 3);
290
291 cache.create_plan("query4", vec![], vec![], vec![], vec![]);
293
294 assert_eq!(cache.stats().size, 3);
295 assert!(cache.get_clone("query1").is_none()); assert!(cache.get_clone("query2").is_some());
297 assert!(cache.get_clone("query3").is_some());
298 assert!(cache.get_clone("query4").is_some());
299 }
300
301 #[test]
302 fn test_cache_lru_touch() {
303 let mut cache = QueryPlanCache::new(3);
304
305 cache.create_plan("query1", vec![], vec![], vec![], vec![]);
307 cache.create_plan("query2", vec![], vec![], vec![], vec![]);
308 cache.create_plan("query3", vec![], vec![], vec![], vec![]);
309
310 let _ = cache.get("query1");
312
313 cache.create_plan("query4", vec![], vec![], vec![], vec![]);
315
316 assert!(cache.get_clone("query1").is_some()); assert!(cache.get_clone("query2").is_none()); }
319
320 #[test]
321 fn test_cache_clear() {
322 let mut cache = QueryPlanCache::new(100);
323 cache.create_plan("query1", vec![], vec![], vec![], vec![]);
324 cache.create_plan("query2", vec![], vec![], vec![], vec![]);
325
326 assert_eq!(cache.stats().size, 2);
327
328 cache.clear();
329
330 assert_eq!(cache.stats().size, 0);
331 }
332
333 #[test]
334 fn test_cache_stats_reset() {
335 let mut cache = QueryPlanCache::new(100);
336 cache.create_plan("query", vec![], vec![], vec![], vec![]);
337 let _ = cache.get("query"); let _ = cache.get("nonexistent"); let stats = cache.stats();
341 assert_eq!(stats.hits, 1);
342 assert_eq!(stats.misses, 1);
343
344 cache.reset_stats();
345
346 let stats = cache.stats();
347 assert_eq!(stats.hits, 0);
348 assert_eq!(stats.misses, 0);
349 }
350
351 #[test]
352 fn test_cache_hit_rate() {
353 let mut cache = QueryPlanCache::new(100);
354 cache.create_plan("query", vec![], vec![], vec![], vec![]);
355
356 let _ = cache.get("query");
358 let _ = cache.get("query");
359 let _ = cache.get("nonexistent");
361
362 let stats = cache.stats();
363 assert!((stats.hit_rate - 0.666).abs() < 0.01);
364 }
365
366 #[test]
367 fn test_cache_default() {
368 let cache = QueryPlanCache::default();
369 assert_eq!(cache.stats().capacity, 1000);
370 }
371
372 #[test]
373 fn test_cache_zero_capacity() {
374 let cache = QueryPlanCache::new(0);
376 assert_eq!(cache.stats().capacity, 1000);
377 }
378
379 #[test]
380 fn test_cached_plan_fields() {
381 let plan = test_plan(vec!["test"], vec![0.5], vec![1, 2, 3], vec![("boost", 1.2)]);
382 assert_eq!(plan.terms.len(), 1);
383 assert_eq!(plan.term_weights.len(), 1);
384 assert_eq!(plan.candidate_docs.len(), 3);
385 assert_eq!(plan.component_boosts.len(), 1);
386 }
387
388 #[test]
389 fn test_cache_stats_fields() {
390 let stats = CacheStats { hits: 10, misses: 5, hit_rate: 0.666, size: 100, capacity: 1000 };
391 assert_eq!(stats.hits, 10);
392 assert_eq!(stats.misses, 5);
393 assert_eq!(stats.size, 100);
394 assert_eq!(stats.capacity, 1000);
395 }
396
397 #[test]
398 fn test_get_clone_returns_owned() {
399 let mut cache = QueryPlanCache::new(100);
400 cache.create_plan("query", vec!["term".to_string()], vec![1.0], vec![1], vec![]);
401
402 let cloned = cache.get_clone("query");
403 assert!(cloned.is_some());
404 let plan = cloned.expect("unexpected failure");
405 assert_eq!(plan.terms, vec!["term".to_string()]);
406 }
407
408 #[test]
409 fn test_get_clone_miss() {
410 let mut cache = QueryPlanCache::new(100);
411 let result = cache.get_clone("nonexistent");
412 assert!(result.is_none());
413 }
414
415 #[test]
416 fn test_get_clone_expired() {
417 let mut cache = QueryPlanCache::with_ttl(100, Duration::from_millis(1));
418 cache.create_plan("query", vec![], vec![], vec![], vec![]);
419
420 std::thread::sleep(Duration::from_millis(10));
421 let result = cache.get_clone("query");
422 assert!(result.is_none());
423 }
424
425 #[test]
426 fn test_put_replaces_existing() {
427 let mut cache = QueryPlanCache::new(100);
428
429 cache.put("query", test_plan(vec!["old"], vec![], vec![], vec![]));
430 cache.put("query", test_plan(vec!["new"], vec![], vec![], vec![]));
431
432 let retrieved = cache.get("query").expect("key not found");
433 assert_eq!(retrieved.terms, vec!["new".to_string()]);
434 }
435
436 #[test]
437 fn test_hit_rate_no_accesses() {
438 let cache = QueryPlanCache::new(100);
439 let stats = cache.stats();
440 assert_eq!(stats.hit_rate, 0.0);
441 }
442
443 #[test]
444 fn test_create_plan_returns_reference() {
445 let mut cache = QueryPlanCache::new(100);
446 let plan = cache.create_plan(
447 "query",
448 vec!["term".to_string()],
449 vec![1.0, 2.0],
450 vec![1, 2, 3],
451 vec![("boost".to_string(), 1.5)],
452 );
453 assert_eq!(plan.terms.len(), 1);
454 assert_eq!(plan.term_weights.len(), 2);
455 assert_eq!(plan.candidate_docs.len(), 3);
456 }
457
458 #[test]
459 fn test_with_ttl_custom_duration() {
460 let cache = QueryPlanCache::with_ttl(50, Duration::from_secs(60));
461 assert_eq!(cache.stats().capacity, 50);
462 }
463
464 #[test]
465 fn test_cached_plan_clone() {
466 let plan = test_plan(vec!["a", "b"], vec![1.0, 2.0], vec![10, 20], vec![]);
467 let cloned = plan.clone();
468 assert_eq!(cloned.terms, plan.terms);
469 assert_eq!(cloned.candidate_docs, plan.candidate_docs);
470 }
471}