use super::*;
use std::collections::HashSet;
#[test]
fn test_capacity_law_guard_prevents_recall_collapse() {
let dim = 16;
let n = 300;
let mut rng_seed: u64 = 42;
let mut next = || -> f32 {
rng_seed = rng_seed.wrapping_mul(6364136223846793005).wrapping_add(1);
((rng_seed >> 33) as f32) / (u32::MAX as f32) - 0.5
};
let params = HNSWParams {
m: 16,
m_max: 32,
ef_construction: 200,
ef_search: 50,
metric: DistanceMetric::L2,
seed: Some(42),
..Default::default()
};
let mut index = HNSWIndex::with_params(dim, params).unwrap();
let mut all_vecs = Vec::new();
for i in 0..n {
let v: Vec<f32> = (0..dim).map(|_| next()).collect();
all_vecs.push(v.clone());
index.add(i as u32, v).unwrap();
}
index.build().unwrap();
let query: Vec<f32> = (0..dim).map(|_| next()).collect();
let k = 20;
let results = index.search(&query, k, 5).unwrap();
let mut gt: Vec<(u32, f32)> = all_vecs
.iter()
.enumerate()
.map(|(i, v)| (i as u32, crate::distance::l2_distance(&query, v)))
.collect();
gt.sort_unstable_by(|a, b| a.1.total_cmp(&b.1));
let gt_set: HashSet<u32> = gt.iter().take(k).map(|&(id, _)| id).collect();
let result_ids: HashSet<u32> = results.iter().map(|&(doc_id, _)| doc_id).collect();
let recall = gt_set.intersection(&result_ids).count() as f32 / k as f32;
assert!(
recall > 0.3,
"Capacity-law guard should prevent collapse. recall={:.1}%",
recall * 100.0
);
}
#[cfg(feature = "parallel")]
#[test]
fn test_build_parallel_recall() {
let dim = 32;
let n = 500;
let k = 10;
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let params = HNSWParams {
m: 16,
m_max: 32,
ef_construction: 200,
metric: DistanceMetric::L2,
seed: Some(42),
..Default::default()
};
let mut index = HNSWIndex::with_params(dim, params).unwrap();
let mut all_vecs = Vec::new();
for i in 0..n {
let v: Vec<f32> = (0..dim)
.map(|_| {
use rand::Rng;
rng.random::<f32>() * 2.0 - 1.0
})
.collect();
all_vecs.push(v.clone());
index.add(i as u32, v).unwrap();
}
index.build_parallel(64).unwrap();
let query = &all_vecs[0];
let results = index.search(query, k, 100).unwrap();
let mut gt: Vec<(u32, f32)> = all_vecs
.iter()
.enumerate()
.map(|(i, v)| (i as u32, crate::distance::l2_distance(query, v)))
.collect();
gt.sort_unstable_by(|a, b| a.1.total_cmp(&b.1));
let gt_ids: HashSet<u32> = gt.iter().take(k).map(|(id, _)| *id).collect();
let result_ids: HashSet<u32> = results.iter().map(|(id, _)| *id).collect();
let recall = gt_ids.intersection(&result_ids).count() as f32 / k as f32;
assert!(
recall >= 0.5,
"Parallel build recall@{k} = {:.1}%, expected >= 50%",
recall * 100.0
);
}