use iqdb::{DistanceMetric, Error, Iqdb, Payload, PayloadValue, Record, RecordId, Vector};
fn record(id: u64, components: Vec<f32>, topic: Option<&str>) -> Record {
let v = Vector::new(components).expect("finite components");
match topic {
None => Record::new(RecordId::new(id), v),
Some(label) => {
let mut payload = Payload::new();
let _ = payload.insert("topic", label);
Record::with_payload(RecordId::new(id), v, payload)
}
}
}
fn seed(db: &Iqdb) {
db.upsert(record(1, vec![1.0, 0.0, 0.0], Some("rust")))
.unwrap();
db.upsert(record(2, vec![0.99, 0.10, 0.0], Some("rust")))
.unwrap();
db.upsert(record(3, vec![0.0, 1.0, 0.0], Some("python")))
.unwrap();
db.upsert(record(4, vec![0.71, 0.71, 0.0], Some("go")))
.unwrap();
db.upsert(record(5, vec![-1.0, 0.0, 0.0], Some("rust")))
.unwrap();
}
#[test]
fn search_returns_top_k_sorted_ascending() {
let db = Iqdb::open_in_memory();
seed(&db);
let probe = Vector::new(vec![1.0, 0.0, 0.0]).unwrap();
let hits = db.search(&probe, 3, DistanceMetric::Cosine).unwrap();
assert_eq!(hits.len(), 3);
for pair in hits.windows(2) {
assert!(pair[0].score <= pair[1].score, "results must be ascending");
}
}
#[test]
fn search_caps_at_k_when_store_exceeds_k() {
let db = Iqdb::open_in_memory();
seed(&db);
let probe = Vector::new(vec![1.0, 0.0, 0.0]).unwrap();
let hits = db.search(&probe, 2, DistanceMetric::L2).unwrap();
assert_eq!(hits.len(), 2);
}
#[test]
fn search_returns_all_when_k_exceeds_store() {
let db = Iqdb::open_in_memory();
seed(&db);
let probe = Vector::new(vec![1.0, 0.0, 0.0]).unwrap();
let hits = db.search(&probe, 100, DistanceMetric::L2).unwrap();
assert_eq!(hits.len(), 5);
}
#[test]
fn search_returns_empty_when_k_is_zero() {
let db = Iqdb::open_in_memory();
seed(&db);
let probe = Vector::new(vec![1.0, 0.0, 0.0]).unwrap();
let hits = db.search(&probe, 0, DistanceMetric::Cosine).unwrap();
assert!(hits.is_empty());
}
#[test]
fn search_returns_empty_on_empty_store() {
let db = Iqdb::open_in_memory();
let probe = Vector::new(vec![1.0, 0.0, 0.0]).unwrap();
let hits = db.search(&probe, 5, DistanceMetric::Cosine).unwrap();
assert!(hits.is_empty());
}
#[test]
fn search_with_filter_excludes_records_before_admission() {
let db = Iqdb::open_in_memory();
seed(&db);
let probe = Vector::new(vec![1.0, 0.0, 0.0]).unwrap();
let only_rust = db
.search_with(&probe, 5, DistanceMetric::Cosine, |rec| {
rec.payload()
.and_then(|p| p.get("topic"))
.and_then(PayloadValue::as_text)
== Some("rust")
})
.unwrap();
assert_eq!(only_rust.len(), 3);
for hit in &only_rust {
let topic = hit
.payload
.as_ref()
.and_then(|p| p.get("topic"))
.and_then(PayloadValue::as_text);
assert_eq!(topic, Some("rust"));
}
}
#[test]
fn search_with_filter_returning_false_yields_empty() {
let db = Iqdb::open_in_memory();
seed(&db);
let probe = Vector::new(vec![1.0, 0.0, 0.0]).unwrap();
let none = db
.search_with(&probe, 5, DistanceMetric::Cosine, |_| false)
.unwrap();
assert!(none.is_empty());
}
#[test]
fn search_propagates_dimension_mismatch() {
let db = Iqdb::open_in_memory();
seed(&db); let probe = Vector::new(vec![1.0, 0.0]).unwrap(); let err = db.search(&probe, 5, DistanceMetric::L2).unwrap_err();
assert!(matches!(err, Error::DimensionMismatch { .. }));
}
#[test]
fn search_attaches_payload_when_present() {
let db = Iqdb::open_in_memory();
seed(&db);
let probe = Vector::new(vec![1.0, 0.0, 0.0]).unwrap();
let hits = db.search(&probe, 1, DistanceMetric::Cosine).unwrap();
assert_eq!(hits.len(), 1);
let payload = hits[0].payload.as_ref().expect("payload preserved");
assert!(payload.contains_key("topic"));
}
#[test]
fn search_omits_payload_for_records_without_one() {
let db = Iqdb::open_in_memory();
db.upsert(record(99, vec![1.0, 0.0, 0.0], None)).unwrap();
let probe = Vector::new(vec![1.0, 0.0, 0.0]).unwrap();
let hits = db.search(&probe, 1, DistanceMetric::L2).unwrap();
assert_eq!(hits.len(), 1);
assert!(hits[0].payload.is_none());
}
#[test]
fn search_batch_preserves_input_order() {
let db = Iqdb::open_in_memory();
seed(&db);
let probes = vec![
Vector::new(vec![1.0, 0.0, 0.0]).unwrap(),
Vector::new(vec![0.0, 1.0, 0.0]).unwrap(),
Vector::new(vec![-1.0, 0.0, 0.0]).unwrap(),
];
let batches = db.search_batch(&probes, 1, DistanceMetric::Cosine).unwrap();
assert_eq!(batches.len(), 3);
assert_eq!(batches[0][0].id, RecordId::new(1));
assert_eq!(batches[1][0].id, RecordId::new(3));
assert_eq!(batches[2][0].id, RecordId::new(5));
}
#[test]
fn search_batch_returns_empty_per_query_on_empty_store() {
let db = Iqdb::open_in_memory();
let probes = vec![
Vector::new(vec![1.0, 0.0]).unwrap(),
Vector::new(vec![0.0, 1.0]).unwrap(),
];
let batches = db.search_batch(&probes, 3, DistanceMetric::L2).unwrap();
assert_eq!(batches.len(), 2);
assert!(batches.iter().all(Vec::is_empty));
}
#[test]
fn search_batch_with_shared_filter_applies_to_every_query() {
let db = Iqdb::open_in_memory();
seed(&db);
let probes = vec![
Vector::new(vec![1.0, 0.0, 0.0]).unwrap(),
Vector::new(vec![0.0, 1.0, 0.0]).unwrap(),
];
let batches = db
.search_batch_with(&probes, 5, DistanceMetric::Cosine, |rec| {
rec.payload()
.and_then(|p| p.get("topic"))
.and_then(PayloadValue::as_text)
== Some("rust")
})
.unwrap();
for batch in &batches {
for hit in batch {
let topic = hit
.payload
.as_ref()
.and_then(|p| p.get("topic"))
.and_then(PayloadValue::as_text);
assert_eq!(topic, Some("rust"));
}
}
}
#[test]
fn search_supports_concurrent_readers_through_arc() {
use std::sync::Arc;
use std::thread;
let db = Arc::new(Iqdb::open_in_memory());
for id in 0..16_u64 {
db.upsert(Record::new(
RecordId::new(id),
Vector::new(vec![id as f32, 0.0]).unwrap(),
))
.unwrap();
}
let mut handles = Vec::new();
for _ in 0..8 {
let db = Arc::clone(&db);
handles.push(thread::spawn(move || {
let probe = Vector::new(vec![5.0, 0.0]).unwrap();
let hits = db.search(&probe, 3, DistanceMetric::L2).expect("ok");
assert_eq!(hits.len(), 3);
assert_eq!(hits[0].id, RecordId::new(5));
}));
}
for h in handles {
h.join().expect("worker panicked");
}
}