use ordvec::rank::rank_transform;
use ordvec::{Bitmap, RankQuant};
use rand::{RngExt, SeedableRng};
use rand_chacha::ChaCha8Rng;
use crate::{make_corpus, D, N};
#[test]
fn rank_io_round_trip_bitmap_index() {
let corpus = make_corpus(42);
let mut idx = Bitmap::new(D, D / 4);
idx.add(&corpus);
let tmp = std::env::temp_dir().join("bitmap_index_io.tvbm");
idx.write(&tmp).expect("write");
let loaded = Bitmap::load(&tmp).expect("load");
std::fs::remove_file(&tmp).ok();
assert_eq!(loaded.len(), idx.len());
assert_eq!(loaded.dim(), idx.dim());
assert_eq!(loaded.n_top(), idx.n_top());
let mut rng = ChaCha8Rng::seed_from_u64(142);
let q: Vec<f32> = (0..D).map(|_| rng.random_range(-1.0..1.0)).collect();
let r1 = idx.search(&q, 10);
let r2 = loaded.search(&q, 10);
assert_eq!(r1.indices_for_query(0), r2.indices_for_query(0));
}
#[test]
fn bitmap_index_constant_composition_invariant() {
let corpus = make_corpus(20);
let n_top = D / 4;
let mut idx = Bitmap::new(D, n_top);
idx.add(&corpus);
assert_eq!(idx.len(), N);
assert_eq!(idx.bytes_per_vec(), D / 8);
for di in 0..N {
let v = &corpus[di * D..(di + 1) * D];
let r = rank_transform(v);
let expected_top: std::collections::HashSet<usize> =
(0..D).filter(|&j| (r[j] as usize) >= D - n_top).collect();
assert_eq!(expected_top.len(), n_top, "doc {di} top-set size");
}
}
#[test]
fn bitmap_then_subset_recovers_exact_when_m_eq_n() {
let corpus = make_corpus(21);
let n_top = D / 4;
let mut bitmap = Bitmap::new(D, n_top);
bitmap.add(&corpus);
let mut rq = RankQuant::new(D, 2);
rq.add(&corpus);
let mut rng = ChaCha8Rng::seed_from_u64(99_999);
let query: Vec<f32> = (0..D).map(|_| rng.random_range(-1.0..1.0)).collect();
let cands = bitmap.top_m_candidates(&query, N);
assert_eq!(cands.len(), N);
let (_, two_stage_top) = rq.search_asymmetric_subset(&query, &cands, 10);
let exact = rq.search_asymmetric(&query, 10);
let two_stage_set: std::collections::HashSet<i64> = two_stage_top.iter().copied().collect();
let exact_set: std::collections::HashSet<i64> =
exact.indices_for_query(0).iter().copied().collect();
assert_eq!(two_stage_set, exact_set, "M=N two-stage must equal exact");
}
#[test]
fn body_overlap_subset_accepts_unsorted_ids_in_input_order() {
let corpus = make_corpus(7_654);
let n_top = D / 4;
let mut bitmap = Bitmap::new(D, n_top);
bitmap.add(&corpus);
let query: Vec<f32> = corpus[7 * D..8 * D].to_vec();
let q_bitmap = bitmap.build_query_bitmap_fp32(&query);
let ids: [u32; 3] = [7, 1, 4]; let mut out = vec![0u32; ids.len()];
bitmap.body_overlap_scores_subset(&q_bitmap, &ids, &mut out);
assert_eq!(
out[0], n_top as u32,
"unsorted batch must score in input order: out[0] is doc 7 (self-query = n_top)"
);
for (i, &id) in ids.iter().enumerate() {
let mut single = [0u32; 1];
bitmap.body_overlap_scores_subset(&q_bitmap, &[id], &mut single);
assert_eq!(
out[i], single[0],
"unsorted id {id} at position {i}: batch {} != individual {}",
out[i], single[0]
);
}
}
#[test]
fn bitmap_top_m_candidates_uses_no_ground_truth() {
let corpus = make_corpus(22);
let n_top = D / 4;
let mut bitmap = Bitmap::new(D, n_top);
bitmap.add(&corpus);
let mut rng = ChaCha8Rng::seed_from_u64(50);
let query: Vec<f32> = (0..D).map(|_| rng.random_range(-1.0..1.0)).collect();
let a = bitmap.top_m_candidates(&query, 50);
let b = bitmap.top_m_candidates(&query, 50);
assert_eq!(a, b, "candidate selection must be deterministic");
}
#[test]
fn bitmap_top_m_candidates_deterministic_at_ties() {
const TIE_D: usize = 128;
const TIE_N: usize = 200;
let mut rng = ChaCha8Rng::seed_from_u64(404);
let duplicate_vec: Vec<f32> = (0..TIE_D).map(|_| rng.random_range(-1.0..1.0)).collect();
let mut corpus: Vec<f32> = Vec::with_capacity(TIE_N * TIE_D);
for _ in 0..150 {
corpus.extend_from_slice(&duplicate_vec);
}
for _ in 0..50 {
for _ in 0..TIE_D {
corpus.push(rng.random_range(-1.0..1.0));
}
}
let _ = rank_transform(&duplicate_vec); let n_top = TIE_D / 4;
let mut bitmap = Bitmap::new(TIE_D, n_top);
bitmap.add(&corpus);
let query: Vec<f32> = (0..TIE_D).map(|_| rng.random_range(-1.0..1.0)).collect();
let m = 100;
let c1 = bitmap.top_m_candidates(&query, m);
let c2 = bitmap.top_m_candidates(&query, m);
assert_eq!(
c1, c2,
"single-query candidates must be deterministic at ties"
);
let queries: Vec<f32> = (0..3 * TIE_D)
.map(|_| rng.random_range(-1.0..1.0))
.collect();
for q in [
&queries[..TIE_D],
&queries[TIE_D..2 * TIE_D],
&queries[2 * TIE_D..],
] {
let single = bitmap.top_m_candidates(q, m);
let batched = bitmap.top_m_candidates_batched(q, m);
assert_eq!(batched.len(), 1);
assert_eq!(
batched[0], single,
"batched path must match single-query path under heavy ties",
);
}
}
#[test]
fn bitmap_batched_avx512_production_dim() {
const PROD_D: usize = 1024;
const N_DOCS: usize = 256;
const BATCH: usize = 5;
let mut rng = ChaCha8Rng::seed_from_u64(7);
let corpus: Vec<f32> = (0..N_DOCS * PROD_D)
.map(|_| rng.random_range(-1.0..1.0))
.collect();
let queries: Vec<f32> = (0..BATCH * PROD_D)
.map(|_| rng.random_range(-1.0..1.0))
.collect();
let n_top = PROD_D / 4;
let mut bitmap = Bitmap::new(PROD_D, n_top);
bitmap.add(&corpus);
for m in [10usize, 50, 200] {
let single: Vec<Vec<u32>> = (0..BATCH)
.map(|bi| bitmap.top_m_candidates(&queries[bi * PROD_D..(bi + 1) * PROD_D], m))
.collect();
let batched = bitmap.top_m_candidates_batched(&queries, m);
for bi in 0..BATCH {
assert_eq!(
single[bi], batched[bi],
"AVX-512 batched diverged from single-query at dim={PROD_D}, M={m}, batch idx {bi}",
);
}
}
}
#[test]
fn bitmap_batched_hot_plus_tail_split() {
const PROD_D: usize = 1024;
const N_DOCS: usize = 256;
const BATCH: usize = 11;
let mut rng = ChaCha8Rng::seed_from_u64(101);
let corpus: Vec<f32> = (0..N_DOCS * PROD_D)
.map(|_| rng.random_range(-1.0..1.0))
.collect();
let queries: Vec<f32> = (0..BATCH * PROD_D)
.map(|_| rng.random_range(-1.0..1.0))
.collect();
let n_top = PROD_D / 4;
let mut bitmap = Bitmap::new(PROD_D, n_top);
bitmap.add(&corpus);
let single: Vec<Vec<u32>> = (0..BATCH)
.map(|bi| bitmap.top_m_candidates(&queries[bi * PROD_D..(bi + 1) * PROD_D], 32))
.collect();
let batched = bitmap.top_m_candidates_batched(&queries, 32);
assert_eq!(single.len(), batched.len());
for bi in 0..BATCH {
assert_eq!(
single[bi], batched[bi],
"hot+tail mismatch at batch idx {bi} (hot for bi<8, tail for bi in 8..11)",
);
}
}
#[test]
fn bitmap_batched_edge_cases() {
let corpus = make_corpus(13);
let n_top = D / 4;
let mut bitmap = Bitmap::new(D, n_top);
bitmap.add(&corpus);
let empty: Vec<f32> = Vec::new();
let res = bitmap.top_m_candidates_batched(&empty, 10);
assert!(res.is_empty(), "empty queries must produce empty result");
let mut rng = ChaCha8Rng::seed_from_u64(202);
let queries: Vec<f32> = (0..3 * D).map(|_| rng.random_range(-1.0..1.0)).collect();
let res = bitmap.top_m_candidates_batched(&queries, 0);
assert_eq!(res.len(), 3);
for c in &res {
assert!(c.is_empty(), "m=0 must produce empty candidate sets");
}
for bi in 0..3 {
assert!(bitmap
.top_m_candidates(&queries[bi * D..(bi + 1) * D], 0)
.is_empty());
}
let res = bitmap.top_m_candidates_batched(&queries, N * 2);
assert_eq!(res.len(), 3);
for c in &res {
assert_eq!(c.len(), N, "m > n_vectors must clamp to n_vectors");
}
for bi in 0..3 {
let single = bitmap.top_m_candidates(&queries[bi * D..(bi + 1) * D], N * 2);
assert_eq!(single.len(), N);
}
assert!(bitmap
.top_m_candidates_batched_chunked(&empty, 10, 4)
.is_empty());
}
#[test]
fn bitmap_batched_avx512_high_qpv_no_panic() {
const HIGH_D: usize = 4608; const N_DOCS: usize = 64;
const BATCH: usize = 3;
let mut rng = ChaCha8Rng::seed_from_u64(123);
let corpus: Vec<f32> = (0..N_DOCS * HIGH_D)
.map(|_| rng.random_range(-1.0..1.0))
.collect();
let queries: Vec<f32> = (0..BATCH * HIGH_D)
.map(|_| rng.random_range(-1.0..1.0))
.collect();
let n_top = HIGH_D / 4;
let mut bitmap = Bitmap::new(HIGH_D, n_top);
bitmap.add(&corpus);
let single: Vec<Vec<u32>> = (0..BATCH)
.map(|bi| bitmap.top_m_candidates(&queries[bi * HIGH_D..(bi + 1) * HIGH_D], 16))
.collect();
let batched = bitmap.top_m_candidates_batched(&queries, 16);
for bi in 0..BATCH {
assert_eq!(
single[bi], batched[bi],
"high-qpv batched candidates diverged from single-query for batch idx {bi}",
);
}
}
#[test]
fn bitmap_batched_matches_single_query() {
let corpus = make_corpus(31);
let n_top = D / 4;
let mut bitmap = Bitmap::new(D, n_top);
bitmap.add(&corpus);
let mut rng = ChaCha8Rng::seed_from_u64(99);
let batch: usize = 7; let queries: Vec<f32> = (0..batch * D)
.map(|_| rng.random_range(-1.0..1.0))
.collect();
for m in [10usize, 50, 100] {
let single: Vec<Vec<u32>> = (0..batch)
.map(|bi| bitmap.top_m_candidates(&queries[bi * D..(bi + 1) * D], m))
.collect();
let batched = bitmap.top_m_candidates_batched(&queries, m);
assert_eq!(single.len(), batched.len());
for bi in 0..batch {
assert_eq!(
single[bi], batched[bi],
"batched candidates diverged from single-query for batch idx {bi}, M={m}",
);
}
}
let chunked = bitmap.top_m_candidates_batched_chunked(&queries, 50, 3);
let reference: Vec<Vec<u32>> = (0..batch)
.map(|bi| bitmap.top_m_candidates(&queries[bi * D..(bi + 1) * D], 50))
.collect();
assert_eq!(chunked, reference);
}
#[test]
#[should_panic(expected = "u16 rank invariant")]
fn bitmap_new_rejects_dim_above_u16_max() {
let _ = Bitmap::new(65_536, 256);
}
#[test]
#[should_panic(expected = "batch_size must be > 0")]
fn bitmap_batched_chunked_rejects_zero_batch_size() {
let corpus = make_corpus(77);
let mut idx = Bitmap::new(D, D / 4);
idx.add(&corpus);
let q = corpus[..D].to_vec();
let _ = idx.top_m_candidates_batched_chunked(&q, 10, 0);
}