use crate::storage::memory::MemoryStorage;
use crate::vector::core::distance::DistanceMetric;
use crate::vector::core::vector::Vector;
use crate::vector::index::config::IvfIndexConfig;
use crate::vector::index::ivf::writer::IvfIndexWriter;
use crate::vector::writer::{VectorIndexWriter, VectorIndexWriterConfig};
use std::sync::Arc;
#[test]
fn test_ivf_partition_rebalancing() {
let storage = Arc::new(MemoryStorage::default());
let config = IvfIndexConfig {
dimension: 2,
distance_metric: DistanceMetric::Euclidean,
n_clusters: 4,
n_probe: 2,
normalize_vectors: false,
..IvfIndexConfig::default()
};
let writer_config = VectorIndexWriterConfig::default();
let mut writer =
IvfIndexWriter::with_storage(config, writer_config, "test_ivf_vectors", storage).unwrap();
let mut vectors = Vec::new();
vectors.push((0, "f".to_string(), Vector::new(vec![0.0, 0.0])));
for i in 0..10 {
vectors.push((
i + 1,
"f".to_string(),
Vector::new(vec![100.0 + i as f32 * 0.1, 100.0 + i as f32 * 0.1]),
));
}
for i in 0..4 {
vectors.push((
i + 11,
"f".to_string(),
Vector::new(vec![0.0 + i as f32 * 0.1, 100.0 + i as f32 * 0.1]),
));
}
for i in 0..4 {
vectors.push((
i + 15,
"f".to_string(),
Vector::new(vec![100.0 + i as f32 * 0.1, 0.0 + i as f32 * 0.1]),
));
}
writer.build(vectors).unwrap();
writer.finalize().unwrap();
let initial_stats = writer.get_cluster_stats();
println!("Initial stats: {:?}", initial_stats);
let merged = writer.merge_sparse_clusters(2).unwrap();
assert!(merged > 0, "Should have merged at least one sparse cluster");
let stats_after_merge = writer.get_cluster_stats();
assert_eq!(stats_after_merge.len(), initial_stats.len() - merged);
let split = writer.split_dense_clusters(5).unwrap();
assert!(split > 0, "Should have split at least one dense cluster");
let stats_after_split = writer.get_cluster_stats();
assert_eq!(stats_after_split.len(), stats_after_merge.len() + split);
let total_vectors: usize = stats_after_split.iter().map(|s| s.count).sum();
assert_eq!(total_vectors, 19);
}
fn build_singleton_cluster_index(name: &str) -> Arc<MemoryStorage> {
let storage = Arc::new(MemoryStorage::default());
let config = IvfIndexConfig {
dimension: 2,
distance_metric: DistanceMetric::Euclidean,
n_clusters: 12,
n_probe: 1,
normalize_vectors: false,
..IvfIndexConfig::default()
};
let mut writer = IvfIndexWriter::with_storage(
config,
VectorIndexWriterConfig::default(),
name,
storage.clone(),
)
.unwrap();
let vectors: Vec<(u64, String, Vector)> = (0..12)
.map(|i| {
(
i as u64,
"f".to_string(),
Vector::new(vec![(i as f32 + 1.0) * 1000.0, 0.0]),
)
})
.collect();
writer.build(vectors).unwrap();
writer.finalize().unwrap();
writer.write().unwrap();
storage
}
#[test]
fn test_ivf_searcher_honors_n_probe() {
use crate::vector::index::ivf::reader::IvfIndexReader;
use crate::vector::index::ivf::searcher::IvfSearcher;
use crate::vector::search::searcher::{VectorIndexQuery, VectorIndexSearcher};
let storage = build_singleton_cluster_index("test_ivf_honors_n_probe");
let reader: Arc<dyn crate::vector::reader::VectorIndexReader> = Arc::new(
IvfIndexReader::load(
storage,
"test_ivf_honors_n_probe",
DistanceMetric::Euclidean,
)
.unwrap(),
);
let n_centroids = reader
.as_any()
.downcast_ref::<IvfIndexReader>()
.unwrap()
.centroids()
.len();
assert_eq!(n_centroids, 12, "expected one centroid per input vector");
let query = Vector::new(vec![1000.0, 0.0]);
for (n_probe, expected) in [(1usize, 1usize), (5, 5), (11, 11), (12, 12)] {
let searcher = IvfSearcher::with_n_probe(reader.clone(), n_probe).unwrap();
let request = VectorIndexQuery::new(query.clone()).top_k(12);
let results = searcher.search(&request).unwrap();
assert_eq!(
results.candidates_examined, expected,
"n_probe = {n_probe} should probe {expected} singleton clusters"
);
}
}
#[test]
fn test_ivf_searcher_honors_filter_inline() {
use crate::vector::index::ivf::reader::IvfIndexReader;
use crate::vector::index::ivf::searcher::IvfSearcher;
use crate::vector::search::filter_set::FilterSet;
use crate::vector::search::searcher::{VectorIndexQuery, VectorIndexSearcher};
let storage = build_singleton_cluster_index("test_ivf_filter_inline");
let reader: Arc<dyn crate::vector::reader::VectorIndexReader> = Arc::new(
IvfIndexReader::load(storage, "test_ivf_filter_inline", DistanceMetric::Euclidean).unwrap(),
);
let query = Vector::new(vec![1000.0, 0.0]);
let searcher = IvfSearcher::with_n_probe(reader, 12).unwrap();
let unfiltered = searcher
.search(&VectorIndexQuery::new(query.clone()).top_k(12))
.unwrap();
assert_eq!(unfiltered.candidates_examined, 12);
let allow: Arc<FilterSet> = Arc::new(FilterSet::Hash([0u64, 5, 11].into_iter().collect()));
let filtered = searcher
.search(
&VectorIndexQuery::new(query.clone())
.top_k(12)
.filter(allow.clone()),
)
.unwrap();
assert_eq!(
filtered.candidates_examined, 3,
"only allowed docs should reach the distance kernel"
);
for r in &filtered.results {
assert!(
allow.contains(r.doc_id),
"result {} not in allow-set",
r.doc_id
);
}
}
#[test]
fn test_ivf_index_searcher_uses_configured_n_probe() {
use crate::vector::index::VectorIndex;
use crate::vector::index::ivf::IvfIndex;
use crate::vector::search::searcher::VectorIndexQuery;
let storage = Arc::new(MemoryStorage::default());
let config = IvfIndexConfig {
dimension: 2,
distance_metric: DistanceMetric::Euclidean,
n_clusters: 12,
n_probe: 5,
normalize_vectors: false,
..IvfIndexConfig::default()
};
let index = IvfIndex::create(storage, "test_ivf_factory_n_probe", config).unwrap();
let mut writer = index.writer().unwrap();
let vectors: Vec<(u64, String, Vector)> = (0..12)
.map(|i| {
(
i as u64,
"f".to_string(),
Vector::new(vec![(i as f32 + 1.0) * 1000.0, 0.0]),
)
})
.collect();
writer.build(vectors).unwrap();
writer.finalize().unwrap();
writer.write().unwrap();
let searcher = index.searcher().unwrap();
let query = Vector::new(vec![1000.0, 0.0]);
let request = VectorIndexQuery::new(query).top_k(12);
let results = searcher.search(&request).unwrap();
assert_eq!(
results.candidates_examined, 5,
"IvfIndex::searcher() should probe the configured n_probe (5) clusters"
);
}