1use std::collections::HashMap;
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::sync::{Arc, Mutex, RwLock};
17use std::time::{Duration, Instant};
18
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
25pub struct QueryKey {
26 pub query_hash: u64,
28 pub top_k: usize,
30 pub ef: usize,
32}
33
34impl QueryKey {
35 pub fn new(query_vec: &[f32], top_k: usize, ef: usize) -> Self {
37 let query_hash = fnv1a_hash_f32_slice(query_vec);
38 Self {
39 query_hash,
40 top_k,
41 ef,
42 }
43 }
44}
45
46fn fnv1a_hash_f32_slice(values: &[f32]) -> u64 {
48 const FNV_OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
49 const FNV_PRIME: u64 = 1_099_511_628_211;
50
51 let mut hash = FNV_OFFSET_BASIS;
52 for &v in values {
53 for byte in v.to_le_bytes() {
54 hash ^= u64::from(byte);
55 hash = hash.wrapping_mul(FNV_PRIME);
56 }
57 }
58 hash
59}
60
61#[derive(Debug, Clone)]
67pub struct SearchResult {
68 pub cid: String,
70 pub score: f32,
72 pub peer_id: String,
74 pub metadata: HashMap<String, String>,
76}
77
78#[derive(Debug, Clone)]
84pub struct SearchPeer {
85 pub peer_id: String,
87 pub multiaddr: String,
89 pub estimated_latency_ms: u64,
91 pub index_size: u64,
93 pub last_seen: Instant,
95}
96
97impl SearchPeer {
98 pub fn new(peer_id: impl Into<String>, multiaddr: impl Into<String>) -> Self {
100 Self {
101 peer_id: peer_id.into(),
102 multiaddr: multiaddr.into(),
103 estimated_latency_ms: 50,
104 index_size: 0,
105 last_seen: Instant::now(),
106 }
107 }
108}
109
110#[derive(Debug, Clone)]
116pub struct CachedSearchResult {
117 pub results: Vec<SearchResult>,
119 pub cached_at: Instant,
121 pub ttl: Duration,
123}
124
125impl CachedSearchResult {
126 pub fn is_expired(&self, now: Instant) -> bool {
128 now.duration_since(self.cached_at) > self.ttl
129 }
130}
131
132pub struct FederatedSearchStats {
138 pub total_queries: AtomicU64,
140 pub total_cache_hits: AtomicU64,
142 pub total_peers_queried: AtomicU64,
144 pub total_results_merged: AtomicU64,
146 pub total_deduped: AtomicU64,
148}
149
150impl FederatedSearchStats {
151 fn new() -> Self {
152 Self {
153 total_queries: AtomicU64::new(0),
154 total_cache_hits: AtomicU64::new(0),
155 total_peers_queried: AtomicU64::new(0),
156 total_results_merged: AtomicU64::new(0),
157 total_deduped: AtomicU64::new(0),
158 }
159 }
160
161 pub fn snapshot(&self) -> FederatedSearchStatsSnapshot {
163 FederatedSearchStatsSnapshot {
164 total_queries: self.total_queries.load(Ordering::Relaxed),
165 total_cache_hits: self.total_cache_hits.load(Ordering::Relaxed),
166 total_peers_queried: self.total_peers_queried.load(Ordering::Relaxed),
167 total_results_merged: self.total_results_merged.load(Ordering::Relaxed),
168 total_deduped: self.total_deduped.load(Ordering::Relaxed),
169 }
170 }
171}
172
173impl std::fmt::Debug for FederatedSearchStats {
174 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175 f.debug_struct("FederatedSearchStats")
176 .field("total_queries", &self.total_queries.load(Ordering::Relaxed))
177 .field(
178 "total_cache_hits",
179 &self.total_cache_hits.load(Ordering::Relaxed),
180 )
181 .field(
182 "total_peers_queried",
183 &self.total_peers_queried.load(Ordering::Relaxed),
184 )
185 .field(
186 "total_results_merged",
187 &self.total_results_merged.load(Ordering::Relaxed),
188 )
189 .field("total_deduped", &self.total_deduped.load(Ordering::Relaxed))
190 .finish()
191 }
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct FederatedSearchStatsSnapshot {
197 pub total_queries: u64,
198 pub total_cache_hits: u64,
199 pub total_peers_queried: u64,
200 pub total_results_merged: u64,
201 pub total_deduped: u64,
202}
203
204pub struct FederatedSearchCoordinator {
211 known_peers: RwLock<Vec<SearchPeer>>,
213 result_cache: Mutex<HashMap<QueryKey, CachedSearchResult>>,
215 pub cache_ttl: Duration,
217 pub max_peers_per_query: usize,
219 stats: Arc<FederatedSearchStats>,
221}
222
223impl FederatedSearchCoordinator {
224 pub fn new() -> Self {
229 Self {
230 known_peers: RwLock::new(Vec::new()),
231 result_cache: Mutex::new(HashMap::new()),
232 cache_ttl: Duration::from_secs(30),
233 max_peers_per_query: 8,
234 stats: Arc::new(FederatedSearchStats::new()),
235 }
236 }
237
238 pub fn with_config(cache_ttl: Duration, max_peers_per_query: usize) -> Self {
240 Self {
241 known_peers: RwLock::new(Vec::new()),
242 result_cache: Mutex::new(HashMap::new()),
243 cache_ttl,
244 max_peers_per_query,
245 stats: Arc::new(FederatedSearchStats::new()),
246 }
247 }
248
249 pub fn register_peer(&self, peer: SearchPeer) {
255 let mut peers = match self.known_peers.write() {
256 Ok(g) => g,
257 Err(poisoned) => poisoned.into_inner(),
258 };
259 if let Some(existing) = peers.iter_mut().find(|p| p.peer_id == peer.peer_id) {
261 *existing = peer;
262 } else {
263 peers.push(peer);
264 }
265 }
266
267 pub fn unregister_peer(&self, peer_id: &str) {
269 let mut peers = match self.known_peers.write() {
270 Ok(g) => g,
271 Err(poisoned) => poisoned.into_inner(),
272 };
273 peers.retain(|p| p.peer_id != peer_id);
274 }
275
276 pub fn peer_count(&self) -> usize {
278 let peers = match self.known_peers.read() {
279 Ok(g) => g,
280 Err(poisoned) => poisoned.into_inner(),
281 };
282 peers.len()
283 }
284
285 pub fn select_peers_for_query(&self, _top_k: usize) -> Vec<SearchPeer> {
295 let peers = match self.known_peers.read() {
296 Ok(g) => g,
297 Err(poisoned) => poisoned.into_inner(),
298 };
299
300 let mut selected: Vec<SearchPeer> = peers.clone();
301 selected.sort_by_key(|p| p.estimated_latency_ms);
302 selected.truncate(self.max_peers_per_query);
303 selected
304 }
305
306 pub fn merge_results(
316 &self,
317 results: Vec<Vec<SearchResult>>,
318 top_k: usize,
319 ) -> Vec<SearchResult> {
320 let mut best: HashMap<String, SearchResult> = HashMap::new();
322 let mut total_before_dedup: u64 = 0;
323
324 for list in results {
325 for item in list {
326 total_before_dedup += 1;
327 match best.get(&item.cid) {
328 Some(existing) if existing.score >= item.score => {
329 }
331 _ => {
332 best.insert(item.cid.clone(), item);
333 }
334 }
335 }
336 }
337
338 let total_after_dedup = best.len() as u64;
339 let deduped = total_before_dedup.saturating_sub(total_after_dedup);
340
341 self.stats
342 .total_results_merged
343 .fetch_add(total_before_dedup, Ordering::Relaxed);
344 self.stats
345 .total_deduped
346 .fetch_add(deduped, Ordering::Relaxed);
347
348 let mut merged: Vec<SearchResult> = best.into_values().collect();
350 merged.sort_by(|a, b| {
351 b.score
352 .partial_cmp(&a.score)
353 .unwrap_or(std::cmp::Ordering::Equal)
354 });
355 merged.truncate(top_k);
356 merged
357 }
358
359 pub fn cache_result(&self, key: QueryKey, results: Vec<SearchResult>) {
365 let entry = CachedSearchResult {
366 results,
367 cached_at: Instant::now(),
368 ttl: self.cache_ttl,
369 };
370 let mut cache = match self.result_cache.lock() {
371 Ok(g) => g,
372 Err(poisoned) => poisoned.into_inner(),
373 };
374 cache.insert(key, entry);
375 }
376
377 pub fn get_cached(&self, key: &QueryKey) -> Option<Vec<SearchResult>> {
380 let now = Instant::now();
381 let cache = match self.result_cache.lock() {
382 Ok(g) => g,
383 Err(poisoned) => poisoned.into_inner(),
384 };
385 cache
386 .get(key)
387 .filter(|entry| !entry.is_expired(now))
388 .map(|entry| {
389 self.stats.total_cache_hits.fetch_add(1, Ordering::Relaxed);
390 entry.results.clone()
391 })
392 }
393
394 pub fn evict_expired_cache(&self) {
396 let now = Instant::now();
397 let mut cache = match self.result_cache.lock() {
398 Ok(g) => g,
399 Err(poisoned) => poisoned.into_inner(),
400 };
401 cache.retain(|_key, entry| !entry.is_expired(now));
402 }
403
404 pub fn search<F>(
433 &self,
434 query: &[f32],
435 top_k: usize,
436 ef: usize,
437 fetch_fn: F,
438 ) -> Vec<SearchResult>
439 where
440 F: Fn(&SearchPeer) -> Vec<SearchResult>,
441 {
442 self.stats.total_queries.fetch_add(1, Ordering::Relaxed);
443
444 let key = QueryKey::new(query, top_k, ef);
445
446 if let Some(cached) = self.get_cached(&key) {
448 return cached;
449 }
450
451 let peers = self.select_peers_for_query(top_k);
452 self.stats
453 .total_peers_queried
454 .fetch_add(peers.len() as u64, Ordering::Relaxed);
455
456 let per_peer_results: Vec<Vec<SearchResult>> = peers.iter().map(fetch_fn).collect();
457
458 let merged = self.merge_results(per_peer_results, top_k);
459 self.cache_result(key, merged.clone());
460 merged
461 }
462
463 pub fn stats(&self) -> FederatedSearchStatsSnapshot {
469 self.stats.snapshot()
470 }
471}
472
473impl Default for FederatedSearchCoordinator {
474 fn default() -> Self {
475 Self::new()
476 }
477}
478
479impl std::fmt::Debug for FederatedSearchCoordinator {
480 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
481 let peer_count = self.peer_count();
482 let cache_len = self.result_cache.lock().map(|g| g.len()).unwrap_or(0);
483 f.debug_struct("FederatedSearchCoordinator")
484 .field("peer_count", &peer_count)
485 .field("cache_entries", &cache_len)
486 .field("cache_ttl", &self.cache_ttl)
487 .field("max_peers_per_query", &self.max_peers_per_query)
488 .field("stats", &self.stats)
489 .finish()
490 }
491}
492
493#[cfg(test)]
498mod tests {
499 use super::*;
500 use std::time::Duration;
501
502 fn make_peer(id: &str, latency: u64) -> SearchPeer {
507 let mut p = SearchPeer::new(id, format!("/ip4/127.0.0.1/tcp/{}", 4000 + latency));
508 p.estimated_latency_ms = latency;
509 p
510 }
511
512 fn make_result(cid: &str, score: f32, peer_id: &str) -> SearchResult {
513 SearchResult {
514 cid: cid.to_string(),
515 score,
516 peer_id: peer_id.to_string(),
517 metadata: HashMap::new(),
518 }
519 }
520
521 #[test]
526 fn test_register_peer_increases_count() {
527 let coord = FederatedSearchCoordinator::new();
528 assert_eq!(coord.peer_count(), 0);
529 coord.register_peer(make_peer("p1", 10));
530 assert_eq!(coord.peer_count(), 1);
531 coord.register_peer(make_peer("p2", 20));
532 assert_eq!(coord.peer_count(), 2);
533 }
534
535 #[test]
540 fn test_unregister_peer_decreases_count() {
541 let coord = FederatedSearchCoordinator::new();
542 coord.register_peer(make_peer("p1", 10));
543 coord.register_peer(make_peer("p2", 20));
544 coord.unregister_peer("p1");
545 assert_eq!(coord.peer_count(), 1);
546 }
547
548 #[test]
549 fn test_unregister_nonexistent_peer_noop() {
550 let coord = FederatedSearchCoordinator::new();
551 coord.register_peer(make_peer("p1", 10));
552 coord.unregister_peer("does_not_exist");
553 assert_eq!(coord.peer_count(), 1);
554 }
555
556 #[test]
561 fn test_peer_count_reflects_all_operations() {
562 let coord = FederatedSearchCoordinator::new();
563 for i in 0..5u64 {
564 coord.register_peer(make_peer(&format!("p{}", i), i * 5));
565 }
566 assert_eq!(coord.peer_count(), 5);
567 coord.unregister_peer("p2");
568 coord.unregister_peer("p4");
569 assert_eq!(coord.peer_count(), 3);
570 }
571
572 #[test]
577 fn test_select_peers_respects_max() {
578 let coord = FederatedSearchCoordinator::with_config(Duration::from_secs(30), 3);
579 for i in 0..10u64 {
580 coord.register_peer(make_peer(&format!("p{}", i), i * 5));
581 }
582 let selected = coord.select_peers_for_query(10);
583 assert_eq!(selected.len(), 3);
584 }
585
586 #[test]
591 fn test_select_peers_sorted_by_latency() {
592 let coord = FederatedSearchCoordinator::with_config(Duration::from_secs(30), 5);
593 coord.register_peer(make_peer("slow", 200));
595 coord.register_peer(make_peer("medium", 100));
596 coord.register_peer(make_peer("fast", 10));
597
598 let selected = coord.select_peers_for_query(5);
599 assert_eq!(selected[0].peer_id, "fast");
600 assert_eq!(selected[1].peer_id, "medium");
601 assert_eq!(selected[2].peer_id, "slow");
602 }
603
604 #[test]
609 fn test_merge_results_deduplicates_by_cid() {
610 let coord = FederatedSearchCoordinator::new();
611 let list_a = vec![
612 make_result("cid1", 0.9, "p1"),
613 make_result("cid2", 0.7, "p1"),
614 ];
615 let list_b = vec![
616 make_result("cid1", 0.8, "p2"), make_result("cid3", 0.6, "p2"),
618 ];
619 let merged = coord.merge_results(vec![list_a, list_b], 10);
620 let cid1_results: Vec<_> = merged.iter().filter(|r| r.cid == "cid1").collect();
621 assert_eq!(cid1_results.len(), 1);
623 assert!((cid1_results[0].score - 0.9).abs() < f32::EPSILON);
625 }
626
627 #[test]
632 fn test_merge_results_top_k_sorted_descending() {
633 let coord = FederatedSearchCoordinator::new();
634 let list = vec![
635 make_result("c1", 0.3, "p1"),
636 make_result("c2", 0.9, "p1"),
637 make_result("c3", 0.6, "p1"),
638 make_result("c4", 0.1, "p1"),
639 make_result("c5", 0.8, "p1"),
640 ];
641 let merged = coord.merge_results(vec![list], 3);
642 assert_eq!(merged.len(), 3);
643 assert!(merged[0].score >= merged[1].score);
644 assert!(merged[1].score >= merged[2].score);
645 assert_eq!(merged[0].cid, "c2");
647 }
648
649 #[test]
654 fn test_merge_results_empty_input() {
655 let coord = FederatedSearchCoordinator::new();
656 let merged = coord.merge_results(vec![], 10);
657 assert!(merged.is_empty());
658 }
659
660 #[test]
665 fn test_merge_results_all_empty_lists() {
666 let coord = FederatedSearchCoordinator::new();
667 let merged = coord.merge_results(vec![vec![], vec![], vec![]], 10);
668 assert!(merged.is_empty());
669 }
670
671 #[test]
676 fn test_query_key_hash_stability() {
677 let vec_a = vec![0.1_f32, 0.2, 0.3, 0.4];
678 let key1 = QueryKey::new(&vec_a, 5, 64);
679 let key2 = QueryKey::new(&vec_a, 5, 64);
680 assert_eq!(key1, key2);
681 assert_eq!(key1.query_hash, key2.query_hash);
682 }
683
684 #[test]
685 fn test_query_key_different_inputs_differ() {
686 let vec_a = vec![0.1_f32, 0.2, 0.3];
687 let vec_b = vec![0.9_f32, 0.8, 0.7];
688 let key_a = QueryKey::new(&vec_a, 5, 64);
689 let key_b = QueryKey::new(&vec_b, 5, 64);
690 assert_ne!(key_a.query_hash, key_b.query_hash);
691 }
692
693 #[test]
698 fn test_cache_hit_on_repeated_query() {
699 let coord = FederatedSearchCoordinator::new();
700 let results = vec![make_result("cid1", 0.95, "p1")];
701 let key = QueryKey::new(&[0.5_f32; 4], 5, 64);
702
703 assert!(coord.get_cached(&key).is_none());
704 coord.cache_result(key.clone(), results);
705 let cached = coord.get_cached(&key);
706 assert!(cached.is_some());
707 let list = cached.expect("cache entry must exist");
708 assert_eq!(list.len(), 1);
709 assert_eq!(list[0].cid, "cid1");
710 }
711
712 #[test]
717 fn test_cache_eviction_on_ttl_expiry() {
718 let coord = FederatedSearchCoordinator::with_config(Duration::from_nanos(1), 8);
720 let key = QueryKey::new(&[0.1_f32; 4], 5, 64);
721 coord.cache_result(key.clone(), vec![make_result("cid_expired", 0.5, "p1")]);
722
723 let deadline = std::time::Instant::now() + Duration::from_millis(10);
725 while std::time::Instant::now() < deadline {}
726
727 assert!(coord.get_cached(&key).is_none());
729 }
730
731 #[test]
732 fn test_evict_expired_cache_removes_stale_entries() {
733 let coord = FederatedSearchCoordinator::with_config(Duration::from_nanos(1), 8);
734 let key1 = QueryKey::new(&[0.1_f32; 4], 5, 64);
735 let key2 = QueryKey::new(&[0.2_f32; 4], 5, 64);
736 coord.cache_result(key1.clone(), vec![make_result("c1", 0.5, "p1")]);
737 coord.cache_result(key2.clone(), vec![make_result("c2", 0.6, "p1")]);
738
739 let deadline = std::time::Instant::now() + Duration::from_millis(10);
741 while std::time::Instant::now() < deadline {}
742
743 coord.evict_expired_cache();
744
745 let cache_len = coord
746 .result_cache
747 .lock()
748 .map(|g| g.len())
749 .unwrap_or(usize::MAX);
750 assert_eq!(cache_len, 0);
751 }
752
753 #[test]
758 fn test_stats_accumulation() {
759 let coord = FederatedSearchCoordinator::new();
760 coord.register_peer(make_peer("p1", 10));
761 coord.register_peer(make_peer("p2", 20));
762
763 let query = vec![0.5_f32; 4];
764
765 coord.search(&query, 3, 64, |_peer| {
767 vec![make_result("c1", 0.9, "p1"), make_result("c2", 0.5, "p2")]
768 });
769
770 coord.search(&query, 3, 64, |_peer| vec![]);
772
773 let snap = coord.stats();
774 assert_eq!(snap.total_queries, 2);
775 assert_eq!(snap.total_cache_hits, 1);
776 assert_eq!(snap.total_peers_queried, 2);
778 assert_eq!(snap.total_results_merged, 4);
780 }
781
782 #[test]
787 fn test_register_peer_upsert_does_not_duplicate() {
788 let coord = FederatedSearchCoordinator::new();
789 coord.register_peer(make_peer("p1", 50));
790 let mut updated = make_peer("p1", 10);
792 updated.index_size = 9999;
793 coord.register_peer(updated);
794 assert_eq!(coord.peer_count(), 1);
796 let selected = coord.select_peers_for_query(1);
798 assert_eq!(selected[0].estimated_latency_ms, 10);
799 assert_eq!(selected[0].index_size, 9999);
800 }
801
802 #[test]
807 fn test_select_peers_fewer_than_max() {
808 let coord = FederatedSearchCoordinator::with_config(Duration::from_secs(30), 8);
809 coord.register_peer(make_peer("only_peer", 15));
810 let selected = coord.select_peers_for_query(10);
811 assert_eq!(selected.len(), 1);
812 }
813
814 #[test]
819 fn test_cached_result_is_expired() {
820 let entry = CachedSearchResult {
821 results: vec![],
822 cached_at: Instant::now() - Duration::from_secs(60),
823 ttl: Duration::from_secs(30),
824 };
825 assert!(entry.is_expired(Instant::now()));
826
827 let fresh = CachedSearchResult {
828 results: vec![],
829 cached_at: Instant::now(),
830 ttl: Duration::from_secs(30),
831 };
832 assert!(!fresh.is_expired(Instant::now()));
833 }
834}