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(
46 &self,
47 query: &[f32],
48 predicate_signature: Option<&PredicateSignature>,
49 allowed: RoaringBitmap,
50 k: usize,
51 ef_search: usize,
52 metric: DistanceMetric,
53 ) -> Result<Vec<SearchResult>, crate::error::VectorError> {
54 if let Some(sig) = predicate_signature
56 && let Some(subindex) = self.collection.get(sig)
57 {
58 return subindex.search(query, k, ef_search);
59 }
60
61 let opts = NavixSearchOptions {
63 k,
64 ef_search,
65 allowed,
66 brute_force_threshold: 0.001,
67 };
68 Ok(navix_search(self.fallback, query, &opts, metric)?
69 .into_iter()
70 .map(|r| SearchResult {
71 id: r.id,
72 distance: r.distance,
73 })
74 .collect())
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use crate::hnsw::{HnswIndex, HnswParams};
82 use crate::sieve::collection::SieveCollection;
83 use nodedb_types::vector_distance::DistanceMetric;
84
85 fn build_fallback(n: usize) -> HnswIndex {
86 let mut idx = HnswIndex::with_seed(
87 3,
88 HnswParams {
89 m: 8,
90 m0: 16,
91 ef_construction: 50,
92 metric: DistanceMetric::L2,
93 dtype: nodedb_types::vector_dtype::VectorStorageDtype::F32,
94 },
95 99,
96 );
97 for i in 0..n {
98 idx.insert(vec![i as f32, 0.0, 0.0]).unwrap();
99 }
100 idx
101 }
102
103 fn all_allowed(n: u32) -> RoaringBitmap {
104 let mut b = RoaringBitmap::new();
105 for i in 0..n {
106 b.insert(i);
107 }
108 b
109 }
110
111 #[test]
115 fn route_hits_subindex() {
116 let mut coll = SieveCollection::new(8);
118 let sub_vecs: Vec<(u32, Vec<f32>)> =
119 (0u32..5).map(|i| (i, vec![i as f32, 0.0, 0.0])).collect();
120 coll.build_subindex("T".to_string(), &sub_vecs, 3, DistanceMetric::L2)
121 .expect("build subindex");
122
123 let fallback = build_fallback(20);
124 let router = SieveRouter {
125 collection: &coll,
126 fallback: &fallback,
127 };
128
129 let results = router
130 .route(
131 &[2.0, 0.0, 0.0],
132 Some(&"T".to_string()),
133 all_allowed(20), 3,
135 32,
136 DistanceMetric::L2,
137 )
138 .unwrap();
139
140 assert!(!results.is_empty());
141 for r in &results {
143 assert!(r.id < 5, "expected subindex id < 5, got {}", r.id);
144 }
145 }
146
147 #[test]
150 fn route_falls_back_to_navix() {
151 let coll = SieveCollection::new(8); let fallback = build_fallback(20);
153 let router = SieveRouter {
154 collection: &coll,
155 fallback: &fallback,
156 };
157
158 let allowed = all_allowed(20);
159 let results = router
160 .route(
161 &[10.0, 0.0, 0.0],
162 Some(&"unknown_sig".to_string()),
163 allowed,
164 3,
165 64,
166 DistanceMetric::L2,
167 )
168 .unwrap();
169
170 assert!(!results.is_empty());
171 assert_eq!(results[0].id, 10);
173 }
174
175 #[test]
177 fn route_no_signature_uses_navix() {
178 let mut coll = SieveCollection::new(8);
179 let sub_vecs: Vec<(u32, Vec<f32>)> =
180 (0u32..5).map(|i| (i, vec![i as f32, 0.0, 0.0])).collect();
181 coll.build_subindex("T".to_string(), &sub_vecs, 3, DistanceMetric::L2)
182 .expect("build subindex");
183
184 let fallback = build_fallback(20);
185 let router = SieveRouter {
186 collection: &coll,
187 fallback: &fallback,
188 };
189
190 let allowed = all_allowed(20);
191 let results = router
192 .route(&[5.0, 0.0, 0.0], None, allowed, 3, 64, DistanceMetric::L2)
193 .unwrap();
194
195 assert!(!results.is_empty());
196 assert_eq!(results[0].id, 5);
198 }
199}