#![allow(clippy::unwrap_used)]
use std::sync::Arc;
use iqdb_flat::{FlatConfig, FlatIndex};
use iqdb_index::{Index, IndexCore};
use iqdb_types::{DistanceMetric, Filter, IqdbError, SearchParams, Value, VectorId};
use proptest::prelude::*;
fn arc(v: &[f32]) -> Arc<[f32]> {
Arc::from(v)
}
#[test]
fn nan_and_inf_in_stored_vectors_do_not_panic() {
let mut idx = FlatIndex::new(3, DistanceMetric::Euclidean, FlatConfig).unwrap();
idx.insert(VectorId::from(1u64), arc(&[f32::NAN, 0.0, 0.0]), None)
.unwrap();
idx.insert(VectorId::from(2u64), arc(&[f32::INFINITY, 1.0, 2.0]), None)
.unwrap();
idx.insert(VectorId::from(3u64), arc(&[0.0, 0.0, 0.0]), None)
.unwrap();
let hits = idx
.search(
&[0.0, 0.0, 0.0],
&SearchParams::new(3, DistanceMetric::Euclidean),
)
.unwrap();
assert_eq!(hits.len(), 3);
assert_eq!(hits[0].id, VectorId::U64(3));
}
#[test]
fn nan_query_does_not_panic_and_is_deterministic() {
let mut idx = FlatIndex::new(2, DistanceMetric::Cosine, FlatConfig).unwrap();
for id in 0..10u64 {
idx.insert(VectorId::from(id), arc(&[id as f32, 1.0]), None)
.unwrap();
}
let q = [f32::NAN, f32::NEG_INFINITY];
let a = idx
.search(&q, &SearchParams::new(5, DistanceMetric::Cosine))
.unwrap();
let b = idx
.search(&q, &SearchParams::new(5, DistanceMetric::Cosine))
.unwrap();
let key = |hits: &[iqdb_flat::Hit]| -> Vec<(VectorId, u32)> {
hits.iter()
.map(|h| (h.id.clone(), h.distance.to_bits()))
.collect()
};
assert_eq!(
key(&a),
key(&b),
"a NaN query must still produce a deterministic result",
);
}
#[test]
fn over_deep_filter_returns_invalid_filter_not_stack_overflow() {
let mut idx = FlatIndex::new(1, DistanceMetric::Euclidean, FlatConfig).unwrap();
idx.insert(VectorId::from(1u64), arc(&[0.0]), None).unwrap();
let mut filter = Filter::eq("k", Value::Int(1));
for _ in 0..(iqdb_filter::MAX_FILTER_DEPTH + 16) {
filter = Filter::not(filter);
}
let params = SearchParams {
filter: Some(filter),
..SearchParams::new(1, DistanceMetric::Euclidean)
};
let err = idx.search(&[0.0], ¶ms).unwrap_err();
assert_eq!(err, IqdbError::InvalidFilter);
}
#[test]
fn k_at_usize_max_returns_all_without_overflow() {
let mut idx = FlatIndex::new(1, DistanceMetric::Euclidean, FlatConfig).unwrap();
for id in 0..5u64 {
idx.insert(VectorId::from(id), arc(&[id as f32]), None)
.unwrap();
}
let hits = idx
.search(
&[0.0],
&SearchParams::new(usize::MAX, DistanceMetric::Euclidean),
)
.unwrap();
assert_eq!(hits.len(), 5, "k > n must clamp to n, not over-allocate");
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(200))]
#[test]
fn search_never_panics_on_arbitrary_input(
dim in 1usize..12,
rows in proptest::collection::vec(
proptest::collection::vec(
prop_oneof![
any::<f32>(),
Just(f32::NAN),
Just(f32::INFINITY),
Just(f32::NEG_INFINITY),
],
1..12,
),
0..40,
),
k in 0usize..64,
with_filter in any::<bool>(),
) {
let metric = DistanceMetric::Euclidean;
let mut idx = FlatIndex::new(dim, metric, FlatConfig).unwrap();
for (i, row) in rows.iter().enumerate() {
if row.len() == dim {
let _ = idx.insert(VectorId::from(i as u64), arc(row), None);
}
}
let query: Vec<f32> = (0..dim).map(|j| j as f32 - 3.0).collect();
let params = if with_filter {
SearchParams {
filter: Some(Filter::eq("missing", Value::Bool(true))),
..SearchParams::new(k, metric)
}
} else {
SearchParams::new(k, metric)
};
let result = idx.search(&query, ¶ms);
prop_assert!(result.is_ok());
if let Ok(hits) = result {
prop_assert!(hits.len() <= k);
}
}
}