nodedb_vector/sieve/
router.rs1use roaring::RoaringBitmap;
7
8use super::collection::{PredicateSignature, SieveCollection};
9use crate::hnsw::graph::{HnswIndex, SearchResult};
10use crate::navix::traversal::{NavixSearchOptions, navix_search};
11use nodedb_types::vector_distance::DistanceMetric;
12
13pub struct SieveRouter<'a> {
20 pub collection: &'a SieveCollection,
22 pub fallback: &'a HnswIndex,
24}
25
26impl<'a> SieveRouter<'a> {
27 pub fn route(
44 &self,
45 query: &[f32],
46 predicate_signature: Option<&PredicateSignature>,
47 allowed: RoaringBitmap,
48 k: usize,
49 ef_search: usize,
50 metric: DistanceMetric,
51 ) -> Vec<SearchResult> {
52 if let Some(sig) = predicate_signature
54 && let Some(subindex) = self.collection.get(sig)
55 {
56 return subindex.search(query, k, ef_search);
57 }
58
59 let opts = NavixSearchOptions {
61 k,
62 ef_search,
63 allowed,
64 brute_force_threshold: 0.001,
65 };
66 navix_search(self.fallback, query, &opts, metric)
67 .into_iter()
68 .map(|r| SearchResult {
69 id: r.id,
70 distance: r.distance,
71 })
72 .collect()
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use crate::hnsw::{HnswIndex, HnswParams};
80 use crate::sieve::collection::SieveCollection;
81 use nodedb_types::vector_distance::DistanceMetric;
82
83 fn build_fallback(n: usize) -> HnswIndex {
84 let mut idx = HnswIndex::with_seed(
85 3,
86 HnswParams {
87 m: 8,
88 m0: 16,
89 ef_construction: 50,
90 metric: DistanceMetric::L2,
91 },
92 99,
93 );
94 for i in 0..n {
95 idx.insert(vec![i as f32, 0.0, 0.0]).unwrap();
96 }
97 idx
98 }
99
100 fn all_allowed(n: u32) -> RoaringBitmap {
101 let mut b = RoaringBitmap::new();
102 for i in 0..n {
103 b.insert(i);
104 }
105 b
106 }
107
108 #[test]
112 fn route_hits_subindex() {
113 let mut coll = SieveCollection::new(8);
115 let sub_vecs: Vec<(u32, Vec<f32>)> =
116 (0u32..5).map(|i| (i, vec![i as f32, 0.0, 0.0])).collect();
117 coll.build_subindex("T".to_string(), &sub_vecs, 3, DistanceMetric::L2)
118 .expect("build subindex");
119
120 let fallback = build_fallback(20);
121 let router = SieveRouter {
122 collection: &coll,
123 fallback: &fallback,
124 };
125
126 let results = router.route(
127 &[2.0, 0.0, 0.0],
128 Some(&"T".to_string()),
129 all_allowed(20), 3,
131 32,
132 DistanceMetric::L2,
133 );
134
135 assert!(!results.is_empty());
136 for r in &results {
138 assert!(r.id < 5, "expected subindex id < 5, got {}", r.id);
139 }
140 }
141
142 #[test]
145 fn route_falls_back_to_navix() {
146 let coll = SieveCollection::new(8); let fallback = build_fallback(20);
148 let router = SieveRouter {
149 collection: &coll,
150 fallback: &fallback,
151 };
152
153 let allowed = all_allowed(20);
154 let results = router.route(
155 &[10.0, 0.0, 0.0],
156 Some(&"unknown_sig".to_string()),
157 allowed,
158 3,
159 64,
160 DistanceMetric::L2,
161 );
162
163 assert!(!results.is_empty());
164 assert_eq!(results[0].id, 10);
166 }
167
168 #[test]
170 fn route_no_signature_uses_navix() {
171 let mut coll = SieveCollection::new(8);
172 let sub_vecs: Vec<(u32, Vec<f32>)> =
173 (0u32..5).map(|i| (i, vec![i as f32, 0.0, 0.0])).collect();
174 coll.build_subindex("T".to_string(), &sub_vecs, 3, DistanceMetric::L2)
175 .expect("build subindex");
176
177 let fallback = build_fallback(20);
178 let router = SieveRouter {
179 collection: &coll,
180 fallback: &fallback,
181 };
182
183 let allowed = all_allowed(20);
184 let results = router.route(&[5.0, 0.0, 0.0], None, allowed, 3, 64, DistanceMetric::L2);
185
186 assert!(!results.is_empty());
187 assert_eq!(results[0].id, 5);
189 }
190}