use rand::{RngExt, SeedableRng};
use rand_chacha::ChaCha8Rng;
use ordvec::rank::{bucket_centre, bucket_ranks, rank_transform, rankquant_norm};
#[cfg(feature = "bench-utils")]
use ordvec::SearchResults;
use ordvec::{Rank, RankQuant, SignBitmap};
fn make_corpus(seed: u64, n: usize, dim: usize) -> Vec<f32> {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
(0..n * dim).map(|_| rng.random_range(-1.0..1.0)).collect()
}
fn ref_rankquant_asymmetric(query: &[f32], doc: &[f32], bits: u8) -> f32 {
let dim = query.len();
let norm: f32 = query.iter().map(|x| x * x).sum::<f32>().sqrt();
let q_unit: Vec<f32> = if norm <= 1e-12 {
vec![0.0; dim]
} else {
query.iter().map(|&x| x / norm).collect()
};
let doc_ranks = rank_transform(doc);
let doc_buckets = bucket_ranks(&doc_ranks, bits);
let inv_norm = 1.0_f32 / rankquant_norm(dim, bits);
let mut acc = 0.0f32;
for d in 0..dim {
acc += q_unit[d] * bucket_centre(doc_buckets[d], bits);
}
acc * inv_norm
}
#[cfg(feature = "bench-utils")]
fn assert_asym_matches_byte_lut(dim: usize, bits: u8, seed: u64) {
use ordvec::search_asymmetric_byte_lut;
let n = 64;
let corpus = make_corpus(seed, n, dim);
let mut idx = RankQuant::new(dim, bits);
idx.add(&corpus);
let mut rng = ChaCha8Rng::seed_from_u64(seed.wrapping_add(7));
let query: Vec<f32> = (0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
let k = 10;
let prod = idx.search_asymmetric(&query, k);
let reference = search_asymmetric_byte_lut(&idx, &query, k);
let prod_idx = prod.indices_for_query(0);
let ref_idx = reference.indices_for_query(0);
let prod_set: std::collections::HashSet<i64> = prod_idx.iter().copied().collect();
let ref_set: std::collections::HashSet<i64> = ref_idx.iter().copied().collect();
assert_eq!(
prod_set, ref_set,
"dim={dim} b={bits}: search_asymmetric top-{k} set diverged from scalar byte-LUT reference",
);
let prod_scores = prod.scores_for_query(0);
for slot in 0..k.min(n) {
let di = prod_idx[slot];
if di < 0 {
continue;
}
let ri = ref_idx.iter().position(|&x| x == di).unwrap();
let sp = prod_scores[slot];
let sr = reference.scores_for_query(0)[ri];
assert!(
(sp - sr).abs() < 1e-3,
"dim={dim} b={bits} doc {di}: score {sp} vs reference {sr}",
);
}
}
#[test]
#[cfg(feature = "bench-utils")]
fn rt2_asym_b2_dim48_matches_scalar() {
assert_asym_matches_byte_lut(48, 2, 101);
}
#[test]
#[cfg(feature = "bench-utils")]
fn rt2_asym_b4_dim80_matches_scalar() {
assert_asym_matches_byte_lut(80, 4, 102);
}
#[test]
#[cfg(feature = "bench-utils")]
fn rt2_asym_b2_dim20_matches_scalar() {
assert_asym_matches_byte_lut(20, 2, 103);
}
#[test]
#[cfg(feature = "bench-utils")]
fn rt2_asym_b2_dim4_matches_scalar() {
assert_asym_matches_byte_lut(4, 2, 104);
}
#[test]
#[cfg(feature = "bench-utils")]
fn rt2_asym_b2_dim64_happy_path_matches_scalar() {
assert_asym_matches_byte_lut(64, 2, 105);
}
#[test]
#[cfg(feature = "bench-utils")]
fn rt2_asym_b4_dim128_happy_path_matches_scalar() {
assert_asym_matches_byte_lut(128, 4, 106);
}
#[test]
#[cfg(feature = "bench-utils")]
fn rt2_asym_b4_dim768_happy_path_matches_scalar() {
assert_asym_matches_byte_lut(768, 4, 107);
}
#[test]
#[should_panic(expected = "candidate id out of range")]
fn subset_rejects_out_of_range_candidate() {
let dim = 64;
let n = 32;
let corpus = make_corpus(201, n, dim);
let mut idx = RankQuant::new(dim, 2);
idx.add(&corpus);
let mut rng = ChaCha8Rng::seed_from_u64(202);
let query: Vec<f32> = (0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
let candidates: Vec<u32> = vec![0, 1, 999];
let _ = idx.search_asymmetric_subset(&query, &candidates, 3);
}
#[test]
fn subset_accepts_in_range_candidates() {
let dim = 64;
let n = 32;
let corpus = make_corpus(203, n, dim);
let mut idx = RankQuant::new(dim, 2);
idx.add(&corpus);
let mut rng = ChaCha8Rng::seed_from_u64(204);
let query: Vec<f32> = (0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
let candidates: Vec<u32> = vec![0, 5, (n - 1) as u32];
let (scores, global) = idx.search_asymmetric_subset(&query, &candidates, 3);
assert_eq!(scores.len(), 3);
assert_eq!(global.len(), 3);
for &g in &global {
assert!(g < 0 || candidates.contains(&(g as u32)));
}
}
#[test]
fn rankquant_search_huge_k_clamps() {
let dim = 64;
let n = 16;
let corpus = make_corpus(301, n, dim);
let mut idx = RankQuant::new(dim, 2);
idx.add(&corpus);
let query = make_corpus(302, 1, dim);
let res = idx.search(&query, usize::MAX);
let returned: usize = res.indices_for_query(0).iter().filter(|&&i| i >= 0).count();
assert!(returned <= n, "search returned more than n_vectors results");
assert_eq!(returned, n, "all n docs should be returned for huge k");
}
#[test]
fn rankquant_search_asymmetric_huge_k_clamps() {
let dim = 64;
let n = 16;
let corpus = make_corpus(303, n, dim);
let mut idx = RankQuant::new(dim, 2);
idx.add(&corpus);
let query = make_corpus(304, 1, dim);
let res = idx.search_asymmetric(&query, usize::MAX);
let returned: usize = res.indices_for_query(0).iter().filter(|&&i| i >= 0).count();
assert!(returned <= n);
assert_eq!(returned, n);
}
#[test]
fn rank_index_search_huge_k_clamps() {
let dim = 64;
let n = 16;
let corpus = make_corpus(305, n, dim);
let mut idx = Rank::new(dim);
idx.add(&corpus);
let query = make_corpus(306, 1, dim);
let res = idx.search(&query, usize::MAX);
let returned: usize = res.indices_for_query(0).iter().filter(|&&i| i >= 0).count();
assert!(returned <= n);
assert_eq!(returned, n);
}
#[test]
fn rank_index_search_asymmetric_huge_k_clamps() {
let dim = 64;
let n = 16;
let corpus = make_corpus(307, n, dim);
let mut idx = Rank::new(dim);
idx.add(&corpus);
let query = make_corpus(308, 1, dim);
let res = idx.search_asymmetric(&query, usize::MAX);
let returned: usize = res.indices_for_query(0).iter().filter(|&&i| i >= 0).count();
assert!(returned <= n);
assert_eq!(returned, n);
}
#[test]
fn sign_bitmap_top_m_huge_m_clamps() {
let dim = 64;
let n = 16;
let corpus = make_corpus(309, n, dim);
let mut idx = SignBitmap::new(dim);
idx.add(&corpus);
let query = make_corpus(310, 1, dim);
let cands = idx.top_m_candidates(&query, usize::MAX);
assert!(cands.len() <= n);
assert_eq!(cands.len(), n);
let batched = idx.top_m_candidates_batched(&query, usize::MAX);
assert_eq!(batched.len(), 1);
assert!(batched[0].len() <= n);
assert_eq!(batched[0].len(), n);
}
#[test]
#[cfg(feature = "bench-utils")]
fn byte_lut_huge_k_clamps_no_overflow() {
use ordvec::search_asymmetric_byte_lut;
let dim = 64;
let n = 16;
let corpus = make_corpus(501, n, dim);
let mut idx = RankQuant::new(dim, 2);
idx.add(&corpus);
let query = make_corpus(502, 1, dim);
let res: SearchResults = search_asymmetric_byte_lut(&idx, &query, usize::MAX);
assert_eq!(res.nq, 1);
assert_eq!(res.k, n, "byte-LUT k must clamp to n_vectors");
let returned: usize = res.indices_for_query(0).iter().filter(|&&i| i >= 0).count();
assert!(
returned <= n,
"byte-LUT search returned more than n_vectors results",
);
assert_eq!(returned, n, "all n docs should be returned for huge k");
}
#[test]
#[cfg(feature = "bench-utils")]
fn byte_lut_huge_k_multi_query_clamps_no_overflow() {
use ordvec::search_asymmetric_byte_lut;
let dim = 64;
let n = 16;
let nq = 3;
let corpus = make_corpus(503, n, dim);
let mut idx = RankQuant::new(dim, 2);
idx.add(&corpus);
let queries = make_corpus(504, nq, dim);
let res: SearchResults = search_asymmetric_byte_lut(&idx, &queries, usize::MAX);
assert_eq!(res.nq, nq);
assert_eq!(res.k, n);
for qi in 0..nq {
let returned: usize = res
.indices_for_query(qi)
.iter()
.filter(|&&i| i >= 0)
.count();
assert_eq!(returned, n, "query {qi}: all n docs should be returned");
}
}
#[test]
fn rankquant_b1_asymmetric_works_and_matches_reference() {
let dim = 64;
let n = 64;
let corpus = make_corpus(401, n, dim);
let mut idx = RankQuant::new(dim, 1);
idx.add(&corpus);
let mut rng = ChaCha8Rng::seed_from_u64(402);
let query: Vec<f32> = (0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
let k = 10;
let res = idx.search_asymmetric(&query, k);
let ref_scores: Vec<f32> = (0..n)
.map(|di| ref_rankquant_asymmetric(&query, &corpus[di * dim..(di + 1) * dim], 1))
.collect();
let mut ref_sorted: Vec<(usize, f32)> = ref_scores
.iter()
.enumerate()
.map(|(i, &s)| (i, s))
.collect();
ref_sorted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
let top_ref: std::collections::HashSet<usize> = ref_sorted[..k].iter().map(|x| x.0).collect();
let top_idx: std::collections::HashSet<usize> = res
.indices_for_query(0)
.iter()
.filter(|&&i| i >= 0)
.map(|&i| i as usize)
.collect();
assert_eq!(top_idx, top_ref, "b=1 asymmetric top-{k} set mismatch");
let prod_scores = res.scores_for_query(0);
let prod_idx = res.indices_for_query(0);
for slot in 0..k {
let di = prod_idx[slot];
if di < 0 {
continue;
}
let sp = prod_scores[slot];
let sr = ref_scores[di as usize];
assert!(
(sp - sr).abs() < 1e-4,
"b=1 slot {slot} doc {di}: {sp} vs {sr}",
);
}
}