use crate::dht::{
ReplicationStrategy, SemanticDHTConfig, SemanticDHTStats, SemanticPeer, SemanticRoutingTable,
};
use crate::hnsw::{SearchResult, VectorIndex};
use futures::future;
use ipfrs_core::{Cid, Result};
use ipfrs_network::libp2p::PeerId;
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
pub struct SemanticDHTNode {
config: SemanticDHTConfig,
local_peer_id: PeerId,
local_index: Arc<RwLock<VectorIndex>>,
routing_table: Arc<SemanticRoutingTable>,
replication_strategy: ReplicationStrategy,
stats: Arc<RwLock<SemanticDHTStats>>,
pending_queries: Arc<RwLock<HashMap<String, Instant>>>,
last_sync_timestamp: Arc<RwLock<u64>>,
pending_syncs: Arc<RwLock<usize>>,
}
impl SemanticDHTNode {
pub fn new(config: SemanticDHTConfig, local_peer_id: PeerId, local_index: VectorIndex) -> Self {
let routing_table = Arc::new(SemanticRoutingTable::new(config.clone()));
let stats = SemanticDHTStats {
num_peers: 0,
num_clusters: 0,
num_local_entries: 0,
queries_processed: 0,
avg_query_latency_ms: 0.0,
multi_hop_queries: 0,
};
Self {
config,
local_peer_id,
local_index: Arc::new(RwLock::new(local_index)),
routing_table,
replication_strategy: ReplicationStrategy::NearestPeers(3),
stats: Arc::new(RwLock::new(stats)),
pending_queries: Arc::new(RwLock::new(HashMap::new())),
last_sync_timestamp: Arc::new(RwLock::new(0)),
pending_syncs: Arc::new(RwLock::new(0)),
}
}
pub async fn insert(&self, cid: &Cid, embedding: &[f32]) -> Result<()> {
self.local_index.write().insert(cid, embedding)?;
self.update_local_embedding().await?;
let replica_peers = self.select_replica_peers(embedding).await?;
for peer in replica_peers {
if let Err(e) = self.replicate_to_peer(&peer, cid, embedding).await {
tracing::warn!("Replication to {:?} failed: {}", peer, e);
}
}
Ok(())
}
pub fn search_local(&self, embedding: &[f32], k: usize) -> Result<Vec<SearchResult>> {
let index = self.local_index.read();
let ef_search = self.config.max_hops * 10; index.search(embedding, k, ef_search)
}
pub async fn search_distributed(
&self,
embedding: &[f32],
k: usize,
) -> Result<Vec<SearchResult>> {
let query_id = format!("{:?}-{}", self.local_peer_id, uuid::Uuid::new_v4());
let start_time = Instant::now();
self.pending_queries
.write()
.insert(query_id.clone(), start_time);
let mut all_results = self.search_local(embedding, k)?;
let nearest_peers = self
.routing_table
.find_nearest_peers_balanced(embedding, self.config.routing_table_size);
if !nearest_peers.is_empty() && self.config.max_hops > 0 {
let remote_results = self
.multi_hop_search(embedding, k, query_id.clone(), 0)
.await?;
all_results.extend(remote_results);
}
let final_results = self.aggregate_results(all_results, k);
let latency = start_time.elapsed().as_millis() as f64;
self.update_query_stats(latency, !nearest_peers.is_empty());
self.pending_queries.write().remove(&query_id);
Ok(final_results)
}
async fn multi_hop_search(
&self,
embedding: &[f32],
_k: usize,
_query_id: String,
hop: usize,
) -> Result<Vec<SearchResult>> {
if hop >= self.config.max_hops {
return Ok(Vec::new());
}
let nearest_peers = self.routing_table.find_nearest_peers_balanced(embedding, 3);
let mut all_results = Vec::new();
let peer_futures: Vec<_> = nearest_peers
.iter()
.filter(|(peer_id, _)| *peer_id != self.local_peer_id)
.map(|(peer_id, _)| {
let peer_id = *peer_id;
async move {
tracing::debug!("Querying peer {:?} at hop {}", peer_id, hop);
self.query_peer(&peer_id, embedding).await
}
})
.collect();
let results = future::join_all(peer_futures).await;
for peer_results in results.into_iter().flatten() {
all_results.extend(peer_results);
}
Ok(all_results)
}
fn aggregate_results(&self, results: Vec<SearchResult>, k: usize) -> Vec<SearchResult> {
let mut seen = HashMap::new();
let mut deduplicated = Vec::new();
for result in results {
if let Some(&existing_score) = seen.get(&result.cid) {
if result.score < existing_score {
if let Some(pos) = deduplicated
.iter()
.position(|r: &SearchResult| r.cid == result.cid)
{
deduplicated[pos] = result.clone();
seen.insert(result.cid, result.score);
}
}
} else {
seen.insert(result.cid, result.score);
deduplicated.push(result);
}
}
deduplicated.sort_by(|a, b| {
a.score
.partial_cmp(&b.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
deduplicated.into_iter().take(k).collect()
}
async fn select_replica_peers(&self, embedding: &[f32]) -> Result<Vec<PeerId>> {
match &self.replication_strategy {
ReplicationStrategy::NearestPeers(n) => {
let peers = self.routing_table.find_nearest_peers(embedding, *n);
Ok(peers.into_iter().map(|(peer_id, _)| peer_id).collect())
}
ReplicationStrategy::SameCluster => {
Ok(Vec::new())
}
ReplicationStrategy::CrossCluster(_n) => {
Ok(Vec::new())
}
}
}
async fn update_local_embedding(&self) -> Result<()> {
let index = self.local_index.read();
let dim = self.config.embedding_dim;
let mut centroid = vec![0.0; dim];
let _count = 0;
drop(index);
let norm: f32 = centroid.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 1e-6 {
for x in &mut centroid {
*x /= norm;
}
}
self.routing_table.update_local_embedding(centroid)?;
Ok(())
}
fn update_query_stats(&self, latency_ms: f64, is_multi_hop: bool) {
let mut stats = self.stats.write();
stats.queries_processed += 1;
let alpha = 0.1; stats.avg_query_latency_ms =
alpha * latency_ms + (1.0 - alpha) * stats.avg_query_latency_ms;
if is_multi_hop {
stats.multi_hop_queries += 1;
}
}
pub fn add_peer(&self, peer: SemanticPeer) -> Result<()> {
self.routing_table.add_peer(peer)?;
let mut stats = self.stats.write();
stats.num_peers = self.routing_table.num_peers();
Ok(())
}
pub fn remove_peer(&self, peer_id: &PeerId) {
self.routing_table.remove_peer(peer_id);
let mut stats = self.stats.write();
stats.num_peers = self.routing_table.num_peers();
}
pub fn update_clusters(&self, num_clusters: usize) -> Result<()> {
self.routing_table.update_clusters(num_clusters)?;
let mut stats = self.stats.write();
stats.num_clusters = self.routing_table.num_clusters();
Ok(())
}
pub fn stats(&self) -> SemanticDHTStats {
let mut stats = self.stats.read().clone();
stats.num_local_entries = self.local_index.read().len();
stats
}
pub fn get_stats(&self) -> SemanticDHTStats {
self.stats()
}
pub fn routing_table(&self) -> &SemanticRoutingTable {
&self.routing_table
}
pub fn set_replication_strategy(&mut self, strategy: ReplicationStrategy) {
self.replication_strategy = strategy;
}
pub fn get_index_snapshot(&self) -> Vec<Cid> {
let index = self.local_index.read();
index.get_all_cids()
}
pub fn has_entry(&self, cid: &Cid) -> bool {
let index = self.local_index.read();
index.contains(cid)
}
pub fn prepare_sync_delta(&self, peer_snapshot: &[Cid]) -> Vec<Cid> {
let local_snapshot = self.get_index_snapshot();
let peer_set: std::collections::HashSet<_> = peer_snapshot.iter().collect();
local_snapshot
.into_iter()
.filter(|cid| !peer_set.contains(cid))
.collect()
}
pub async fn apply_sync_delta(&self, delta_cids: Vec<Cid>) -> Result<usize> {
Ok(delta_cids.len())
}
pub async fn apply_sync_delta_with_embeddings(
&self,
delta_entries: Vec<(Cid, Vec<f32>)>,
) -> Result<usize> {
*self.pending_syncs.write() += 1;
let mut synced_count = 0;
for (cid, embedding) in delta_entries {
match self.local_index.write().insert(&cid, &embedding) {
Ok(_) => {
synced_count += 1;
}
Err(e) => {
tracing::warn!("Failed to insert CID {:?} during sync: {}", cid, e);
}
}
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
*self.last_sync_timestamp.write() = now;
*self.pending_syncs.write() -= 1;
self.update_local_embedding().await?;
tracing::debug!("Synced {} entries from peer", synced_count);
Ok(synced_count)
}
async fn replicate_to_peer(
&self,
peer: &PeerId,
key: &Cid,
value: &[f32],
) -> ipfrs_core::Result<()> {
tracing::debug!(
"replicate_to_peer: peer={:?} key={} value_len={} (no transport)",
peer,
key,
value.len()
);
Ok(())
}
async fn query_peer(
&self,
peer: &PeerId,
embedding: &[f32],
) -> Option<Vec<crate::hnsw::SearchResult>> {
tracing::debug!(
"query_peer: peer={:?} embedding_len={} (no transport)",
peer,
embedding.len()
);
None
}
pub fn sync_stats(&self) -> SyncStats {
SyncStats {
local_entries: self.local_index.read().len(),
last_sync_timestamp: *self.last_sync_timestamp.read(),
pending_syncs: *self.pending_syncs.read(),
}
}
}
#[derive(Debug, Clone)]
pub struct SyncStats {
pub local_entries: usize,
pub last_sync_timestamp: u64,
pub pending_syncs: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hnsw::DistanceMetric;
#[tokio::test]
async fn test_dht_node_creation() {
let config = SemanticDHTConfig::default();
let peer_id = PeerId::random();
let index = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
.expect("test: VectorIndex creation should succeed");
let node = SemanticDHTNode::new(config, peer_id, index);
let stats = node.stats();
assert_eq!(stats.num_peers, 0);
assert_eq!(stats.queries_processed, 0);
}
#[tokio::test]
async fn test_local_insert_and_search() {
let config = SemanticDHTConfig::default();
let peer_id = PeerId::random();
let index = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
.expect("test: VectorIndex creation should succeed");
let node = SemanticDHTNode::new(config, peer_id, index);
for i in 0..10 {
use multihash_codetable::{Code, MultihashDigest};
let data = format!("test_vector_{}", i);
let hash = Code::Sha2_256.digest(data.as_bytes());
let cid = Cid::new_v1(0x55, hash);
let embedding = vec![i as f32 * 0.1; 768];
node.insert(&cid, &embedding)
.await
.expect("test: node insert should succeed");
}
let query = vec![0.5; 768];
let results = node
.search_local(&query, 5)
.expect("test: local search should succeed");
assert!(!results.is_empty());
assert!(results.len() <= 5);
}
#[tokio::test]
async fn test_add_peers() {
let config = SemanticDHTConfig::default();
let peer_id = PeerId::random();
let index = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
.expect("test: VectorIndex creation should succeed");
let node = SemanticDHTNode::new(config, peer_id, index);
for i in 0..5 {
let peer_id = PeerId::random();
let embedding = vec![i as f32 * 0.2; 768];
let peer = SemanticPeer::new(peer_id, embedding);
node.add_peer(peer).expect("test: add_peer should succeed");
}
let stats = node.stats();
assert_eq!(stats.num_peers, 5);
}
#[tokio::test]
async fn test_clustering() {
let config = SemanticDHTConfig::default();
let peer_id = PeerId::random();
let index = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
.expect("test: VectorIndex creation should succeed");
let node = SemanticDHTNode::new(config, peer_id, index);
for i in 0..20 {
let peer_id = PeerId::random();
let mut embedding = vec![0.0; 768];
embedding[0] = if i < 10 { 1.0 } else { -1.0 };
let peer = SemanticPeer::new(peer_id, embedding);
node.add_peer(peer).expect("test: add_peer should succeed");
}
node.update_clusters(2)
.expect("test: update_clusters should succeed");
let stats = node.stats();
assert!(stats.num_clusters > 0);
}
#[tokio::test]
async fn test_index_synchronization() {
use multihash_codetable::{Code, MultihashDigest};
let config = SemanticDHTConfig::default();
let peer_id1 = PeerId::random();
let peer_id2 = PeerId::random();
let index1 = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
.expect("test: VectorIndex creation should succeed");
let index2 = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
.expect("test: VectorIndex creation should succeed");
let node1 = SemanticDHTNode::new(config.clone(), peer_id1, index1);
let node2 = SemanticDHTNode::new(config, peer_id2, index2);
let mut cids1 = Vec::new();
for i in 0..5 {
let data = format!("node1_vector_{}", i);
let hash = Code::Sha2_256.digest(data.as_bytes());
let cid = Cid::new_v1(0x55, hash);
let embedding = vec![i as f32 * 0.1; 768];
node1
.insert(&cid, &embedding)
.await
.expect("test: node insert should succeed");
cids1.push(cid);
}
let mut cids2 = Vec::new();
for i in 5..10 {
let data = format!("node2_vector_{}", i);
let hash = Code::Sha2_256.digest(data.as_bytes());
let cid = Cid::new_v1(0x55, hash);
let embedding = vec![i as f32 * 0.1; 768];
node2
.insert(&cid, &embedding)
.await
.expect("test: node insert should succeed");
cids2.push(cid);
}
let snapshot1 = node1.get_index_snapshot();
let snapshot2 = node2.get_index_snapshot();
assert_eq!(snapshot1.len(), 5);
assert_eq!(snapshot2.len(), 5);
for cid in &cids1 {
assert!(node1.has_entry(cid));
}
let delta = node1.prepare_sync_delta(&snapshot2);
assert_eq!(delta.len(), 5);
let synced_count = node2
.apply_sync_delta(delta)
.await
.expect("test: apply_sync_delta should succeed");
assert_eq!(synced_count, 5);
let sync_stats = node1.sync_stats();
assert_eq!(sync_stats.local_entries, 5);
}
#[tokio::test]
async fn test_sync_with_embeddings() {
use multihash_codetable::{Code, MultihashDigest};
let config = SemanticDHTConfig::default();
let peer_id1 = PeerId::random();
let peer_id2 = PeerId::random();
let index1 = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
.expect("test: VectorIndex creation should succeed");
let index2 = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
.expect("test: VectorIndex creation should succeed");
let node1 = SemanticDHTNode::new(config.clone(), peer_id1, index1);
let node2 = SemanticDHTNode::new(config, peer_id2, index2);
let mut entries_to_sync = Vec::new();
for i in 0..5 {
let data = format!("sync_test_vector_{}", i);
let hash = Code::Sha2_256.digest(data.as_bytes());
let cid = Cid::new_v1(0x55, hash);
let embedding = vec![i as f32 * 0.1; 768];
node1
.insert(&cid, &embedding)
.await
.expect("test: node insert should succeed");
entries_to_sync.push((cid, embedding));
}
let sync_stats_before = node2.sync_stats();
assert_eq!(sync_stats_before.local_entries, 0);
assert_eq!(sync_stats_before.last_sync_timestamp, 0);
assert_eq!(sync_stats_before.pending_syncs, 0);
let synced_count = node2
.apply_sync_delta_with_embeddings(entries_to_sync.clone())
.await
.expect("test: apply_sync_delta_with_embeddings should succeed");
assert_eq!(synced_count, 5);
let sync_stats_after = node2.sync_stats();
assert_eq!(sync_stats_after.local_entries, 5);
assert!(sync_stats_after.last_sync_timestamp > 0); assert_eq!(sync_stats_after.pending_syncs, 0);
for (cid, _) in &entries_to_sync {
assert!(node2.has_entry(cid));
}
let query = vec![0.15; 768];
let results = node2
.search_local(&query, 3)
.expect("test: local search after sync should succeed");
assert!(!results.is_empty());
}
#[tokio::test]
async fn test_dht_replication_stub() {
use multihash_codetable::{Code, MultihashDigest};
let config = SemanticDHTConfig::default();
let peer_id = PeerId::random();
let index = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
.expect("test: VectorIndex creation should succeed");
let node = SemanticDHTNode::new(config, peer_id, index);
let hash = Code::Sha2_256.digest(b"replication_stub_test");
let cid = Cid::new_v1(0x55, hash);
let embedding = vec![0.1_f32; 768];
let target_peer = PeerId::random();
let result = node.replicate_to_peer(&target_peer, &cid, &embedding).await;
assert!(
result.is_ok(),
"replicate_to_peer stub should return Ok(())"
);
}
#[tokio::test]
async fn test_dht_remote_query_stub() {
let config = SemanticDHTConfig::default();
let peer_id = PeerId::random();
let index = VectorIndex::new(768, DistanceMetric::Cosine, 16, 200)
.expect("test: VectorIndex creation should succeed");
let node = SemanticDHTNode::new(config, peer_id, index);
let embedding = vec![0.5_f32; 768];
let remote_peer = PeerId::random();
let result = node.query_peer(&remote_peer, &embedding).await;
assert!(
result.is_none(),
"query_peer stub should return None without a transport"
);
}
}