1use std::collections::{HashMap, VecDeque};
17use std::sync::Mutex;
18
19use dashmap::DashMap;
20use serde::{Deserialize, Serialize};
21
22use crate::hnsw_service::HnswServiceConfig;
23use crate::vector_backend::{SearchResult, VectorBackend, VectorError, VectorResult};
24use crate::vector_diskann::{DiskAnnBackend, DiskAnnConfig};
25use crate::vector_hnsw::HnswBackend;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
31#[serde(rename_all = "lowercase")]
32pub enum EvictionPolicy {
33 #[default]
35 Lru,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct HybridConfig {
41 #[serde(default = "default_hot_capacity")]
43 pub hot_capacity: usize,
44
45 #[serde(default = "default_promotion_threshold")]
47 pub promotion_threshold: u32,
48
49 #[serde(default)]
51 pub eviction_policy: EvictionPolicy,
52}
53
54fn default_hot_capacity() -> usize {
55 50_000
56}
57fn default_promotion_threshold() -> u32 {
58 3
59}
60
61impl Default for HybridConfig {
62 fn default() -> Self {
63 Self {
64 hot_capacity: default_hot_capacity(),
65 promotion_threshold: default_promotion_threshold(),
66 eviction_policy: EvictionPolicy::default(),
67 }
68 }
69}
70
71struct LruTracker {
78 order: VecDeque<u64>,
79 positions: HashMap<u64, usize>,
80}
81
82impl LruTracker {
83 fn new() -> Self {
84 Self {
85 order: VecDeque::new(),
86 positions: HashMap::new(),
87 }
88 }
89
90 fn touch(&mut self, id: u64) {
92 if self.positions.contains_key(&id) {
93 self.order.retain(|&x| x != id);
95 }
96 self.order.push_back(id);
97 self.rebuild_positions();
98 }
99
100 fn evict(&mut self) -> Option<u64> {
102 let id = self.order.pop_front()?;
103 self.positions.remove(&id);
104 self.rebuild_positions();
105 Some(id)
106 }
107
108 fn remove(&mut self, id: u64) {
110 if self.positions.remove(&id).is_some() {
111 self.order.retain(|&x| x != id);
112 self.rebuild_positions();
113 }
114 }
115
116 fn len(&self) -> usize {
117 self.order.len()
118 }
119
120 fn rebuild_positions(&mut self) {
121 self.positions.clear();
122 for (i, &id) in self.order.iter().enumerate() {
123 self.positions.insert(id, i);
124 }
125 }
126}
127
128struct VectorSnapshot {
132 key: String,
133 vector: Vec<f32>,
134 metadata: serde_json::Value,
135}
136
137struct ColdVectorCache {
142 vectors: HashMap<u64, VectorSnapshot>,
143}
144
145impl ColdVectorCache {
146 fn new() -> Self {
147 Self {
148 vectors: HashMap::new(),
149 }
150 }
151
152 fn insert(&mut self, id: u64, key: String, vector: Vec<f32>, metadata: serde_json::Value) {
153 self.vectors.insert(
154 id,
155 VectorSnapshot {
156 key,
157 vector,
158 metadata,
159 },
160 );
161 }
162
163 fn get(&self, id: u64) -> Option<&VectorSnapshot> {
164 self.vectors.get(&id)
165 }
166
167 fn remove(&mut self, id: u64) {
168 self.vectors.remove(&id);
169 }
170}
171
172pub struct HybridBackend {
188 hot: HnswBackend,
189 cold: DiskAnnBackend,
190 config: HybridConfig,
191 access_counts: DashMap<u64, u32>,
193 lru: Mutex<LruTracker>,
195 cold_cache: Mutex<ColdVectorCache>,
197 hot_ids: DashMap<u64, ()>,
199}
200
201impl HybridBackend {
202 pub fn new(
204 hnsw_config: HnswServiceConfig,
205 diskann_config: DiskAnnConfig,
206 hybrid_config: HybridConfig,
207 ) -> Self {
208 Self {
209 hot: HnswBackend::new(hnsw_config),
210 cold: DiskAnnBackend::new(diskann_config),
211 config: hybrid_config,
212 access_counts: DashMap::new(),
213 lru: Mutex::new(LruTracker::new()),
214 cold_cache: Mutex::new(ColdVectorCache::new()),
215 hot_ids: DashMap::new(),
216 }
217 }
218
219 pub fn with_defaults() -> Self {
221 Self::new(
222 HnswServiceConfig::default(),
223 DiskAnnConfig::default(),
224 HybridConfig::default(),
225 )
226 }
227
228 pub fn config(&self) -> &HybridConfig {
230 &self.config
231 }
232
233 pub fn hot_len(&self) -> usize {
235 self.hot_ids.len()
236 }
237
238 pub fn cold_len(&self) -> usize {
240 self.cold.len()
241 }
242
243 fn try_promote(&self, id: u64) {
245 if self.hot_ids.contains_key(&id) {
247 return;
248 }
249
250 let count = self.access_counts.get(&id).map(|v| *v).unwrap_or(0);
252
253 if count < self.config.promotion_threshold {
254 return;
255 }
256
257 let snapshot = {
259 let cache = self.cold_cache.lock().expect("cold_cache lock poisoned");
260 cache.get(id).map(|s| VectorSnapshot {
261 key: s.key.clone(),
262 vector: s.vector.clone(),
263 metadata: s.metadata.clone(),
264 })
265 };
266
267 let Some(snap) = snapshot else { return };
268
269 self.ensure_hot_capacity();
271
272 if self
274 .hot
275 .insert(id, &snap.key, &snap.vector, snap.metadata)
276 .is_ok()
277 {
278 self.hot_ids.insert(id, ());
279 let mut lru = self.lru.lock().expect("LRU lock poisoned");
280 lru.touch(id);
281 }
282 }
283
284 fn ensure_hot_capacity(&self) {
286 let mut lru = self.lru.lock().expect("LRU lock poisoned");
287 while lru.len() >= self.config.hot_capacity {
288 if let Some(evict_id) = lru.evict() {
289 self.hot.remove(evict_id);
290 self.hot_ids.remove(&evict_id);
291 } else {
292 break;
293 }
294 }
295 }
296
297 fn record_access(&self, id: u64) {
299 self.access_counts
300 .entry(id)
301 .and_modify(|c| {
302 *c = c.saturating_add(1);
303 })
304 .or_insert(1);
305
306 if self.hot_ids.contains_key(&id) {
308 let mut lru = self.lru.lock().expect("LRU lock poisoned");
309 lru.touch(id);
310 }
311 }
312
313 const RRF_K: f32 = 60.0;
317
318 fn merge_results(
342 hot_results: Vec<SearchResult>,
343 cold_results: Vec<SearchResult>,
344 k: usize,
345 ) -> Vec<SearchResult> {
346 if k == 0 {
347 return Vec::new();
348 }
349
350 let mut rrf: HashMap<u64, f32> = HashMap::new();
352 let mut by_id: HashMap<u64, SearchResult> = HashMap::new();
354
355 for (rank, r) in hot_results.into_iter().enumerate() {
356 *rrf.entry(r.id).or_insert(0.0) += 1.0 / (Self::RRF_K + rank as f32);
357 by_id.entry(r.id).or_insert(r);
358 }
359 for (rank, r) in cold_results.into_iter().enumerate() {
360 *rrf.entry(r.id).or_insert(0.0) += 1.0 / (Self::RRF_K + rank as f32);
361 by_id.entry(r.id).or_insert(r);
363 }
364
365 let mut merged: Vec<SearchResult> = by_id.into_values().collect();
366 merged.sort_by(|a, b| {
368 let sa = rrf.get(&a.id).copied().unwrap_or(0.0);
369 let sb = rrf.get(&b.id).copied().unwrap_or(0.0);
370 sb.partial_cmp(&sa)
371 .unwrap_or(std::cmp::Ordering::Equal)
372 .then_with(|| {
373 a.distance
374 .partial_cmp(&b.distance)
375 .unwrap_or(std::cmp::Ordering::Equal)
376 })
377 .then_with(|| a.id.cmp(&b.id))
378 });
379 merged.truncate(k);
380 merged
381 }
382}
383
384impl VectorBackend for HybridBackend {
385 fn insert(
386 &self,
387 id: u64,
388 key: &str,
389 vector: &[f32],
390 metadata: serde_json::Value,
391 ) -> VectorResult<()> {
392 self.cold.insert(id, key, vector, metadata.clone())?;
394
395 {
397 let mut cache = self.cold_cache.lock().expect("cold_cache lock poisoned");
398 cache.insert(id, key.to_owned(), vector.to_vec(), metadata.clone());
399 }
400
401 if self.hot_ids.len() < self.config.hot_capacity {
403 self.hot.insert(id, key, vector, metadata)?;
404 self.hot_ids.insert(id, ());
405 let mut lru = self.lru.lock().expect("LRU lock poisoned");
406 lru.touch(id);
407 }
408
409 Ok(())
410 }
411
412 fn search(&self, query: &[f32], k: usize) -> Vec<SearchResult> {
413 let hot_results = self.hot.search(query, k);
415 let cold_results = self.cold.search(query, k);
416
417 let merged = Self::merge_results(hot_results, cold_results, k);
418
419 for r in &merged {
421 self.record_access(r.id);
422 self.try_promote(r.id);
423 }
424
425 merged
426 }
427
428 fn len(&self) -> usize {
429 self.cold.len()
431 }
432
433 fn contains(&self, id: u64) -> bool {
434 self.cold.contains(id)
435 }
436
437 fn remove(&self, id: u64) -> bool {
438 let hot_removed = self.hot.remove(id);
440 if hot_removed {
441 self.hot_ids.remove(&id);
442 let mut lru = self.lru.lock().expect("LRU lock poisoned");
443 lru.remove(id);
444 }
445 let cold_removed = self.cold.remove(id);
446 self.access_counts.remove(&id);
447 {
448 let mut cache = self.cold_cache.lock().expect("cold_cache lock poisoned");
449 cache.remove(id);
450 }
451 cold_removed
452 }
453
454 fn flush(&self) -> VectorResult<()> {
455 self.hot.flush()?;
456 self.cold.flush()?;
457 Ok(())
458 }
459
460 fn backend_name(&self) -> &str {
461 "hybrid"
462 }
463
464 fn current_epoch(&self) -> u64 {
467 self.cold.current_epoch()
468 }
469
470 fn insert_with_epoch(
471 &self,
472 id: u64,
473 key: &str,
474 vector: &[f32],
475 metadata: serde_json::Value,
476 parent_epoch: u64,
477 ) -> VectorResult<()> {
478 let current = self.cold.current_epoch();
479 if parent_epoch < current {
480 return Err(VectorError::EpochConflict {
481 expected: parent_epoch,
482 actual: current,
483 });
484 }
485 self.insert(id, key, vector, metadata)
486 }
487
488 fn soft_delete(&self, id: u64) -> bool {
491 let deleted = self.cold.soft_delete(id);
493 if deleted {
494 self.hot.soft_delete(id);
496 if self.hot_ids.contains_key(&id) {
497 self.hot_ids.remove(&id);
498 let mut lru = self.lru.lock().expect("LRU lock poisoned");
499 lru.remove(id);
500 }
501 }
502 deleted
503 }
504
505 fn compact(&self, older_than_epoch: u64) -> usize {
506 let purged = self.cold.compact(older_than_epoch);
508 self.hot.compact(older_than_epoch);
510 purged
511 }
512
513 fn tombstone_count(&self) -> usize {
514 self.cold.tombstone_count()
515 }
516
517 fn max_vectors(&self) -> Option<usize> {
520 self.cold.max_vectors()
521 }
522
523 fn set_max_vectors(&self, limit: Option<usize>) {
524 self.cold.set_max_vectors(limit);
525 }
526}
527
528impl std::fmt::Debug for HybridBackend {
529 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
530 f.debug_struct("HybridBackend")
531 .field("hot_len", &self.hot_len())
532 .field("cold_len", &self.cold_len())
533 .field("epoch", &self.current_epoch())
534 .field("tombstones", &self.tombstone_count())
535 .field("config", &self.config)
536 .finish()
537 }
538}
539
540#[cfg(test)]
543mod tests {
544 use super::*;
545
546 fn small_hybrid() -> HybridBackend {
547 HybridBackend::new(
548 HnswServiceConfig::default(),
549 DiskAnnConfig {
550 max_points: 1000,
551 ..DiskAnnConfig::default()
552 },
553 HybridConfig {
554 hot_capacity: 3,
555 promotion_threshold: 2,
556 eviction_policy: EvictionPolicy::Lru,
557 },
558 )
559 }
560
561 #[test]
562 fn insert_populates_both_tiers_under_capacity() {
563 let h = small_hybrid();
564 h.insert(1, "a", &[1.0, 0.0, 0.0], serde_json::json!({}))
565 .unwrap();
566 h.insert(2, "b", &[0.0, 1.0, 0.0], serde_json::json!({}))
567 .unwrap();
568
569 assert_eq!(h.len(), 2);
570 assert_eq!(h.hot_len(), 2);
571 assert_eq!(h.cold_len(), 2);
572 }
573
574 #[test]
575 fn insert_beyond_hot_capacity_stays_cold_only() {
576 let h = small_hybrid();
577 h.insert(1, "a", &[1.0, 0.0, 0.0], serde_json::json!({}))
578 .unwrap();
579 h.insert(2, "b", &[0.0, 1.0, 0.0], serde_json::json!({}))
580 .unwrap();
581 h.insert(3, "c", &[0.0, 0.0, 1.0], serde_json::json!({}))
582 .unwrap();
583
584 h.insert(4, "d", &[0.5, 0.5, 0.0], serde_json::json!({}))
586 .unwrap();
587
588 assert_eq!(h.len(), 4);
589 assert_eq!(h.hot_len(), 3);
590 assert_eq!(h.cold_len(), 4);
591 }
592
593 #[test]
594 fn search_returns_merged_results() {
595 let h = small_hybrid();
596 h.insert(1, "a", &[1.0, 0.0, 0.0], serde_json::json!({}))
597 .unwrap();
598 h.insert(2, "b", &[0.0, 1.0, 0.0], serde_json::json!({}))
599 .unwrap();
600
601 let results = h.search(&[1.0, 0.0, 0.0], 2);
602 assert_eq!(results.len(), 2);
603 assert_eq!(results[0].id, 1);
604 assert!(results[0].distance < 0.01);
605 }
606
607 #[test]
608 fn promotion_after_threshold() {
609 let h = small_hybrid();
610 h.insert(1, "a", &[1.0, 0.0, 0.0], serde_json::json!({}))
611 .unwrap();
612 h.insert(2, "b", &[0.0, 1.0, 0.0], serde_json::json!({}))
613 .unwrap();
614 h.insert(3, "c", &[0.0, 0.0, 1.0], serde_json::json!({}))
615 .unwrap();
616
617 h.insert(4, "d", &[0.5, 0.5, 0.0], serde_json::json!({}))
619 .unwrap();
620 assert!(!h.hot_ids.contains_key(&4));
621
622 h.record_access(4);
623 h.record_access(4);
624 h.try_promote(4);
625
626 assert!(h.hot_ids.contains_key(&4));
627 assert_eq!(h.hot_len(), 3);
628 }
629
630 #[test]
631 fn remove_cleans_both_tiers() {
632 let h = small_hybrid();
633 h.insert(1, "a", &[1.0, 0.0], serde_json::json!({}))
634 .unwrap();
635 assert!(h.contains(1));
636 assert!(h.remove(1));
637 assert!(!h.contains(1));
638 assert_eq!(h.hot_len(), 0);
639 assert_eq!(h.cold_len(), 0);
640 }
641
642 #[test]
643 fn merge_results_deduplicates() {
644 let hot = vec![
645 SearchResult::new(1, "a".into(), 0.1, serde_json::json!({})),
646 SearchResult::new(2, "b".into(), 0.3, serde_json::json!({})),
647 ];
648 let cold = vec![
649 SearchResult::new(1, "a".into(), 0.1, serde_json::json!({})),
650 SearchResult::new(3, "c".into(), 0.2, serde_json::json!({})),
651 ];
652
653 let merged = HybridBackend::merge_results(hot, cold, 3);
654 assert_eq!(merged.len(), 3);
655 assert_eq!(merged[0].id, 1);
657 assert_eq!(merged[1].id, 3);
660 assert_eq!(merged[2].id, 2);
661 }
662
663 #[test]
665 fn merge_results_rrf_not_raw_distance() {
666 let hot = vec![
669 SearchResult::new(10, "hot0".into(), 0.001, serde_json::Value::Null),
670 SearchResult::new(11, "hot1".into(), 0.002, serde_json::Value::Null),
671 ];
672 let cold = vec![
673 SearchResult::new(20, "cold0".into(), 100.0, serde_json::Value::Null),
674 SearchResult::new(21, "cold1".into(), 200.0, serde_json::Value::Null),
675 ];
676
677 let merged = HybridBackend::merge_results(hot, cold, 2);
678 assert_eq!(merged.len(), 2);
679 let ids: std::collections::HashSet<u64> = merged.iter().map(|r| r.id).collect();
680 assert!(ids.contains(&10), "hot rank-0 must survive RRF: {ids:?}");
682 assert!(ids.contains(&20), "cold rank-0 must survive RRF: {ids:?}");
683 }
684
685 #[test]
687 fn merge_results_dedup_prefers_hot_payload() {
688 let hot = vec![SearchResult::new(
689 42,
690 "hot-key".into(),
691 0.05,
692 serde_json::json!({"tier": "hot"}),
693 )];
694 let cold = vec![SearchResult::new(
695 42,
696 "cold-key".into(),
697 50.0,
698 serde_json::json!({"tier": "cold"}),
699 )];
700
701 let merged = HybridBackend::merge_results(hot, cold, 1);
702 assert_eq!(merged.len(), 1);
703 assert_eq!(merged[0].id, 42);
704 assert_eq!(merged[0].key, "hot-key");
705 assert_eq!(merged[0].metadata, serde_json::json!({"tier": "hot"}));
706 assert!((merged[0].distance - 0.05).abs() < f32::EPSILON);
708 }
709
710 #[test]
711 fn merge_results_empty_and_k_zero() {
712 assert!(HybridBackend::merge_results(vec![], vec![], 5).is_empty());
713 let hot = vec![SearchResult::new(1, "a".into(), 0.1, serde_json::Value::Null)];
714 assert!(HybridBackend::merge_results(hot, vec![], 0).is_empty());
715 }
716
717 #[test]
718 fn merge_results_cold_only_ids_survive() {
719 let hot = vec![SearchResult::new(1, "a".into(), 0.01, serde_json::Value::Null)];
720 let cold = vec![
721 SearchResult::new(2, "b".into(), 99.0, serde_json::Value::Null),
722 SearchResult::new(3, "c".into(), 100.0, serde_json::Value::Null),
723 ];
724 let merged = HybridBackend::merge_results(hot, cold, 3);
725 assert_eq!(merged.len(), 3);
726 let ids: std::collections::HashSet<u64> = merged.iter().map(|r| r.id).collect();
727 assert_eq!(ids, [1, 2, 3].into_iter().collect());
728 }
729
730 #[test]
731 fn backend_name() {
732 let h = small_hybrid();
733 assert_eq!(h.backend_name(), "hybrid");
734 }
735
736 #[test]
737 fn flush_both_tiers() {
738 let h = small_hybrid();
739 h.flush().unwrap();
740 }
741
742 #[test]
743 fn eviction_policy_default() {
744 assert_eq!(EvictionPolicy::default(), EvictionPolicy::Lru);
745 }
746
747 #[test]
748 fn lru_tracker_basics() {
749 let mut lru = LruTracker::new();
750 lru.touch(1);
751 lru.touch(2);
752 lru.touch(3);
753 assert_eq!(lru.len(), 3);
754
755 lru.touch(1);
756 assert_eq!(lru.evict(), Some(2));
757 assert_eq!(lru.len(), 2);
758 }
759
760 #[test]
763 fn hybrid_epoch_tracks_cold_tier() {
764 let h = small_hybrid();
765 assert_eq!(h.current_epoch(), 0);
766 h.insert(1, "a", &[1.0, 0.0, 0.0], serde_json::json!({}))
767 .unwrap();
768 assert!(h.current_epoch() > 0);
769 }
770
771 #[test]
772 fn hybrid_insert_with_epoch_rejects_stale() {
773 let h = small_hybrid();
774 h.insert(1, "a", &[1.0], serde_json::json!({})).unwrap();
775 let result = h.insert_with_epoch(2, "b", &[0.0], serde_json::json!({}), 0);
776 assert!(matches!(result, Err(VectorError::EpochConflict { .. })));
777 }
778
779 #[test]
782 fn hybrid_soft_delete_hides_from_both_tiers() {
783 let h = small_hybrid();
784 h.insert(1, "a", &[1.0, 0.0, 0.0], serde_json::json!({}))
785 .unwrap();
786 h.insert(2, "b", &[0.0, 1.0, 0.0], serde_json::json!({}))
787 .unwrap();
788
789 assert!(h.soft_delete(1));
790 assert_eq!(h.tombstone_count(), 1);
791 assert_eq!(h.len(), 1);
792 assert!(!h.contains(1));
793 }
794
795 #[test]
796 fn hybrid_compact() {
797 let h = small_hybrid();
798 h.insert(1, "a", &[1.0], serde_json::json!({})).unwrap();
799 h.soft_delete(1);
800 let epoch = h.current_epoch();
801 let purged = h.compact(epoch + 1);
802 assert_eq!(purged, 1);
803 assert_eq!(h.tombstone_count(), 0);
804 }
805
806 #[test]
809 fn hybrid_capacity_delegates_to_cold() {
810 let h = small_hybrid();
811 assert_eq!(h.max_vectors(), Some(1000));
812 h.set_max_vectors(Some(5));
813 assert_eq!(h.max_vectors(), Some(5));
814 }
815}