use super::distance::CachedSimdDistance;
use super::precision_contract_tests as suite;
use super::quantization::ScalarQuantizer;
use super::sq8_precision::{Sq8Codec, Sq8PrecisionConfig, Sq8PrecisionHnsw};
use crate::distance::DistanceMetric;
use std::sync::Arc;
fn int8_path_config() -> Sq8PrecisionConfig {
Sq8PrecisionConfig {
min_index_size: 0,
..Sq8PrecisionConfig::default()
}
}
#[test]
fn test_sq8_precision_empty_index() {
suite::check_empty_index::<Sq8Codec>(128);
}
#[test]
fn test_sq8_precision_fallback_when_untrained() {
suite::check_untrained_fallback::<Sq8Codec>(32);
}
#[test]
fn test_sq8_precision_insert_trains_lazily() {
suite::check_lazy_train_at_threshold::<Sq8Codec>(32);
}
#[test]
fn test_sq8_precision_force_train() {
suite::check_force_train::<Sq8Codec>(32);
}
#[test]
fn test_sq8_precision_search_after_training() {
suite::check_post_training_search_sorted::<Sq8Codec>(32);
}
#[test]
fn test_sq8_precision_insert_after_training() {
suite::check_insert_after_training_alignment::<Sq8Codec>(32);
}
#[test]
fn test_sq8_euclidean_returns_sqrt_not_squared() {
suite::check_euclidean_rerank_is_sqrt::<Sq8Codec>(32);
}
#[test]
fn test_sq8_below_min_index_size_falls_back_to_f32() {
suite::check_below_min_index_fallback::<Sq8Codec>(32);
}
#[test]
fn test_sq8_precision_recall_above_threshold() {
suite::check_recall_10k::<Sq8Codec>();
}
fn train_sq8(samples: &[Vec<f32>]) -> Arc<ScalarQuantizer> {
let refs: Vec<&[f32]> = samples.iter().map(Vec::as_slice).collect();
Arc::new(ScalarQuantizer::train(&refs).expect("train"))
}
#[test]
fn test_install_trained_sq8_encodes_existing_vectors() {
suite::check_install_encodes_existing::<Sq8Codec>(64, train_sq8, 0.9);
}
#[test]
fn test_install_trained_sq8_then_insert_keeps_alignment() {
suite::check_install_then_insert_alignment::<Sq8Codec>(64, train_sq8);
}
#[test]
fn test_sq8_precision_config_defaults() {
let config = Sq8PrecisionConfig::default();
assert_eq!(config.oversampling_ratio, 4);
assert_eq!(config.min_index_size, 10_000);
let engine = CachedSimdDistance::new_prenormalized(DistanceMetric::Euclidean, 32);
let hnsw = Sq8PrecisionHnsw::new(engine, 32, 16, 100, 1000).expect("test");
for i in 0..50 {
let v: Vec<f32> = (0..32).map(|j| (i * 32 + j) as f32).collect();
hnsw.insert(&v).expect("test");
}
hnsw.force_train_quantizer().expect("test");
let query: Vec<f32> = (0..32).map(|j| j as f32).collect();
let with_default = hnsw.search_with_config(&query, 10, 50, &config);
let plain = hnsw.search(&query, 10, 50);
assert_eq!(
with_default, plain,
"default min_index_size=10_000 must force small indexes onto the f32 fallback path"
);
}
#[test]
fn test_sq8_unsupported_metric_stays_exact_f32() {
let engine = CachedSimdDistance::new_prenormalized(DistanceMetric::DotProduct, 32);
let hnsw = Sq8PrecisionHnsw::new(engine, 32, 16, 100, 100).expect("test");
for v in &suite::sinusoidal_vectors(100, 32) {
hnsw.insert(v).expect("test");
}
assert!(
!hnsw.is_quantizer_trained(),
"unsupported metric must never train a quantizer"
);
hnsw.force_train_quantizer().expect("test");
assert!(
!hnsw.is_quantizer_trained(),
"force_train must stay a no-op on an unsupported metric"
);
let query: Vec<f32> = (0..32)
.map(|j| ((42 * 32 + j) as f32 * 0.01).sin())
.collect();
let exact = hnsw.search(&query, 10, 100);
let forced = hnsw.search_with_config(&query, 10, 100, &int8_path_config());
assert!(!exact.is_empty(), "exact search must return results");
assert_eq!(forced, exact, "unsupported metric must stay on exact f32");
}
#[test]
fn test_sq8_install_refused_on_unsupported_metric() {
let engine = CachedSimdDistance::new_prenormalized(DistanceMetric::Hamming, 8);
let hnsw = Sq8PrecisionHnsw::new(engine, 8, 16, 100, 1000).expect("test");
for i in 0..10 {
let v: Vec<f32> = (0..8).map(|j| ((i + j) % 2) as f32).collect();
hnsw.insert(&v).expect("test");
}
let samples: Vec<Vec<f32>> = vec![vec![0.0; 8], vec![1.0; 8]];
let installed = hnsw
.install_trained_sq8(train_sq8(&samples))
.expect("install call");
assert!(!installed, "install must be refused on Hamming");
assert!(!hnsw.is_quantizer_trained());
}
#[test]
fn test_int8_cosine_rerank_keeps_best_candidates() {
let (dim, k) = (32, 10);
let (hnsw, vectors) = suite::trained_planted_backend::<Sq8Codec>(DistanceMetric::Cosine, dim);
let query = &vectors[suite::PLANTED_QUERY_ID];
let results = hnsw.search_with_config(query, k, 100, &int8_path_config());
suite::assert_top1_and_recall(
&results,
&vectors,
suite::PLANTED_QUERY_ID,
DistanceMetric::Cosine,
k,
);
let (low, high) = DistanceMetric::Cosine
.score_range()
.expect("test: cosine is a bounded metric");
for (id, score) in &results {
assert!(
(low..=high).contains(score),
"Cosine score for node {id} should be in [{low},{high}], got {score}"
);
assert!(
*score > 0.0,
"this fixture plants correlated neighbours, so node {id} should \
score positively; got {score}"
);
}
}
#[test]
fn test_int8_cosine_unnormalized_query_matches_normalized() {
suite::check_cosine_scale_invariance::<Sq8Codec>(32);
}
#[test]
fn test_concurrent_inserts_and_search_keep_store_aligned() {
let (dim, per_thread, threads) = (16, 60, 4);
let engine = CachedSimdDistance::new_prenormalized(DistanceMetric::Euclidean, dim);
let hnsw = Arc::new(Sq8PrecisionHnsw::new(engine, dim, 16, 100, 100).expect("test"));
let handles: Vec<_> = (0..threads)
.map(|t| {
let hnsw = Arc::clone(&hnsw);
std::thread::spawn(move || {
for i in 0..per_thread {
let seed = t * per_thread + i;
let v: Vec<f32> = (0..dim)
.map(|j| ((seed * dim + j) as f32 * 0.017).sin())
.collect();
hnsw.insert(&v).expect("concurrent insert");
if i % 8 == 0 {
let _ = hnsw.search(&v, 3, 30);
}
}
})
})
.collect();
for handle in handles {
handle.join().expect("insert thread panicked");
}
assert_eq!(hnsw.len(), threads * per_thread);
assert!(hnsw.is_quantizer_trained(), "threshold crossed → trained");
let config = int8_path_config();
for node_id in [0_usize, 57, 119, 200, 239] {
let vector = hnsw
.inner
.with_vectors_read(|vectors| vectors.get(node_id).map(<[f32]>::to_vec))
.expect("node vector present");
let results = hnsw.search_with_config(&vector, 1, 60, &config);
assert_eq!(
results.first().map(|&(id, _)| id),
Some(node_id),
"self-query must rank node {node_id} first — a shifted store entry surfaces here"
);
}
}
#[test]
fn test_sq8_recall_survives_locality_reorder() {
suite::check_recall_survives_reorder::<Sq8Codec>();
}