#![allow(
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
clippy::redundant_closure_for_method_calls
)]
use super::index::{HnswIndex, VacuumError};
use super::params::{HnswParams, SearchQuality};
use crate::distance::DistanceMetric;
use crate::index::VectorIndex;
#[test]
fn test_tombstone_count_empty_index() {
let index = HnswIndex::new(64, DistanceMetric::Cosine).unwrap();
assert_eq!(index.tombstone_count(), 0);
assert!((index.tombstone_ratio() - 0.0).abs() < f64::EPSILON);
assert!(!index.needs_vacuum());
}
#[test]
fn test_tombstone_count_after_deletions() {
let index = HnswIndex::new(64, DistanceMetric::Cosine).unwrap();
for i in 0..10 {
let v: Vec<f32> = (0..64).map(|j| (i + j) as f32 * 0.01).collect();
index.insert(i as u64, &v);
}
index.remove(1);
index.remove(3);
index.remove(5);
assert_eq!(index.len(), 7);
assert_eq!(index.tombstone_count(), 3);
assert!((index.tombstone_ratio() - 0.3).abs() < 0.01);
assert!(index.needs_vacuum()); }
#[test]
fn test_vacuum_rebuilds_index() {
let index = HnswIndex::new(64, DistanceMetric::Cosine).unwrap();
for i in 0..20 {
let v: Vec<f32> = (0..64).map(|j| (i + j) as f32 * 0.01).collect();
index.insert(i as u64, &v);
}
for i in 0..10 {
index.remove(i as u64);
}
assert_eq!(index.len(), 10);
assert!(index.needs_vacuum());
let result = index.vacuum();
assert!(result.is_ok());
assert_eq!(result.unwrap(), 10);
assert_eq!(index.len(), 10);
assert_eq!(index.tombstone_count(), 0);
assert!(!index.needs_vacuum());
}
#[test]
fn test_vacuum_preserves_search_results() {
let index = HnswIndex::new(64, DistanceMetric::Cosine).unwrap();
for i in 0..50 {
let v: Vec<f32> = (0..64).map(|j| (i * 100 + j) as f32 * 0.001).collect();
index.insert(i as u64, &v);
}
for i in 0..25 {
index.remove(i as u64);
}
let query: Vec<f32> = (0..64).map(|j| (30 * 100 + j) as f32 * 0.001).collect();
let _results_before = index.search(&query, 5);
let _ = index.vacuum();
let results_after = index.search(&query, 5);
assert_eq!(results_after.len(), 5);
for sr in &results_after {
assert!(sr.id >= 25 && sr.id < 50);
}
}
#[test]
fn test_drop_after_vacuum_and_reload_is_safe() {
use tempfile::tempdir;
let dir = tempdir().expect("Failed to create temp dir");
{
let index = HnswIndex::new(32, DistanceMetric::Euclidean).unwrap();
for i in 0u64..120 {
let vector: Vec<f32> = (0..32).map(|j| (i + j as u64) as f32 * 0.01).collect();
index.insert(i, &vector);
}
for i in 0u64..40 {
index.remove(i);
}
let rebuilt = index.vacuum().expect("vacuum should succeed");
assert_eq!(rebuilt, 80);
index.save(dir.path()).expect("Failed to save");
}
let loaded = HnswIndex::load(dir.path(), 32, DistanceMetric::Euclidean)
.expect("Failed to load after vacuum+drop");
let query = vec![0.42; 32];
let results = loaded.search(&query, 10);
assert!(!results.is_empty(), "Loaded index should remain searchable");
}
#[test]
fn test_vacuum_fails_with_fast_insert_mode() {
let index = HnswIndex::new_fast_insert(64, DistanceMetric::Cosine).unwrap();
for i in 0..10 {
let v: Vec<f32> = (0..64).map(|j| (i + j) as f32 * 0.01).collect();
index.insert(i as u64, &v);
}
let result = index.vacuum();
assert!(result.is_err());
assert_eq!(result.unwrap_err(), VacuumError::VectorStorageDisabled);
}
#[test]
fn test_vacuum_empty_index() {
let index = HnswIndex::new(64, DistanceMetric::Cosine).unwrap();
let result = index.vacuum();
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0);
}
#[test]
fn test_hnsw_new_creates_empty_index() {
let index = HnswIndex::new(768, DistanceMetric::Cosine).unwrap();
assert!(index.is_empty());
assert_eq!(index.len(), 0);
assert_eq!(index.dimension(), 768);
assert_eq!(index.metric(), DistanceMetric::Cosine);
}
#[test]
fn test_hnsw_new_turbo_mode() {
let index = HnswIndex::new_turbo(64, DistanceMetric::Cosine).unwrap();
for i in 0..100 {
let v: Vec<f32> = (0..64).map(|j| (i + j) as f32 * 0.01).collect();
index.insert(i as u64, &v);
}
assert_eq!(index.len(), 100);
let query: Vec<f32> = (0..64).map(|j| j as f32 * 0.01).collect();
let results = index.search(&query, 10);
assert!(!results.is_empty()); }
#[test]
fn test_hnsw_new_fast_insert_mode() {
let index = HnswIndex::new_fast_insert(64, DistanceMetric::Cosine).unwrap();
for i in 0..100 {
let v: Vec<f32> = (0..64).map(|j| (i + j) as f32 * 0.01).collect();
index.insert(i as u64, &v);
}
assert_eq!(index.len(), 100);
let query: Vec<f32> = (0..64).map(|j| j as f32 * 0.01).collect();
let results = index.search(&query, 10);
assert_eq!(results.len(), 10);
}
#[test]
fn test_hnsw_insert_single_vector() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
let vector = vec![1.0, 0.0, 0.0];
index.insert(1, &vector);
assert_eq!(index.len(), 1);
assert!(!index.is_empty());
}
#[test]
fn test_hnsw_insert_multiple_vectors() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
index.insert(1, &[1.0, 0.0, 0.0]);
index.insert(2, &[0.0, 1.0, 0.0]);
index.insert(3, &[0.0, 0.0, 1.0]);
assert_eq!(index.len(), 3);
}
#[test]
fn test_hnsw_search_returns_k_nearest() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
index.insert(1, &[1.0, 0.0, 0.0]);
index.insert(2, &[0.9, 0.1, 0.0]); index.insert(3, &[0.0, 1.0, 0.0]); index.insert(4, &[0.8, 0.2, 0.0]); index.insert(5, &[0.0, 0.0, 1.0]);
let results = index.search(&[1.0, 0.0, 0.0], 3);
assert!(
!results.is_empty() && results.len() <= 3,
"Should return 1-3 results, got {}",
results.len()
);
let top_ids: Vec<u64> = results.iter().map(|sr| sr.id).collect();
assert!(top_ids.contains(&1), "Exact match should be in top results");
}
#[test]
fn test_hnsw_search_empty_index() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
let results = index.search(&[1.0, 0.0, 0.0], 10);
assert!(results.is_empty());
}
#[test]
fn test_hnsw_remove_existing_vector() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
index.insert(1, &[1.0, 0.0, 0.0]);
index.insert(2, &[0.0, 1.0, 0.0]);
let removed = index.remove(1);
assert!(removed);
assert_eq!(index.len(), 1);
}
#[test]
fn test_hnsw_remove_nonexistent_vector() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
index.insert(1, &[1.0, 0.0, 0.0]);
let removed = index.remove(999);
assert!(!removed);
assert_eq!(index.len(), 1);
}
#[test]
fn test_hnsw_euclidean_metric() {
let index = HnswIndex::new(3, DistanceMetric::Euclidean).unwrap();
index.insert(1, &[0.0, 0.0, 0.0]);
index.insert(2, &[1.0, 0.0, 0.0]); index.insert(3, &[3.0, 4.0, 0.0]); index.insert(4, &[2.0, 0.0, 0.0]); index.insert(5, &[0.5, 0.5, 0.0]);
let results = index.search(&[0.0, 0.0, 0.0], 3);
assert!(!results.is_empty(), "Should return results");
assert_eq!(results[0].id, 1, "Closest should be exact match");
}
#[test]
fn test_hnsw_dot_product_metric() {
let index = HnswIndex::new(3, DistanceMetric::DotProduct).unwrap();
index.insert(1, &[1.0, 0.0, 0.0]); index.insert(2, &[0.5, 0.5, 0.5]); index.insert(3, &[0.1, 0.1, 0.1]); index.insert(4, &[0.8, 0.2, 0.0]); index.insert(5, &[0.3, 0.3, 0.3]);
let query = [1.0, 0.0, 0.0];
let results = index.search(&query, 3);
assert!(!results.is_empty(), "Should return results");
assert_eq!(results[0].id, 1, "Highest dot product should be first");
}
#[test]
#[should_panic(expected = "Vector dimension mismatch")]
fn test_hnsw_insert_wrong_dimension_panics() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
index.insert(1, &[1.0, 0.0]); }
#[test]
#[should_panic(expected = "Query dimension mismatch")]
fn test_hnsw_search_wrong_dimension_panics() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
index.insert(1, &[1.0, 0.0, 0.0]);
let _ = index.search(&[1.0, 0.0], 10); }
#[test]
fn test_hnsw_duplicate_insert_upserts_vector() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
index.insert(1, &[1.0, 0.0, 0.0]);
index.insert(1, &[0.0, 1.0, 0.0]);
assert_eq!(index.len(), 1);
let results = index.search(&[0.0, 1.0, 0.0], 1);
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, 1);
assert!(
results[0].score > 0.9,
"Updated vector should be indexed, got score {}",
results[0].score,
);
}
#[test]
fn test_hnsw_thread_safety() {
use std::sync::Arc;
use std::thread;
let index = Arc::new(HnswIndex::new(3, DistanceMetric::Cosine).unwrap());
let mut handles = vec![];
for i in 0..10 {
let index_clone = Arc::clone(&index);
handles.push(thread::spawn(move || {
#[allow(clippy::cast_precision_loss)]
index_clone.insert(i, &[i as f32, 0.0, 0.0]);
}));
}
for handle in handles {
handle.join().expect("Thread panicked");
}
index.set_searching_mode();
assert_eq!(index.len(), 10);
}
#[test]
fn test_hnsw_persistence() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
index.insert(1, &[1.0, 0.0, 0.0]);
index.insert(2, &[0.0, 1.0, 0.0]);
index.save(dir.path()).unwrap();
let loaded_index = HnswIndex::load(dir.path(), 3, DistanceMetric::Cosine).unwrap();
assert_eq!(loaded_index.len(), 2);
assert_eq!(loaded_index.dimension(), 3);
assert_eq!(loaded_index.metric(), DistanceMetric::Cosine);
let results = loaded_index.search(&[1.0, 0.0, 0.0], 1);
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, 1);
}
#[test]
fn test_hnsw_load_legacy_snapshot_without_vectors_disables_vacuum() {
use std::fs;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
index.insert(1, &[1.0, 0.0, 0.0]);
index.insert(2, &[0.0, 1.0, 0.0]);
index.save(dir.path()).unwrap();
fs::remove_file(dir.path().join("native_vectors.bin")).unwrap();
let loaded_index = HnswIndex::load(dir.path(), 3, DistanceMetric::Cosine).unwrap();
assert_eq!(loaded_index.len(), 2);
assert!(!loaded_index.has_vector_storage());
assert_eq!(
loaded_index.vacuum(),
Err(VacuumError::VectorStorageDisabled)
);
assert_eq!(loaded_index.len(), 2);
}
#[test]
fn test_hnsw_fast_insert_save_does_not_persist_vectors_file() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let index = HnswIndex::new_fast_insert(3, DistanceMetric::Cosine).unwrap();
index.insert(1, &[1.0, 0.0, 0.0]);
index.insert(2, &[0.0, 1.0, 0.0]);
index.save(dir.path()).unwrap();
assert!(!dir.path().join("native_vectors.bin").exists());
let loaded = HnswIndex::load(dir.path(), 3, DistanceMetric::Cosine).unwrap();
assert!(!loaded.has_vector_storage());
}
#[test]
fn test_hnsw_fast_insert_save_removes_stale_vectors_file() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let regular = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
regular.insert(1, &[1.0, 0.0, 0.0]);
regular.save(dir.path()).unwrap();
assert!(dir.path().join("native_vectors.bin").exists());
let fast = HnswIndex::new_fast_insert(3, DistanceMetric::Cosine).unwrap();
fast.insert(2, &[0.0, 1.0, 0.0]);
fast.save(dir.path()).unwrap();
assert!(!dir.path().join("native_vectors.bin").exists());
}
#[test]
fn test_hnsw_insert_batch_parallel() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
let vectors: Vec<(u64, Vec<f32>)> = vec![
(1, vec![1.0, 0.0, 0.0]),
(2, vec![0.0, 1.0, 0.0]),
(3, vec![0.0, 0.0, 1.0]),
(4, vec![0.5, 0.5, 0.0]),
(5, vec![0.5, 0.0, 0.5]),
];
let inserted = index.insert_batch_parallel(vectors.iter().map(|(id, v)| (*id, v.as_slice())));
index.set_searching_mode();
assert_eq!(inserted, 5);
assert_eq!(index.len(), 5);
let results = index.search(&[1.0, 0.0, 0.0], 3);
assert_eq!(results.len(), 3);
let result_ids: Vec<u64> = results.iter().map(|r| r.id).collect();
assert!(result_ids.contains(&1), "ID 1 should be in top 3 results");
}
#[test]
fn test_hnsw_insert_batch_parallel_upserts_duplicates() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
index.insert(1, &[1.0, 0.0, 0.0]);
let vectors: Vec<(u64, Vec<f32>)> = vec![
(1, vec![0.0, 1.0, 0.0]), (2, vec![0.0, 0.0, 1.0]), ];
let inserted = index.insert_batch_parallel(vectors.iter().map(|(id, v)| (*id, v.as_slice())));
index.set_searching_mode();
assert_eq!(inserted, 2);
assert_eq!(index.len(), 2);
}
#[test]
fn test_hnsw_insert_batch_parallel_empty() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
let vectors: Vec<(u64, &[f32])> = vec![];
let inserted = index.insert_batch_parallel(vectors);
assert_eq!(inserted, 0);
assert!(index.is_empty());
}
#[test]
#[should_panic(expected = "Vector dimension mismatch")]
fn test_hnsw_insert_batch_parallel_wrong_dimension() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
let vectors: Vec<(u64, &[f32])> = vec![(1, &[1.0, 0.0])];
index.insert_batch_parallel(vectors);
}
#[test]
fn test_hnsw_with_params() {
let params = HnswParams::custom(48, 600, 500_000);
let index = HnswIndex::with_params(1536, DistanceMetric::Cosine, params).unwrap();
assert_eq!(index.dimension(), 1536);
assert!(index.is_empty());
}
#[test]
fn test_search_with_rerank_returns_k_results() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
index.insert(1, &[1.0, 0.0, 0.0]);
index.insert(2, &[0.9, 0.1, 0.0]);
index.insert(3, &[0.8, 0.2, 0.0]);
index.insert(4, &[0.0, 1.0, 0.0]);
index.insert(5, &[0.0, 0.0, 1.0]);
let results = index.search_with_rerank(&[1.0, 0.0, 0.0], 3, 5);
assert_eq!(results.len(), 3, "Should return exactly k results");
}
#[test]
#[allow(clippy::cast_precision_loss)]
fn test_search_with_rerank_improves_ranking() {
let index = HnswIndex::new(128, DistanceMetric::Cosine).unwrap();
let base: Vec<f32> = (0..128).map(|i| (i as f32 * 0.01).sin()).collect();
let mut v1 = base.clone();
v1[0] += 0.001;
let mut v2 = base.clone();
v2[0] += 0.01;
let mut v3 = base.clone();
v3[0] += 0.1;
index.insert(1, &v1);
index.insert(2, &v2);
index.insert(3, &v3);
let results = index.search_with_rerank(&base, 3, 3);
assert_eq!(results[0].id, 1, "Most similar vector should be first");
}
#[test]
fn test_search_with_rerank_handles_rerank_k_greater_than_index_size() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
index.insert(1, &[1.0, 0.0, 0.0]);
index.insert(2, &[0.0, 1.0, 0.0]);
index.insert(3, &[0.0, 0.0, 1.0]);
index.insert(4, &[0.5, 0.5, 0.0]);
index.insert(5, &[0.5, 0.0, 0.5]);
let results = index.search_with_rerank(&[1.0, 0.0, 0.0], 3, 100);
assert!(!results.is_empty(), "Should return results");
assert!(results.len() <= 5, "Should not exceed index size");
}
#[test]
#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
fn test_search_with_rerank_uses_simd_distances() {
let index = HnswIndex::new(768, DistanceMetric::Cosine).unwrap();
for i in 0..100_u64 {
let v: Vec<f32> = (0..768)
.map(|j| ((i + j as u64) as f32 * 0.01).sin())
.collect();
index.insert(i, &v);
}
let query: Vec<f32> = (0..768).map(|j| (j as f32 * 0.01).sin()).collect();
let results = index.search_with_rerank(&query, 10, 50);
assert!(!results.is_empty(), "Should return at least one result");
for sr in &results {
assert!(
sr.score >= -1.0 && sr.score <= 1.0,
"Cosine should be in [-1, 1]"
);
}
for i in 1..results.len() {
assert!(
results[i - 1].score >= results[i].score,
"Results should be sorted by similarity descending"
);
}
}
#[test]
fn test_search_with_rerank_euclidean_metric() {
let index = HnswIndex::new(3, DistanceMetric::Euclidean).unwrap();
index.insert(1, &[0.0, 0.0, 0.0]);
index.insert(2, &[1.0, 0.0, 0.0]);
index.insert(3, &[2.0, 0.0, 0.0]);
let results = index.search_with_rerank(&[0.0, 0.0, 0.0], 3, 3);
assert_eq!(results[0].id, 1, "Origin should be closest to itself");
for i in 1..results.len() {
assert!(
results[i - 1].score <= results[i].score,
"Euclidean results should be sorted ascending"
);
}
}
#[test]
#[allow(
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::uninlined_format_args
)]
fn test_hnsw_multi_tenant_load_unload() {
use tempfile::tempdir;
let dir = tempdir().expect("Failed to create temp dir");
{
let index = HnswIndex::new(128, DistanceMetric::Cosine).unwrap();
for i in 0..100_u64 {
let v: Vec<f32> = (0..128)
.map(|j| ((i + j as u64) as f32 * 0.01).sin())
.collect();
index.insert(i, &v);
}
index.save(dir.path()).expect("Failed to save index");
}
for iteration in 0..5 {
let loaded =
HnswIndex::load(dir.path(), 128, DistanceMetric::Cosine).expect("Failed to load index");
assert_eq!(
loaded.len(),
100,
"Iteration {}: Should have 100 vectors",
iteration
);
let query: Vec<f32> = (0..128).map(|j| (j as f32 * 0.01).sin()).collect();
let results = loaded.search(&query, 5);
assert!(
!results.is_empty() && results.len() <= 5,
"Iteration {}: Should return 1-5 results, got {}",
iteration,
results.len()
);
}
}
#[test]
fn test_hnsw_drop_cleans_up_properly() {
use tempfile::tempdir;
let dir = tempdir().expect("Failed to create temp dir");
{
let index = HnswIndex::new(64, DistanceMetric::Euclidean).unwrap();
index.insert(1, &vec![0.5; 64]);
index.insert(2, &vec![0.3; 64]);
index.save(dir.path()).expect("Failed to save");
}
{
let _loaded =
HnswIndex::load(dir.path(), 64, DistanceMetric::Euclidean).expect("Failed to load");
}
{
let loaded = HnswIndex::load(dir.path(), 64, DistanceMetric::Euclidean)
.expect("Failed to load after previous drop");
assert_eq!(loaded.len(), 2);
}
}
#[test]
#[allow(clippy::cast_precision_loss, clippy::uninlined_format_args)]
fn test_hnsw_save_load_preserves_all_metrics() {
use tempfile::tempdir;
for metric in [DistanceMetric::Cosine, DistanceMetric::Euclidean] {
let dir = tempdir().expect("Failed to create temp dir");
let dim = 32;
let v1: Vec<f32> = (0..dim).map(|i| (i as f32 * 0.1).sin()).collect();
let v2: Vec<f32> = (0..dim).map(|i| (i as f32 * 0.2).cos()).collect();
let query: Vec<f32> = (0..dim).map(|i| (i as f32 * 0.15).sin()).collect();
{
let index = HnswIndex::new(dim, metric).unwrap();
index.insert(1, &v1);
index.insert(2, &v2);
index.save(dir.path()).expect("Failed to save");
}
{
let loaded = HnswIndex::load(dir.path(), dim, metric).expect("Failed to load");
assert_eq!(
loaded.len(),
2,
"Metric {:?}: Should have 2 vectors",
metric
);
assert_eq!(loaded.metric(), metric, "Metric should be preserved");
assert_eq!(loaded.dimension(), dim, "Dimension should be preserved");
let results = loaded.search(&query, 2);
assert!(
!results.is_empty(),
"Metric {:?}: Should return results",
metric
);
}
}
}
#[test]
fn test_search_quality_fast() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
index.insert(1, &[1.0, 0.0, 0.0]);
index.insert(2, &[0.9, 0.1, 0.0]);
index.insert(3, &[0.8, 0.2, 0.0]);
index.insert(4, &[0.7, 0.3, 0.0]);
index.insert(5, &[0.0, 1.0, 0.0]);
let results = index.search_with_quality(&[1.0, 0.0, 0.0], 2, SearchQuality::Fast);
assert!(!results.is_empty(), "Should return at least one result");
assert!(results.len() <= 2, "Should not exceed requested k");
}
#[test]
fn test_search_quality_balanced() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
index.insert(1, &[1.0, 0.0, 0.0]);
index.insert(2, &[0.9, 0.1, 0.0]);
let results = index.search_with_quality(&[1.0, 0.0, 0.0], 2, SearchQuality::Balanced);
assert!(!results.is_empty(), "Should return at least one result");
assert_eq!(
results[0].id, 1,
"Balanced search should find exact match first"
);
}
#[test]
fn test_search_quality_custom_ef() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
index.insert(1, &[1.0, 0.0, 0.0]);
index.insert(2, &[0.9, 0.1, 0.0]);
index.insert(3, &[0.8, 0.2, 0.0]);
index.insert(4, &[0.0, 1.0, 0.0]);
index.insert(5, &[0.0, 0.0, 1.0]);
let results = index.search_with_quality(&[1.0, 0.0, 0.0], 3, SearchQuality::Custom(512));
assert_eq!(results.len(), 3);
}
#[test]
fn test_hnsw_load_nonexistent_path() {
let result = HnswIndex::load("nonexistent_path_12345", 128, DistanceMetric::Cosine);
assert!(result.is_err(), "Loading from nonexistent path should fail");
}
#[test]
fn test_hnsw_search_with_rerank_empty_index() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
let results = index.search_with_rerank(&[1.0, 0.0, 0.0], 10, 50);
assert!(
results.is_empty(),
"Empty index should return empty results"
);
}
#[test]
fn test_hnsw_search_with_rerank_dot_product() {
let index = HnswIndex::new(3, DistanceMetric::DotProduct).unwrap();
index.insert(1, &[1.0, 0.0, 0.0]);
index.insert(2, &[0.5, 0.5, 0.0]);
index.insert(3, &[0.0, 1.0, 0.0]);
let results = index.search_with_rerank(&[1.0, 0.0, 0.0], 3, 3);
assert!(!results.is_empty(), "Should return at least one result");
assert_eq!(results[0].id, 1, "Highest dot product should be first");
}
#[test]
fn test_hnsw_io_holder_is_none_for_new_index() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
index.insert(1, &[1.0, 0.0, 0.0]);
assert_eq!(index.len(), 1);
}
#[test]
#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
fn test_hnsw_large_batch_parallel_insert() {
let index = HnswIndex::new(128, DistanceMetric::Cosine).unwrap();
let vectors: Vec<(u64, Vec<f32>)> = (0..200)
.map(|i| {
let v: Vec<f32> = (0..128).map(|j| ((i + j) as f32 * 0.001).sin()).collect();
(i as u64, v)
})
.collect();
let inserted = index.insert_batch_parallel(vectors.iter().map(|(id, v)| (*id, v.as_slice())));
index.set_searching_mode();
assert_eq!(inserted, 200, "Should insert 200 vectors");
assert_eq!(index.len(), 200);
let query: Vec<f32> = (0..128).map(|j| (j as f32 * 0.001).sin()).collect();
let results = index.search(&query, 10);
assert_eq!(results.len(), 10);
}
#[test]
#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
fn test_search_with_rerank_768d_prefetch() {
let index = HnswIndex::new(768, DistanceMetric::Cosine).unwrap();
for i in 0u64..100 {
let v: Vec<f32> = (0..768)
.map(|j| ((i + j as u64) as f32 * 0.001).sin())
.collect();
index.insert(i, &v);
}
let query: Vec<f32> = (0..768).map(|j| (j as f32 * 0.001).sin()).collect();
let results = index.search_with_rerank(&query, 10, 50);
assert!(!results.is_empty(), "Should return results");
assert!(results.len() <= 10, "Should not exceed k");
}
#[test]
#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
fn test_search_with_rerank_small_dim_prefetch() {
let index = HnswIndex::new(32, DistanceMetric::Cosine).unwrap();
for i in 0u64..50 {
let v: Vec<f32> = (0..32)
.map(|j| ((i + j as u64) as f32 * 0.01).sin())
.collect();
index.insert(i, &v);
}
let query: Vec<f32> = (0..32).map(|j| (j as f32 * 0.01).sin()).collect();
let results = index.search_with_rerank(&query, 5, 20);
assert!(!results.is_empty(), "Should return results");
}
#[test]
#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
fn test_search_batch_parallel_consistency() {
let index = HnswIndex::new(64, DistanceMetric::Cosine).unwrap();
for i in 0u64..100 {
let v: Vec<f32> = (0..64)
.map(|j| ((i + j as u64) as f32 * 0.01).sin())
.collect();
index.insert(i, &v);
}
let queries: Vec<Vec<f32>> = (0..10)
.map(|i| {
(0..64)
.map(|j| ((200 + i + j) as f32 * 0.01).sin())
.collect()
})
.collect();
let query_refs: Vec<&[f32]> = queries.iter().map(Vec::as_slice).collect();
let batch_results = index.search_batch_parallel(&query_refs, 5, SearchQuality::Balanced);
let individual_results: Vec<Vec<crate::scored_result::ScoredResult>> = queries
.iter()
.map(|q| index.search_with_quality(q, 5, SearchQuality::Balanced))
.collect();
assert_eq!(batch_results.len(), individual_results.len());
for (batch, individual) in batch_results.iter().zip(&individual_results) {
assert_eq!(batch.len(), individual.len(), "Result counts should match");
}
}
#[test]
fn test_search_batch_parallel_empty_queries() {
let index = HnswIndex::new(3, DistanceMetric::Cosine).unwrap();
index.insert(1, &[1.0, 0.0, 0.0]);
let queries: Vec<&[f32]> = vec![];
let results = index.search_batch_parallel(&queries, 5, SearchQuality::Fast);
assert!(
results.is_empty(),
"Empty queries should return empty results"
);
}
#[test]
#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
fn test_search_batch_parallel_large_batch() {
let index = HnswIndex::new(128, DistanceMetric::Cosine).unwrap();
for i in 0u64..150 {
let v: Vec<f32> = (0..128)
.map(|j| ((i + j as u64) as f32 * 0.001).sin())
.collect();
index.insert(i, &v);
}
index.set_searching_mode();
let queries: Vec<Vec<f32>> = (0..20)
.map(|i| {
(0..128)
.map(|j| ((150 + i + j) as f32 * 0.001).sin())
.collect()
})
.collect();
let query_refs: Vec<&[f32]> = queries.iter().map(Vec::as_slice).collect();
let results = index.search_batch_parallel(&query_refs, 10, SearchQuality::Balanced);
assert_eq!(results.len(), 20, "Should return 20 result sets");
for result in &results {
assert_eq!(result.len(), 10, "Each result should have 10 neighbors");
}
}
#[test]
#[allow(clippy::cast_precision_loss)]
fn test_recall_quality_minimum_threshold() {
let dim = 64;
let n = 500;
let k = 10;
let index = HnswIndex::new(dim, DistanceMetric::Cosine).unwrap();
let dataset: Vec<Vec<f32>> = (0..n)
.map(|i| {
(0..dim)
.map(|j| ((i * dim + j) as f32 * 0.001).sin())
.collect()
})
.collect();
for (idx, vec) in dataset.iter().enumerate() {
#[allow(clippy::cast_possible_truncation)]
index.insert(idx as u64, vec);
}
let query: Vec<f32> = (0..dim).map(|j| (j as f32 * 0.001).sin()).collect();
let mut distances: Vec<crate::scored_result::ScoredResult> = dataset
.iter()
.enumerate()
.map(|(idx, vec)| {
let sim = crate::simd_native::cosine_similarity_native(&query, vec);
#[allow(clippy::cast_possible_truncation)]
crate::scored_result::ScoredResult::new(idx as u64, sim)
})
.collect();
distances.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
let ground_truth: Vec<u64> = distances.iter().take(k).map(|sr| sr.id).collect();
let results = index.search_with_quality(&query, k, SearchQuality::Accurate);
let result_ids: std::collections::HashSet<u64> = results.iter().map(|sr| sr.id).collect();
let gt_set: std::collections::HashSet<u64> = ground_truth.iter().copied().collect();
let recall = result_ids.intersection(>_set).count() as f64 / k as f64;
assert!(
recall >= 0.8,
"Recall@{k} should be >= 80% for Accurate, got {:.1}%",
recall * 100.0
);
}
#[test]
fn test_rerank_latency_target_configuration_roundtrip() {
let index = HnswIndex::new(32, DistanceMetric::Cosine).unwrap();
assert_eq!(index.rerank_latency_target_us(), 0);
index.set_rerank_latency_target_us(250);
assert_eq!(index.rerank_latency_target_us(), 250);
}
#[test]
fn test_rerank_latency_ema_updates_after_two_stage_search() {
let index = HnswIndex::new(64, DistanceMetric::Cosine).unwrap();
index.set_rerank_latency_target_us(1);
for i in 0u64..1500 {
let v: Vec<f32> = (0..64)
.map(|j| ((i * 5 + j as u64) as f32 * 0.0013).sin())
.collect();
index.insert(i, &v);
}
let query: Vec<f32> = (0..64).map(|j| (j as f32 * 0.009).cos()).collect();
let _ = index.search_with_quality(&query, 20, SearchQuality::Accurate);
assert!(index.rerank_latency_ema_us() > 0);
}
#[test]
fn test_update_rerank_latency_ema_large_current_does_not_overflow() {
let index = HnswIndex::new(64, DistanceMetric::Cosine).unwrap();
for i in 0u64..400 {
let v: Vec<f32> = (0..64)
.map(|j| ((i * 3 + j as u64) as f32 * 0.0021).sin())
.collect();
index.insert(i, &v);
}
index
.rerank_latency_ema_us
.store(u64::MAX, std::sync::atomic::Ordering::Relaxed);
let query: Vec<f32> = (0..64).map(|j| (j as f32 * 0.011).cos()).collect();
let results = index.search_with_quality(&query, 20, SearchQuality::Accurate);
assert!(!results.is_empty());
let ema = index.rerank_latency_ema_us();
let expected_min = (u64::MAX / 10) * 7;
assert!(ema >= expected_min, "EMA underflow/wrap detected: {ema}");
}
#[test]
fn test_search_with_quality_custom_ef_uses_high_recall_path_without_regression() {
let index = HnswIndex::new(64, DistanceMetric::Cosine).unwrap();
for i in 0u64..2000 {
let v: Vec<f32> = (0..64)
.map(|j| ((i + j as u64) as f32 * 0.001).sin())
.collect();
index.insert(i, &v);
}
let query: Vec<f32> = (0..64).map(|j| (j as f32 * 0.013).cos()).collect();
let results = index.search_with_quality(&query, 20, SearchQuality::Custom(512));
assert!(!results.is_empty());
assert!(results.len() <= 20);
}
#[test]
fn test_search_with_quality_accurate_stays_stable_on_medium_dataset() {
let index = HnswIndex::new(128, DistanceMetric::Cosine).unwrap();
for i in 0u64..5000 {
let v: Vec<f32> = (0..128)
.map(|j| ((i * 3 + j as u64) as f32 * 0.0007).sin())
.collect();
index.insert(i, &v);
}
let query: Vec<f32> = (0..128).map(|j| (j as f32 * 0.007).sin()).collect();
let results = index.search_with_quality(&query, 15, SearchQuality::Accurate);
assert!(!results.is_empty());
assert!(results.len() <= 15);
}
#[test]
fn test_brute_force_buffered_same_results_as_original() {
let index = HnswIndex::new(32, DistanceMetric::Cosine).unwrap();
for i in 0u64..50 {
let v: Vec<f32> = (0..32)
.map(|j| ((i + j as u64) as f32 * 0.01).sin())
.collect();
index.insert(i, &v);
}
let query: Vec<f32> = (0..32).map(|j| (j as f32 * 0.02).cos()).collect();
let original = index.search_brute_force(&query, 10);
let buffered = index.search_brute_force_buffered(&query, 10);
assert_eq!(original.len(), buffered.len());
for (orig, buf) in original.iter().zip(buffered.iter()) {
assert_eq!(orig.id, buf.id, "IDs should match");
assert!(
(orig.score - buf.score).abs() < 1e-6,
"Distances should match"
);
}
}
#[test]
fn test_brute_force_buffered_empty_index() {
let index = HnswIndex::new(16, DistanceMetric::Euclidean).unwrap();
let query: Vec<f32> = vec![0.0; 16];
let results = index.search_brute_force_buffered(&query, 5);
assert!(results.is_empty());
}
#[test]
fn test_brute_force_buffered_all_metrics() {
for metric in [
DistanceMetric::Cosine,
DistanceMetric::Euclidean,
DistanceMetric::DotProduct,
DistanceMetric::Hamming,
DistanceMetric::Jaccard,
] {
let index = HnswIndex::new(8, metric).unwrap();
index.insert(1, &[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
index.insert(2, &[0.5, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
index.insert(3, &[0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
let results =
index.search_brute_force_buffered(&[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 3);
assert_eq!(results.len(), 3, "Should return 3 results for {metric:?}");
}
}
#[test]
fn test_brute_force_buffered_repeated_calls_stable() {
let index = HnswIndex::new(16, DistanceMetric::Cosine).unwrap();
for i in 0u64..20 {
let v: Vec<f32> = (0..16)
.map(|j| ((i + j as u64) as f32 * 0.1).sin())
.collect();
index.insert(i, &v);
}
let query: Vec<f32> = vec![0.5; 16];
let r1 = index.search_brute_force_buffered(&query, 5);
let r2 = index.search_brute_force_buffered(&query, 5);
let r3 = index.search_brute_force_buffered(&query, 5);
assert_eq!(r1, r2);
assert_eq!(r2, r3);
}
#[test]
#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
fn test_concurrent_search_stress() {
use std::sync::Arc;
use std::thread;
let index = Arc::new(HnswIndex::new(64, DistanceMetric::Cosine).unwrap());
for i in 0u64..100 {
let v: Vec<f32> = (0..64)
.map(|j| ((i + j as u64) as f32 * 0.01).sin())
.collect();
index.insert(i, &v);
}
let handles: Vec<_> = (0..4)
.map(|t| {
let idx = Arc::clone(&index);
thread::spawn(move || {
for i in 0..50 {
let query: Vec<f32> = (0..64)
.map(|j| ((t * 100 + i + j) as f32 * 0.01).sin())
.collect();
let results = idx.search(&query, 5);
assert!(!results.is_empty());
}
})
})
.collect();
for handle in handles {
handle.join().expect("Thread panicked");
}
}
#[test]
fn test_all_distance_metrics_search_with_rerank() {
for metric in [
DistanceMetric::Cosine,
DistanceMetric::Euclidean,
DistanceMetric::DotProduct,
DistanceMetric::Hamming,
DistanceMetric::Jaccard,
] {
let index = HnswIndex::new(8, metric).unwrap();
index.insert(1, &[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
index.insert(2, &[0.5, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
index.insert(3, &[0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
let results = index.search_with_rerank(&[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 3, 3);
assert!(
!results.is_empty(),
"search_with_rerank should work for {metric:?}"
);
}
}
#[test]
fn test_drop_safety_loaded_index_no_segfault() {
use tempfile::tempdir;
let dir = tempdir().expect("Failed to create temp dir");
{
let index = HnswIndex::new(4, DistanceMetric::Cosine).unwrap();
index.insert(1, &[1.0, 0.0, 0.0, 0.0]);
index.insert(2, &[0.0, 1.0, 0.0, 0.0]);
index.insert(3, &[0.0, 0.0, 1.0, 0.0]);
index.save(dir.path()).expect("Failed to save");
}
for _ in 0..5 {
let loaded =
HnswIndex::load(dir.path(), 4, DistanceMetric::Cosine).expect("Failed to load");
let results = loaded.search(&[1.0, 0.0, 0.0, 0.0], 2);
assert!(!results.is_empty(), "Search should return results");
}
}
#[test]
fn test_drop_safety_loaded_index_concurrent_drop() {
use std::sync::Arc;
use std::thread;
use tempfile::tempdir;
let dir = tempdir().expect("Failed to create temp dir");
{
let index = HnswIndex::new(4, DistanceMetric::Cosine).unwrap();
for i in 0u64..10 {
let v = vec![i as f32, 0.0, 0.0, 0.0];
index.insert(i, &v);
}
index.save(dir.path()).expect("Failed to save");
}
let path = Arc::new(dir.path().to_path_buf());
let handles: Vec<_> = (0..4)
.map(|_| {
let p = Arc::clone(&path);
thread::spawn(move || {
for _ in 0..3 {
let loaded =
HnswIndex::load(&*p, 4, DistanceMetric::Cosine).expect("Failed to load");
let results = loaded.search(&[1.0, 0.0, 0.0, 0.0], 3);
assert!(!results.is_empty());
}
})
})
.collect();
for h in handles {
h.join().expect("Thread should not panic from Drop");
}
}
#[test]
fn test_drop_safety_search_after_partial_operations() {
use tempfile::tempdir;
let dir = tempdir().expect("Failed to create temp dir");
{
let index = HnswIndex::new(8, DistanceMetric::Euclidean).unwrap();
for i in 0u64..20 {
let v: Vec<f32> = (0..8).map(|j| (i + j) as f32 * 0.1).collect();
index.insert(i, &v);
}
index.save(dir.path()).expect("Failed to save");
}
let loaded = HnswIndex::load(dir.path(), 8, DistanceMetric::Euclidean).expect("Failed to load");
for i in 0..10 {
let query: Vec<f32> = (0..8).map(|j| (i + j) as f32 * 0.1).collect();
let results = loaded.search(&query, 5);
assert!(results.len() <= 5);
}
let queries: Vec<Vec<f32>> = (0..5)
.map(|i| (0..8).map(|j| (i + j) as f32 * 0.1).collect())
.collect();
let query_refs: Vec<&[f32]> = queries.iter().map(|v| v.as_slice()).collect();
let batch_results = loaded.search_batch_parallel(&query_refs, 3, SearchQuality::Balanced);
assert_eq!(batch_results.len(), 5);
drop(loaded);
}
#[test]
fn test_drop_stress_concurrent_create_destroy_loop() {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
let success_count = Arc::new(AtomicUsize::new(0));
let iterations = 50;
for _ in 0..iterations {
let success = Arc::clone(&success_count);
let index = Arc::new(HnswIndex::new(16, DistanceMetric::Cosine).unwrap());
let handles: Vec<_> = (0..4)
.map(|t| {
let idx = Arc::clone(&index);
std::thread::spawn(move || {
for i in 0..10 {
let id = (t * 100 + i) as u64;
let v: Vec<f32> = (0..16).map(|j| (id + j) as f32 * 0.01).collect();
idx.insert(id, &v);
}
let q: Vec<f32> = (0..16).map(|i| i as f32 * 0.01).collect();
let _ = idx.search(&q, 5);
})
})
.collect();
for h in handles {
h.join().expect("Thread panicked during stress test");
}
drop(index);
success.fetch_add(1, Ordering::SeqCst);
}
assert_eq!(
success_count.load(Ordering::SeqCst),
iterations,
"All iterations should complete without panic"
);
}
#[test]
fn test_drop_stress_load_search_destroy_cycle() {
use tempfile::tempdir;
let dir = tempdir().expect("Failed to create temp dir");
{
let index = HnswIndex::new(32, DistanceMetric::Euclidean).unwrap();
for i in 0u64..100 {
let v: Vec<f32> = (0..32).map(|j| ((i + j) as f32).sin()).collect();
index.insert(i, &v);
}
index.save(dir.path()).expect("Failed to save");
}
for cycle in 0..20 {
let loaded = HnswIndex::load(dir.path(), 32, DistanceMetric::Euclidean)
.unwrap_or_else(|e| panic!("Cycle {cycle}: Failed to load: {e}"));
for i in 0..50 {
let q: Vec<f32> = (0..32).map(|j| ((i + j) as f32).cos()).collect();
let results = loaded.search(&q, 10);
assert!(
results.len() <= 10,
"Cycle {cycle}: Search returned too many results"
);
}
drop(loaded);
}
}
#[test]
fn test_drop_stress_parallel_insert_then_drop() {
for _ in 0..5 {
let index = HnswIndex::new(64, DistanceMetric::Euclidean).unwrap();
let batch: Vec<(u64, Vec<f32>)> = (0..100)
.map(|i| {
let v: Vec<f32> = (0..64).map(|j| (i + j) as f32 * 0.01).collect();
(i as u64, v)
})
.collect();
let inserted = index.insert_batch_parallel(batch.iter().map(|(id, v)| (*id, v.as_slice())));
assert!(inserted > 0, "Should insert at least some vectors");
drop(index);
}
}
#[test]
#[cfg(feature = "gpu")]
fn test_search_brute_force_gpu_returns_same_results_as_cpu() {
let index = HnswIndex::new(128, DistanceMetric::Cosine).unwrap();
for i in 0u64..100 {
let v: Vec<f32> = (0..128)
.map(|j| ((i + j as u64) as f32 * 0.01).sin())
.collect();
index.insert(i, &v);
}
let query: Vec<f32> = (0..128).map(|j| (j as f32 * 0.02).cos()).collect();
let cpu_results = index.search_brute_force(&query, 10);
if let Some(gpu_results) = index.search_brute_force_gpu(&query, 10) {
assert_eq!(
cpu_results.len(),
gpu_results.len(),
"Result count mismatch"
);
let cpu_ids: std::collections::HashSet<u64> = cpu_results.iter().map(|sr| sr.id).collect();
let gpu_ids: std::collections::HashSet<u64> = gpu_results.iter().map(|sr| sr.id).collect();
let overlap = cpu_ids.intersection(&gpu_ids).count();
assert!(
overlap >= 8,
"GPU and CPU should return mostly same IDs (got {overlap}/10 overlap)"
);
}
}
#[test]
fn test_search_brute_force_gpu_fallback_to_none_without_gpu() {
let index = HnswIndex::new(64, DistanceMetric::Cosine).unwrap();
index.insert(1, &vec![0.5; 64]);
let query = vec![0.5; 64];
let _result = index.search_brute_force_gpu(&query, 5);
#[cfg(not(feature = "gpu"))]
assert!(_result.is_none(), "Should return None without GPU feature");
}
#[test]
#[cfg(feature = "gpu")]
fn test_brute_force_gpu_euclidean() {
let index = HnswIndex::new(4, DistanceMetric::Euclidean).unwrap();
index.insert(10, &[0.0, 0.0, 0.0, 0.0]);
index.insert(20, &[1.0, 0.0, 0.0, 0.0]);
index.insert(30, &[2.0, 0.0, 0.0, 0.0]);
let query = [0.0, 0.0, 0.0, 0.0];
if let Some(results) = index.search_brute_force_gpu(&query, 3) {
assert_eq!(results.len(), 3, "Should return 3 results");
assert_eq!(results[0].id, 10, "Closest should be id=10 (at origin)");
assert_eq!(results[1].id, 20, "Second closest should be id=20");
assert_eq!(results[2].id, 30, "Farthest should be id=30");
assert!(
results[0].score.abs() < 0.01,
"Distance to origin should be ~0, got {}",
results[0].score
);
assert!(
(results[1].score - 1.0).abs() < 0.01,
"Distance to (1,0,0,0) should be ~1, got {}",
results[1].score
);
}
}
#[test]
#[cfg(feature = "gpu")]
fn test_brute_force_gpu_dot_product() {
let index = HnswIndex::new(4, DistanceMetric::DotProduct).unwrap();
index.insert(10, &[1.0, 0.0, 0.0, 0.0]); index.insert(20, &[2.0, 0.0, 0.0, 0.0]); index.insert(30, &[3.0, 0.0, 0.0, 0.0]);
let query = [1.0, 0.0, 0.0, 0.0];
if let Some(results) = index.search_brute_force_gpu(&query, 3) {
assert_eq!(results.len(), 3, "Should return 3 results");
assert_eq!(
results[0].id, 30,
"Highest dot product should be id=30, got id={}",
results[0].id
);
assert_eq!(
results[1].id, 20,
"Second highest dot product should be id=20, got id={}",
results[1].id
);
assert_eq!(
results[2].id, 10,
"Lowest dot product should be id=10, got id={}",
results[2].id
);
assert!(
(results[0].score - 3.0).abs() < 0.01,
"Dot product with (3,0,0,0) should be ~3, got {}",
results[0].score
);
}
}
#[test]
fn test_compute_backend_selection() {
use crate::gpu::ComputeBackend;
let backend = ComputeBackend::best_available();
match backend {
ComputeBackend::Simd => {
}
#[cfg(feature = "gpu")]
ComputeBackend::Gpu => {
}
}
}
mod proptest_tests {
use super::*;
use proptest::prelude::*;
fn dimension_strategy() -> impl Strategy<Value = usize> {
8usize..=256
}
#[allow(dead_code)]
fn vector_strategy(dim: usize) -> impl Strategy<Value = Vec<f32>> {
proptest::collection::vec(-1.0f32..1.0, dim)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(50))]
#[test]
fn prop_len_equals_insertions(
dim in dimension_strategy(),
vectors in proptest::collection::vec(
proptest::collection::vec(-1.0f32..1.0, 8usize..=64),
1usize..=20
)
) {
let index = HnswIndex::new(dim, DistanceMetric::Euclidean).unwrap();
let mut inserted = 0usize;
for (i, v) in vectors.into_iter().enumerate() {
if v.len() == dim {
index.insert(i as u64, &v);
inserted += 1;
}
}
prop_assert_eq!(index.len(), inserted);
}
#[test]
fn prop_search_returns_at_most_k(
dim in 16usize..=64,
k in 1usize..=20,
num_vectors in 5usize..=50
) {
let index = HnswIndex::new(dim, DistanceMetric::Euclidean).unwrap();
for i in 0..num_vectors {
let v: Vec<f32> = (0..dim).map(|j| ((i + j) as f32 * 0.01).sin()).collect();
index.insert(i as u64, &v);
}
let query: Vec<f32> = (0..dim).map(|j| (j as f32 * 0.02).cos()).collect();
let results = index.search(&query, k);
prop_assert!(results.len() <= k, "Search returned {} results, expected <= {}", results.len(), k);
}
#[test]
fn prop_brute_force_exact(
dim in 8usize..=32,
num_vectors in 3usize..=20
) {
let index = HnswIndex::new(dim, DistanceMetric::Euclidean).unwrap();
for i in 0..num_vectors {
let mut v = vec![0.0f32; dim];
v[0] = i as f32; index.insert(i as u64, &v);
}
let query = vec![0.0f32; dim];
let results = index.search_brute_force(&query, 3);
if !results.is_empty() {
prop_assert_eq!(results[0].id, 0, "Closest should be id=0 (at origin)");
}
}
#[test]
fn prop_remove_decreases_len(
dim in 16usize..=32,
id_to_remove in 0u64..10
) {
let index = HnswIndex::new(dim, DistanceMetric::Cosine).unwrap();
for i in 0u64..10 {
let v: Vec<f32> = (0..dim).map(|j| ((i + j as u64) as f32 * 0.01).sin()).collect();
index.insert(i, &v);
}
let len_before = index.len();
let removed = index.remove(id_to_remove);
if removed {
prop_assert_eq!(index.len(), len_before - 1);
} else {
prop_assert_eq!(index.len(), len_before);
}
}
#[test]
fn prop_duplicate_insert_idempotent(
dim in 16usize..=32
) {
let index = HnswIndex::new(dim, DistanceMetric::Euclidean).unwrap();
let v: Vec<f32> = (0..dim).map(|j| j as f32 * 0.1).collect();
index.insert(42, &v);
let len_after_first = index.len();
index.insert(42, &v); let len_after_second = index.len();
prop_assert_eq!(len_after_first, len_after_second, "Duplicate insert should be idempotent");
}
#[test]
fn prop_batch_insert_count(
dim in 16usize..=32,
batch_size in 5usize..=30
) {
let index = HnswIndex::new(dim, DistanceMetric::Euclidean).unwrap();
let batch: Vec<(u64, Vec<f32>)> = (0..batch_size)
.map(|i| {
let v: Vec<f32> = (0..dim).map(|j| ((i + j) as f32 * 0.01).sin()).collect();
(i as u64, v)
})
.collect();
let count = index.insert_batch_parallel(batch.iter().map(|(id, v)| (*id, v.as_slice())));
prop_assert_eq!(count, batch_size, "Batch insert count mismatch");
prop_assert_eq!(index.len(), batch_size, "Index len mismatch after batch");
}
}
}
#[test]
fn test_manuallydrop_pattern_integrity() {
let index = HnswIndex::new(64, DistanceMetric::Cosine).unwrap();
for i in 0..10 {
let v: Vec<f32> = (0..64).map(|j| (i + j) as f32 * 0.01).collect();
index.insert(i as u64, &v);
}
drop(index);
}
#[test]
fn test_load_and_drop_safety() {
use tempfile::TempDir;
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let path = temp_dir.path();
{
let index = HnswIndex::new(64, DistanceMetric::Cosine).unwrap();
for i in 0..50 {
let v: Vec<f32> = (0..64).map(|j| (i + j) as f32 * 0.01).collect();
index.insert(i as u64, &v);
}
index.save(path).expect("Save failed");
}
for _ in 0..3 {
let loaded = HnswIndex::load(path, 64, DistanceMetric::Cosine).expect("Load failed");
let results = loaded.search(&vec![0.0f32; 64], 5);
assert!(!results.is_empty(), "Search should return results");
drop(loaded);
}
}
#[test]
#[allow(clippy::cast_precision_loss)]
fn test_adaptive_search_spread_positive_for_distance_metrics() {
let dim = 32;
let index = HnswIndex::new(dim, DistanceMetric::Euclidean).unwrap();
for i in 0u64..120 {
let v: Vec<f32> = (0..dim)
.map(|j| ((i + j as u64) as f32 * 0.001).sin() * 0.1)
.collect();
index.insert(i, &v);
}
for i in 120u64..150 {
let v: Vec<f32> = (0..dim)
.map(|j| 10.0 + (i + j as u64) as f32 * 0.05)
.collect();
index.insert(i, &v);
}
let query: Vec<f32> = vec![0.0; dim];
let results = index.search_with_quality(
&query,
10,
SearchQuality::Adaptive {
min_ef: 32,
max_ef: 256,
},
);
assert!(
!results.is_empty(),
"Adaptive Euclidean search should return results"
);
assert!(results.len() <= 10, "Should not exceed requested k");
for pair in results.windows(2) {
assert!(
pair[0].score <= pair[1].score + f32::EPSILON,
"Euclidean results must be sorted ascending: {} > {}",
pair[0].score,
pair[1].score,
);
}
let closest_id = results[0].id;
assert!(
closest_id < 120,
"Closest result should be from the tight cluster, got id={closest_id}"
);
}
#[test]
#[allow(clippy::cast_precision_loss)]
fn test_adaptive_search_spread_works_for_similarity_metrics() {
let dim = 32;
let index = HnswIndex::new(dim, DistanceMetric::Cosine).unwrap();
for i in 0u64..150 {
let v: Vec<f32> = (0..dim)
.map(|j| ((i * 7 + j as u64) as f32 * 0.013).sin())
.collect();
index.insert(i, &v);
}
let query: Vec<f32> = (0..dim).map(|j| (j as f32 * 0.013).sin()).collect();
let results = index.search_with_quality(
&query,
10,
SearchQuality::Adaptive {
min_ef: 32,
max_ef: 256,
},
);
assert!(
!results.is_empty(),
"Adaptive Cosine search should return results"
);
assert!(results.len() <= 10, "Should not exceed requested k");
for pair in results.windows(2) {
assert!(
pair[0].score >= pair[1].score - f32::EPSILON,
"Cosine results must be sorted descending: {} < {}",
pair[0].score,
pair[1].score,
);
}
}
#[test]
fn test_insert_same_id_updates_vector() {
let index = HnswIndex::new(4, DistanceMetric::Cosine).unwrap();
let vector_a = [1.0, 0.0, 0.0, 0.0];
index.insert(1, &vector_a);
let vector_b = [0.0, 1.0, 0.0, 0.0];
index.insert(1, &vector_b);
assert_eq!(index.len(), 1, "Upsert must not create duplicate entries");
let results = index.search(&vector_b, 1);
assert_eq!(results.len(), 1, "Should find exactly one result");
assert_eq!(results[0].id, 1, "Result must be id=1");
assert!(
results[0].score > 0.9,
"Similarity to updated vector B should be > 0.9, got {}",
results[0].score,
);
}
#[test]
fn test_upsert_tombstone_accumulation() {
let dim = 10;
let index = HnswIndex::new(dim, DistanceMetric::Cosine).unwrap();
for i in 0..10_usize {
index.insert(i as u64, &make_dominant_vector(dim, i, 0));
}
assert_eq!(
index.tombstone_count(),
0,
"Fresh index should have 0 tombstones"
);
assert_eq!(index.len(), 10, "Should have 10 active vectors");
for i in 0..10_usize {
index.insert(i as u64, &make_dominant_vector(dim, i, 1));
}
assert_eq!(
index.tombstone_count(),
10,
"Updating 10 vectors should leave 10 tombstones",
);
assert_eq!(index.len(), 10, "Active count must remain 10 after upserts");
assert!(
index.needs_vacuum(),
"Tombstone ratio {:.2} should exceed 0.20 threshold",
index.tombstone_ratio(),
);
}
#[test]
fn test_upsert_then_vacuum_cleans() {
let dim = 10;
let index = HnswIndex::new(dim, DistanceMetric::Cosine).unwrap();
for i in 0..10_usize {
index.insert(i as u64, &make_dominant_vector(dim, i, 0));
}
for i in 0..5_usize {
index.insert(i as u64, &make_dominant_vector(dim, i, 1));
}
assert_eq!(
index.tombstone_count(),
5,
"Updating 5 vectors should create 5 tombstones",
);
assert_eq!(index.len(), 10);
let rebuilt = index.vacuum().expect("vacuum should succeed");
assert_eq!(rebuilt, 10, "Vacuum should report 10 active vectors");
assert_eq!(
index.tombstone_count(),
0,
"Vacuum must eliminate all tombstones",
);
assert_eq!(index.len(), 10, "All 10 vectors must survive vacuum");
for i in 0..10_usize {
let gen = u8::from(i < 5);
let query = make_dominant_vector(dim, i, gen);
let results = index.search(&query, 1);
assert!(
!results.is_empty(),
"Search for id={i} returned no results after vacuum",
);
assert_eq!(
results[0].id, i as u64,
"Top-1 for id={i} should be itself, got id={}",
results[0].id,
);
}
}
#[test]
fn test_batch_upsert_updates_existing() {
let dim = 10;
let index = HnswIndex::new(dim, DistanceMetric::Cosine).unwrap();
let original_vectors: Vec<(u64, Vec<f32>)> = (0..10)
.map(|id| (id, make_dominant_vector(dim, id as usize, 0)))
.collect();
let refs: Vec<(u64, &[f32])> = original_vectors
.iter()
.map(|(id, v)| (*id, v.as_slice()))
.collect();
let inserted = index.insert_batch_parallel(refs);
assert_eq!(inserted, 10, "Should insert all 10 vectors");
assert_eq!(index.len(), 10, "Index should contain 10 entries");
let updated_vectors: Vec<(u64, Vec<f32>)> = (0..5)
.map(|id| (id, make_dominant_vector(dim, id as usize, 1)))
.collect();
let update_refs: Vec<(u64, &[f32])> = updated_vectors
.iter()
.map(|(id, v)| (*id, v.as_slice()))
.collect();
let upserted = index.insert_batch_parallel(update_refs);
assert_eq!(upserted, 5, "Should upsert all 5 vectors");
assert_eq!(
index.len(),
10,
"Batch upsert must not create duplicate entries: expected 10, got {}",
index.len(),
);
for id in 0..5_u64 {
let query = make_dominant_vector(dim, id as usize, 1);
let results = index.search(&query, 1);
assert_eq!(
results.len(),
1,
"Updated id={id}: should find exactly one result"
);
assert_eq!(
results[0].id, id,
"Updated id={id}: top-1 result should be the updated vector"
);
assert!(
results[0].score > 0.9,
"Updated id={id}: similarity to updated vector should be > 0.9, got {}",
results[0].score,
);
}
for id in 5..10_u64 {
let query = make_dominant_vector(dim, id as usize, 0);
let results = index.search(&query, 1);
assert!(
!results.is_empty(),
"Non-updated id={id}: should find results"
);
assert_eq!(
results[0].id, id,
"Non-updated id={id}: top-1 result should be the original vector"
);
}
}
#[allow(clippy::similar_names)] #[test]
fn test_upsert_search_recall() {
let index = HnswIndex::new(8, DistanceMetric::Cosine).unwrap();
let origin_center: Vec<f32> = vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let target_center: Vec<f32> = vec![0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0];
let origin_vectors: Vec<(u64, Vec<f32>)> = (0..50)
.map(|i| {
let mut v = origin_center.clone();
for (d, component) in v.iter_mut().enumerate() {
*component += 0.01 * (i * 8 + d) as f32;
}
(i as u64, v)
})
.collect();
let target_vectors: Vec<(u64, Vec<f32>)> = (0..50)
.map(|i| {
let mut v = target_center.clone();
for (d, component) in v.iter_mut().enumerate() {
*component += 0.01 * (i * 8 + d) as f32;
}
(i as u64 + 50, v)
})
.collect();
let all_vectors: Vec<(u64, Vec<f32>)> = origin_vectors
.iter()
.chain(target_vectors.iter())
.map(|(id, v)| (*id, v.clone()))
.collect();
let refs: Vec<(u64, &[f32])> = all_vectors
.iter()
.map(|(id, v)| (*id, v.as_slice()))
.collect();
let inserted = index.insert_batch_parallel(refs);
assert_eq!(inserted, 100, "Should insert all 100 vectors");
let moved_vector: Vec<f32> = vec![0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0];
index.insert(25, &moved_vector);
assert_eq!(index.len(), 100, "Length must remain 100 after upsert");
let results_target = index.search(&target_center, 10);
let found_in_target = results_target.iter().any(|r| r.id == 25);
assert!(
found_in_target,
"id=25 should appear in target cluster results after upsert, got ids: {:?}",
results_target.iter().map(|r| r.id).collect::<Vec<_>>(),
);
let results_origin = index.search(&origin_center, 10);
let found_in_origin = results_origin.iter().any(|r| r.id == 25);
assert!(
!found_in_origin,
"id=25 should NOT appear in origin cluster results after upsert, got ids: {:?}",
results_origin.iter().map(|r| r.id).collect::<Vec<_>>(),
);
}
#[test]
fn test_upsert_entry_point_vector() {
let index = HnswIndex::new(4, DistanceMetric::Cosine).unwrap();
let ep_original = vec![1.0, 0.0, 0.0, 0.0];
index.insert(0, &ep_original);
let ep_updated = vec![0.0, 0.0, 0.0, 1.0];
index.insert(0, &ep_updated);
let additional_vectors: Vec<(u64, Vec<f32>)> = (1..=20)
.map(|i| {
let dominant_dim = (i as usize) % 4;
let mut v = vec![0.1_f32; 4];
v[dominant_dim] += 1.0 + 0.05 * i as f32;
(i as u64, v)
})
.collect();
let refs: Vec<(u64, &[f32])> = additional_vectors
.iter()
.map(|(id, v)| (*id, v.as_slice()))
.collect();
let inserted = index.insert_batch_parallel(refs);
assert_eq!(inserted, 20, "Should insert all 20 vectors");
assert_eq!(index.len(), 21, "Index should contain 21 entries");
let results_ep = index.search(&ep_updated, 1);
assert_eq!(results_ep.len(), 1, "Should find at least 1 result for EP");
assert_eq!(
results_ep[0].id, 0,
"Top-1 for updated entry point should be id=0, got id={}",
results_ep[0].id,
);
for (id, vec) in &additional_vectors {
let results = index.search(vec.as_slice(), 1);
assert!(
!results.is_empty(),
"Search for id={id} should return results",
);
assert_eq!(
results[0].id, *id,
"Top-1 for id={id} should be itself, got id={}",
results[0].id,
);
}
}
#[test]
fn test_batch_upsert_within_batch_duplicates() {
let dim = 4;
let index = HnswIndex::new(dim, DistanceMetric::Cosine).unwrap();
let vec_a = vec![1.0_f32, 0.0, 0.0, 0.0];
let vec_b = vec![0.0_f32, 1.0, 0.0, 0.0];
let batch: Vec<(u64, &[f32])> = vec![(1, &vec_a), (1, &vec_b), (2, &vec_a)];
let inserted = index.insert_batch_parallel(batch);
assert!(
inserted >= 2,
"At least 2 entries processed, got {inserted}"
);
assert_eq!(index.len(), 2, "Only 2 unique IDs should be mapped");
let results = index.search(&vec_b, 1);
assert_eq!(results.len(), 1);
assert_eq!(
results[0].id, 1,
"id=1 must be findable after within-batch upsert"
);
assert!(
results[0].score > 0.9,
"id=1 should match vec_b, got score {}",
results[0].score,
);
let results2 = index.search(&vec_a, 1);
assert_eq!(results2[0].id, 2, "id=2 must be findable");
}
fn make_dominant_vector(dim: usize, dominant_dim: usize, gen: u8) -> Vec<f32> {
let mut v = vec![0.1_f32; dim];
v[dominant_dim % dim] += 1.0 + f32::from(gen) * 0.5;
v
}
#[test]
fn test_upsert_after_delete() {
let dim = 8;
let index = HnswIndex::new(dim, DistanceMetric::Cosine).unwrap();
let vec_a = make_dominant_vector(dim, 0, 0);
index.insert(1, &vec_a);
assert_eq!(index.len(), 1);
assert!(index.remove(1));
assert_eq!(index.len(), 0);
let vec_b = make_dominant_vector(dim, 4, 1);
index.insert(1, &vec_b);
assert_eq!(index.len(), 1, "Re-inserted id should count as 1 vector");
let results = index.search(&vec_b, 1);
assert_eq!(results.len(), 1);
assert_eq!(
results[0].id, 1,
"After delete + re-insert, search must find re-inserted id=1"
);
}
#[test]
fn test_repeated_upsert_accumulates_tombstones() {
let dim = 8;
let index = HnswIndex::new(dim, DistanceMetric::Cosine).unwrap();
let num_upserts: usize = 20;
for gen in 0..num_upserts {
let v = make_dominant_vector(dim, gen % dim, gen as u8);
index.insert(1, &v);
}
assert_eq!(
index.len(),
1,
"Repeated upserts must not duplicate entries"
);
assert_eq!(
index.tombstone_count(),
num_upserts - 1,
"Each upsert after the first should leave one tombstone"
);
let latest = make_dominant_vector(dim, (num_upserts - 1) % dim, (num_upserts - 1) as u8);
let results = index.search(&latest, 1);
assert_eq!(results.len(), 1);
assert_eq!(
results[0].id, 1,
"Search must return the latest upserted vector"
);
}
#[test]
fn test_insert_and_correct_mapping_fixes_diverged_idx() {
let dim = 4;
let index = HnswIndex::new(dim, DistanceMetric::Euclidean).unwrap();
index.insert(100, &[1.0, 0.0, 0.0, 0.0]);
assert_eq!(index.len(), 1);
assert_eq!(index.mappings.get_idx(100), Some(0));
let ghost_vec = [0.0, 1.0, 0.0, 0.0];
let ghost_node_id = index.inner.read().insert((&ghost_vec, 999)).unwrap();
assert_eq!(ghost_node_id, 1, "Graph should assign node_id=1");
let result = index.upsert_mapping(200);
assert_eq!(result.idx, 1, "Mapping should allocate idx=1");
assert_eq!(result.old_idx, None, "New ID has no old mapping");
let vector = [0.0, 0.0, 1.0, 0.0];
let success = index.insert_and_correct_mapping(200, &vector, &result);
assert!(success, "insert_and_correct_mapping should succeed");
let corrected_idx = index.mappings.get_idx(200).unwrap();
assert_eq!(
corrected_idx, 2,
"Mapping must be corrected to actual graph node_id"
);
assert_eq!(
index.mappings.get_id(2),
Some(200),
"Reverse mapping must point to id=200"
);
assert_eq!(
index.mappings.get_id(1),
None,
"Stale reverse mapping for original idx must be removed"
);
}
#[test]
fn test_search_works_after_mapping_correction() {
let dim = 4;
let index = HnswIndex::new(dim, DistanceMetric::Euclidean).unwrap();
index.insert(100, &[1.0, 0.0, 0.0, 0.0]);
let ghost_vec = [0.5, 0.5, 0.0, 0.0];
index.inner.read().insert((&ghost_vec, 999)).unwrap();
let result = index.upsert_mapping(200);
let target = [0.0, 0.0, 0.0, 1.0];
index.insert_and_correct_mapping(200, &target, &result);
let results = index.search(&[0.0, 0.0, 0.0, 1.0], 1);
assert_eq!(results.len(), 1);
assert_eq!(
results[0].id, 200,
"Search must find the vector after mapping correction"
);
}
#[test]
fn test_insert_and_correct_mapping_no_divergence_happy_path() {
let dim = 4;
let index = HnswIndex::new(dim, DistanceMetric::Euclidean).unwrap();
let result = index.upsert_mapping(42);
assert_eq!(result.idx, 0);
let vector = [1.0, 0.0, 0.0, 0.0];
let success = index.insert_and_correct_mapping(42, &vector, &result);
assert!(success, "Happy path should succeed");
assert_eq!(index.mappings.get_idx(42), Some(0));
assert_eq!(index.mappings.get_id(0), Some(42));
assert_eq!(index.len(), 1);
}
#[test]
fn test_batch_after_single_insert_mapping_consistency() {
let index = HnswIndex::new(4, DistanceMetric::Euclidean).unwrap();
index.insert(100, &[1.0, 0.0, 0.0, 0.0]);
assert_eq!(index.len(), 1);
let batch: Vec<(u64, &[f32])> = vec![
(200, &[0.0, 1.0, 0.0, 0.0]),
(201, &[0.0, 0.0, 1.0, 0.0]),
(202, &[0.0, 0.0, 0.0, 1.0]),
(203, &[0.5, 0.5, 0.0, 0.0]),
(204, &[0.0, 0.5, 0.5, 0.0]),
];
let inserted = index.insert_batch_parallel(batch);
assert_eq!(inserted, 5);
assert_eq!(index.len(), 6);
for &ext_id in &[100u64, 200, 201, 202, 203, 204] {
let idx = index.mappings.get_idx(ext_id);
assert!(idx.is_some(), "get_idx({ext_id}) must return Some");
let reverse = index.mappings.get_id(idx.unwrap());
assert_eq!(
reverse,
Some(ext_id),
"Reverse mapping for idx {} must be {ext_id}, got {reverse:?}",
idx.unwrap()
);
}
let results = index.search(&[1.0, 0.0, 0.0, 0.0], 1);
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, 100, "Nearest to [1,0,0,0] must be id=100");
}
#[test]
fn test_batch_insert_vector_storage_uses_assigned_ids() {
let index = HnswIndex::new(4, DistanceMetric::Euclidean).unwrap();
for i in 0..3u64 {
let v = [i as f32, 0.0, 0.0, 0.0];
index.insert(i, &v);
}
assert_eq!(index.len(), 3);
let batch: Vec<(u64, &[f32])> = vec![
(10, &[10.0, 0.0, 0.0, 0.0]),
(11, &[11.0, 0.0, 0.0, 0.0]),
(12, &[12.0, 0.0, 0.0, 0.0]),
(13, &[13.0, 0.0, 0.0, 0.0]),
];
let inserted = index.insert_batch_parallel(batch);
assert_eq!(inserted, 4);
for ext_id in 10..=13u64 {
let idx = index.mappings.get_idx(ext_id).expect("mapping must exist");
let stored = index.vectors.get(idx);
assert!(
stored.is_some(),
"ShardedVectors must have a vector at idx {idx} for id {ext_id}"
);
let expected_first = ext_id as f32;
let stored_vec = stored.unwrap();
assert!(
(stored_vec[0] - expected_first).abs() < f32::EPSILON,
"Vector for id {ext_id} at idx {idx}: expected first component {expected_first}, got {}",
stored_vec[0]
);
}
}
#[test]
fn test_batch_upsert_mapping_consistency() {
let index = HnswIndex::new(4, DistanceMetric::Euclidean).unwrap();
for i in 0..5u64 {
index.insert(i, &[i as f32, 0.0, 0.0, 0.0]);
}
assert_eq!(index.len(), 5);
let batch: Vec<(u64, &[f32])> = vec![
(1, &[100.0, 0.0, 0.0, 0.0]),
(2, &[200.0, 0.0, 0.0, 0.0]),
(3, &[300.0, 0.0, 0.0, 0.0]),
];
let inserted = index.insert_batch_parallel(batch);
assert_eq!(inserted, 3);
assert_eq!(index.len(), 5);
for ext_id in 0..5u64 {
let idx = index.mappings.get_idx(ext_id);
assert!(idx.is_some(), "get_idx({ext_id}) must return Some");
let reverse = index.mappings.get_id(idx.unwrap());
assert_eq!(
reverse,
Some(ext_id),
"Reverse mapping for idx {} must be {ext_id}, got {reverse:?}",
idx.unwrap()
);
}
let results = index.search(&[100.0, 0.0, 0.0, 0.0], 1);
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, 1, "Nearest to [100,0,0,0] must be id=1");
}