use std::collections::HashSet;
use laurus::storage::StorageConfig;
use laurus::storage::StorageFactory;
use laurus::storage::memory::MemoryStorageConfig;
use laurus::vector::core::distance::DistanceMetric;
use laurus::vector::core::distance_quantized::{QuantizedQuery, distance_quantized};
use laurus::vector::core::quantization::{QuantizedVectorMeta, ScalarQuantParams};
use laurus::vector::core::rerank::RerankStorageKind;
use laurus::vector::core::vector::Vector;
use laurus::vector::index::VectorIndex;
use laurus::vector::index::config::HnswIndexConfig;
use laurus::vector::index::hnsw::HnswIndex;
use laurus::vector::index::hnsw::searcher::HnswSearcher;
use laurus::vector::search::searcher::{VectorIndexQuery, VectorIndexSearcher};
const QUANT_KERNEL_RECALL_THRESHOLD: f32 = 0.95;
const HNSW_RECALL_THRESHOLD: f32 = 0.85;
const STAGE2_KERNEL_RECALL_THRESHOLD: f32 = 0.99;
const STAGE2_HNSW_RECALL_THRESHOLD: f32 = 0.98;
const DIM: usize = 128;
const N_QUERIES: usize = 100;
const TOP_K: usize = 10;
fn pseudo_random_f32(seed: u32, len: usize, lo: f32, hi: f32) -> Vec<f32> {
let mut state = seed.wrapping_mul(0x9E37_79B9).wrapping_add(0xDEAD_BEEF);
let range = hi - lo;
(0..len)
.map(|_| {
state = state.wrapping_mul(1103515245).wrapping_add(12345);
let bits = (state >> 16) as u16;
lo + (bits as f32 / u16::MAX as f32) * range
})
.collect()
}
fn pseudo_random_unit_norm(seed: u32, len: usize) -> Vec<f32> {
let mut v = pseudo_random_f32(seed, len, -1.0, 1.0);
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 0.0 {
for x in &mut v {
*x /= norm;
}
}
v
}
#[allow(dead_code)]
fn pseudo_random_clustered(
seed: u32,
n: usize,
dim: usize,
n_clusters: usize,
jitter: f32,
) -> Vec<Vec<f32>> {
let centroids: Vec<Vec<f32>> = (0..n_clusters)
.map(|c| pseudo_random_unit_norm(seed.wrapping_add(c as u32 * 0x9E37_79B9), dim))
.collect();
(0..n)
.map(|i| {
let mut state = seed
.wrapping_add(0xABCD_0000)
.wrapping_add(i as u32)
.wrapping_mul(2654435761);
state = state.wrapping_mul(1103515245).wrapping_add(12345);
let cluster = (state >> 16) as usize % n_clusters;
let noise =
pseudo_random_f32(seed.wrapping_add(0xDEAD_0000 + i as u32), dim, -1.0, 1.0);
let mut v: Vec<f32> = centroids[cluster]
.iter()
.zip(noise.iter())
.map(|(c, n)| (1.0 - jitter) * c + jitter * n)
.collect();
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 0.0 {
for x in &mut v {
*x /= norm;
}
}
v
})
.collect()
}
fn exact_cosine_distance(a: &[f32], b: &[f32]) -> f32 {
debug_assert_eq!(a.len(), b.len());
let mut dot = 0.0_f32;
let mut na = 0.0_f32;
let mut nb = 0.0_f32;
for (x, y) in a.iter().zip(b.iter()) {
dot += x * y;
na += x * x;
nb += y * y;
}
let denom = na.sqrt() * nb.sqrt();
if denom == 0.0 {
1.0
} else {
let cos = (dot / denom).clamp(-1.0, 1.0);
1.0 - cos
}
}
fn exact_top_k(corpus: &[Vec<f32>], query: &[f32], k: usize) -> HashSet<u64> {
let mut scored: Vec<(u64, f32)> = corpus
.iter()
.enumerate()
.map(|(idx, v)| (idx as u64, exact_cosine_distance(query, v)))
.collect();
scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
scored.into_iter().take(k).map(|(id, _)| id).collect()
}
fn recall_at_k(exact: &HashSet<u64>, approx: &HashSet<u64>, k: usize) -> f32 {
debug_assert!(k > 0);
let intersection = exact.intersection(approx).count();
intersection as f32 / k as f32
}
fn measure_recall(corpus: Vec<Vec<f32>>, queries: &[Vec<f32>], ef_search: usize) -> f32 {
let storage = StorageFactory::create(StorageConfig::Memory(MemoryStorageConfig::default()))
.expect("memory storage");
let config = HnswIndexConfig {
dimension: DIM,
m: 16,
ef_construction: 200,
distance_metric: DistanceMetric::Cosine,
..Default::default()
};
let index = HnswIndex::create(storage, "recall_index", config).expect("create index");
let mut writer = index.writer().expect("writer");
let docs: Vec<(u64, String, Vector)> = corpus
.iter()
.enumerate()
.map(|(i, v)| (i as u64, "embedding".to_string(), Vector::new(v.clone())))
.collect();
writer.build(docs).expect("build");
writer.finalize().expect("finalize");
writer.commit().expect("commit");
let reader = index.reader().expect("reader");
let mut searcher = HnswSearcher::new(reader).expect("searcher");
searcher.set_ef_search(ef_search);
let mut total_recall = 0.0_f32;
for query in queries {
let exact = exact_top_k(&corpus, query, TOP_K);
let request = VectorIndexQuery::new(Vector::new(query.clone()))
.top_k(TOP_K)
.field_name("embedding".to_string());
let results = searcher.search(&request).expect("search");
let approx: HashSet<u64> = results.results.iter().map(|r| r.doc_id).collect();
total_recall += recall_at_k(&exact, &approx, TOP_K);
}
total_recall / queries.len() as f32
}
fn measure_recall_with_rerank(
corpus: Vec<Vec<f32>>,
queries: &[Vec<f32>],
ef_search: usize,
rerank_factor: usize,
) -> f32 {
measure_recall_with_rerank_cfg(corpus, queries, ef_search, rerank_factor, 16, 200)
}
#[allow(dead_code)]
fn measure_recall_with_rerank_cfg(
corpus: Vec<Vec<f32>>,
queries: &[Vec<f32>],
ef_search: usize,
rerank_factor: usize,
m: usize,
ef_construction: usize,
) -> f32 {
let storage = StorageFactory::create(StorageConfig::Memory(MemoryStorageConfig::default()))
.expect("memory storage");
let config = HnswIndexConfig {
dimension: DIM,
m,
ef_construction,
distance_metric: DistanceMetric::Cosine,
rerank_storage: Some(RerankStorageKind::F32),
..Default::default()
};
let index = HnswIndex::create(storage, "stage2_recall_index", config).expect("create index");
let mut writer = index.writer().expect("writer");
let docs: Vec<(u64, String, Vector)> = corpus
.iter()
.enumerate()
.map(|(i, v)| (i as u64, "embedding".to_string(), Vector::new(v.clone())))
.collect();
writer.build(docs).expect("build");
writer.finalize().expect("finalize");
writer.commit().expect("commit");
let reader = index.reader().expect("reader");
let mut searcher = HnswSearcher::new(reader).expect("searcher");
searcher.set_ef_search(ef_search);
let mut total_recall = 0.0_f32;
for query in queries {
let exact = exact_top_k(&corpus, query, TOP_K);
let request = VectorIndexQuery::new(Vector::new(query.clone()))
.top_k(TOP_K)
.field_name("embedding".to_string())
.rerank_factor(rerank_factor);
let results = searcher.search(&request).expect("search");
let approx: HashSet<u64> = results.results.iter().map(|r| r.doc_id).collect();
total_recall += recall_at_k(&exact, &approx, TOP_K);
}
total_recall / queries.len() as f32
}
fn measure_recall_with_pq_rerank_cfg(
corpus: Vec<Vec<f32>>,
queries: &[Vec<f32>],
ef_search: usize,
rerank_factor: usize,
subvector_count: usize,
m: usize,
ef_construction: usize,
) -> f32 {
use laurus::vector::core::quantization::QuantizationMethod;
let storage = StorageFactory::create(StorageConfig::Memory(MemoryStorageConfig::default()))
.expect("memory storage");
let config = HnswIndexConfig {
dimension: DIM,
m,
ef_construction,
distance_metric: DistanceMetric::Cosine,
quantization_method: QuantizationMethod::ProductQuantization { subvector_count },
rerank_storage: Some(RerankStorageKind::F32),
..Default::default()
};
let index = HnswIndex::create(storage, "stage3_recall_index", config).expect("create index");
let mut writer = index.writer().expect("writer");
let docs: Vec<(u64, String, Vector)> = corpus
.iter()
.enumerate()
.map(|(i, v)| (i as u64, "embedding".to_string(), Vector::new(v.clone())))
.collect();
writer.build(docs).expect("build");
writer.finalize().expect("finalize");
writer.commit().expect("commit");
let reader = index.reader().expect("reader");
let mut searcher = HnswSearcher::new(reader).expect("searcher");
searcher.set_ef_search(ef_search);
let mut total_recall = 0.0_f32;
for query in queries {
let exact = exact_top_k(&corpus, query, TOP_K);
let request = VectorIndexQuery::new(Vector::new(query.clone()))
.top_k(TOP_K)
.field_name("embedding".to_string())
.rerank_factor(rerank_factor);
let results = searcher.search(&request).expect("search");
let approx: HashSet<u64> = results.results.iter().map(|r| r.doc_id).collect();
total_recall += recall_at_k(&exact, &approx, TOP_K);
}
total_recall / queries.len() as f32
}
fn brute_force_quantized_recall_with_rerank(
corpus: &[Vec<f32>],
queries: &[Vec<f32>],
rerank_factor: usize,
) -> f32 {
use laurus::vector::core::quantization::{QuantizationMethod, VectorQuantizer};
let mut quantizer = VectorQuantizer::new(QuantizationMethod::Scalar8Bit, DIM);
let training: Vec<Vector> = corpus.iter().cloned().map(Vector::new).collect();
quantizer.train(&training).expect("train");
let params: ScalarQuantParams = *quantizer.params().expect("trained");
let mut q_data: Vec<Vec<u8>> = Vec::with_capacity(corpus.len());
let mut metas: Vec<QuantizedVectorMeta> = Vec::with_capacity(corpus.len());
for v in &training {
let (q, meta) = quantizer.quantize(v).expect("quantize");
q_data.push(q);
metas.push(meta);
}
let metric = DistanceMetric::Cosine;
let widened = TOP_K * rerank_factor;
let mut total = 0.0_f32;
for query in queries {
let exact = exact_top_k(corpus, query, TOP_K);
let prepared = QuantizedQuery::prepare(query, ¶ms);
let mut int8_scored: Vec<(u64, f32)> = (0..corpus.len())
.map(|idx| {
let d = distance_quantized(metric, &prepared, &q_data[idx], metas[idx]);
(idx as u64, d)
})
.collect();
int8_scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
int8_scored.truncate(widened);
let prepared_query = metric.prepare_query(query);
let mut rescored: Vec<(u64, f32)> = int8_scored
.into_iter()
.map(|(id, _)| {
let d = metric
.distance_with_prepared(&prepared_query, &corpus[id as usize])
.expect("f32 distance");
(id, d)
})
.collect();
rescored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let approx: HashSet<u64> = rescored.into_iter().take(TOP_K).map(|(id, _)| id).collect();
total += recall_at_k(&exact, &approx, TOP_K);
}
total / queries.len() as f32
}
fn brute_force_quantized_recall(corpus: &[Vec<f32>], queries: &[Vec<f32>]) -> f32 {
use laurus::vector::core::quantization::{QuantizationMethod, VectorQuantizer};
let mut quantizer = VectorQuantizer::new(QuantizationMethod::Scalar8Bit, DIM);
let training: Vec<Vector> = corpus.iter().cloned().map(Vector::new).collect();
quantizer.train(&training).expect("train");
let params: ScalarQuantParams = *quantizer.params().expect("trained");
let mut q_data: Vec<Vec<u8>> = Vec::with_capacity(corpus.len());
let mut metas: Vec<QuantizedVectorMeta> = Vec::with_capacity(corpus.len());
for v in &training {
let (q, meta) = quantizer.quantize(v).expect("quantize");
q_data.push(q);
metas.push(meta);
}
let mut total = 0.0_f32;
for query in queries {
let exact = exact_top_k(corpus, query, TOP_K);
let prepared = QuantizedQuery::prepare(query, ¶ms);
let mut scored: Vec<(u64, f32)> = (0..corpus.len())
.map(|idx| {
let d =
distance_quantized(DistanceMetric::Cosine, &prepared, &q_data[idx], metas[idx]);
(idx as u64, d)
})
.collect();
scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let approx: HashSet<u64> = scored.into_iter().take(TOP_K).map(|(id, _)| id).collect();
total += recall_at_k(&exact, &approx, TOP_K);
}
total / queries.len() as f32
}
#[test]
fn hnsw_quantized_recall_at_10_meets_stage1_recall_gate() {
let n_corpus = 5_000;
let corpus: Vec<Vec<f32>> = (0..n_corpus)
.map(|i| pseudo_random_unit_norm(0xCAFE_0000 + i as u32, DIM))
.collect();
let queries: Vec<Vec<f32>> = (0..N_QUERIES)
.map(|i| pseudo_random_unit_norm(0xBEEF_0000 + i as u32, DIM))
.collect();
let bf_recall = brute_force_quantized_recall(&corpus, &queries);
eprintln!("Brute-force quantized Recall@{TOP_K} = {bf_recall:.4} (isolates distance kernel)");
let recall = measure_recall(corpus, &queries, 200);
eprintln!(
"HNSW quantized Recall@{TOP_K} = {recall:.4} (corpus = {n_corpus}, dim = {DIM}, queries = {N_QUERIES}, ef_search = 200)"
);
assert!(
bf_recall >= QUANT_KERNEL_RECALL_THRESHOLD,
"Brute-force quantized Recall@{TOP_K} = {bf_recall:.4} < {QUANT_KERNEL_RECALL_THRESHOLD} \
(Issue #481 Stage 1 recall gate, quant-kernel piece). The int8 \
distance kernel is no longer a faithful approximation of f32 — \
most likely a regression in distance_quantized / quantization."
);
assert!(
recall >= HNSW_RECALL_THRESHOLD,
"HNSW quantized Recall@{TOP_K} = {recall:.4} < {HNSW_RECALL_THRESHOLD} \
(Issue #481 Stage 1 recall gate, HNSW piece). corpus = {n_corpus}, \
dim = {DIM}, queries = {N_QUERIES}, ef_search = 200. \
Brute-force quantized = {bf_recall:.4}. Possible causes: \
(1) graph build regression (HnswIndexWriter), \
(2) searcher hot-path change (HnswSearcher), \
(3) HNSW config (m / ef_construction) drifted from defaults."
);
}
#[test]
fn hnsw_quantized_recall_at_10_large_fixture_smoke() {
if std::env::var("LAURUS_RECALL_LARGE").as_deref() != Ok("1") {
eprintln!(
"skipping large-fixture recall test; set LAURUS_RECALL_LARGE=1 to enable \
(50k vectors / dim 128 / 100 queries / ef_search = 1600, ~50s release-mode build)."
);
return;
}
let n_corpus = 50_000;
let large_ef_search = 1600;
let corpus: Vec<Vec<f32>> = (0..n_corpus)
.map(|i| pseudo_random_unit_norm(0xCAFE_0000 + i as u32, DIM))
.collect();
let queries: Vec<Vec<f32>> = (0..N_QUERIES)
.map(|i| pseudo_random_unit_norm(0xBEEF_0000 + i as u32, DIM))
.collect();
let bf_recall = brute_force_quantized_recall(&corpus, &queries);
eprintln!("Brute-force quantized Recall@{TOP_K} = {bf_recall:.4} (large, n = {n_corpus})");
let recall = measure_recall(corpus, &queries, large_ef_search);
eprintln!(
"HNSW quantized Recall@{TOP_K} = {recall:.4} (large, n = {n_corpus}, ef_search = {large_ef_search})"
);
assert!(
bf_recall >= QUANT_KERNEL_RECALL_THRESHOLD,
"Brute-force quantized Recall@{TOP_K} = {bf_recall:.4} < {QUANT_KERNEL_RECALL_THRESHOLD} \
(Issue #481 Stage 1 recall gate, large quant-kernel piece)."
);
assert!(
recall >= HNSW_RECALL_THRESHOLD,
"HNSW quantized Recall@{TOP_K} = {recall:.4} < {HNSW_RECALL_THRESHOLD} \
(Issue #481 Stage 1 recall gate, large HNSW piece). \
corpus = {n_corpus}, ef_search = {large_ef_search}, \
brute-force quantized = {bf_recall:.4}. \
If brute-force is high but HNSW is low, ef_search may need to \
scale further with the corpus size on synthetic random data."
);
}
#[test]
fn stage2_recall_sweep_diagnostic() {
if std::env::var("LAURUS_STAGE2_SWEEP").as_deref() != Ok("1") {
eprintln!("skipping Stage 2 sweep; set LAURUS_STAGE2_SWEEP=1 to enable.");
return;
}
let n_corpus = 5_000;
let random_corpus: Vec<Vec<f32>> = (0..n_corpus)
.map(|i| pseudo_random_unit_norm(0xCAFE_0000 + i as u32, DIM))
.collect();
let clustered_corpus = pseudo_random_clustered(0xCAFE_0000, n_corpus, DIM, 64, 0.3);
let random_queries: Vec<Vec<f32>> = (0..N_QUERIES)
.map(|i| pseudo_random_unit_norm(0xBEEF_0000 + i as u32, DIM))
.collect();
let clustered_queries = pseudo_random_clustered(0xBEEF_0000, N_QUERIES, DIM, 64, 0.3);
let mut report = String::from("distribution, ef_search, rerank_factor -> Recall@10\n");
for (label, corpus, queries, m, ef_construction) in [
(
"random/random m=16 efc=200",
&random_corpus,
&random_queries,
16usize,
200usize,
),
(
"clustered/random m=16 efc=200",
&clustered_corpus,
&random_queries,
16,
200,
),
(
"clustered/clustered m=16 efc=200",
&clustered_corpus,
&clustered_queries,
16,
200,
),
(
"clustered/clustered m=32 efc=500",
&clustered_corpus,
&clustered_queries,
32,
500,
),
] {
for &ef_search in &[50usize, 100, 150, 200, 300, 400] {
for &rerank_factor in &[5usize, 10, 20] {
let recall = measure_recall_with_rerank_cfg(
corpus.clone(),
queries,
ef_search,
rerank_factor,
m,
ef_construction,
);
let line =
format!("{label:>38}, {ef_search:>5}, {rerank_factor:>3} -> {recall:.4}\n");
eprint!("{line}");
report.push_str(&line);
}
}
}
let _ = std::fs::write("/tmp/stage2_recall_sweep.txt", report);
}
#[test]
fn stage2_brute_force_rerank_recall_at_10_meets_kernel_gate() {
let n_corpus = 5_000;
let rerank_factor = 5;
let corpus: Vec<Vec<f32>> = (0..n_corpus)
.map(|i| pseudo_random_unit_norm(0xCAFE_0000 + i as u32, DIM))
.collect();
let queries: Vec<Vec<f32>> = (0..N_QUERIES)
.map(|i| pseudo_random_unit_norm(0xBEEF_0000 + i as u32, DIM))
.collect();
let recall = brute_force_quantized_recall_with_rerank(&corpus, &queries, rerank_factor);
eprintln!(
"Brute-force Stage 2 (rerank) Recall@{TOP_K} = {recall:.4} \
(corpus = {n_corpus}, dim = {DIM}, queries = {N_QUERIES}, rerank_factor = {rerank_factor})"
);
assert!(
recall >= STAGE2_KERNEL_RECALL_THRESHOLD,
"Brute-force Stage 2 (rerank) Recall@{TOP_K} = {recall:.4} < {STAGE2_KERNEL_RECALL_THRESHOLD} \
(Issue #481 Stage 2 recall gate, kernel piece). \
corpus = {n_corpus}, rerank_factor = {rerank_factor}. Possible causes: \
(1) the int8 distance kernel ranks the true top-K outside \
the top `top_k * rerank_factor` candidates (quantization \
regression), (2) the f32 rerank pass does not actually \
rescore (`DistanceMetric::distance_with_prepared` for the \
segment's metric), (3) candidate widening is wrong."
);
}
#[test]
fn hnsw_quantized_recall_at_10_with_rerank_meets_stage2_recall_gate() {
let n_corpus = 5_000;
let ef_search = 400;
let rerank_factor = 5;
let corpus: Vec<Vec<f32>> = (0..n_corpus)
.map(|i| pseudo_random_unit_norm(0xCAFE_0000 + i as u32, DIM))
.collect();
let queries: Vec<Vec<f32>> = (0..N_QUERIES)
.map(|i| pseudo_random_unit_norm(0xBEEF_0000 + i as u32, DIM))
.collect();
let recall = measure_recall_with_rerank(corpus, &queries, ef_search, rerank_factor);
eprintln!(
"HNSW Stage 2 (rerank) Recall@{TOP_K} = {recall:.4} \
(corpus = {n_corpus}, dim = {DIM}, queries = {N_QUERIES}, \
ef_search = {ef_search}, rerank_factor = {rerank_factor})"
);
assert!(
recall >= STAGE2_HNSW_RECALL_THRESHOLD,
"HNSW Stage 2 (rerank) Recall@{TOP_K} = {recall:.4} < {STAGE2_HNSW_RECALL_THRESHOLD} \
(Issue #481 Stage 2 recall gate, HNSW piece). corpus = {n_corpus}, \
ef_search = {ef_search}, rerank_factor = {rerank_factor}. Possible causes: \
(1) HNSW build regression dropping graph quality below the noise floor, \
(2) rerank rescore did not pick up the LRS1 sidecar \
(HnswIndexReader.rerank_storage None?), \
(3) rerank candidate widening is wrong \
(top_k * rerank_factor not honored in HnswSearcher::search_graph)."
);
}
#[test]
fn hnsw_quantized_recall_at_10_with_rerank_large_fixture_smoke() {
if std::env::var("LAURUS_RECALL_LARGE").as_deref() != Ok("1") {
eprintln!(
"skipping Stage 2 large-fixture recall test; set LAURUS_RECALL_LARGE=1 to enable \
(50k vectors / dim 128 / 100 queries / ef_search = 3200 / rerank_factor = 5)."
);
return;
}
let n_corpus = 50_000;
let ef_search = 1600;
let rerank_factor = 5;
let corpus: Vec<Vec<f32>> = (0..n_corpus)
.map(|i| pseudo_random_unit_norm(0xCAFE_0000 + i as u32, DIM))
.collect();
let queries: Vec<Vec<f32>> = (0..N_QUERIES)
.map(|i| pseudo_random_unit_norm(0xBEEF_0000 + i as u32, DIM))
.collect();
let recall = measure_recall_with_rerank(corpus, &queries, ef_search, rerank_factor);
eprintln!(
"HNSW Stage 2 (rerank, large) Recall@{TOP_K} = {recall:.4} \
(corpus = {n_corpus}, ef_search = {ef_search}, rerank_factor = {rerank_factor})"
);
assert!(
recall >= STAGE2_HNSW_RECALL_THRESHOLD,
"HNSW Stage 2 (rerank, large) Recall@{TOP_K} = {recall:.4} < {STAGE2_HNSW_RECALL_THRESHOLD} \
(Issue #481 Stage 2 recall gate, large fixture). \
corpus = {n_corpus}, ef_search = {ef_search}, rerank_factor = {rerank_factor}."
);
}
#[test]
fn hnsw_quantized_recall_at_10_with_rerank_on_sift_meets_stage2_real_data_recall_gate() {
if std::env::var("LAURUS_REAL_BENCHMARK").as_deref() != Ok("1") {
eprintln!(
"skipping Issue #498 real-data recall test; set \
LAURUS_REAL_BENCHMARK=1 and run ./scripts/fetch-sift.sh \
--large to enable."
);
return;
}
let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let cache = manifest
.parent()
.expect("workspace root is one level up from laurus/")
.join(".cache")
.join("sift");
let base_path = cache.join("sift").join("sift_base.fvecs");
let query_path = cache.join("sift").join("sift_query.fvecs");
if !base_path.exists() || !query_path.exists() {
eprintln!(
"skipping Issue #498 real-data recall test: SIFT1M fixture \
not found at {}. Run ./scripts/fetch-sift.sh --large.",
base_path.display()
);
return;
}
let n_corpus = 50_000usize;
let n_queries = 200usize;
let ef_search = 200usize;
let rerank_factor = 5usize;
let mut corpus = read_fvecs_unit_norm(&base_path, DIM, Some(n_corpus));
let mut queries = read_fvecs_unit_norm(&query_path, DIM, Some(n_queries));
assert_eq!(corpus.len(), n_corpus, "subsample size mismatch");
assert!(!queries.is_empty(), "query set must be non-empty");
for v in corpus.iter_mut() {
debug_assert_eq!(v.len(), DIM);
}
for v in queries.iter_mut() {
debug_assert_eq!(v.len(), DIM);
}
let recall = measure_recall_with_rerank(corpus, &queries, ef_search, rerank_factor);
eprintln!(
"Issue #498 SIFT1M-{} Stage 2 Recall@{TOP_K} = {recall:.4} \
(m=16, ef_construction=200, ef_search={ef_search}, rerank_factor={rerank_factor}, \
queries={})",
n_corpus,
queries.len()
);
const REAL_DATA_RECALL_THRESHOLD: f32 = 0.99;
assert!(
recall >= REAL_DATA_RECALL_THRESHOLD,
"Issue #498 SIFT1M-50k Stage 2 Recall@{TOP_K} = {recall:.4} < {REAL_DATA_RECALL_THRESHOLD} \
(m=16, ef_construction=200, ef_search={ef_search}, rerank_factor={rerank_factor}). \
Phase 0 measured 0.9985 at this configuration; a sub-0.99 \
result here points at a Stage 2 regression."
);
}
fn read_fvecs_unit_norm(
path: &std::path::Path,
expect_dim: usize,
max: Option<usize>,
) -> Vec<Vec<f32>> {
use std::io::{BufReader, Read};
let file = std::fs::File::open(path).unwrap_or_else(|e| panic!("open {}: {e}", path.display()));
let mut reader = BufReader::new(file);
let mut out = Vec::new();
let mut hdr = [0u8; 4];
let mut vec_buf = vec![0u8; expect_dim * 4];
loop {
if reader.read_exact(&mut hdr).is_err() {
break;
}
let dim = u32::from_le_bytes(hdr) as usize;
assert_eq!(dim, expect_dim, "dim mismatch in {}", path.display());
reader.read_exact(&mut vec_buf).expect("vec body");
let mut v = Vec::with_capacity(dim);
for chunk in vec_buf.chunks_exact(4) {
v.push(f32::from_le_bytes(chunk.try_into().unwrap()));
}
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 0.0 {
for x in v.iter_mut() {
*x /= norm;
}
}
out.push(v);
if let Some(cap) = max
&& out.len() >= cap
{
break;
}
}
out
}
const STAGE3_HNSW_PQ_RECALL_THRESHOLD: f32 = 0.95;
#[test]
fn hnsw_pq_rerank_recall_at_10_meets_stage3_recall_gate() {
let n_corpus = 5_000;
let ef_search = 200;
let rerank_factor = 10;
let subvector_count = 32;
let corpus: Vec<Vec<f32>> = (0..n_corpus)
.map(|i| pseudo_random_unit_norm(0xCAFE_0000 + i as u32, DIM))
.collect();
let queries: Vec<Vec<f32>> = (0..N_QUERIES)
.map(|i| pseudo_random_unit_norm(0xBEEF_0000 + i as u32, DIM))
.collect();
let recall = measure_recall_with_pq_rerank_cfg(
corpus,
&queries,
ef_search,
rerank_factor,
subvector_count,
16, 200, );
eprintln!(
"HNSW Stage 3 (PQ + rerank) Recall@{TOP_K} = {recall:.4} \
(corpus = {n_corpus}, dim = {DIM}, queries = {N_QUERIES}, \
ef_search = {ef_search}, rerank_factor = {rerank_factor}, \
subvector_count = {subvector_count})"
);
assert!(
recall >= STAGE3_HNSW_PQ_RECALL_THRESHOLD,
"HNSW Stage 3 (PQ + rerank) Recall@{TOP_K} = {recall:.4} < \
{STAGE3_HNSW_PQ_RECALL_THRESHOLD} (Issue #481 Stage 3 recall gate, \
synthetic HNSW piece). corpus = {n_corpus}, ef_search = {ef_search}, \
rerank_factor = {rerank_factor}, subvector_count = {subvector_count}. \
Possible causes: (1) PQ codebook drift after a k-means or encoding \
change, (2) rerank rescore did not pick up the LRS1 sidecar in the \
PQ search path, (3) HNSW + PQ candidate generation regression."
);
}
#[test]
fn hnsw_pq_rerank_recall_at_10_on_sift_meets_stage3_real_data_recall_gate() {
if std::env::var("LAURUS_REAL_BENCHMARK").as_deref() != Ok("1") {
eprintln!(
"skipping Issue #481 Stage 3 real-data recall test; set \
LAURUS_REAL_BENCHMARK=1 and run ./scripts/fetch-sift.sh \
--large to enable."
);
return;
}
let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let cache = manifest
.parent()
.expect("workspace root is one level up from laurus/")
.join(".cache")
.join("sift");
let base_path = cache.join("sift").join("sift_base.fvecs");
let query_path = cache.join("sift").join("sift_query.fvecs");
if !base_path.exists() || !query_path.exists() {
eprintln!(
"skipping Issue #481 Stage 3 real-data recall test: SIFT1M \
fixture not found at {}. Run ./scripts/fetch-sift.sh --large.",
base_path.display()
);
return;
}
let n_corpus = 50_000usize;
let n_queries = 200usize;
let ef_search = 200usize;
let rerank_factor = 10usize;
let subvector_count = 32usize;
let mut corpus = read_fvecs_unit_norm(&base_path, DIM, Some(n_corpus));
let mut queries = read_fvecs_unit_norm(&query_path, DIM, Some(n_queries));
assert_eq!(corpus.len(), n_corpus, "subsample size mismatch");
assert!(!queries.is_empty(), "query set must be non-empty");
for v in corpus.iter_mut() {
debug_assert_eq!(v.len(), DIM);
}
for v in queries.iter_mut() {
debug_assert_eq!(v.len(), DIM);
}
let recall = measure_recall_with_pq_rerank_cfg(
corpus,
&queries,
ef_search,
rerank_factor,
subvector_count,
16, 200, );
eprintln!(
"Issue #481 SIFT1M-{n_corpus} Stage 3 (PQ + rerank) \
Recall@{TOP_K} = {recall:.4} (m=16, ef_construction=200, \
ef_search={ef_search}, rerank_factor={rerank_factor}, \
subvector_count={subvector_count}, queries={n_queries})"
);
const STAGE3_SIFT_RECALL_THRESHOLD: f32 = 0.95;
assert!(
recall >= STAGE3_SIFT_RECALL_THRESHOLD,
"Issue #481 SIFT1M-{n_corpus} Stage 3 (PQ + rerank) \
Recall@{TOP_K} = {recall:.4} < {STAGE3_SIFT_RECALL_THRESHOLD} \
(m=16, ef_construction=200, ef_search={ef_search}, \
rerank_factor={rerank_factor}, subvector_count={subvector_count}). \
PQ-only on SIFT1M tops out at 0.92 (PR #501); the rerank pass must \
recover the remaining 0.03+ via the LRS1 sidecar."
);
}