use laurus::storage::Storage;
use laurus::storage::memory::{MemoryStorage, MemoryStorageConfig};
use laurus::vector::DistanceMetric;
use laurus::vector::StoredVector;
use laurus::vector::Vector;
use laurus::vector::VectorFieldConfig;
use laurus::vector::core::rerank::RerankStorageKind;
use laurus::vector::index::field::{FieldSearchInput, VectorFieldReader, VectorFieldWriter};
use laurus::vector::index::hnsw;
use laurus::vector::index::hnsw::reader::HnswIndexReader;
use laurus::vector::index::segment::manager::{SegmentManager, SegmentManagerConfig};
use laurus::vector::index::segmented_field::SegmentedVectorField;
use laurus::vector::store::request::QueryVector;
use laurus::vector::{FieldOption, HnswOption};
use std::sync::Arc;
#[tokio::test]
async fn test_segmented_field_manual_merge() -> Result<(), Box<dyn std::error::Error>> {
let storage = Arc::new(MemoryStorage::new(MemoryStorageConfig::default()));
let manager_config = SegmentManagerConfig {
max_segments: 2, merge_factor: 2, min_vectors_per_segment: 1, ..Default::default()
};
let manager = Arc::new(SegmentManager::new(
manager_config,
storage.clone(),
hnsw::segment::LAYOUT,
)?);
let field_config = VectorFieldConfig {
vector: Some(FieldOption::Hnsw(HnswOption {
dimension: 4,
distance: DistanceMetric::Euclidean,
m: 16, ef_construction: 200, default_ef_search: None,
base_weight: 1.0,
quantizer: Default::default(),
rerank_storage: None,
embedder: None,
pq_codebook_path: None,
})),
lexical: None,
};
let field = SegmentedVectorField::create(
"test_field",
field_config,
manager.clone(),
storage.clone(),
None,
)?;
field
.add_stored_vector(1, &StoredVector::new(vec![1.0, 0.0, 0.0, 0.0]), 0)
.await?;
field.flush().await?;
field
.add_stored_vector(2, &StoredVector::new(vec![0.0, 1.0, 0.0, 0.0]), 0)
.await?;
field.flush().await?;
field
.add_stored_vector(3, &StoredVector::new(vec![0.0, 0.0, 1.0, 0.0]), 0)
.await?;
field.flush().await?;
let segments = manager.list_segments();
assert_eq!(segments.len(), 3, "Should have 3 segments before merge");
field.perform_merge()?;
let segments_after = manager.list_segments();
assert_eq!(
segments_after.len(),
2,
"Should have 2 segments after merge"
);
let stats = field.stats()?; assert_eq!(stats.vector_count, 3);
Ok(())
}
#[tokio::test]
async fn segmented_field_reader_cache_reuses_and_invalidates()
-> Result<(), Box<dyn std::error::Error>> {
let storage = Arc::new(MemoryStorage::new(MemoryStorageConfig::default()));
let manager_config = SegmentManagerConfig {
max_segments: 2,
merge_factor: 2,
min_vectors_per_segment: 1,
..Default::default()
};
let manager = Arc::new(SegmentManager::new(
manager_config,
storage.clone(),
hnsw::segment::LAYOUT,
)?);
let field_config = VectorFieldConfig {
vector: Some(FieldOption::Hnsw(HnswOption {
dimension: 4,
distance: DistanceMetric::Euclidean,
m: 16,
ef_construction: 200,
default_ef_search: None,
base_weight: 1.0,
quantizer: Default::default(),
rerank_storage: None,
embedder: None,
pq_codebook_path: None,
})),
lexical: None,
};
let field = SegmentedVectorField::create(
"embedding",
field_config,
manager.clone(),
storage.clone(),
None,
)?;
field
.add_stored_vector(1, &StoredVector::new(vec![1.0, 0.0, 0.0, 0.0]), 0)
.await?;
field.flush().await?;
field
.add_stored_vector(2, &StoredVector::new(vec![0.0, 1.0, 0.0, 0.0]), 0)
.await?;
field.flush().await?;
field
.add_stored_vector(3, &StoredVector::new(vec![0.0, 0.0, 1.0, 0.0]), 0)
.await?;
field.flush().await?;
assert_eq!(manager.list_segments().len(), 3);
assert!(
field.reader_cache.is_empty(),
"cache should be empty before the first search"
);
let segment_ids: Vec<String> = manager
.list_segments()
.iter()
.map(|s| s.segment_id.clone())
.collect();
let query_input = || FieldSearchInput {
field: "embedding".to_string(),
query_vectors: vec![QueryVector {
vector: Vector::new(vec![1.0, 0.0, 0.0, 0.0]),
weight: 1.0,
fields: None,
}],
limit: 3,
allowed_ids: None,
};
let _first = field.search(query_input())?;
assert_eq!(
field.reader_cache.len(),
3,
"cache should hold one reader per managed segment after the first search"
);
for id in &segment_ids {
assert!(
field.reader_cache.contains(id),
"cache should contain entry for segment {id}"
);
}
let _second = field.search(query_input())?;
assert_eq!(
field.reader_cache.len(),
3,
"cache size should be stable across repeat searches"
);
field.perform_merge()?;
let after = manager.list_segments();
assert_eq!(after.len(), 2, "manager should hold 2 segments after merge");
let remaining: std::collections::HashSet<String> =
after.iter().map(|s| s.segment_id.clone()).collect();
let merged_away: Vec<&String> = segment_ids
.iter()
.filter(|id| !remaining.contains(*id))
.collect();
assert_eq!(
merged_away.len(),
2,
"merge should consume 2 source segments"
);
for id in merged_away {
assert!(
!field.reader_cache.contains(id),
"cache entry for merged-away segment {id} must be invalidated"
);
}
assert!(
field.reader_cache.len() <= 1,
"cache should have at most the one survivor pre-search; got {}",
field.reader_cache.len()
);
Ok(())
}
#[tokio::test]
async fn segmented_flush_and_merge_emit_rerank_sidecar() -> Result<(), Box<dyn std::error::Error>> {
let storage = Arc::new(MemoryStorage::new(MemoryStorageConfig::default()));
let manager_config = SegmentManagerConfig {
max_segments: 2,
merge_factor: 2,
min_vectors_per_segment: 1,
..Default::default()
};
let manager = Arc::new(SegmentManager::new(
manager_config,
storage.clone(),
hnsw::segment::LAYOUT,
)?);
let field_config = VectorFieldConfig {
vector: Some(FieldOption::Hnsw(HnswOption {
dimension: 4,
distance: DistanceMetric::Cosine,
m: 16,
ef_construction: 200,
rerank_storage: Some(RerankStorageKind::F32),
..HnswOption::default()
})),
lexical: None,
};
let field = SegmentedVectorField::create(
"embedding",
field_config,
manager.clone(),
storage.clone(),
None,
)?;
field
.add_stored_vector(1, &StoredVector::new(vec![1.0, 0.0, 0.0, 0.0]), 0)
.await?;
field.flush().await?;
field
.add_stored_vector(2, &StoredVector::new(vec![0.0, 1.0, 0.0, 0.0]), 0)
.await?;
field.flush().await?;
field
.add_stored_vector(3, &StoredVector::new(vec![0.0, 0.0, 1.0, 0.0]), 0)
.await?;
field.flush().await?;
let before_ids: Vec<String> = manager
.list_segments()
.iter()
.map(|s| s.segment_id.clone())
.collect();
assert_eq!(before_ids.len(), 3);
for id in &before_ids {
let sidecar = format!("{id}.hnsw.f32");
assert!(
storage.file_exists(&sidecar),
"flushed segment must emit {sidecar}"
);
}
field.perform_merge()?;
let after = manager.list_segments();
assert_eq!(after.len(), 2);
let merged_id = after
.iter()
.map(|s| s.segment_id.clone())
.find(|id| !before_ids.contains(id))
.expect("merge must create a new segment");
let merged_sidecar = format!("{merged_id}.hnsw.f32");
assert!(
storage.file_exists(&merged_sidecar),
"merged segment must re-emit {merged_sidecar} (Issue #790: the \
merge-engine config previously dropped rerank_storage)"
);
let reader = HnswIndexReader::load(
storage.clone() as Arc<dyn Storage>,
&merged_id,
DistanceMetric::Cosine,
)?;
let pool = reader
.rerank_storage()
.expect("merged sidecar must load into the rerank pool");
let merged_doc_count = [1u64, 2, 3]
.iter()
.filter(|doc_id| pool.contains(**doc_id, "embedding"))
.count();
assert_eq!(
merged_doc_count, 2,
"the merged pool must contain exactly the 2 merged vectors"
);
Ok(())
}
#[tokio::test]
async fn segmented_merge_keeps_vectors_unnormalized_for_euclidean()
-> Result<(), Box<dyn std::error::Error>> {
let storage = Arc::new(MemoryStorage::new(MemoryStorageConfig::default()));
let manager_config = SegmentManagerConfig {
max_segments: 2,
merge_factor: 2,
min_vectors_per_segment: 1,
..Default::default()
};
let manager = Arc::new(SegmentManager::new(
manager_config,
storage.clone(),
hnsw::segment::LAYOUT,
)?);
let field_config = VectorFieldConfig {
vector: Some(FieldOption::Hnsw(HnswOption {
dimension: 4,
distance: DistanceMetric::Euclidean,
m: 16,
ef_construction: 200,
rerank_storage: Some(RerankStorageKind::F32),
..HnswOption::default()
})),
lexical: None,
};
let field = SegmentedVectorField::create(
"embedding",
field_config,
manager.clone(),
storage.clone(),
None,
)?;
let originals: [(u64, [f32; 4]); 3] = [
(1, [2.0, 0.0, 0.0, 0.0]),
(2, [0.0, 3.0, 0.0, 0.0]),
(3, [0.0, 0.0, 4.0, 0.0]),
];
for (doc_id, vec) in &originals {
field
.add_stored_vector(*doc_id, &StoredVector::new(vec.to_vec()), 0)
.await?;
field.flush().await?;
}
let before_ids: Vec<String> = manager
.list_segments()
.iter()
.map(|s| s.segment_id.clone())
.collect();
field.perform_merge()?;
let merged_id = manager
.list_segments()
.iter()
.map(|s| s.segment_id.clone())
.find(|id| !before_ids.contains(id))
.expect("merge must create a new segment");
let reader = HnswIndexReader::load(
storage.clone() as Arc<dyn Storage>,
&merged_id,
DistanceMetric::Euclidean,
)?;
let pool = reader
.rerank_storage()
.expect("merged sidecar must load into the rerank pool");
let mut checked = 0;
for (doc_id, original) in &originals {
if let Some(slice) = pool.get_f32_slice(*doc_id, "embedding") {
let norm: f32 = slice.iter().map(|v| v * v).sum::<f32>().sqrt();
let expected: f32 = original.iter().map(|v| v * v).sum::<f32>().sqrt();
assert!(
(norm - expected).abs() < 0.1,
"merged vector for doc {doc_id} must keep its original norm \
{expected} (got {norm}); norm 1.0 means the merge config \
normalized a Euclidean field (Issue #790 latent bug)"
);
checked += 1;
}
}
assert_eq!(checked, 2, "the merged pool must contain 2 of the 3 docs");
Ok(())
}
#[tokio::test]
async fn segmented_merge_preserves_rerank_sidecar_f32_losslessly()
-> Result<(), Box<dyn std::error::Error>> {
let storage = Arc::new(MemoryStorage::new(MemoryStorageConfig::default()));
let manager_config = SegmentManagerConfig {
max_segments: 2,
merge_factor: 2,
min_vectors_per_segment: 1,
..Default::default()
};
let manager = Arc::new(SegmentManager::new(
manager_config,
storage.clone(),
hnsw::segment::LAYOUT,
)?);
let field_config = VectorFieldConfig {
vector: Some(FieldOption::Hnsw(HnswOption {
dimension: 4,
distance: DistanceMetric::Euclidean,
m: 16,
ef_construction: 200,
rerank_storage: Some(RerankStorageKind::F32),
..HnswOption::default()
})),
lexical: None,
};
let field = SegmentedVectorField::create(
"embedding",
field_config,
manager.clone(),
storage.clone(),
None,
)?;
let probes: [(u64, [f32; 4]); 3] = [
(1, [0.137, 0.642, 0.319, 0.808]),
(2, [0.251, 0.563, 0.174, 0.926]),
(3, [0.488, 0.071, 0.655, 0.302]),
];
let anchor = [40.0_f32, 40.0, 40.0, 40.0];
for (i, (doc_id, vec)) in probes.iter().enumerate() {
field
.add_stored_vector(*doc_id, &StoredVector::new(vec.to_vec()), 0)
.await?;
field
.add_stored_vector(100 + i as u64, &StoredVector::new(anchor.to_vec()), 0)
.await?;
field.flush().await?;
}
let before_ids: Vec<String> = manager
.list_segments()
.iter()
.map(|s| s.segment_id.clone())
.collect();
field.perform_merge()?;
let merged_id = manager
.list_segments()
.iter()
.map(|s| s.segment_id.clone())
.find(|id| !before_ids.contains(id))
.expect("merge must create a new segment");
let reader = HnswIndexReader::load(
storage.clone() as Arc<dyn Storage>,
&merged_id,
DistanceMetric::Euclidean,
)?;
let pool = reader
.rerank_storage()
.expect("merged sidecar must load into the rerank pool");
let mut checked = 0;
for (doc_id, original) in &probes {
if let Some(slice) = pool.get_f32_slice(*doc_id, "embedding") {
for (i, (got, want)) in slice.iter().zip(original.iter()).enumerate() {
assert!(
(got - want).abs() <= f32::EPSILON,
"probe doc {doc_id} component {i}: merged f32 {got} != original {want} \
(diff {:.2e}); a coarse difference means the merge rebuilt the \
sidecar from int8-dequantized values (Issue #795)",
(got - want).abs()
);
}
checked += 1;
}
}
assert_eq!(
checked, 2,
"the merged pool must contain the probes from the 2 merged segments"
);
Ok(())
}
fn cosine_field(
dimension: usize,
) -> Result<(SegmentedVectorField, Arc<SegmentManager>), Box<dyn std::error::Error>> {
let storage = Arc::new(MemoryStorage::new(MemoryStorageConfig::default()));
let manager = Arc::new(SegmentManager::new(
SegmentManagerConfig {
max_segments: 8,
merge_factor: 2,
min_vectors_per_segment: 1,
..Default::default()
},
storage.clone(),
hnsw::segment::LAYOUT,
)?);
let field_config = VectorFieldConfig {
vector: Some(FieldOption::Hnsw(HnswOption {
dimension,
distance: DistanceMetric::Cosine,
m: 16,
ef_construction: 200,
default_ef_search: None,
base_weight: 1.0,
quantizer: Default::default(),
rerank_storage: None,
embedder: None,
pq_codebook_path: None,
})),
lexical: None,
};
let field = SegmentedVectorField::create(
"embedding",
field_config,
manager.clone(),
storage.clone(),
None,
)?;
Ok((field, manager))
}
fn top_k_input(query: Vec<f32>, limit: usize) -> FieldSearchInput {
FieldSearchInput {
field: "embedding".to_string(),
query_vectors: vec![QueryVector {
vector: Vector::new(query),
weight: 1.0,
fields: None,
}],
limit,
allowed_ids: None,
}
}
#[tokio::test]
async fn search_before_flush_returns_active_hits() -> Result<(), Box<dyn std::error::Error>> {
let (field, _manager) = cosine_field(4)?;
field
.add_stored_vector(1, &StoredVector::new(vec![1.0, 0.0, 0.0, 0.0]), 0)
.await?;
field
.add_stored_vector(2, &StoredVector::new(vec![0.8, 0.6, 0.0, 0.0]), 0)
.await?;
field
.add_stored_vector(3, &StoredVector::new(vec![0.0, 1.0, 0.0, 0.0]), 0)
.await?;
let results = field.search(top_k_input(vec![1.0, 0.0, 0.0, 0.0], 2))?;
let ids: Vec<u64> = results.hits.iter().map(|h| h.doc_id).collect();
assert_eq!(
ids,
vec![1, 2],
"top-2 must be doc 1 (cos 1.0) then doc 2 (cos 0.8), got {ids:?}"
);
assert!(
results.hits[0].score > results.hits[1].score,
"scores must be ranked descending: {} vs {}",
results.hits[0].score,
results.hits[1].score
);
Ok(())
}
#[tokio::test]
async fn search_merges_active_and_managed_hits() -> Result<(), Box<dyn std::error::Error>> {
let (field, _manager) = cosine_field(4)?;
field
.add_stored_vector(1, &StoredVector::new(vec![1.0, 0.0, 0.0, 0.0]), 0)
.await?;
field.flush().await?;
field
.add_stored_vector(2, &StoredVector::new(vec![0.9, 0.1, 0.0, 0.0]), 0)
.await?;
let results = field.search(top_k_input(vec![1.0, 0.0, 0.0, 0.0], 3))?;
let mut ids: Vec<u64> = results.hits.iter().map(|h| h.doc_id).collect();
ids.sort_unstable();
assert_eq!(
ids,
vec![1, 2],
"search must merge managed (doc 1) and active (doc 2) hits, got {ids:?}"
);
Ok(())
}
#[tokio::test]
async fn search_active_segment_above_parallel_threshold() -> Result<(), Box<dyn std::error::Error>>
{
let (field, _manager) = cosine_field(4)?;
for doc_id in 1..=2500u64 {
let v = if doc_id == 777 {
vec![1.0, 0.0, 0.0, 0.0]
} else {
vec![0.0, 1.0, 0.0, 0.0]
};
field
.add_stored_vector(doc_id, &StoredVector::new(v), 0)
.await?;
}
let results = field.search(top_k_input(vec![1.0, 0.0, 0.0, 0.0], 1))?;
assert_eq!(results.hits.len(), 1, "top-1 must return exactly one hit");
assert_eq!(
results.hits[0].doc_id, 777,
"the unique aligned vector must win the unflushed scan"
);
Ok(())
}