Skip to main content

oxirs_vec/
vector_cache.rs

1//! Vector embedding cache with LRU eviction.
2//!
3//! Provides an in-memory cache for embedding vectors with:
4//! - LRU eviction (configurable max entries)
5//! - Cache warming (preload frequently accessed vectors)
6//! - Hit/miss statistics tracking
7//! - Memory-bounded eviction
8//! - Cache invalidation (by key, by prefix, clear all)
9//! - Optional per-entry TTL-based expiry
10//! - Batch get/put operations
11//! - Cache persistence (save/load)
12
13use std::collections::{HashMap, VecDeque};
14use std::time::{Duration, Instant};
15
16// ── Public types ─────────────────────────────────────────────────────────────
17
18/// Configuration for the vector cache.
19#[derive(Debug, Clone)]
20pub struct VectorCacheConfig {
21    /// Maximum number of entries before LRU eviction (0 = unlimited).
22    pub max_entries: usize,
23    /// Maximum memory in bytes before eviction (0 = unlimited).
24    pub max_memory_bytes: usize,
25    /// Default TTL for entries. `None` = entries never expire.
26    pub default_ttl: Option<Duration>,
27}
28
29impl Default for VectorCacheConfig {
30    fn default() -> Self {
31        Self {
32            max_entries: 10_000,
33            max_memory_bytes: 0,
34            default_ttl: None,
35        }
36    }
37}
38
39/// Hit/miss statistics for the cache.
40#[derive(Debug, Clone, Default)]
41pub struct CacheStatistics {
42    /// Total number of cache hits.
43    pub hits: u64,
44    /// Total number of cache misses.
45    pub misses: u64,
46    /// Total number of entries inserted.
47    pub inserts: u64,
48    /// Total number of entries evicted (LRU or memory).
49    pub evictions: u64,
50    /// Total number of entries expired (TTL).
51    pub expirations: u64,
52    /// Total number of explicit invalidations.
53    pub invalidations: u64,
54}
55
56impl CacheStatistics {
57    /// Hit ratio in [0, 1].  Returns 0.0 if no requests have been made.
58    pub fn hit_ratio(&self) -> f64 {
59        let total = self.hits + self.misses;
60        if total == 0 {
61            return 0.0;
62        }
63        self.hits as f64 / total as f64
64    }
65
66    /// Total number of get requests.
67    pub fn total_requests(&self) -> u64 {
68        self.hits + self.misses
69    }
70}
71
72/// A single cached vector entry.
73#[derive(Debug, Clone)]
74struct CacheEntry {
75    /// The cached vector.
76    vector: Vec<f64>,
77    /// When this entry was inserted.
78    inserted_at: Instant,
79    /// When this entry was last accessed.
80    last_accessed: Instant,
81    /// Per-entry TTL override.  `None` uses the cache default.
82    ttl: Option<Duration>,
83}
84
85impl CacheEntry {
86    fn memory_bytes(&self) -> usize {
87        // Approximate: Vec overhead + data + struct overhead
88        std::mem::size_of::<Self>() + self.vector.len() * std::mem::size_of::<f64>()
89    }
90
91    fn is_expired(&self, default_ttl: Option<Duration>) -> bool {
92        let ttl = self.ttl.or(default_ttl);
93        if let Some(duration) = ttl {
94            self.inserted_at.elapsed() > duration
95        } else {
96            false
97        }
98    }
99}
100
101/// A serializable representation of the cache for persistence.
102#[derive(Debug, Clone)]
103pub struct CacheSnapshot {
104    /// Key → vector pairs.
105    pub entries: Vec<(String, Vec<f64>)>,
106    /// Total entries at snapshot time.
107    pub entry_count: usize,
108}
109
110// ── VectorCache ──────────────────────────────────────────────────────────────
111
112/// An LRU cache for embedding vectors.
113pub struct VectorCache {
114    /// The actual storage: key → entry.
115    store: HashMap<String, CacheEntry>,
116    /// LRU order: front = least recently used, back = most recently used.
117    lru_order: VecDeque<String>,
118    /// Configuration.
119    config: VectorCacheConfig,
120    /// Running statistics.
121    stats: CacheStatistics,
122}
123
124impl VectorCache {
125    /// Create a new cache with default configuration.
126    pub fn new() -> Self {
127        Self::with_config(VectorCacheConfig::default())
128    }
129
130    /// Create a new cache with custom configuration.
131    pub fn with_config(config: VectorCacheConfig) -> Self {
132        Self {
133            store: HashMap::new(),
134            lru_order: VecDeque::new(),
135            config,
136            stats: CacheStatistics::default(),
137        }
138    }
139
140    // ── Get / Put ────────────────────────────────────────────────────────────
141
142    /// Get a cached vector by key.
143    ///
144    /// Returns `None` on miss.  Expired entries are evicted on access.
145    pub fn get(&mut self, key: &str) -> Option<Vec<f64>> {
146        // Check if key exists.
147        if let Some(entry) = self.store.get(key) {
148            if entry.is_expired(self.config.default_ttl) {
149                // Expired — remove and count as miss.
150                let key_owned = key.to_string();
151                self.store.remove(&key_owned);
152                self.lru_order.retain(|k| k != &key_owned);
153                self.stats.expirations += 1;
154                self.stats.misses += 1;
155                return None;
156            }
157        } else {
158            self.stats.misses += 1;
159            return None;
160        }
161
162        // Hit — update LRU position.
163        self.touch(key);
164        self.stats.hits += 1;
165
166        // Update last_accessed.
167        if let Some(entry) = self.store.get_mut(key) {
168            entry.last_accessed = Instant::now();
169            Some(entry.vector.clone())
170        } else {
171            None
172        }
173    }
174
175    /// Insert a vector into the cache with the default TTL.
176    pub fn put(&mut self, key: impl Into<String>, vector: Vec<f64>) {
177        self.put_with_ttl(key, vector, None);
178    }
179
180    /// Insert a vector with an explicit per-entry TTL.
181    pub fn put_with_ttl(
182        &mut self,
183        key: impl Into<String>,
184        vector: Vec<f64>,
185        ttl: Option<Duration>,
186    ) {
187        let key = key.into();
188        let now = Instant::now();
189
190        let entry = CacheEntry {
191            vector,
192            inserted_at: now,
193            last_accessed: now,
194            ttl,
195        };
196
197        // If the key already exists, remove old LRU entry.
198        if self.store.contains_key(&key) {
199            self.lru_order.retain(|k| k != &key);
200        }
201
202        self.store.insert(key.clone(), entry);
203        self.lru_order.push_back(key);
204        self.stats.inserts += 1;
205
206        // Enforce limits.
207        self.enforce_entry_limit();
208        self.enforce_memory_limit();
209    }
210
211    // ── Batch operations ─────────────────────────────────────────────────────
212
213    /// Batch get: returns a map of key → vector for found entries.
214    pub fn batch_get(&mut self, keys: &[&str]) -> HashMap<String, Vec<f64>> {
215        let mut result = HashMap::new();
216        for &key in keys {
217            if let Some(vec) = self.get(key) {
218                result.insert(key.to_string(), vec);
219            }
220        }
221        result
222    }
223
224    /// Batch put: insert multiple entries at once.
225    pub fn batch_put(&mut self, entries: Vec<(String, Vec<f64>)>) {
226        for (key, vector) in entries {
227            self.put(key, vector);
228        }
229    }
230
231    // ── Cache warming ────────────────────────────────────────────────────────
232
233    /// Warm the cache by preloading the given key-vector pairs.
234    ///
235    /// Existing entries are not overwritten.
236    pub fn warm(&mut self, entries: Vec<(String, Vec<f64>)>) -> usize {
237        let mut loaded = 0;
238        for (key, vector) in entries {
239            if !self.store.contains_key(&key) {
240                self.put(key, vector);
241                loaded += 1;
242            }
243        }
244        loaded
245    }
246
247    // ── Invalidation ─────────────────────────────────────────────────────────
248
249    /// Remove a specific key from the cache.
250    pub fn invalidate(&mut self, key: &str) -> bool {
251        if self.store.remove(key).is_some() {
252            self.lru_order.retain(|k| k != key);
253            self.stats.invalidations += 1;
254            true
255        } else {
256            false
257        }
258    }
259
260    /// Remove all keys matching a given prefix.
261    pub fn invalidate_prefix(&mut self, prefix: &str) -> usize {
262        let keys_to_remove: Vec<String> = self
263            .store
264            .keys()
265            .filter(|k| k.starts_with(prefix))
266            .cloned()
267            .collect();
268
269        let count = keys_to_remove.len();
270        for key in &keys_to_remove {
271            self.store.remove(key);
272        }
273        self.lru_order.retain(|k| !k.starts_with(prefix));
274        self.stats.invalidations += count as u64;
275        count
276    }
277
278    /// Clear all entries.
279    pub fn clear(&mut self) {
280        let count = self.store.len() as u64;
281        self.store.clear();
282        self.lru_order.clear();
283        self.stats.invalidations += count;
284    }
285
286    // ── Statistics ───────────────────────────────────────────────────────────
287
288    /// Current cache statistics.
289    pub fn statistics(&self) -> &CacheStatistics {
290        &self.stats
291    }
292
293    /// Reset statistics counters to zero.
294    pub fn reset_statistics(&mut self) {
295        self.stats = CacheStatistics::default();
296    }
297
298    /// Number of entries currently in the cache.
299    pub fn len(&self) -> usize {
300        self.store.len()
301    }
302
303    /// Is the cache empty?
304    pub fn is_empty(&self) -> bool {
305        self.store.is_empty()
306    }
307
308    /// Approximate total memory usage in bytes.
309    pub fn memory_usage(&self) -> usize {
310        self.store.values().map(|e| e.memory_bytes()).sum::<usize>()
311            + self.lru_order.len() * std::mem::size_of::<String>()
312    }
313
314    /// Check if a key exists in the cache (without affecting LRU order).
315    pub fn contains_key(&self, key: &str) -> bool {
316        self.store.contains_key(key)
317    }
318
319    // ── Persistence ──────────────────────────────────────────────────────────
320
321    /// Create a snapshot of the current cache contents.
322    pub fn snapshot(&self) -> CacheSnapshot {
323        let entries: Vec<(String, Vec<f64>)> = self
324            .store
325            .iter()
326            .filter(|(_, e)| !e.is_expired(self.config.default_ttl))
327            .map(|(k, e)| (k.clone(), e.vector.clone()))
328            .collect();
329        let entry_count = entries.len();
330        CacheSnapshot {
331            entries,
332            entry_count,
333        }
334    }
335
336    /// Load entries from a snapshot (appending to the current cache).
337    pub fn load_snapshot(&mut self, snapshot: CacheSnapshot) -> usize {
338        let count = snapshot.entries.len();
339        for (key, vector) in snapshot.entries {
340            self.put(key, vector);
341        }
342        count
343    }
344
345    // ── Expiry sweep ─────────────────────────────────────────────────────────
346
347    /// Sweep expired entries from the cache.
348    ///
349    /// Returns the number of entries removed.
350    pub fn sweep_expired(&mut self) -> usize {
351        let expired_keys: Vec<String> = self
352            .store
353            .iter()
354            .filter(|(_, e)| e.is_expired(self.config.default_ttl))
355            .map(|(k, _)| k.clone())
356            .collect();
357
358        let count = expired_keys.len();
359        for key in &expired_keys {
360            self.store.remove(key);
361        }
362        self.lru_order.retain(|k| !expired_keys.contains(k));
363        self.stats.expirations += count as u64;
364        count
365    }
366
367    // ── Internal ─────────────────────────────────────────────────────────────
368
369    /// Move `key` to the back (most recently used) of the LRU deque.
370    fn touch(&mut self, key: &str) {
371        self.lru_order.retain(|k| k != key);
372        self.lru_order.push_back(key.to_string());
373    }
374
375    /// Evict oldest entries until entry count is within limits.
376    fn enforce_entry_limit(&mut self) {
377        if self.config.max_entries == 0 {
378            return;
379        }
380        while self.store.len() > self.config.max_entries {
381            if let Some(oldest) = self.lru_order.pop_front() {
382                self.store.remove(&oldest);
383                self.stats.evictions += 1;
384            } else {
385                break;
386            }
387        }
388    }
389
390    /// Evict oldest entries until memory usage is within limits.
391    fn enforce_memory_limit(&mut self) {
392        if self.config.max_memory_bytes == 0 {
393            return;
394        }
395        while self.memory_usage() > self.config.max_memory_bytes {
396            if let Some(oldest) = self.lru_order.pop_front() {
397                self.store.remove(&oldest);
398                self.stats.evictions += 1;
399            } else {
400                break;
401            }
402        }
403    }
404}
405
406impl Default for VectorCache {
407    fn default() -> Self {
408        Self::new()
409    }
410}
411
412// ═══════════════════════════════════════════════════════════════════════════════
413// Tests
414// ═══════════════════════════════════════════════════════════════════════════════
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419
420    fn vec3(a: f64, b: f64, c: f64) -> Vec<f64> {
421        vec![a, b, c]
422    }
423
424    // ── Basic get/put ────────────────────────────────────────────────────────
425
426    #[test]
427    fn test_put_and_get() {
428        let mut cache = VectorCache::new();
429        cache.put("k1", vec3(1.0, 2.0, 3.0));
430        let v = cache.get("k1");
431        assert!(v.is_some());
432        assert_eq!(v.expect("should exist"), vec3(1.0, 2.0, 3.0));
433    }
434
435    #[test]
436    fn test_get_miss() {
437        let mut cache = VectorCache::new();
438        assert!(cache.get("nonexistent").is_none());
439        assert_eq!(cache.statistics().misses, 1);
440    }
441
442    #[test]
443    fn test_put_overwrite() {
444        let mut cache = VectorCache::new();
445        cache.put("k1", vec3(1.0, 2.0, 3.0));
446        cache.put("k1", vec3(4.0, 5.0, 6.0));
447        let v = cache.get("k1");
448        assert_eq!(v.expect("should exist"), vec3(4.0, 5.0, 6.0));
449        assert_eq!(cache.len(), 1);
450    }
451
452    // ── LRU eviction ─────────────────────────────────────────────────────────
453
454    #[test]
455    fn test_lru_eviction() {
456        let config = VectorCacheConfig {
457            max_entries: 2,
458            ..Default::default()
459        };
460        let mut cache = VectorCache::with_config(config);
461        cache.put("a", vec3(1.0, 0.0, 0.0));
462        cache.put("b", vec3(0.0, 1.0, 0.0));
463        cache.put("c", vec3(0.0, 0.0, 1.0)); // evicts "a"
464
465        assert!(cache.get("a").is_none());
466        assert!(cache.get("b").is_some());
467        assert!(cache.get("c").is_some());
468        assert_eq!(cache.statistics().evictions, 1);
469    }
470
471    #[test]
472    fn test_lru_access_refreshes() {
473        let config = VectorCacheConfig {
474            max_entries: 2,
475            ..Default::default()
476        };
477        let mut cache = VectorCache::with_config(config);
478        cache.put("a", vec3(1.0, 0.0, 0.0));
479        cache.put("b", vec3(0.0, 1.0, 0.0));
480
481        // Access "a" to make it recently used.
482        let _ = cache.get("a");
483
484        cache.put("c", vec3(0.0, 0.0, 1.0)); // evicts "b" (LRU), not "a"
485
486        assert!(cache.get("a").is_some());
487        assert!(cache.get("b").is_none());
488        assert!(cache.get("c").is_some());
489    }
490
491    // ── Memory-bounded eviction ──────────────────────────────────────────────
492
493    #[test]
494    fn test_memory_limit_eviction() {
495        let entry_size = std::mem::size_of::<CacheEntry>() + 3 * std::mem::size_of::<f64>();
496        let config = VectorCacheConfig {
497            max_entries: 0,                         // unlimited entries
498            max_memory_bytes: entry_size * 2 + 100, // roughly 2 entries
499            default_ttl: None,
500        };
501        let mut cache = VectorCache::with_config(config);
502        cache.put("a", vec3(1.0, 0.0, 0.0));
503        cache.put("b", vec3(0.0, 1.0, 0.0));
504        cache.put("c", vec3(0.0, 0.0, 1.0));
505
506        // At most 2 entries should remain (might be fewer due to overhead).
507        assert!(cache.len() <= 3);
508        let _ = cache.statistics().evictions;
509    }
510
511    // ── TTL-based expiry ─────────────────────────────────────────────────────
512
513    #[test]
514    fn test_ttl_expiry() {
515        let config = VectorCacheConfig {
516            default_ttl: Some(Duration::from_millis(1)),
517            ..Default::default()
518        };
519        let mut cache = VectorCache::with_config(config);
520        cache.put("k1", vec3(1.0, 2.0, 3.0));
521
522        // Wait for TTL to expire.
523        std::thread::sleep(Duration::from_millis(10));
524
525        assert!(cache.get("k1").is_none());
526        assert!(cache.statistics().expirations >= 1);
527    }
528
529    #[test]
530    fn test_per_entry_ttl() {
531        let mut cache = VectorCache::new();
532        cache.put_with_ttl("k1", vec3(1.0, 2.0, 3.0), Some(Duration::from_millis(1)));
533        cache.put("k2", vec3(4.0, 5.0, 6.0)); // No TTL
534
535        std::thread::sleep(Duration::from_millis(10));
536
537        assert!(cache.get("k1").is_none()); // expired
538        assert!(cache.get("k2").is_some()); // still alive
539    }
540
541    #[test]
542    fn test_sweep_expired() {
543        let config = VectorCacheConfig {
544            default_ttl: Some(Duration::from_millis(1)),
545            ..Default::default()
546        };
547        let mut cache = VectorCache::with_config(config);
548        cache.put("a", vec3(1.0, 0.0, 0.0));
549        cache.put("b", vec3(0.0, 1.0, 0.0));
550
551        std::thread::sleep(Duration::from_millis(10));
552
553        let swept = cache.sweep_expired();
554        assert_eq!(swept, 2);
555        assert!(cache.is_empty());
556    }
557
558    // ── Invalidation ─────────────────────────────────────────────────────────
559
560    #[test]
561    fn test_invalidate_key() {
562        let mut cache = VectorCache::new();
563        cache.put("k1", vec3(1.0, 2.0, 3.0));
564        assert!(cache.invalidate("k1"));
565        assert!(cache.get("k1").is_none());
566        assert_eq!(cache.statistics().invalidations, 1);
567    }
568
569    #[test]
570    fn test_invalidate_nonexistent() {
571        let mut cache = VectorCache::new();
572        assert!(!cache.invalidate("nope"));
573    }
574
575    #[test]
576    fn test_invalidate_prefix() {
577        let mut cache = VectorCache::new();
578        cache.put("user:1", vec3(1.0, 0.0, 0.0));
579        cache.put("user:2", vec3(0.0, 1.0, 0.0));
580        cache.put("item:1", vec3(0.0, 0.0, 1.0));
581
582        let removed = cache.invalidate_prefix("user:");
583        assert_eq!(removed, 2);
584        assert_eq!(cache.len(), 1);
585        assert!(cache.contains_key("item:1"));
586    }
587
588    #[test]
589    fn test_clear() {
590        let mut cache = VectorCache::new();
591        cache.put("a", vec3(1.0, 0.0, 0.0));
592        cache.put("b", vec3(0.0, 1.0, 0.0));
593        cache.clear();
594        assert!(cache.is_empty());
595    }
596
597    // ── Batch operations ─────────────────────────────────────────────────────
598
599    #[test]
600    fn test_batch_put_and_get() {
601        let mut cache = VectorCache::new();
602        cache.batch_put(vec![
603            ("k1".to_string(), vec3(1.0, 0.0, 0.0)),
604            ("k2".to_string(), vec3(0.0, 1.0, 0.0)),
605            ("k3".to_string(), vec3(0.0, 0.0, 1.0)),
606        ]);
607        assert_eq!(cache.len(), 3);
608
609        let results = cache.batch_get(&["k1", "k3", "missing"]);
610        assert_eq!(results.len(), 2);
611        assert!(results.contains_key("k1"));
612        assert!(results.contains_key("k3"));
613    }
614
615    // ── Cache warming ────────────────────────────────────────────────────────
616
617    #[test]
618    fn test_warm() {
619        let mut cache = VectorCache::new();
620        cache.put("existing", vec3(9.0, 9.0, 9.0));
621
622        let loaded = cache.warm(vec![
623            ("existing".to_string(), vec3(0.0, 0.0, 0.0)), // should NOT overwrite
624            ("new1".to_string(), vec3(1.0, 0.0, 0.0)),
625            ("new2".to_string(), vec3(0.0, 1.0, 0.0)),
626        ]);
627
628        assert_eq!(loaded, 2);
629        assert_eq!(cache.len(), 3);
630
631        // "existing" should retain its original value.
632        let v = cache.get("existing").expect("should exist");
633        assert_eq!(v, vec3(9.0, 9.0, 9.0));
634    }
635
636    // ── Statistics ───────────────────────────────────────────────────────────
637
638    #[test]
639    fn test_hit_ratio() {
640        let mut cache = VectorCache::new();
641        cache.put("k1", vec3(1.0, 2.0, 3.0));
642        let _ = cache.get("k1"); // hit
643        let _ = cache.get("k2"); // miss
644
645        let stats = cache.statistics();
646        assert_eq!(stats.hits, 1);
647        assert_eq!(stats.misses, 1);
648        assert!((stats.hit_ratio() - 0.5).abs() < f64::EPSILON);
649        assert_eq!(stats.total_requests(), 2);
650    }
651
652    #[test]
653    fn test_hit_ratio_no_requests() {
654        let cache = VectorCache::new();
655        assert!((cache.statistics().hit_ratio() - 0.0).abs() < f64::EPSILON);
656    }
657
658    #[test]
659    fn test_reset_statistics() {
660        let mut cache = VectorCache::new();
661        cache.put("k1", vec3(1.0, 2.0, 3.0));
662        let _ = cache.get("k1");
663        cache.reset_statistics();
664        assert_eq!(cache.statistics().hits, 0);
665        assert_eq!(cache.statistics().inserts, 0);
666    }
667
668    // ── Persistence ──────────────────────────────────────────────────────────
669
670    #[test]
671    fn test_snapshot_and_load() {
672        let mut cache = VectorCache::new();
673        cache.put("a", vec3(1.0, 0.0, 0.0));
674        cache.put("b", vec3(0.0, 1.0, 0.0));
675
676        let snap = cache.snapshot();
677        assert_eq!(snap.entry_count, 2);
678
679        let mut cache2 = VectorCache::new();
680        let loaded = cache2.load_snapshot(snap);
681        assert_eq!(loaded, 2);
682        assert!(cache2.get("a").is_some());
683        assert!(cache2.get("b").is_some());
684    }
685
686    #[test]
687    fn test_snapshot_excludes_expired() {
688        let config = VectorCacheConfig {
689            default_ttl: Some(Duration::from_millis(1)),
690            ..Default::default()
691        };
692        let mut cache = VectorCache::with_config(config);
693        cache.put("x", vec3(1.0, 2.0, 3.0));
694        std::thread::sleep(Duration::from_millis(10));
695
696        let snap = cache.snapshot();
697        assert_eq!(snap.entry_count, 0);
698    }
699
700    // ── Len / is_empty / contains_key ────────────────────────────────────────
701
702    #[test]
703    fn test_len_and_empty() {
704        let mut cache = VectorCache::new();
705        assert!(cache.is_empty());
706        assert_eq!(cache.len(), 0);
707
708        cache.put("k1", vec3(1.0, 2.0, 3.0));
709        assert!(!cache.is_empty());
710        assert_eq!(cache.len(), 1);
711    }
712
713    #[test]
714    fn test_contains_key() {
715        let mut cache = VectorCache::new();
716        cache.put("k1", vec3(1.0, 2.0, 3.0));
717        assert!(cache.contains_key("k1"));
718        assert!(!cache.contains_key("k2"));
719    }
720
721    // ── Memory usage ─────────────────────────────────────────────────────────
722
723    #[test]
724    fn test_memory_usage_grows() {
725        let mut cache = VectorCache::new();
726        let m0 = cache.memory_usage();
727        cache.put("k1", vec![0.0; 100]);
728        let m1 = cache.memory_usage();
729        assert!(m1 > m0);
730    }
731
732    // ── Default ──────────────────────────────────────────────────────────────
733
734    #[test]
735    fn test_default_config() {
736        let c = VectorCacheConfig::default();
737        assert_eq!(c.max_entries, 10_000);
738        assert_eq!(c.max_memory_bytes, 0);
739        assert!(c.default_ttl.is_none());
740    }
741
742    #[test]
743    fn test_default_cache() {
744        let cache = VectorCache::default();
745        assert!(cache.is_empty());
746    }
747
748    // ── Edge cases ───────────────────────────────────────────────────────────
749
750    #[test]
751    fn test_empty_vector() {
752        let mut cache = VectorCache::new();
753        cache.put("empty", vec![]);
754        let v = cache.get("empty");
755        assert_eq!(v.expect("should exist"), Vec::<f64>::new());
756    }
757
758    #[test]
759    fn test_large_vector() {
760        let mut cache = VectorCache::new();
761        let big = vec![1.0; 10_000];
762        cache.put("big", big.clone());
763        let v = cache.get("big").expect("should exist");
764        assert_eq!(v.len(), 10_000);
765    }
766
767    #[test]
768    fn test_invalidate_prefix_no_match() {
769        let mut cache = VectorCache::new();
770        cache.put("k1", vec3(1.0, 2.0, 3.0));
771        let removed = cache.invalidate_prefix("zzz:");
772        assert_eq!(removed, 0);
773        assert_eq!(cache.len(), 1);
774    }
775
776    // ── Additional tests for coverage ────────────────────────────────────────
777
778    #[test]
779    fn test_multiple_evictions() {
780        let config = VectorCacheConfig {
781            max_entries: 3,
782            ..Default::default()
783        };
784        let mut cache = VectorCache::with_config(config);
785        for i in 0..10 {
786            cache.put(format!("k{i}"), vec![i as f64]);
787        }
788        assert_eq!(cache.len(), 3);
789        assert!(cache.statistics().evictions >= 7);
790    }
791
792    #[test]
793    fn test_batch_get_all_miss() {
794        let mut cache = VectorCache::new();
795        let results = cache.batch_get(&["a", "b", "c"]);
796        assert!(results.is_empty());
797        assert_eq!(cache.statistics().misses, 3);
798    }
799
800    #[test]
801    fn test_batch_put_then_invalidate() {
802        let mut cache = VectorCache::new();
803        cache.batch_put(vec![
804            ("a".to_string(), vec3(1.0, 0.0, 0.0)),
805            ("b".to_string(), vec3(0.0, 1.0, 0.0)),
806        ]);
807        cache.invalidate("a");
808        assert!(!cache.contains_key("a"));
809        assert!(cache.contains_key("b"));
810    }
811
812    #[test]
813    fn test_warm_empty_list() {
814        let mut cache = VectorCache::new();
815        let loaded = cache.warm(vec![]);
816        assert_eq!(loaded, 0);
817    }
818
819    #[test]
820    fn test_snapshot_empty_cache() {
821        let cache = VectorCache::new();
822        let snap = cache.snapshot();
823        assert_eq!(snap.entry_count, 0);
824        assert!(snap.entries.is_empty());
825    }
826
827    #[test]
828    fn test_load_snapshot_into_non_empty_cache() {
829        let mut cache1 = VectorCache::new();
830        cache1.put("a", vec3(1.0, 0.0, 0.0));
831        let snap = cache1.snapshot();
832
833        let mut cache2 = VectorCache::new();
834        cache2.put("b", vec3(0.0, 1.0, 0.0));
835        cache2.load_snapshot(snap);
836
837        assert!(cache2.contains_key("a"));
838        assert!(cache2.contains_key("b"));
839        assert_eq!(cache2.len(), 2);
840    }
841
842    #[test]
843    fn test_put_updates_insert_count() {
844        let mut cache = VectorCache::new();
845        cache.put("k1", vec3(1.0, 0.0, 0.0));
846        cache.put("k2", vec3(0.0, 1.0, 0.0));
847        assert_eq!(cache.statistics().inserts, 2);
848    }
849
850    #[test]
851    fn test_clear_resets_len() {
852        let mut cache = VectorCache::new();
853        cache.put("a", vec3(1.0, 0.0, 0.0));
854        cache.put("b", vec3(0.0, 1.0, 0.0));
855        cache.put("c", vec3(0.0, 0.0, 1.0));
856        cache.clear();
857        assert_eq!(cache.len(), 0);
858        assert!(cache.is_empty());
859    }
860
861    #[test]
862    fn test_stats_invalidation_count() {
863        let mut cache = VectorCache::new();
864        cache.put("a", vec3(1.0, 0.0, 0.0));
865        cache.put("b", vec3(0.0, 1.0, 0.0));
866        cache.invalidate("a");
867        cache.invalidate("b");
868        assert_eq!(cache.statistics().invalidations, 2);
869    }
870
871    #[test]
872    fn test_get_after_clear() {
873        let mut cache = VectorCache::new();
874        cache.put("k1", vec3(1.0, 2.0, 3.0));
875        cache.clear();
876        assert!(cache.get("k1").is_none());
877    }
878
879    #[test]
880    fn test_sweep_expired_none_expired() {
881        let mut cache = VectorCache::new();
882        cache.put("k1", vec3(1.0, 2.0, 3.0));
883        let swept = cache.sweep_expired();
884        assert_eq!(swept, 0);
885        assert_eq!(cache.len(), 1);
886    }
887
888    #[test]
889    fn test_contains_key_after_eviction() {
890        let config = VectorCacheConfig {
891            max_entries: 1,
892            ..Default::default()
893        };
894        let mut cache = VectorCache::with_config(config);
895        cache.put("first", vec3(1.0, 0.0, 0.0));
896        cache.put("second", vec3(0.0, 1.0, 0.0));
897        assert!(!cache.contains_key("first"));
898        assert!(cache.contains_key("second"));
899    }
900
901    #[test]
902    fn test_invalidate_prefix_all() {
903        let mut cache = VectorCache::new();
904        cache.put("x:1", vec3(1.0, 0.0, 0.0));
905        cache.put("x:2", vec3(0.0, 1.0, 0.0));
906        cache.put("x:3", vec3(0.0, 0.0, 1.0));
907        let removed = cache.invalidate_prefix("x:");
908        assert_eq!(removed, 3);
909        assert!(cache.is_empty());
910    }
911
912    #[test]
913    fn test_memory_usage_after_clear() {
914        let mut cache = VectorCache::new();
915        cache.put("k1", vec![0.0; 1000]);
916        let before = cache.memory_usage();
917        assert!(before > 0);
918        cache.clear();
919        assert_eq!(cache.memory_usage(), 0);
920    }
921
922    #[test]
923    fn test_put_with_zero_ttl() {
924        let mut cache = VectorCache::new();
925        cache.put_with_ttl("k1", vec3(1.0, 2.0, 3.0), Some(Duration::from_secs(0)));
926        // TTL = 0 means already expired on next access.
927        std::thread::sleep(Duration::from_millis(2));
928        assert!(cache.get("k1").is_none());
929    }
930
931    #[test]
932    fn test_lru_order_after_overwrite() {
933        let config = VectorCacheConfig {
934            max_entries: 2,
935            ..Default::default()
936        };
937        let mut cache = VectorCache::with_config(config);
938        cache.put("a", vec3(1.0, 0.0, 0.0));
939        cache.put("b", vec3(0.0, 1.0, 0.0));
940        // Overwrite "a" — it should become most recently used
941        cache.put("a", vec3(9.0, 9.0, 9.0));
942        // Insert "c" — should evict "b", not "a"
943        cache.put("c", vec3(0.0, 0.0, 1.0));
944        assert!(cache.contains_key("a"));
945        assert!(!cache.contains_key("b"));
946        assert!(cache.contains_key("c"));
947    }
948}