1use cid::Cid;
27use dashmap::DashMap;
28use libp2p::PeerId;
29use multihash_codetable::{Code, MultihashDigest};
30use parking_lot::RwLock;
31use serde::{Deserialize, Serialize};
32use std::cmp::Reverse;
33use std::collections::HashMap;
34use std::sync::Arc;
35use std::time::{Duration, Instant};
36use thiserror::Error;
37
38#[derive(Error, Debug)]
40pub enum SemanticDhtError {
41 #[error("Invalid embedding dimension: expected {expected}, got {actual}")]
42 InvalidDimension { expected: usize, actual: usize },
43
44 #[error("Unknown namespace: {0}")]
45 UnknownNamespace(String),
46
47 #[error("No peers found for embedding region")]
48 NoPeersFound,
49
50 #[error("Query timeout after {0:?}")]
51 QueryTimeout(Duration),
52
53 #[error("Embedding encoding error: {0}")]
54 EncodingError(String),
55
56 #[error("Index not initialized; call register_namespace first")]
58 IndexNotInitialized,
59
60 #[error("Vector dimension mismatch: expected {expected}, got {got}")]
61 VectorDimensionMismatch { expected: usize, got: usize },
62
63 #[error("Routing convergence timed out")]
64 RoutingConvergenceTimeout,
65
66 #[error("Peer unreachable: {0}")]
67 PeerUnreachable(String),
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct VectorAnnotatedRecord {
77 pub cid: String,
79 pub vector: Vec<f32>,
81 pub dimension: usize,
83 pub provider_id: String,
85 pub ttl_secs: u64,
87 pub metadata: HashMap<String, String>,
89}
90
91impl VectorAnnotatedRecord {
92 pub fn new(
94 cid: impl Into<String>,
95 vector: Vec<f32>,
96 provider_id: impl Into<String>,
97 ttl_secs: u64,
98 metadata: HashMap<String, String>,
99 ) -> Self {
100 let dimension = vector.len();
101 Self {
102 cid: cid.into(),
103 vector,
104 dimension,
105 provider_id: provider_id.into(),
106 ttl_secs,
107 metadata,
108 }
109 }
110
111 pub fn is_consistent(&self) -> bool {
114 self.vector.len() == self.dimension
115 }
116}
117
118#[derive(Debug, Clone, Default, Serialize, Deserialize)]
120pub struct SemanticDhtMetrics {
121 pub recall_rate: f32,
123 pub mean_latency_ms: f64,
125 pub routing_convergence: f32,
127 pub indexed_cid_count: usize,
129 pub known_hash_regions: usize,
131 pub cache_hit_ratio: f32,
133 pub partial_sync_count: u64,
135}
136
137#[derive(Debug, Clone)]
139pub struct SemanticDhtConfig {
140 pub lsh_hash_functions: usize,
142
143 pub lsh_hash_tables: usize,
145
146 pub lsh_bucket_width: f32,
148
149 pub max_query_peers: usize,
151
152 pub query_timeout: Duration,
154
155 pub enable_caching: bool,
157
158 pub cache_ttl: Duration,
160
161 pub max_cache_size: usize,
163
164 pub top_k: usize,
166
167 pub dimension: usize,
171
172 pub ef_search: usize,
174
175 pub max_routing_peers: usize,
177
178 pub vector_ttl: Duration,
180
181 pub sync_interval: Duration,
183
184 pub convergence_threshold: f32,
187}
188
189impl Default for SemanticDhtConfig {
190 fn default() -> Self {
191 Self {
192 lsh_hash_functions: 8,
193 lsh_hash_tables: 4,
194 lsh_bucket_width: 4.0,
195 max_query_peers: 20,
196 query_timeout: Duration::from_secs(10),
197 enable_caching: true,
198 cache_ttl: Duration::from_secs(300),
199 max_cache_size: 1000,
200 top_k: 10,
201 dimension: 384,
203 ef_search: 50,
204 max_routing_peers: 20,
205 vector_ttl: Duration::from_secs(3600), sync_interval: Duration::from_secs(300), convergence_threshold: 0.95,
208 }
209 }
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
214pub struct NamespaceId(pub String);
215
216impl NamespaceId {
217 pub fn new(name: impl Into<String>) -> Self {
218 Self(name.into())
219 }
220
221 pub fn text() -> Self {
223 Self("text".to_string())
224 }
225
226 pub fn image() -> Self {
228 Self("image".to_string())
229 }
230
231 pub fn audio() -> Self {
233 Self("audio".to_string())
234 }
235}
236
237#[derive(Debug, Clone)]
239pub struct SemanticNamespace {
240 pub id: NamespaceId,
242
243 pub dimension: usize,
245
246 pub distance_metric: DistanceMetric,
248
249 pub lsh_config: LshConfig,
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
255pub enum DistanceMetric {
256 Euclidean,
258
259 Cosine,
261
262 Manhattan,
264
265 DotProduct,
267}
268
269#[derive(Debug, Clone)]
271pub struct LshConfig {
272 pub hash_functions: usize,
274
275 pub num_tables: usize,
277
278 pub bucket_width: f32,
280}
281
282impl Default for LshConfig {
283 fn default() -> Self {
284 Self {
285 hash_functions: 8,
286 num_tables: 4,
287 bucket_width: 4.0,
288 }
289 }
290}
291
292#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
294pub struct LshHash {
295 pub table: usize,
297
298 pub bucket: Vec<i32>,
300}
301
302impl LshHash {
303 pub fn to_cid(&self) -> Cid {
305 let mut data = Vec::new();
307 data.push(self.table as u8);
308 for &val in &self.bucket {
309 data.extend_from_slice(&val.to_le_bytes());
310 }
311
312 let hash = Code::Sha2_256.digest(&data);
314
315 Cid::new_v1(0x55, hash) }
318}
319
320#[derive(Debug, Clone)]
322pub struct SemanticQuery {
323 pub embedding: Vec<f32>,
325
326 pub namespace: NamespaceId,
328
329 pub top_k: usize,
331
332 pub metadata_filter: Option<HashMap<String, String>>,
334
335 pub timeout: Duration,
337}
338
339#[derive(Debug, Clone)]
341pub struct SemanticResult {
342 pub cid: Cid,
344
345 pub score: f32,
347
348 pub peer: PeerId,
350
351 pub metadata: HashMap<String, String>,
353}
354
355#[derive(Debug, Clone)]
357struct CacheEntry {
358 results: Vec<SemanticResult>,
359 timestamp: Instant,
360}
361
362#[derive(Debug, Clone)]
368pub struct ShardBalancerConfig {
369 pub max_vectors_per_peer: usize,
372 pub rebalance_threshold: f32,
375 pub target_redundancy: usize,
378}
379
380impl Default for ShardBalancerConfig {
381 fn default() -> Self {
382 Self {
383 max_vectors_per_peer: 10_000,
384 rebalance_threshold: 0.8,
385 target_redundancy: 3,
386 }
387 }
388}
389
390pub struct ShardBalancer {
393 config: ShardBalancerConfig,
394 peer_loads: std::collections::HashMap<String, usize>,
396 cid_owners: std::collections::HashMap<String, Vec<String>>,
398 overloaded_peers: std::collections::HashSet<String>,
400}
401
402impl ShardBalancer {
403 pub fn new(config: ShardBalancerConfig) -> Self {
405 Self {
406 config,
407 peer_loads: std::collections::HashMap::new(),
408 cid_owners: std::collections::HashMap::new(),
409 overloaded_peers: std::collections::HashSet::new(),
410 }
411 }
412
413 pub fn record_vector_assignment(&mut self, peer_id: &str, cid: &str) {
419 let load = self.peer_loads.entry(peer_id.to_string()).or_insert(0);
421 *load = load.saturating_add(1);
422
423 let owners = self.cid_owners.entry(cid.to_string()).or_default();
425 if !owners.contains(&peer_id.to_string()) {
426 owners.push(peer_id.to_string());
427 }
428
429 let threshold =
431 (self.config.max_vectors_per_peer as f32 * self.config.rebalance_threshold) as usize;
432 let current_load = self.peer_loads.get(peer_id).copied().unwrap_or(0);
433 if current_load >= threshold {
434 self.overloaded_peers.insert(peer_id.to_string());
435 }
436 }
437
438 pub fn suggest_peers_for_vector(&self, count: usize) -> Vec<String> {
442 if self.peer_loads.is_empty() {
443 return Vec::new();
444 }
445
446 let mut sorted: Vec<(&String, &usize)> = self.peer_loads.iter().collect();
447 sorted.sort_by_key(|(_, &load)| load);
448 sorted
449 .into_iter()
450 .take(count)
451 .map(|(id, _)| id.clone())
452 .collect()
453 }
454
455 pub fn is_overloaded(&self, peer_id: &str) -> bool {
458 self.overloaded_peers.contains(peer_id)
459 }
460
461 pub fn load_distribution(&self) -> std::collections::HashMap<String, f32> {
464 let max = self.config.max_vectors_per_peer as f32;
465 self.peer_loads
466 .iter()
467 .map(|(id, &load)| (id.clone(), (load as f32 / max).min(1.0)))
468 .collect()
469 }
470
471 pub fn vectors_to_migrate(&self) -> Vec<(String, String)> {
478 let threshold =
479 (self.config.max_vectors_per_peer as f32 * self.config.rebalance_threshold) as usize;
480
481 let mut migrations = Vec::new();
482
483 for peer_id in &self.overloaded_peers {
484 let load = self.peer_loads.get(peer_id).copied().unwrap_or(0);
485 if load <= threshold {
486 continue;
487 }
488 let excess = load - threshold;
489
490 let mut candidates: Vec<String> = self
492 .cid_owners
493 .iter()
494 .filter(|(_, owners)| owners.contains(peer_id))
495 .map(|(cid, _)| cid.clone())
496 .take(excess)
497 .collect();
498 candidates.sort(); for cid in candidates.into_iter().take(excess) {
501 migrations.push((cid, peer_id.clone()));
502 }
503 }
504
505 migrations
506 }
507
508 pub fn balance_score(&self) -> f32 {
516 if self.peer_loads.is_empty() {
517 return 1.0; }
519
520 let loads: Vec<f32> = self.peer_loads.values().map(|&l| l as f32).collect();
521 let n = loads.len() as f32;
522
523 let mean = loads.iter().sum::<f32>() / n;
524 if mean == 0.0 {
525 return 1.0; }
527
528 let variance = loads.iter().map(|&l| (l - mean).powi(2)).sum::<f32>() / n;
529 let std_dev = variance.sqrt();
530 let cv = std_dev / mean; (1.0 - cv.min(1.0)).clamp(0.0, 1.0)
535 }
536
537 pub fn assign_vector(&self, vector: &[f32], n_replicas: usize) -> Vec<String> {
548 if self.peer_loads.is_empty() {
549 return Vec::new();
550 }
551
552 let vec_hash: u64 = vector.iter().enumerate().fold(0u64, |acc, (i, &v)| {
554 let bits = v.to_bits() as u64;
555 acc ^ bits.wrapping_mul(
556 (i as u64)
557 .wrapping_add(1)
558 .wrapping_mul(0x9e37_79b9_7f4a_7c15),
559 )
560 });
561
562 let mut peers_scored: Vec<(&String, u64)> = self
563 .peer_loads
564 .iter()
565 .map(|(peer_id, &load)| {
566 let peer_hash: u64 = peer_id
568 .bytes()
569 .fold(0u64, |acc, b| acc.wrapping_mul(131).wrapping_add(b as u64));
570 let distance = vec_hash ^ peer_hash;
571 let cap = self.config.max_vectors_per_peer.max(1) as u64;
573 let load_penalty = (load as u64).saturating_mul(u64::MAX / cap);
574 (peer_id, distance.wrapping_add(load_penalty))
575 })
576 .collect();
577
578 peers_scored.sort_by_key(|(_, score)| *score);
579 peers_scored
580 .into_iter()
581 .take(n_replicas)
582 .map(|(id, _)| id.clone())
583 .collect()
584 }
585
586 pub fn imbalance_score(&self) -> f64 {
592 if self.peer_loads.is_empty() {
593 return 0.0;
594 }
595
596 let loads: Vec<f64> = self.peer_loads.values().map(|&l| l as f64).collect();
597 let n = loads.len() as f64;
598 let mean = loads.iter().sum::<f64>() / n;
599
600 if mean == 0.0 {
601 return 0.0; }
603
604 let variance = loads.iter().map(|&l| (l - mean).powi(2)).sum::<f64>() / n;
605 let cv = variance.sqrt() / mean;
606
607 cv.min(1.0)
608 }
609
610 pub fn migration_plan(&self, limit: usize) -> Vec<(String, String)> {
616 if limit == 0 || self.peer_loads.is_empty() {
617 return Vec::new();
618 }
619
620 let least_loaded = self
622 .peer_loads
623 .iter()
624 .min_by_key(|(_, &l)| l)
625 .map(|(id, _)| id.clone());
626
627 let Some(target_peer) = least_loaded else {
628 return Vec::new();
629 };
630
631 let threshold = (self.config.max_vectors_per_peer as f64
632 * self.config.rebalance_threshold as f64) as usize;
633
634 let mut plan = Vec::new();
635
636 let mut hot: Vec<(&String, usize)> = self
638 .overloaded_peers
639 .iter()
640 .filter_map(|p| self.peer_loads.get(p).map(|&l| (p, l)))
641 .collect();
642 hot.sort_by_key(|b| Reverse(b.1));
643
644 'outer: for (from_peer, load) in hot {
645 if load <= threshold {
646 continue;
647 }
648 let excess = load - threshold;
649
650 let mut candidates: Vec<String> = self
651 .cid_owners
652 .iter()
653 .filter(|(_, owners)| owners.contains(from_peer) && !owners.contains(&target_peer))
654 .map(|(cid, _)| cid.clone())
655 .take(excess)
656 .collect();
657 candidates.sort();
658
659 for cid in candidates {
660 plan.push((cid, target_peer.clone()));
661 if plan.len() >= limit {
662 break 'outer;
663 }
664 }
665 }
666
667 plan
668 }
669
670 pub fn update_peer_capacity(&mut self, peer: &str, capacity: usize) {
676 self.config.max_vectors_per_peer = capacity;
678
679 let threshold = (capacity as f32 * self.config.rebalance_threshold) as usize;
681 let load = self.peer_loads.get(peer).copied().unwrap_or(0);
682 if load >= threshold {
683 self.overloaded_peers.insert(peer.to_string());
684 } else {
685 self.overloaded_peers.remove(peer);
686 }
687 }
688
689 pub fn remove_peer(&mut self, peer: &str) -> Vec<String> {
693 self.peer_loads.remove(peer);
694 self.overloaded_peers.remove(peer);
695
696 let owned: Vec<String> = self
698 .cid_owners
699 .iter()
700 .filter(|(_, owners)| owners.contains(&peer.to_string()))
701 .map(|(cid, _)| cid.clone())
702 .collect();
703
704 for cid in &owned {
706 if let Some(owners) = self.cid_owners.get_mut(cid) {
707 owners.retain(|p| p != peer);
708 }
709 }
710
711 owned
712 }
713
714 pub fn record_migration(&mut self, cid: &str, from_peer: &str, to_peer: &str) {
720 if let Some(load) = self.peer_loads.get_mut(from_peer) {
722 *load = load.saturating_sub(1);
723 }
724
725 let dest_load = self.peer_loads.entry(to_peer.to_string()).or_insert(0);
727 *dest_load = dest_load.saturating_add(1);
728
729 if let Some(owners) = self.cid_owners.get_mut(cid) {
731 owners.retain(|p| p != from_peer);
732 if !owners.contains(&to_peer.to_string()) {
733 owners.push(to_peer.to_string());
734 }
735 }
736
737 let threshold =
739 (self.config.max_vectors_per_peer as f32 * self.config.rebalance_threshold) as usize;
740
741 for peer in [from_peer, to_peer] {
742 let load = self.peer_loads.get(peer).copied().unwrap_or(0);
743 if load >= threshold {
744 self.overloaded_peers.insert(peer.to_string());
745 } else {
746 self.overloaded_peers.remove(peer);
747 }
748 }
749 }
750}
751
752#[derive(Debug, Clone)]
757pub struct PartialSyncConfig {
758 pub sync_threshold: f32,
761 pub batch_size: usize,
764 pub max_rounds: usize,
766}
767
768impl Default for PartialSyncConfig {
769 fn default() -> Self {
770 Self {
771 sync_threshold: 0.05,
772 batch_size: 32,
773 max_rounds: 100,
774 }
775 }
776}
777
778#[derive(Debug, Clone, Default)]
780pub struct PartialSyncStats {
781 pub vectors_synced: usize,
783 pub vectors_skipped: usize,
785 pub bytes_sent: usize,
787 pub rounds_completed: usize,
789}
790
791#[derive(Debug, Clone)]
793pub struct SearchResult {
794 pub cid: String,
796 pub score: f32,
798 pub peer: Option<String>,
800 pub vector_id: String,
802}
803
804#[derive(Debug, Clone, Default)]
806pub struct MergeResult {
807 pub added: usize,
809 pub updated: usize,
812 pub skipped: usize,
814 pub conflicts: usize,
816}
817
818pub struct SemanticDht {
820 config: SemanticDhtConfig,
822
823 namespaces: Arc<DashMap<NamespaceId, SemanticNamespace>>,
825
826 lsh_projections: Arc<DashMap<NamespaceId, Vec<Vec<f32>>>>,
828
829 hash_to_peers: Arc<DashMap<LshHash, Vec<PeerId>>>,
831
832 local_index: Arc<DashMap<Cid, (Vec<f32>, NamespaceId)>>,
834
835 query_cache: Arc<DashMap<Vec<u8>, CacheEntry>>,
837
838 stats: Arc<RwLock<SemanticDhtStats>>,
840
841 vector_records: Arc<DashMap<String, (VectorAnnotatedRecord, Instant)>>,
848
849 routing_version: Arc<RwLock<u64>>,
855
856 last_routing_change: Arc<RwLock<Instant>>,
858
859 pub shard_balancer: parking_lot::Mutex<ShardBalancer>,
863}
864
865#[derive(Debug, Clone, Default, Serialize, Deserialize)]
867pub struct SemanticDhtStats {
868 pub total_queries: u64,
870
871 pub successful_queries: u64,
873
874 pub failed_queries: u64,
876
877 pub cache_hits: u64,
879
880 pub cache_misses: u64,
882
883 pub avg_query_latency_ms: f64,
885
886 pub indexed_content: u64,
888
889 pub queries_per_namespace: HashMap<String, u64>,
891
892 pub vector_puts: u64,
895
896 pub vector_searches: u64,
898
899 pub partial_syncs: u64,
901
902 pub last_convergence_score: f32,
904}
905
906impl SemanticDht {
907 pub fn new(config: SemanticDhtConfig) -> Self {
909 Self {
910 config,
911 namespaces: Arc::new(DashMap::new()),
912 lsh_projections: Arc::new(DashMap::new()),
913 hash_to_peers: Arc::new(DashMap::new()),
914 local_index: Arc::new(DashMap::new()),
915 query_cache: Arc::new(DashMap::new()),
916 stats: Arc::new(RwLock::new(SemanticDhtStats::default())),
917 vector_records: Arc::new(DashMap::new()),
918 routing_version: Arc::new(RwLock::new(0)),
919 last_routing_change: Arc::new(RwLock::new(Instant::now())),
920 shard_balancer: parking_lot::Mutex::new(ShardBalancer::new(
921 ShardBalancerConfig::default(),
922 )),
923 }
924 }
925
926 pub fn register_namespace(&self, namespace: SemanticNamespace) -> Result<(), SemanticDhtError> {
928 let namespace_id = namespace.id.clone();
929
930 let projections = self.generate_lsh_projections(
932 namespace.dimension,
933 namespace.lsh_config.hash_functions,
934 namespace.lsh_config.num_tables,
935 );
936
937 self.lsh_projections
938 .insert(namespace_id.clone(), projections);
939 self.namespaces.insert(namespace_id, namespace);
940
941 Ok(())
942 }
943
944 fn generate_lsh_projections(
946 &self,
947 dimension: usize,
948 hash_functions: usize,
949 num_tables: usize,
950 ) -> Vec<Vec<f32>> {
951 use std::f32::consts::PI;
952
953 let mut projections = Vec::new();
954 let total_projections = hash_functions * num_tables;
955
956 for i in 0..total_projections {
958 let mut projection = Vec::with_capacity(dimension);
959
960 for j in 0..dimension {
961 let seed = (i * dimension + j) as f32;
963 let angle = seed * 2.0 * PI / 1000.0;
964 let value = angle.sin();
965 projection.push(value);
966 }
967
968 let norm: f32 = projection.iter().map(|x| x * x).sum::<f32>().sqrt();
970 if norm > 0.0 {
971 for val in &mut projection {
972 *val /= norm;
973 }
974 }
975
976 projections.push(projection);
977 }
978
979 projections
980 }
981
982 pub fn compute_lsh_hashes(
984 &self,
985 embedding: &[f32],
986 namespace: &NamespaceId,
987 ) -> Result<Vec<LshHash>, SemanticDhtError> {
988 let ns = self
989 .namespaces
990 .get(namespace)
991 .ok_or_else(|| SemanticDhtError::UnknownNamespace(namespace.0.clone()))?;
992
993 if embedding.len() != ns.dimension {
994 return Err(SemanticDhtError::InvalidDimension {
995 expected: ns.dimension,
996 actual: embedding.len(),
997 });
998 }
999
1000 let projections = self
1001 .lsh_projections
1002 .get(namespace)
1003 .ok_or_else(|| SemanticDhtError::UnknownNamespace(namespace.0.clone()))?;
1004
1005 let mut hashes = Vec::new();
1006 let hash_functions = ns.lsh_config.hash_functions;
1007
1008 for table in 0..ns.lsh_config.num_tables {
1009 let mut bucket = Vec::with_capacity(hash_functions);
1010
1011 for func in 0..hash_functions {
1012 let proj_idx = table * hash_functions + func;
1013 let projection = &projections[proj_idx];
1014
1015 let dot_product: f32 = embedding
1017 .iter()
1018 .zip(projection.iter())
1019 .map(|(a, b)| a * b)
1020 .sum();
1021
1022 let quantized = (dot_product / ns.lsh_config.bucket_width).floor() as i32;
1024 bucket.push(quantized);
1025 }
1026
1027 hashes.push(LshHash { table, bucket });
1028 }
1029
1030 Ok(hashes)
1031 }
1032
1033 pub fn index_content(
1035 &self,
1036 cid: Cid,
1037 embedding: Vec<f32>,
1038 namespace: NamespaceId,
1039 ) -> Result<(), SemanticDhtError> {
1040 let ns = self
1042 .namespaces
1043 .get(&namespace)
1044 .ok_or_else(|| SemanticDhtError::UnknownNamespace(namespace.0.clone()))?;
1045
1046 if embedding.len() != ns.dimension {
1047 return Err(SemanticDhtError::InvalidDimension {
1048 expected: ns.dimension,
1049 actual: embedding.len(),
1050 });
1051 }
1052
1053 self.local_index
1055 .insert(cid, (embedding.clone(), namespace.clone()));
1056
1057 let hashes = self.compute_lsh_hashes(&embedding, &namespace)?;
1059
1060 for hash in hashes {
1062 let _ = hash.to_cid();
1064 }
1065
1066 let mut stats = self.stats.write();
1068 stats.indexed_content += 1;
1069
1070 Ok(())
1071 }
1072
1073 pub fn query(&self, query: SemanticQuery) -> Result<Vec<SemanticResult>, SemanticDhtError> {
1075 let start = Instant::now();
1076
1077 if self.config.enable_caching {
1079 let cache_key = self.compute_cache_key(&query);
1080 if let Some(entry) = self.query_cache.get(&cache_key) {
1081 if start.duration_since(entry.timestamp) < self.config.cache_ttl {
1082 let mut stats = self.stats.write();
1083 stats.cache_hits += 1;
1084 return Ok(entry.results.clone());
1085 }
1086 }
1087 }
1088
1089 let _ns = self
1091 .namespaces
1092 .get(&query.namespace)
1093 .ok_or_else(|| SemanticDhtError::UnknownNamespace(query.namespace.0.clone()))?;
1094
1095 let hashes = self.compute_lsh_hashes(&query.embedding, &query.namespace)?;
1097
1098 let mut candidate_peers = Vec::new();
1100 for hash in &hashes {
1101 if let Some(peers) = self.hash_to_peers.get(hash) {
1102 candidate_peers.extend(peers.iter().cloned());
1103 }
1104 }
1105
1106 let mut results = Vec::new();
1108 for entry in self.local_index.iter() {
1109 let (cid, (embedding, ns)) = entry.pair();
1110
1111 if ns != &query.namespace {
1112 continue;
1113 }
1114
1115 let distance = self.compute_distance(&query.embedding, embedding, &query.namespace)?;
1116 let score = 1.0 / (1.0 + distance); results.push(SemanticResult {
1119 cid: *cid,
1120 score,
1121 peer: PeerId::random(), metadata: HashMap::new(),
1123 });
1124 }
1125
1126 results.sort_by(|a, b| {
1128 b.score
1129 .partial_cmp(&a.score)
1130 .unwrap_or(std::cmp::Ordering::Equal)
1131 });
1132 results.truncate(query.top_k);
1133
1134 if self.config.enable_caching {
1136 let cache_key = self.compute_cache_key(&query);
1137 let entry = CacheEntry {
1138 results: results.clone(),
1139 timestamp: start,
1140 };
1141 self.query_cache.insert(cache_key, entry);
1142
1143 self.cleanup_cache();
1145 }
1146
1147 let latency = start.elapsed().as_millis() as f64;
1149 let mut stats = self.stats.write();
1150 stats.total_queries += 1;
1151 stats.successful_queries += 1;
1152 stats.cache_misses += 1;
1153
1154 let alpha = 0.1;
1156 stats.avg_query_latency_ms = alpha * latency + (1.0 - alpha) * stats.avg_query_latency_ms;
1157
1158 *stats
1159 .queries_per_namespace
1160 .entry(query.namespace.0.clone())
1161 .or_insert(0) += 1;
1162
1163 Ok(results)
1164 }
1165
1166 fn compute_distance(
1168 &self,
1169 a: &[f32],
1170 b: &[f32],
1171 namespace: &NamespaceId,
1172 ) -> Result<f32, SemanticDhtError> {
1173 let ns = self
1174 .namespaces
1175 .get(namespace)
1176 .ok_or_else(|| SemanticDhtError::UnknownNamespace(namespace.0.clone()))?;
1177
1178 let distance = match ns.distance_metric {
1179 DistanceMetric::Euclidean => a
1180 .iter()
1181 .zip(b.iter())
1182 .map(|(x, y)| (x - y).powi(2))
1183 .sum::<f32>()
1184 .sqrt(),
1185 DistanceMetric::Cosine => {
1186 let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
1187 let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
1188 let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
1189 1.0 - (dot / (norm_a * norm_b))
1190 }
1191 DistanceMetric::Manhattan => a.iter().zip(b.iter()).map(|(x, y)| (x - y).abs()).sum(),
1192 DistanceMetric::DotProduct => {
1193 -a.iter().zip(b.iter()).map(|(x, y)| x * y).sum::<f32>() }
1195 };
1196
1197 Ok(distance)
1198 }
1199
1200 fn compute_cache_key(&self, query: &SemanticQuery) -> Vec<u8> {
1202 let mut data = Vec::new();
1204 data.extend_from_slice(query.namespace.0.as_bytes());
1205 for &val in &query.embedding {
1206 data.extend_from_slice(&val.to_le_bytes());
1207 }
1208 data
1209 }
1210
1211 fn cleanup_cache(&self) {
1213 if self.query_cache.len() <= self.config.max_cache_size {
1214 return;
1215 }
1216
1217 let now = Instant::now();
1218 let ttl = self.config.cache_ttl;
1219
1220 self.query_cache
1221 .retain(|_, entry| now.duration_since(entry.timestamp) < ttl);
1222 }
1223
1224 pub fn stats(&self) -> SemanticDhtStats {
1226 self.stats.read().clone()
1227 }
1228
1229 pub fn get_namespace(&self, id: &NamespaceId) -> Option<SemanticNamespace> {
1231 self.namespaces.get(id).map(|ns| ns.clone())
1232 }
1233
1234 pub fn list_namespaces(&self) -> Vec<NamespaceId> {
1236 self.namespaces
1237 .iter()
1238 .map(|entry| entry.key().clone())
1239 .collect()
1240 }
1241
1242 pub fn put_with_vector(
1257 &self,
1258 cid: impl Into<String>,
1259 vector: Vec<f32>,
1260 provider_id: impl Into<String>,
1261 ) -> Result<(), SemanticDhtError> {
1262 let expected = self.config.dimension;
1263 if vector.len() != expected {
1264 return Err(SemanticDhtError::VectorDimensionMismatch {
1265 expected,
1266 got: vector.len(),
1267 });
1268 }
1269
1270 let cid_str = cid.into();
1271 let record = VectorAnnotatedRecord::new(
1272 cid_str.clone(),
1273 vector,
1274 provider_id,
1275 self.config.vector_ttl.as_secs(),
1276 HashMap::new(),
1277 );
1278
1279 self.vector_records
1280 .insert(cid_str, (record, Instant::now()));
1281
1282 {
1284 let mut version = self.routing_version.write();
1285 *version = version.saturating_add(1);
1286 }
1287 *self.last_routing_change.write() = Instant::now();
1288
1289 let mut stats = self.stats.write();
1291 stats.vector_puts = stats.vector_puts.saturating_add(1);
1292
1293 Ok(())
1294 }
1295
1296 pub fn search_similar(
1309 &self,
1310 query_vector: &[f32],
1311 k: usize,
1312 ) -> Result<Vec<(String, f32)>, SemanticDhtError> {
1313 let expected = self.config.dimension;
1314 if query_vector.len() != expected {
1315 return Err(SemanticDhtError::VectorDimensionMismatch {
1316 expected,
1317 got: query_vector.len(),
1318 });
1319 }
1320
1321 let now = Instant::now();
1322 let ttl = self.config.vector_ttl;
1323
1324 let query_norm: f32 = query_vector.iter().map(|x| x * x).sum::<f32>().sqrt();
1326 if query_norm == 0.0 {
1327 return Ok(Vec::new());
1329 }
1330
1331 let mut scored: Vec<(String, f32)> = self
1332 .vector_records
1333 .iter()
1334 .filter(|entry| {
1335 now.duration_since(entry.value().1) < ttl
1337 })
1338 .filter_map(|entry| {
1339 let (record, _) = entry.value();
1340 let vec = &record.vector;
1341 if vec.len() != expected {
1342 return None;
1343 }
1344 let dot: f32 = query_vector
1345 .iter()
1346 .zip(vec.iter())
1347 .map(|(a, b)| a * b)
1348 .sum();
1349 let norm_b: f32 = vec.iter().map(|x| x * x).sum::<f32>().sqrt();
1350 if norm_b == 0.0 {
1351 return None;
1352 }
1353 let cosine_sim = dot / (query_norm * norm_b);
1354 Some((record.cid.clone(), cosine_sim))
1355 })
1356 .collect();
1357
1358 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1360 scored.truncate(k);
1361
1362 {
1364 let mut stats = self.stats.write();
1365 stats.vector_searches = stats.vector_searches.saturating_add(1);
1366 }
1367
1368 Ok(scored)
1369 }
1370
1371 pub fn get_routing_convergence(&self) -> f32 {
1380 let elapsed = {
1381 let last_change = self.last_routing_change.read();
1382 last_change.elapsed()
1383 };
1384
1385 let ramp_secs = self.config.sync_interval.as_secs_f32() * 2.0;
1387 let score = (elapsed.as_secs_f32() / ramp_secs).min(1.0);
1388
1389 {
1391 let mut stats = self.stats.write();
1392 stats.last_convergence_score = score;
1393 }
1394
1395 score
1396 }
1397
1398 pub fn efficient_partial_sync(
1415 &self,
1416 peer: &PeerId,
1417 changed_region: &LshHash,
1418 ) -> Result<Vec<String>, SemanticDhtError> {
1419 self.efficient_partial_sync_with_config(
1420 peer,
1421 changed_region,
1422 &PartialSyncConfig::default(),
1423 None,
1424 )
1425 .map(|(cids, _stats)| cids)
1426 }
1427
1428 pub fn efficient_partial_sync_with_config(
1443 &self,
1444 peer: &PeerId,
1445 changed_region: &LshHash,
1446 config: &PartialSyncConfig,
1447 prev_vectors: Option<&HashMap<String, Vec<f32>>>,
1448 ) -> Result<(Vec<String>, PartialSyncStats), SemanticDhtError> {
1449 let region_cid = changed_region.to_cid();
1451 let region_key = region_cid.to_bytes();
1452
1453 let now = Instant::now();
1454 let ttl = self.config.vector_ttl;
1455
1456 let mut synced_cids: Vec<String> = Vec::new();
1457 let mut stats = PartialSyncStats::default();
1458
1459 let bytes_per_record = |dim: usize| -> usize { dim * std::mem::size_of::<f32>() + 64 };
1461
1462 for entry in self.vector_records.iter() {
1463 if now.duration_since(entry.value().1) >= ttl {
1465 continue;
1466 }
1467
1468 let (record, _) = entry.value();
1469 let vec = &record.vector;
1470
1471 let region_byte = {
1473 let key: Vec<u8> = vec
1474 .iter()
1475 .enumerate()
1476 .map(|(i, v)| {
1477 let proj = region_key[i % region_key.len()] as f32 / 255.0;
1478 ((v * proj).abs() * 4.0).floor() as u8
1479 })
1480 .take(4)
1481 .collect();
1482 if key.len() == 4 {
1483 Some(key[0])
1484 } else {
1485 None
1486 }
1487 };
1488 let in_region = region_byte.is_some_and(|b| b == region_key[0 % region_key.len()]);
1489 if !in_region {
1490 continue;
1491 }
1492
1493 let should_sync = if let Some(prev) = prev_vectors.and_then(|m| m.get(&record.cid)) {
1495 if prev.len() != vec.len() {
1496 true } else {
1498 let dot: f32 = prev.iter().zip(vec.iter()).map(|(a, b)| a * b).sum();
1499 let norm_a: f32 = prev.iter().map(|x| x * x).sum::<f32>().sqrt();
1500 let norm_b: f32 = vec.iter().map(|x| x * x).sum::<f32>().sqrt();
1501 let cosine_sim = if norm_a > 0.0 && norm_b > 0.0 {
1502 dot / (norm_a * norm_b)
1503 } else {
1504 0.0
1505 };
1506 let cosine_dist = 1.0 - cosine_sim;
1507 cosine_dist > config.sync_threshold
1508 }
1509 } else {
1510 true };
1512
1513 if should_sync {
1514 stats.bytes_sent += bytes_per_record(vec.len());
1515 synced_cids.push(record.cid.clone());
1516 stats.vectors_synced += 1;
1517 } else {
1518 stats.vectors_skipped += 1;
1519 }
1520 }
1521
1522 stats.rounds_completed = if synced_cids.is_empty() {
1524 0
1525 } else {
1526 synced_cids.len().div_ceil(config.batch_size)
1527 };
1528
1529 {
1531 let mut global_stats = self.stats.write();
1532 global_stats.partial_syncs = global_stats.partial_syncs.saturating_add(1);
1533 }
1534
1535 let _ = peer; Ok((synced_cids, stats))
1537 }
1538
1539 pub fn metrics(&self) -> SemanticDhtMetrics {
1541 let stats = self.stats.read();
1542
1543 let recall_rate = if stats.total_queries == 0 {
1544 0.0
1545 } else {
1546 stats.successful_queries as f32 / stats.total_queries as f32
1547 };
1548
1549 let cache_hit_ratio = {
1550 let total_cache = stats.cache_hits + stats.cache_misses;
1551 if total_cache == 0 {
1552 0.0
1553 } else {
1554 stats.cache_hits as f32 / total_cache as f32
1555 }
1556 };
1557
1558 let elapsed = self.last_routing_change.read().elapsed();
1560 let ramp_secs = self.config.sync_interval.as_secs_f32() * 2.0;
1561 let routing_convergence = (elapsed.as_secs_f32() / ramp_secs).min(1.0);
1562
1563 SemanticDhtMetrics {
1564 recall_rate,
1565 mean_latency_ms: stats.avg_query_latency_ms,
1566 routing_convergence,
1567 indexed_cid_count: self.local_index.len() + self.vector_records.len(),
1568 known_hash_regions: self.hash_to_peers.len(),
1569 cache_hit_ratio,
1570 partial_sync_count: stats.partial_syncs,
1571 }
1572 }
1573
1574 pub fn rebalance_if_needed(&self) -> Vec<(String, String)> {
1583 let balancer = self.shard_balancer.lock();
1585 let score = balancer.balance_score();
1586 let threshold_complement = 1.0 - balancer.config.rebalance_threshold;
1587
1588 if score < threshold_complement || !balancer.overloaded_peers.is_empty() {
1589 balancer.vectors_to_migrate()
1590 } else {
1591 Vec::new()
1592 }
1593 }
1594
1595 pub fn merge_partial_index(
1616 &self,
1617 records: Vec<VectorAnnotatedRecord>,
1618 source_peer: &str,
1619 ) -> MergeResult {
1620 let expected_dim = self.config.dimension;
1621 let now = Instant::now();
1622 let mut result = MergeResult::default();
1623
1624 for mut record in records {
1625 if record.vector.len() != expected_dim {
1627 result.conflicts += 1;
1628 continue;
1629 }
1630
1631 record.dimension = record.vector.len();
1633
1634 if record.provider_id.is_empty() {
1636 record.provider_id = source_peer.to_string();
1637 }
1638
1639 match self.vector_records.get(&record.cid) {
1640 None => {
1641 self.vector_records
1643 .insert(record.cid.clone(), (record, now));
1644 result.added += 1;
1645 }
1646 Some(existing) => {
1647 let (existing_record, _ts) = existing.value();
1648 if existing_record.vector.len() != record.vector.len() {
1649 result.conflicts += 1;
1651 } else if record.ttl_secs > existing_record.ttl_secs {
1652 drop(existing);
1654 self.vector_records
1655 .insert(record.cid.clone(), (record, now));
1656 result.updated += 1;
1657 } else {
1658 result.skipped += 1;
1659 }
1660 }
1661 }
1662 }
1663
1664 if result.added > 0 || result.updated > 0 {
1666 let mut version = self.routing_version.write();
1667 *version = version.saturating_add(1);
1668 *self.last_routing_change.write() = now;
1669
1670 let mut stats = self.stats.write();
1671 stats.partial_syncs = stats.partial_syncs.saturating_add(1);
1672 }
1673
1674 result
1675 }
1676
1677 pub fn evict_expired_records(&self) {
1681 let now = Instant::now();
1682 let ttl = self.config.vector_ttl;
1683 self.vector_records
1684 .retain(|_, (_, inserted)| now.duration_since(*inserted) < ttl);
1685 }
1686
1687 pub async fn distributed_search(
1702 &self,
1703 query: &[f32],
1704 top_k: usize,
1705 timeout_ms: u64,
1706 ) -> Result<Vec<SearchResult>, SemanticDhtError> {
1707 let expected = self.config.dimension;
1708 if query.len() != expected {
1709 return Err(SemanticDhtError::VectorDimensionMismatch {
1710 expected,
1711 got: query.len(),
1712 });
1713 }
1714
1715 let query_norm: f32 = query.iter().map(|x| x * x).sum::<f32>().sqrt();
1717 if query_norm == 0.0 {
1718 return Ok(Vec::new());
1719 }
1720
1721 let now = Instant::now();
1722 let ttl = self.config.vector_ttl;
1723 let deadline = Duration::from_millis(timeout_ms);
1724
1725 let mut results: Vec<SearchResult> = self
1726 .vector_records
1727 .iter()
1728 .filter(|entry| now.duration_since(entry.value().1) < ttl)
1729 .filter_map(|entry| {
1730 if now.elapsed() > deadline {
1732 return None;
1733 }
1734 let (record, _) = entry.value();
1735 let vec = &record.vector;
1736 if vec.len() != expected {
1737 return None;
1738 }
1739 let dot: f32 = query.iter().zip(vec.iter()).map(|(a, b)| a * b).sum();
1740 let norm_b: f32 = vec.iter().map(|x| x * x).sum::<f32>().sqrt();
1741 if norm_b == 0.0 {
1742 return None;
1743 }
1744 let score = dot / (query_norm * norm_b);
1745 Some(SearchResult {
1746 cid: record.cid.clone(),
1747 score,
1748 peer: None,
1749 vector_id: record.cid.clone(),
1750 })
1751 })
1752 .collect();
1753
1754 results.sort_by(|a, b| {
1756 b.score
1757 .partial_cmp(&a.score)
1758 .unwrap_or(std::cmp::Ordering::Equal)
1759 });
1760 results.truncate(top_k);
1761
1762 {
1764 let mut stats = self.stats.write();
1765 stats.vector_searches = stats.vector_searches.saturating_add(1);
1766 }
1767
1768 Ok(results)
1769 }
1770}
1771
1772#[cfg(test)]
1774#[path = "semantic_dht_tests.rs"]
1775mod semantic_dht_tests;