use std::sync::Arc;
use laurus::storage::Storage;
use laurus::storage::memory::{MemoryStorage, MemoryStorageConfig};
use laurus::vector::core::quantization::QuantizationMethod;
use laurus::vector::index::VectorIndex;
use laurus::vector::index::config::HnswIndexConfig;
use laurus::vector::index::hnsw::HnswIndex;
use laurus::vector::index::hnsw::reader::HnswIndexReader;
use laurus::vector::index::hnsw::segmented::SegmentedHnswIndex;
use laurus::vector::index::pq_codebook::{default_codebook_name, train_and_write_pq_codebook};
use laurus::vector::index::storage::VectorStorage;
use laurus::vector::search::searcher::{VectorIndexQuery, VectorIndexQueryParams};
use laurus::vector::{DistanceMetric, Vector};
const DIM: usize = 32;
const M: usize = 4;
fn make_corpus(count: usize, seed: u64, id_offset: u64) -> Vec<(u64, String, Vector)> {
let mut state = seed;
(0..count)
.map(|i| {
let data: Vec<f32> = (0..DIM)
.map(|_| {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
((state >> 33) as f32 / u32::MAX as f32) * 2.0 - 1.0
})
.collect();
(id_offset + i as u64, "v".to_string(), Vector::new(data))
})
.collect()
}
fn storage() -> Arc<MemoryStorage> {
Arc::new(MemoryStorage::new(MemoryStorageConfig::default()))
}
fn base_config() -> HnswIndexConfig {
HnswIndexConfig {
dimension: DIM,
m: 16,
ef_construction: 100,
normalize_vectors: false,
distance_metric: DistanceMetric::Euclidean,
quantization_method: QuantizationMethod::ProductQuantization { subvector_count: M },
..Default::default()
}
}
fn query(v: &Vector, top_k: usize) -> VectorIndexQuery {
VectorIndexQuery {
query: v.clone(),
params: VectorIndexQueryParams {
top_k,
..Default::default()
},
field_name: Some("v".to_string()),
filter: None,
}
}
fn owned_pq_pool(
reader: &dyn laurus::vector::reader::VectorIndexReader,
) -> Arc<laurus::vector::index::pq_storage::PqVectorPool> {
let hnsw_reader = reader
.as_any()
.downcast_ref::<HnswIndexReader>()
.expect("HnswIndexReader");
match hnsw_reader.vectors() {
VectorStorage::OwnedPq(pool) => pool.clone(),
other => panic!("expected OwnedPq, got {other:?}"),
}
}
#[test]
fn shared_codebook_search_results_match_fresh_training() {
let docs = make_corpus(400, 0x1234_5678_9ABC_DEF0, 0);
let trained_storage = storage();
let trained_index =
HnswIndex::create(trained_storage.clone(), "trained", base_config()).unwrap();
{
let mut writer = trained_index.writer().unwrap();
writer.add_vectors(docs.clone()).unwrap();
writer.commit().unwrap();
}
let shared_storage = storage();
let codebook_name = default_codebook_name("v");
let training_sample: Vec<Vector> = docs.iter().map(|(_, _, v)| v.clone()).collect();
train_and_write_pq_codebook(
shared_storage.as_ref() as &dyn Storage,
&codebook_name,
DIM,
M,
256,
false,
&training_sample,
)
.unwrap();
let mut shared_config = base_config();
shared_config.pq_codebook_path = Some(codebook_name);
shared_config
.resolve_pq_codebook(shared_storage.as_ref() as &dyn Storage)
.unwrap();
assert!(
shared_config.pq_codebook.is_some(),
"codebook must resolve before the index is created"
);
let shared_index = HnswIndex::create(shared_storage.clone(), "shared", shared_config).unwrap();
{
let mut writer = shared_index.writer().unwrap();
writer.add_vectors(docs.clone()).unwrap();
writer.commit().unwrap();
}
let trained_pool = owned_pq_pool(trained_index.reader().unwrap().as_ref());
let shared_pool = owned_pq_pool(shared_index.reader().unwrap().as_ref());
assert_eq!(shared_pool.params, trained_pool.params);
assert_eq!(shared_pool.codebook, trained_pool.codebook);
let trained_searcher = trained_index.searcher().unwrap();
let shared_searcher = shared_index.searcher().unwrap();
for (doc_id, _, vector) in docs.iter().step_by(37) {
let trained_hits = trained_searcher.search(&query(vector, 5)).unwrap();
let shared_hits = shared_searcher.search(&query(vector, 5)).unwrap();
let trained_ids: Vec<u64> = trained_hits.results.iter().map(|r| r.doc_id).collect();
let shared_ids: Vec<u64> = shared_hits.results.iter().map(|r| r.doc_id).collect();
assert_eq!(
trained_ids, shared_ids,
"doc {doc_id}: shared-codebook search must match fresh-trained search"
);
}
}
#[test]
fn shared_codebook_survives_a_forced_merge() {
let storage = storage();
let training_sample: Vec<Vector> = make_corpus(400, 0x0F0F_0F0F_0F0F_0F0F, 100_000)
.into_iter()
.map(|(_, _, v)| v)
.collect();
let codebook_name = default_codebook_name("v");
let trained = train_and_write_pq_codebook(
storage.as_ref() as &dyn Storage,
&codebook_name,
DIM,
M,
256,
false,
&training_sample,
)
.unwrap();
let mut config = base_config();
config.segmented = true;
config.pq_codebook_path = Some(codebook_name);
config
.resolve_pq_codebook(storage.as_ref() as &dyn Storage)
.unwrap();
let index =
SegmentedHnswIndex::open_or_create(storage.clone() as Arc<dyn Storage>, "vi", config)
.unwrap();
for batch in 0..3u64 {
let docs = make_corpus(20, 0xA5A5_0000 + batch, batch * 20);
let mut writer = index.writer().unwrap();
writer.add_vectors(docs).unwrap();
writer.commit().unwrap();
}
index.optimize().unwrap();
let hnsw_files: Vec<String> = storage
.list_files()
.unwrap()
.into_iter()
.filter(|f| f.ends_with(".hnsw"))
.collect();
assert_eq!(
hnsw_files.len(),
1,
"optimize must force-merge every commit's segment into one"
);
let segment_id = hnsw_files[0].trim_end_matches(".hnsw").to_string();
let merged_reader = HnswIndexReader::load(
storage.clone() as Arc<dyn Storage>,
&segment_id,
DistanceMetric::Euclidean,
)
.unwrap();
let merged_pool = match merged_reader.vectors() {
VectorStorage::OwnedPq(pool) => pool.clone(),
other => panic!("expected the merged segment to stay on PQ, got {other:?}"),
};
assert_eq!(
merged_pool.params, trained.params,
"the merged segment's PQ params must match the shared codebook, not a retrain"
);
assert_eq!(
merged_pool.codebook, trained.codebook,
"the merged segment's codebook must be byte-identical to the shared codebook"
);
assert_eq!(merged_pool.vector_count, 60);
let searcher = index.searcher().unwrap();
let (doc_id, _, vector) = &make_corpus(20, 0xA5A5_0001, 20)[0];
let hits = searcher.search(&query(vector, 1)).unwrap();
assert_eq!(hits.results[0].doc_id, *doc_id);
}