1#[derive(Debug, Clone, PartialEq)]
8pub enum ExecutionStrategy {
9 LocalOnly,
11 RemoteFanout { peer_ids: Vec<String> },
13 Hybrid { local: bool, peer_ids: Vec<String> },
15 Cached { cache_key: u64 },
17}
18
19#[derive(Debug, Clone)]
21pub struct ShardInfo {
22 pub shard_id: String,
24 pub peer_id: String,
26 pub vector_count: u64,
28 pub dimension: usize,
30 pub estimated_latency_ms: f64,
32 pub is_local: bool,
34}
35
36#[derive(Debug, Clone)]
38pub struct QueryPlan {
39 pub query_id: u64,
41 pub k: usize,
43 pub strategy: ExecutionStrategy,
45 pub shards: Vec<ShardInfo>,
47 pub estimated_latency_ms: f64,
49 pub estimated_results: usize,
51}
52
53impl QueryPlan {
54 pub fn is_local_only(&self) -> bool {
56 matches!(self.strategy, ExecutionStrategy::LocalOnly)
57 }
58}
59
60#[derive(Debug, Clone)]
62pub struct PlannerConfig {
63 pub max_fanout: usize,
65 pub latency_budget_ms: f64,
68 pub min_vectors_per_shard: u64,
70 pub prefer_local: bool,
73}
74
75impl Default for PlannerConfig {
76 fn default() -> Self {
77 Self {
78 max_fanout: 8,
79 latency_budget_ms: 100.0,
80 min_vectors_per_shard: 100,
81 prefer_local: true,
82 }
83 }
84}
85
86fn fnv1a_hash_f32_slice(values: &[f32]) -> u64 {
93 const OFFSET_BASIS: u64 = 2_166_136_261_u64;
94 const PRIME: u64 = 16_777_619_u64;
95
96 let mut hash = OFFSET_BASIS;
97 for &v in values {
98 for byte in v.to_le_bytes() {
99 hash ^= u64::from(byte);
100 hash = hash.wrapping_mul(PRIME);
101 }
102 }
103 hash
104}
105
106pub struct NearestNeighborQueryPlanner {
112 pub config: PlannerConfig,
114}
115
116impl NearestNeighborQueryPlanner {
117 pub fn new(config: PlannerConfig) -> Self {
119 Self { config }
120 }
121
122 pub fn plan(&self, query_vec: &[f32], k: usize, shards: &[ShardInfo]) -> QueryPlan {
132 let query_id = fnv1a_hash_f32_slice(query_vec);
133
134 let mut candidates: Vec<ShardInfo> = shards
136 .iter()
137 .filter(|s| {
138 s.estimated_latency_ms <= self.config.latency_budget_ms
139 && s.vector_count >= self.config.min_vectors_per_shard
140 })
141 .cloned()
142 .collect();
143
144 if self.config.prefer_local {
146 candidates.sort_by(|a, b| match (a.is_local, b.is_local) {
148 (true, false) => std::cmp::Ordering::Less,
149 (false, true) => std::cmp::Ordering::Greater,
150 _ => a
151 .estimated_latency_ms
152 .partial_cmp(&b.estimated_latency_ms)
153 .unwrap_or(std::cmp::Ordering::Equal),
154 });
155 } else {
156 candidates.sort_by(|a, b| {
157 a.estimated_latency_ms
158 .partial_cmp(&b.estimated_latency_ms)
159 .unwrap_or(std::cmp::Ordering::Equal)
160 });
161 }
162
163 candidates.truncate(self.config.max_fanout);
165
166 let has_local = candidates.iter().any(|s| s.is_local);
168 let remote_peer_ids: Vec<String> = candidates
169 .iter()
170 .filter(|s| !s.is_local)
171 .map(|s| s.peer_id.clone())
172 .collect();
173
174 let strategy = if candidates.is_empty() || (has_local && remote_peer_ids.is_empty()) {
175 ExecutionStrategy::LocalOnly
176 } else if !has_local {
177 ExecutionStrategy::RemoteFanout {
178 peer_ids: remote_peer_ids,
179 }
180 } else {
181 ExecutionStrategy::Hybrid {
182 local: true,
183 peer_ids: remote_peer_ids,
184 }
185 };
186
187 let estimated_latency_ms = candidates
189 .iter()
190 .map(|s| s.estimated_latency_ms)
191 .fold(0.0_f64, f64::max);
192
193 let total_vectors: u64 = candidates.iter().map(|s| s.vector_count).sum();
194 let upper = (k * candidates.len().max(1)) as u64;
195 let raw = upper.min(total_vectors);
196 let estimated_results = (raw as usize).max(k.min(total_vectors as usize));
197
198 QueryPlan {
199 query_id,
200 k,
201 strategy,
202 shards: candidates,
203 estimated_latency_ms,
204 estimated_results,
205 }
206 }
207
208 pub fn explain(&self, plan: &QueryPlan) -> String {
210 let strategy_desc = match &plan.strategy {
211 ExecutionStrategy::LocalOnly => "LocalOnly".to_string(),
212 ExecutionStrategy::RemoteFanout { peer_ids } => {
213 format!("RemoteFanout(peers={})", peer_ids.join(", "))
214 }
215 ExecutionStrategy::Hybrid { local, peer_ids } => {
216 format!("Hybrid(local={}, peers={})", local, peer_ids.join(", "))
217 }
218 ExecutionStrategy::Cached { cache_key } => {
219 format!("Cached(key={cache_key:#x})")
220 }
221 };
222
223 format!(
224 "QueryPlan {{ id={:#x}, k={}, strategy={}, shards={}, \
225 est_latency={:.2}ms, est_results={} }}",
226 plan.query_id,
227 plan.k,
228 strategy_desc,
229 plan.shards.len(),
230 plan.estimated_latency_ms,
231 plan.estimated_results,
232 )
233 }
234
235 pub fn replan_on_failure(&self, plan: &QueryPlan, failed_peer: &str) -> QueryPlan {
240 let surviving: Vec<ShardInfo> = plan
241 .shards
242 .iter()
243 .filter(|s| s.peer_id != failed_peer)
244 .cloned()
245 .collect();
246
247 let has_local = surviving.iter().any(|s| s.is_local);
248 let remote_peer_ids: Vec<String> = surviving
249 .iter()
250 .filter(|s| !s.is_local)
251 .map(|s| s.peer_id.clone())
252 .collect();
253
254 let strategy = if surviving.is_empty() || (has_local && remote_peer_ids.is_empty()) {
255 ExecutionStrategy::LocalOnly
256 } else if !has_local {
257 ExecutionStrategy::RemoteFanout {
258 peer_ids: remote_peer_ids,
259 }
260 } else {
261 ExecutionStrategy::Hybrid {
262 local: true,
263 peer_ids: remote_peer_ids,
264 }
265 };
266
267 let estimated_latency_ms = surviving
268 .iter()
269 .map(|s| s.estimated_latency_ms)
270 .fold(0.0_f64, f64::max);
271
272 let total_vectors: u64 = surviving.iter().map(|s| s.vector_count).sum();
273 let upper = (plan.k * surviving.len().max(1)) as u64;
274 let raw = upper.min(total_vectors);
275 let estimated_results = (raw as usize).max(plan.k.min(total_vectors as usize));
276
277 QueryPlan {
278 query_id: plan.query_id,
279 k: plan.k,
280 strategy,
281 shards: surviving,
282 estimated_latency_ms,
283 estimated_results,
284 }
285 }
286}
287
288#[cfg(test)]
293mod tests {
294 use super::*;
295
296 fn make_local_shard(id: &str, vectors: u64, latency: f64) -> ShardInfo {
297 ShardInfo {
298 shard_id: id.to_string(),
299 peer_id: "local".to_string(),
300 vector_count: vectors,
301 dimension: 128,
302 estimated_latency_ms: latency,
303 is_local: true,
304 }
305 }
306
307 fn make_remote_shard(id: &str, peer: &str, vectors: u64, latency: f64) -> ShardInfo {
308 ShardInfo {
309 shard_id: id.to_string(),
310 peer_id: peer.to_string(),
311 vector_count: vectors,
312 dimension: 128,
313 estimated_latency_ms: latency,
314 is_local: false,
315 }
316 }
317
318 fn default_planner() -> NearestNeighborQueryPlanner {
319 NearestNeighborQueryPlanner::new(PlannerConfig::default())
320 }
321
322 fn query_vec() -> Vec<f32> {
323 vec![0.1, 0.2, 0.3, 0.4]
324 }
325
326 #[test]
328 fn test_plan_empty_shards_local_only() {
329 let planner = default_planner();
330 let plan = planner.plan(&query_vec(), 5, &[]);
331 assert!(matches!(plan.strategy, ExecutionStrategy::LocalOnly));
332 assert!(plan.shards.is_empty());
333 }
334
335 #[test]
337 fn test_plan_single_local_shard() {
338 let planner = default_planner();
339 let shards = vec![make_local_shard("s0", 500, 5.0)];
340 let plan = planner.plan(&query_vec(), 5, &shards);
341 assert!(matches!(plan.strategy, ExecutionStrategy::LocalOnly));
342 assert_eq!(plan.shards.len(), 1);
343 }
344
345 #[test]
347 fn test_plan_single_remote_shard() {
348 let planner = default_planner();
349 let shards = vec![make_remote_shard("s1", "peer-A", 500, 20.0)];
350 let plan = planner.plan(&query_vec(), 5, &shards);
351 assert!(
352 matches!(&plan.strategy, ExecutionStrategy::RemoteFanout { peer_ids } if peer_ids == &["peer-A"])
353 );
354 }
355
356 #[test]
358 fn test_plan_mixed_shards_hybrid() {
359 let planner = default_planner();
360 let shards = vec![
361 make_local_shard("s0", 500, 5.0),
362 make_remote_shard("s1", "peer-B", 500, 30.0),
363 ];
364 let plan = planner.plan(&query_vec(), 5, &shards);
365 assert!(matches!(
366 &plan.strategy,
367 ExecutionStrategy::Hybrid { local: true, .. }
368 ));
369 }
370
371 #[test]
373 fn test_plan_respects_latency_budget() {
374 let config = PlannerConfig {
375 latency_budget_ms: 50.0,
376 ..PlannerConfig::default()
377 };
378 let planner = NearestNeighborQueryPlanner::new(config);
379 let shards = vec![
380 make_local_shard("s0", 500, 40.0),
381 make_remote_shard("s1", "peer-C", 500, 80.0), ];
383 let plan = planner.plan(&query_vec(), 5, &shards);
384 assert_eq!(plan.shards.len(), 1);
385 assert!(plan.shards[0].is_local);
386 }
387
388 #[test]
390 fn test_plan_respects_min_vectors() {
391 let config = PlannerConfig {
392 min_vectors_per_shard: 200,
393 ..PlannerConfig::default()
394 };
395 let planner = NearestNeighborQueryPlanner::new(config);
396 let shards = vec![
397 make_local_shard("s0", 500, 5.0),
398 make_remote_shard("s1", "peer-D", 50, 10.0), ];
400 let plan = planner.plan(&query_vec(), 5, &shards);
401 assert_eq!(plan.shards.len(), 1);
402 assert!(plan.shards[0].is_local);
403 }
404
405 #[test]
407 fn test_plan_respects_max_fanout() {
408 let config = PlannerConfig {
409 max_fanout: 2,
410 ..PlannerConfig::default()
411 };
412 let planner = NearestNeighborQueryPlanner::new(config);
413 let shards: Vec<ShardInfo> = (0..5)
414 .map(|i| {
415 make_remote_shard(&format!("s{i}"), &format!("peer-{i}"), 500, 10.0 + i as f64)
416 })
417 .collect();
418 let plan = planner.plan(&query_vec(), 5, &shards);
419 assert_eq!(plan.shards.len(), 2);
420 }
421
422 #[test]
424 fn test_plan_prefer_local_first() {
425 let planner = default_planner();
426 let shards = vec![
427 make_remote_shard("s1", "peer-E", 500, 5.0), make_local_shard("s0", 500, 20.0),
429 ];
430 let plan = planner.plan(&query_vec(), 5, &shards);
431 assert!(plan.shards[0].is_local, "local shard should be first");
432 }
433
434 #[test]
436 fn test_query_id_deterministic() {
437 let planner = default_planner();
438 let v = vec![1.0_f32, 2.0, 3.0];
439 let p1 = planner.plan(&v, 5, &[]);
440 let p2 = planner.plan(&v, 5, &[]);
441 assert_eq!(p1.query_id, p2.query_id);
442 }
443
444 #[test]
446 fn test_query_id_differs_for_different_vectors() {
447 let planner = default_planner();
448 let p1 = planner.plan(&[1.0_f32, 0.0], 5, &[]);
449 let p2 = planner.plan(&[0.0_f32, 1.0], 5, &[]);
450 assert_ne!(p1.query_id, p2.query_id);
451 }
452
453 #[test]
455 fn test_estimated_latency_is_max() {
456 let planner = default_planner();
457 let shards = vec![
458 make_local_shard("s0", 500, 10.0),
459 make_remote_shard("s1", "peer-F", 500, 45.0),
460 make_remote_shard("s2", "peer-G", 500, 30.0),
461 ];
462 let plan = planner.plan(&query_vec(), 5, &shards);
463 assert!((plan.estimated_latency_ms - 45.0).abs() < 1e-9);
464 }
465
466 #[test]
468 fn test_is_local_only_flag() {
469 let planner = default_planner();
470
471 let local_shards = vec![make_local_shard("s0", 500, 5.0)];
472 let local_plan = planner.plan(&query_vec(), 5, &local_shards);
473 assert!(local_plan.is_local_only());
474
475 let remote_shards = vec![make_remote_shard("s1", "peer-H", 500, 10.0)];
476 let remote_plan = planner.plan(&query_vec(), 5, &remote_shards);
477 assert!(!remote_plan.is_local_only());
478 }
479
480 #[test]
482 fn test_explain_non_empty() {
483 let planner = default_planner();
484 let shards = vec![make_local_shard("s0", 500, 5.0)];
485 let plan = planner.plan(&query_vec(), 5, &shards);
486 let explanation = planner.explain(&plan);
487 assert!(!explanation.is_empty());
488 assert!(explanation.contains("QueryPlan"));
489 }
490
491 #[test]
493 fn test_replan_removes_failed_peer() {
494 let planner = default_planner();
495 let shards = vec![
496 make_local_shard("s0", 500, 5.0),
497 make_remote_shard("s1", "peer-X", 500, 20.0),
498 make_remote_shard("s2", "peer-Y", 500, 25.0),
499 ];
500 let plan = planner.plan(&query_vec(), 5, &shards);
501 let new_plan = planner.replan_on_failure(&plan, "peer-X");
502 assert!(new_plan.shards.iter().all(|s| s.peer_id != "peer-X"));
503 assert_eq!(new_plan.shards.len(), 2);
504 }
505
506 #[test]
508 fn test_replan_updates_strategy() {
509 let planner = default_planner();
510 let shards = vec![
511 make_local_shard("s0", 500, 5.0),
512 make_remote_shard("s1", "peer-Z", 500, 20.0),
513 ];
514 let plan = planner.plan(&query_vec(), 5, &shards);
515 let new_plan = planner.replan_on_failure(&plan, "peer-Z");
517 assert!(matches!(new_plan.strategy, ExecutionStrategy::LocalOnly));
518 }
519
520 #[test]
522 fn test_estimated_results_clamped_to_k_minimum() {
523 let planner = default_planner();
524 let shards = vec![make_local_shard("s0", 200, 5.0)];
525 let plan = planner.plan(&query_vec(), 300, &shards);
528 assert!(plan.estimated_results >= plan.k.min(200));
529 }
530
531 #[test]
533 fn test_all_filtered_returns_local_only_empty() {
534 let config = PlannerConfig {
535 latency_budget_ms: 1.0, ..PlannerConfig::default()
537 };
538 let planner = NearestNeighborQueryPlanner::new(config);
539 let shards = vec![
540 make_local_shard("s0", 500, 50.0),
541 make_remote_shard("s1", "peer-Q", 500, 80.0),
542 ];
543 let plan = planner.plan(&query_vec(), 5, &shards);
544 assert!(matches!(plan.strategy, ExecutionStrategy::LocalOnly));
545 assert!(plan.shards.is_empty());
546 }
547}