use tempfile::TempDir;
use laurus::DistanceMetric;
use laurus::Engine;
use laurus::SearchRequestBuilder;
use laurus::storage::file::FileStorageConfig;
use laurus::storage::prefixed::PrefixedStorage;
use laurus::storage::{Storage, StorageConfig, StorageFactory};
use laurus::vector::HnswOption;
use laurus::vector::Vector;
use laurus::vector::core::distance::DistanceMetric as VectorDistanceMetric;
use laurus::vector::core::rerank::RerankStorageKind;
use laurus::vector::index::hnsw::reader::HnswIndexReader;
use laurus::{DataValue, Document};
use laurus::{FieldOption, QueryVector, Schema, VectorSearchQuery};
use std::sync::Arc;
const SIDECAR_NAME: &str = "vector/embedding/segment_000000.hnsw.f32";
fn vector_request(query: &[f32], rerank_factor: Option<usize>) -> laurus::SearchRequest {
let mut builder = SearchRequestBuilder::new()
.vector_query(VectorSearchQuery::Vectors(vec![QueryVector {
vector: Vector::new(query.to_vec()),
weight: 1.0,
fields: Some(vec!["embedding".to_string()]),
}]))
.limit(1);
if let Some(factor) = rerank_factor {
builder = builder.vector_rerank_factor(factor);
}
builder.build()
}
#[tokio::test(flavor = "multi_thread")]
async fn engine_search_with_rerank_factor_succeeds_on_stage2_field() -> laurus::Result<()> {
let temp_dir = TempDir::new().unwrap();
let storage =
StorageFactory::create(StorageConfig::File(FileStorageConfig::new(temp_dir.path())))?;
let hnsw_opt = HnswOption {
dimension: 4,
distance: DistanceMetric::Cosine,
m: 4,
ef_construction: 16,
rerank_storage: Some(RerankStorageKind::F32),
..HnswOption::default()
};
let schema = Schema::builder()
.add_field("embedding", FieldOption::Hnsw(hnsw_opt))
.build();
let engine = Engine::new(storage.clone(), schema).await?;
let vectors: [(&str, [f32; 4]); 4] = [
("doc1", [0.92, 0.31, 0.17, 0.05]),
("doc2", [0.13, 0.83, 0.41, 0.27]),
("doc3", [0.05, 0.19, 0.77, 0.61]),
("doc4", [0.33, 0.47, 0.29, 0.71]),
];
for (id, vec) in &vectors {
let doc = Document::builder()
.add_field("embedding", DataValue::Vector(vec.to_vec()))
.build();
engine.put_document(id, doc).await?;
}
engine.commit().await?;
assert!(
storage.file_exists(SIDECAR_NAME),
"commit with rerank_storage: Some(F32) must emit {SIDECAR_NAME} \
(Issue #790: option was dropped in extract_index_type_config)"
);
let vector_storage: Arc<dyn Storage> =
Arc::new(PrefixedStorage::new("vector", storage.clone()));
let field_storage: Arc<dyn Storage> =
Arc::new(PrefixedStorage::new("embedding", vector_storage));
let reader = HnswIndexReader::load(
field_storage,
"segment_000000",
VectorDistanceMetric::Cosine,
)?;
let pool = reader
.rerank_storage()
.expect("the committed Stage-2 segment must load its rerank pool");
let positions = pool
.field_position_index("embedding")
.expect("the rerank pool must index the 'embedding' field (Issue #790)");
assert_eq!(
positions.len(),
vectors.len(),
"the rerank pool must hold one f32 vector per committed document"
);
let query = [0.87, 0.36, 0.21, 0.09];
let with_rerank = engine.search(vector_request(&query, Some(4))).await?;
assert_eq!(with_rerank.len(), 1, "expected exactly 1 hit");
assert_eq!(
with_rerank[0].id, "doc1",
"doc1 should be the closest match to the rerank-augmented query"
);
let without_rerank = engine.search(vector_request(&query, None)).await?;
assert_eq!(without_rerank.len(), 1);
assert_eq!(without_rerank[0].id, "doc1");
assert_ne!(
with_rerank[0].score, without_rerank[0].score,
"rerank_factor must change the score via the f32 sidecar pool; \
identical scores mean the silent Stage-1 fallback was taken \
(Issue #790)"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn engine_search_with_rerank_factor_silently_falls_back_on_stage1_field() -> laurus::Result<()>
{
let temp_dir = TempDir::new().unwrap();
let storage =
StorageFactory::create(StorageConfig::File(FileStorageConfig::new(temp_dir.path())))?;
let hnsw_opt = HnswOption {
dimension: 4,
distance: DistanceMetric::Cosine,
m: 4,
ef_construction: 16,
rerank_storage: None,
..HnswOption::default()
};
let schema = Schema::builder()
.add_field("embedding", FieldOption::Hnsw(hnsw_opt))
.build();
let engine = Engine::new(storage.clone(), schema).await?;
let doc1 = Document::builder()
.add_field("embedding", DataValue::Vector(vec![1.0, 0.0, 0.0, 0.0]))
.build();
let doc2 = Document::builder()
.add_field("embedding", DataValue::Vector(vec![0.0, 1.0, 0.0, 0.0]))
.build();
engine.put_document("doc1", doc1).await?;
engine.put_document("doc2", doc2).await?;
engine.commit().await?;
assert!(
!storage.file_exists(SIDECAR_NAME),
"a field without rerank_storage must not emit {SIDECAR_NAME}"
);
let results = engine
.search(vector_request(&[0.95, 0.05, 0.0, 0.0], Some(5)))
.await?;
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "doc1");
Ok(())
}