1use crate::dht::{
10 ReplicationStrategy, SemanticDHTConfig, SemanticDHTStats, SemanticPeer, SemanticRoutingTable,
11};
12use crate::hnsw::{SearchResult, VectorIndex};
13use futures::future;
14use ipfrs_core::{Cid, Result};
15use ipfrs_network::libp2p::PeerId;
16use parking_lot::RwLock;
17use std::collections::HashMap;
18use std::sync::Arc;
19use std::time::Instant;
20
21pub struct SemanticDHTNode {
23 config: SemanticDHTConfig,
25 local_peer_id: PeerId,
27 local_index: Arc<RwLock<VectorIndex>>,
29 routing_table: Arc<SemanticRoutingTable>,
31 replication_strategy: ReplicationStrategy,
33 stats: Arc<RwLock<SemanticDHTStats>>,
35 pending_queries: Arc<RwLock<HashMap<String, Instant>>>,
37 last_sync_timestamp: Arc<RwLock<u64>>,
39 pending_syncs: Arc<RwLock<usize>>,
41}
42
43impl SemanticDHTNode {
44 pub fn new(config: SemanticDHTConfig, local_peer_id: PeerId, local_index: VectorIndex) -> Self {
46 let routing_table = Arc::new(SemanticRoutingTable::new(config.clone()));
47
48 let stats = SemanticDHTStats {
49 num_peers: 0,
50 num_clusters: 0,
51 num_local_entries: 0,
52 queries_processed: 0,
53 avg_query_latency_ms: 0.0,
54 multi_hop_queries: 0,
55 };
56
57 Self {
58 config,
59 local_peer_id,
60 local_index: Arc::new(RwLock::new(local_index)),
61 routing_table,
62 replication_strategy: ReplicationStrategy::NearestPeers(3),
63 stats: Arc::new(RwLock::new(stats)),
64 pending_queries: Arc::new(RwLock::new(HashMap::new())),
65 last_sync_timestamp: Arc::new(RwLock::new(0)),
66 pending_syncs: Arc::new(RwLock::new(0)),
67 }
68 }
69
70 pub async fn insert(&self, cid: &Cid, embedding: &[f32]) -> Result<()> {
72 self.local_index.write().insert(cid, embedding)?;
74
75 self.update_local_embedding().await?;
77
78 let replica_peers = self.select_replica_peers(embedding).await?;
80
81 for peer in replica_peers {
83 if let Err(e) = self.replicate_to_peer(&peer, cid, embedding).await {
84 tracing::warn!("Replication to {:?} failed: {}", peer, e);
85 }
86 }
87
88 Ok(())
89 }
90
91 pub fn search_local(&self, embedding: &[f32], k: usize) -> Result<Vec<SearchResult>> {
93 let index = self.local_index.read();
94 let ef_search = self.config.max_hops * 10; index.search(embedding, k, ef_search)
96 }
97
98 pub async fn search_distributed(
100 &self,
101 embedding: &[f32],
102 k: usize,
103 ) -> Result<Vec<SearchResult>> {
104 let query_id = format!("{:?}-{}", self.local_peer_id, uuid::Uuid::new_v4());
105 let start_time = Instant::now();
106
107 self.pending_queries
109 .write()
110 .insert(query_id.clone(), start_time);
111
112 let mut all_results = self.search_local(embedding, k)?;
114
115 let nearest_peers = self
117 .routing_table
118 .find_nearest_peers_balanced(embedding, self.config.routing_table_size);
119
120 if !nearest_peers.is_empty() && self.config.max_hops > 0 {
122 let remote_results = self
123 .multi_hop_search(embedding, k, query_id.clone(), 0)
124 .await?;
125 all_results.extend(remote_results);
126 }
127
128 let final_results = self.aggregate_results(all_results, k);
130
131 let latency = start_time.elapsed().as_millis() as f64;
133 self.update_query_stats(latency, !nearest_peers.is_empty());
134
135 self.pending_queries.write().remove(&query_id);
137
138 Ok(final_results)
139 }
140
141 async fn multi_hop_search(
143 &self,
144 embedding: &[f32],
145 _k: usize,
146 _query_id: String,
147 hop: usize,
148 ) -> Result<Vec<SearchResult>> {
149 if hop >= self.config.max_hops {
150 return Ok(Vec::new());
151 }
152
153 let nearest_peers = self.routing_table.find_nearest_peers_balanced(embedding, 3); let mut all_results = Vec::new();
156
157 let peer_futures: Vec<_> = nearest_peers
159 .iter()
160 .filter(|(peer_id, _)| *peer_id != self.local_peer_id)
161 .map(|(peer_id, _)| {
162 let peer_id = *peer_id;
163 async move {
164 tracing::debug!("Querying peer {:?} at hop {}", peer_id, hop);
165 self.query_peer(&peer_id, embedding).await
166 }
167 })
168 .collect();
169
170 let results = future::join_all(peer_futures).await;
171 for peer_results in results.into_iter().flatten() {
173 all_results.extend(peer_results);
174 }
175
176 Ok(all_results)
177 }
178
179 fn aggregate_results(&self, results: Vec<SearchResult>, k: usize) -> Vec<SearchResult> {
181 let mut seen = HashMap::new();
183 let mut deduplicated = Vec::new();
184
185 for result in results {
186 if let Some(&existing_score) = seen.get(&result.cid) {
187 if result.score < existing_score {
189 if let Some(pos) = deduplicated
191 .iter()
192 .position(|r: &SearchResult| r.cid == result.cid)
193 {
194 deduplicated[pos] = result.clone();
195 seen.insert(result.cid, result.score);
196 }
197 }
198 } else {
199 seen.insert(result.cid, result.score);
200 deduplicated.push(result);
201 }
202 }
203
204 deduplicated.sort_by(|a, b| {
206 a.score
207 .partial_cmp(&b.score)
208 .unwrap_or(std::cmp::Ordering::Equal)
209 });
210 deduplicated.into_iter().take(k).collect()
211 }
212
213 async fn select_replica_peers(&self, embedding: &[f32]) -> Result<Vec<PeerId>> {
215 match &self.replication_strategy {
216 ReplicationStrategy::NearestPeers(n) => {
217 let peers = self.routing_table.find_nearest_peers(embedding, *n);
218 Ok(peers.into_iter().map(|(peer_id, _)| peer_id).collect())
219 }
220 ReplicationStrategy::SameCluster => {
221 Ok(Vec::new())
224 }
225 ReplicationStrategy::CrossCluster(_n) => {
226 Ok(Vec::new())
229 }
230 }
231 }
232
233 async fn update_local_embedding(&self) -> Result<()> {
235 let index = self.local_index.read();
236 let dim = self.config.embedding_dim;
237
238 let mut centroid = vec![0.0; dim];
240 let _count = 0;
241
242 drop(index);
245
246 let norm: f32 = centroid.iter().map(|x| x * x).sum::<f32>().sqrt();
248 if norm > 1e-6 {
249 for x in &mut centroid {
250 *x /= norm;
251 }
252 }
253
254 self.routing_table.update_local_embedding(centroid)?;
255
256 Ok(())
257 }
258
259 fn update_query_stats(&self, latency_ms: f64, is_multi_hop: bool) {
261 let mut stats = self.stats.write();
262 stats.queries_processed += 1;
263
264 let alpha = 0.1; stats.avg_query_latency_ms =
267 alpha * latency_ms + (1.0 - alpha) * stats.avg_query_latency_ms;
268
269 if is_multi_hop {
270 stats.multi_hop_queries += 1;
271 }
272 }
273
274 pub fn add_peer(&self, peer: SemanticPeer) -> Result<()> {
276 self.routing_table.add_peer(peer)?;
277
278 let mut stats = self.stats.write();
280 stats.num_peers = self.routing_table.num_peers();
281
282 Ok(())
283 }
284
285 pub fn remove_peer(&self, peer_id: &PeerId) {
287 self.routing_table.remove_peer(peer_id);
288
289 let mut stats = self.stats.write();
291 stats.num_peers = self.routing_table.num_peers();
292 }
293
294 pub fn update_clusters(&self, num_clusters: usize) -> Result<()> {
296 self.routing_table.update_clusters(num_clusters)?;
297
298 let mut stats = self.stats.write();
300 stats.num_clusters = self.routing_table.num_clusters();
301
302 Ok(())
303 }
304
305 pub fn stats(&self) -> SemanticDHTStats {
307 let mut stats = self.stats.read().clone();
308 stats.num_local_entries = self.local_index.read().len();
309 stats
310 }
311
312 pub fn get_stats(&self) -> SemanticDHTStats {
314 self.stats()
315 }
316
317 pub fn routing_table(&self) -> &SemanticRoutingTable {
319 &self.routing_table
320 }
321
322 pub fn set_replication_strategy(&mut self, strategy: ReplicationStrategy) {
324 self.replication_strategy = strategy;
325 }
326
327 pub fn get_index_snapshot(&self) -> Vec<Cid> {
330 let index = self.local_index.read();
331 index.get_all_cids()
333 }
334
335 pub fn has_entry(&self, cid: &Cid) -> bool {
337 let index = self.local_index.read();
338 index.contains(cid)
339 }
340
341 pub fn prepare_sync_delta(&self, peer_snapshot: &[Cid]) -> Vec<Cid> {
344 let local_snapshot = self.get_index_snapshot();
345 let peer_set: std::collections::HashSet<_> = peer_snapshot.iter().collect();
346
347 local_snapshot
348 .into_iter()
349 .filter(|cid| !peer_set.contains(cid))
350 .collect()
351 }
352
353 pub async fn apply_sync_delta(&self, delta_cids: Vec<Cid>) -> Result<usize> {
356 Ok(delta_cids.len())
361 }
362
363 pub async fn apply_sync_delta_with_embeddings(
366 &self,
367 delta_entries: Vec<(Cid, Vec<f32>)>,
368 ) -> Result<usize> {
369 *self.pending_syncs.write() += 1;
371
372 let mut synced_count = 0;
373
374 for (cid, embedding) in delta_entries {
376 match self.local_index.write().insert(&cid, &embedding) {
377 Ok(_) => {
378 synced_count += 1;
379 }
380 Err(e) => {
381 tracing::warn!("Failed to insert CID {:?} during sync: {}", cid, e);
382 }
383 }
384 }
385
386 let now = std::time::SystemTime::now()
388 .duration_since(std::time::UNIX_EPOCH)
389 .unwrap_or_default()
390 .as_secs();
391 *self.last_sync_timestamp.write() = now;
392
393 *self.pending_syncs.write() -= 1;
395
396 self.update_local_embedding().await?;
398
399 tracing::debug!("Synced {} entries from peer", synced_count);
400
401 Ok(synced_count)
402 }
403
404 async fn replicate_to_peer(
410 &self,
411 peer: &PeerId,
412 key: &Cid,
413 value: &[f32],
414 ) -> ipfrs_core::Result<()> {
415 tracing::debug!(
417 "replicate_to_peer: peer={:?} key={} value_len={} (no transport)",
418 peer,
419 key,
420 value.len()
421 );
422 Ok(())
423 }
424
425 async fn query_peer(
431 &self,
432 peer: &PeerId,
433 embedding: &[f32],
434 ) -> Option<Vec<crate::hnsw::SearchResult>> {
435 tracing::debug!(
437 "query_peer: peer={:?} embedding_len={} (no transport)",
438 peer,
439 embedding.len()
440 );
441 None
442 }
443
444 pub fn sync_stats(&self) -> SyncStats {
446 SyncStats {
447 local_entries: self.local_index.read().len(),
448 last_sync_timestamp: *self.last_sync_timestamp.read(),
449 pending_syncs: *self.pending_syncs.read(),
450 }
451 }
452}
453
454#[derive(Debug, Clone)]
456pub struct SyncStats {
457 pub local_entries: usize,
459 pub last_sync_timestamp: u64,
461 pub pending_syncs: usize,
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468 use crate::hnsw::DistanceMetric;
469
470 #[tokio::test]
471 async fn test_dht_node_creation() {
472 let config = SemanticDHTConfig::default();
473 let peer_id = PeerId::random();
474 let index = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
475 .expect("test: VectorIndex creation should succeed");
476
477 let node = SemanticDHTNode::new(config, peer_id, index);
478 let stats = node.stats();
479
480 assert_eq!(stats.num_peers, 0);
481 assert_eq!(stats.queries_processed, 0);
482 }
483
484 #[tokio::test]
485 async fn test_local_insert_and_search() {
486 let config = SemanticDHTConfig::default();
487 let peer_id = PeerId::random();
488 let index = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
489 .expect("test: VectorIndex creation should succeed");
490
491 let node = SemanticDHTNode::new(config, peer_id, index);
492
493 for i in 0..10 {
495 use multihash_codetable::{Code, MultihashDigest};
496 let data = format!("test_vector_{}", i);
497 let hash = Code::Sha2_256.digest(data.as_bytes());
498 let cid = Cid::new_v1(0x55, hash);
499 let embedding = vec![i as f32 * 0.1; 768];
500 node.insert(&cid, &embedding)
501 .await
502 .expect("test: node insert should succeed");
503 }
504
505 let query = vec![0.5; 768];
507 let results = node
508 .search_local(&query, 5)
509 .expect("test: local search should succeed");
510
511 assert!(!results.is_empty());
512 assert!(results.len() <= 5);
513 }
514
515 #[tokio::test]
516 async fn test_add_peers() {
517 let config = SemanticDHTConfig::default();
518 let peer_id = PeerId::random();
519 let index = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
520 .expect("test: VectorIndex creation should succeed");
521
522 let node = SemanticDHTNode::new(config, peer_id, index);
523
524 for i in 0..5 {
526 let peer_id = PeerId::random();
527 let embedding = vec![i as f32 * 0.2; 768];
528 let peer = SemanticPeer::new(peer_id, embedding);
529 node.add_peer(peer).expect("test: add_peer should succeed");
530 }
531
532 let stats = node.stats();
533 assert_eq!(stats.num_peers, 5);
534 }
535
536 #[tokio::test]
537 async fn test_clustering() {
538 let config = SemanticDHTConfig::default();
539 let peer_id = PeerId::random();
540 let index = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
541 .expect("test: VectorIndex creation should succeed");
542
543 let node = SemanticDHTNode::new(config, peer_id, index);
544
545 for i in 0..20 {
547 let peer_id = PeerId::random();
548 let mut embedding = vec![0.0; 768];
549 embedding[0] = if i < 10 { 1.0 } else { -1.0 };
550 let peer = SemanticPeer::new(peer_id, embedding);
551 node.add_peer(peer).expect("test: add_peer should succeed");
552 }
553
554 node.update_clusters(2)
556 .expect("test: update_clusters should succeed");
557
558 let stats = node.stats();
559 assert!(stats.num_clusters > 0);
560 }
561
562 #[tokio::test]
563 async fn test_index_synchronization() {
564 use multihash_codetable::{Code, MultihashDigest};
565
566 let config = SemanticDHTConfig::default();
567 let peer_id1 = PeerId::random();
568 let peer_id2 = PeerId::random();
569
570 let index1 = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
571 .expect("test: VectorIndex creation should succeed");
572 let index2 = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
573 .expect("test: VectorIndex creation should succeed");
574
575 let node1 = SemanticDHTNode::new(config.clone(), peer_id1, index1);
576 let node2 = SemanticDHTNode::new(config, peer_id2, index2);
577
578 let mut cids1 = Vec::new();
580 for i in 0..5 {
581 let data = format!("node1_vector_{}", i);
582 let hash = Code::Sha2_256.digest(data.as_bytes());
583 let cid = Cid::new_v1(0x55, hash);
584 let embedding = vec![i as f32 * 0.1; 768];
585 node1
586 .insert(&cid, &embedding)
587 .await
588 .expect("test: node insert should succeed");
589 cids1.push(cid);
590 }
591
592 let mut cids2 = Vec::new();
594 for i in 5..10 {
595 let data = format!("node2_vector_{}", i);
596 let hash = Code::Sha2_256.digest(data.as_bytes());
597 let cid = Cid::new_v1(0x55, hash);
598 let embedding = vec![i as f32 * 0.1; 768];
599 node2
600 .insert(&cid, &embedding)
601 .await
602 .expect("test: node insert should succeed");
603 cids2.push(cid);
604 }
605
606 let snapshot1 = node1.get_index_snapshot();
608 let snapshot2 = node2.get_index_snapshot();
609
610 assert_eq!(snapshot1.len(), 5);
611 assert_eq!(snapshot2.len(), 5);
612
613 for cid in &cids1 {
615 assert!(node1.has_entry(cid));
616 }
617
618 let delta = node1.prepare_sync_delta(&snapshot2);
620 assert_eq!(delta.len(), 5); let synced_count = node2
624 .apply_sync_delta(delta)
625 .await
626 .expect("test: apply_sync_delta should succeed");
627 assert_eq!(synced_count, 5);
628
629 let sync_stats = node1.sync_stats();
631 assert_eq!(sync_stats.local_entries, 5);
632 }
633
634 #[tokio::test]
635 async fn test_sync_with_embeddings() {
636 use multihash_codetable::{Code, MultihashDigest};
637
638 let config = SemanticDHTConfig::default();
639 let peer_id1 = PeerId::random();
640 let peer_id2 = PeerId::random();
641
642 let index1 = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
643 .expect("test: VectorIndex creation should succeed");
644 let index2 = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
645 .expect("test: VectorIndex creation should succeed");
646
647 let node1 = SemanticDHTNode::new(config.clone(), peer_id1, index1);
648 let node2 = SemanticDHTNode::new(config, peer_id2, index2);
649
650 let mut entries_to_sync = Vec::new();
652 for i in 0..5 {
653 let data = format!("sync_test_vector_{}", i);
654 let hash = Code::Sha2_256.digest(data.as_bytes());
655 let cid = Cid::new_v1(0x55, hash);
656 let embedding = vec![i as f32 * 0.1; 768];
657 node1
658 .insert(&cid, &embedding)
659 .await
660 .expect("test: node insert should succeed");
661 entries_to_sync.push((cid, embedding));
662 }
663
664 let sync_stats_before = node2.sync_stats();
666 assert_eq!(sync_stats_before.local_entries, 0);
667 assert_eq!(sync_stats_before.last_sync_timestamp, 0);
668 assert_eq!(sync_stats_before.pending_syncs, 0);
669
670 let synced_count = node2
672 .apply_sync_delta_with_embeddings(entries_to_sync.clone())
673 .await
674 .expect("test: apply_sync_delta_with_embeddings should succeed");
675 assert_eq!(synced_count, 5);
676
677 let sync_stats_after = node2.sync_stats();
679 assert_eq!(sync_stats_after.local_entries, 5);
680 assert!(sync_stats_after.last_sync_timestamp > 0); assert_eq!(sync_stats_after.pending_syncs, 0); for (cid, _) in &entries_to_sync {
685 assert!(node2.has_entry(cid));
686 }
687
688 let query = vec![0.15; 768];
690 let results = node2
691 .search_local(&query, 3)
692 .expect("test: local search after sync should succeed");
693 assert!(!results.is_empty());
694 }
695
696 #[tokio::test]
697 async fn test_dht_replication_stub() {
698 use multihash_codetable::{Code, MultihashDigest};
699
700 let config = SemanticDHTConfig::default();
701 let peer_id = PeerId::random();
702 let index = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
703 .expect("test: VectorIndex creation should succeed");
704 let node = SemanticDHTNode::new(config, peer_id, index);
705
706 let hash = Code::Sha2_256.digest(b"replication_stub_test");
707 let cid = Cid::new_v1(0x55, hash);
708 let embedding = vec![0.1_f32; 768];
709
710 let target_peer = PeerId::random();
712 let result = node.replicate_to_peer(&target_peer, &cid, &embedding).await;
713 assert!(
714 result.is_ok(),
715 "replicate_to_peer stub should return Ok(())"
716 );
717 }
718
719 #[tokio::test]
720 async fn test_dht_remote_query_stub() {
721 let config = SemanticDHTConfig::default();
722 let peer_id = PeerId::random();
723 let index = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
724 .expect("test: VectorIndex creation should succeed");
725 let node = SemanticDHTNode::new(config, peer_id, index);
726
727 let embedding = vec![0.5_f32; 768];
728 let remote_peer = PeerId::random();
729
730 let result = node.query_peer(&remote_peer, &embedding).await;
732 assert!(
733 result.is_none(),
734 "query_peer stub should return None without a transport"
735 );
736 }
737}