nodedb_vector/planner/
routing.rs1use super::query_options::QuantizationKind;
10
11#[derive(Debug, Clone, PartialEq)]
15#[non_exhaustive]
16pub enum FilterRoute {
17 BruteForce,
23
24 SieveSubindex {
29 signature: String,
32 },
33
34 NavixGlobal,
39
40 Compass,
46}
47
48pub struct FilterRouteInputs<'a> {
50 pub global_selectivity: f32,
52
53 pub brute_force_threshold: f32,
57
58 pub matched_sieve_signature: Option<&'a str>,
60
61 pub compass_enabled: bool,
63
64 pub numeric_predicate_count: u8,
69}
70
71pub fn route_filter(inputs: &FilterRouteInputs<'_>) -> FilterRoute {
80 if inputs.global_selectivity < inputs.brute_force_threshold {
81 return FilterRoute::BruteForce;
82 }
83
84 if let Some(sig) = inputs.matched_sieve_signature {
85 return FilterRoute::SieveSubindex {
86 signature: sig.to_owned(),
87 };
88 }
89
90 if inputs.compass_enabled && inputs.numeric_predicate_count >= 3 {
91 return FilterRoute::Compass;
92 }
93
94 FilterRoute::NavixGlobal
95}
96
97pub fn pick_quantization(candidate_set_size: usize, target_recall: f32) -> QuantizationKind {
112 if candidate_set_size < 1_000 {
113 QuantizationKind::None
114 } else if candidate_set_size < 100_000 {
115 if target_recall >= 0.97 {
116 QuantizationKind::Sq8
117 } else {
118 QuantizationKind::Pq
119 }
120 } else if target_recall >= 0.95 {
121 QuantizationKind::Bbq
122 } else {
123 QuantizationKind::RaBitQ
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 fn default_inputs<'a>() -> FilterRouteInputs<'a> {
134 FilterRouteInputs {
135 global_selectivity: 0.5,
136 brute_force_threshold: 0.001,
137 matched_sieve_signature: None,
138 compass_enabled: false,
139 numeric_predicate_count: 0,
140 }
141 }
142
143 #[test]
144 fn brute_force_fires_below_threshold() {
145 let inputs = FilterRouteInputs {
146 global_selectivity: 0.0005,
147 ..default_inputs()
148 };
149 assert_eq!(route_filter(&inputs), FilterRoute::BruteForce);
150 }
151
152 #[test]
153 fn brute_force_fires_exactly_at_threshold_minus_epsilon() {
154 let threshold = 0.001_f32;
155 let inputs = FilterRouteInputs {
156 global_selectivity: threshold - f32::EPSILON,
157 brute_force_threshold: threshold,
158 ..default_inputs()
159 };
160 assert_eq!(route_filter(&inputs), FilterRoute::BruteForce);
161 }
162
163 #[test]
164 fn sieve_subindex_fires_when_signature_matched() {
165 let inputs = FilterRouteInputs {
166 global_selectivity: 0.5,
167 matched_sieve_signature: Some("tenant_id=42"),
168 ..default_inputs()
169 };
170 assert_eq!(
171 route_filter(&inputs),
172 FilterRoute::SieveSubindex {
173 signature: "tenant_id=42".to_owned()
174 }
175 );
176 }
177
178 #[test]
179 fn sieve_takes_priority_over_compass() {
180 let inputs = FilterRouteInputs {
183 global_selectivity: 0.5,
184 matched_sieve_signature: Some("lang=en"),
185 compass_enabled: true,
186 numeric_predicate_count: 5,
187 ..default_inputs()
188 };
189 assert_eq!(
190 route_filter(&inputs),
191 FilterRoute::SieveSubindex {
192 signature: "lang=en".to_owned()
193 }
194 );
195 }
196
197 #[test]
198 fn compass_fires_when_enabled_and_enough_predicates() {
199 let inputs = FilterRouteInputs {
200 global_selectivity: 0.5,
201 matched_sieve_signature: None,
202 compass_enabled: true,
203 numeric_predicate_count: 3,
204 ..default_inputs()
205 };
206 assert_eq!(route_filter(&inputs), FilterRoute::Compass);
207 }
208
209 #[test]
210 fn compass_does_not_fire_with_fewer_than_3_predicates() {
211 let inputs = FilterRouteInputs {
212 global_selectivity: 0.5,
213 matched_sieve_signature: None,
214 compass_enabled: true,
215 numeric_predicate_count: 2,
216 ..default_inputs()
217 };
218 assert_eq!(route_filter(&inputs), FilterRoute::NavixGlobal);
219 }
220
221 #[test]
222 fn compass_does_not_fire_when_disabled() {
223 let inputs = FilterRouteInputs {
224 global_selectivity: 0.5,
225 matched_sieve_signature: None,
226 compass_enabled: false,
227 numeric_predicate_count: 10,
228 ..default_inputs()
229 };
230 assert_eq!(route_filter(&inputs), FilterRoute::NavixGlobal);
231 }
232
233 #[test]
234 fn navix_global_is_default_fallback() {
235 let inputs = default_inputs();
236 assert_eq!(route_filter(&inputs), FilterRoute::NavixGlobal);
237 }
238
239 #[test]
242 fn small_set_returns_none() {
243 assert_eq!(pick_quantization(0, 0.99), QuantizationKind::None);
244 assert_eq!(pick_quantization(999, 0.99), QuantizationKind::None);
245 }
246
247 #[test]
248 fn medium_set_high_recall_returns_sq8() {
249 assert_eq!(pick_quantization(1_000, 0.97), QuantizationKind::Sq8);
250 assert_eq!(pick_quantization(50_000, 0.99), QuantizationKind::Sq8);
251 assert_eq!(pick_quantization(99_999, 1.0), QuantizationKind::Sq8);
252 }
253
254 #[test]
255 fn medium_set_lower_recall_returns_pq() {
256 assert_eq!(pick_quantization(1_000, 0.96), QuantizationKind::Pq);
257 assert_eq!(pick_quantization(50_000, 0.90), QuantizationKind::Pq);
258 }
259
260 #[test]
261 fn large_set_high_recall_returns_bbq() {
262 assert_eq!(pick_quantization(100_000, 0.95), QuantizationKind::Bbq);
263 assert_eq!(pick_quantization(1_000_000, 0.99), QuantizationKind::Bbq);
264 }
265
266 #[test]
267 fn large_set_lower_recall_returns_rabitq() {
268 assert_eq!(pick_quantization(100_000, 0.94), QuantizationKind::RaBitQ);
269 assert_eq!(
270 pick_quantization(10_000_000, 0.80),
271 QuantizationKind::RaBitQ
272 );
273 }
274
275 #[test]
276 fn boundary_1000_uses_medium_path() {
277 assert_ne!(pick_quantization(1_000, 0.99), QuantizationKind::None);
279 }
280
281 #[test]
282 fn boundary_100000_uses_large_path() {
283 let result = pick_quantization(100_000, 0.95);
285 assert!(
286 result == QuantizationKind::Bbq || result == QuantizationKind::RaBitQ,
287 "expected BBQ or RaBitQ at boundary, got {result:?}"
288 );
289 }
290}