use taladb::{Database, HnswOptions, Value, VectorMetric, VectorQueryOptions, VectorSearchMode};
fn vec_val(v: &[f32]) -> Value {
Value::Array(v.iter().map(|f| Value::Float(f64::from(*f))).collect())
}
fn insert_vec(col: &taladb::Collection, label: &str, v: &[f32]) {
col.insert(vec![
("label".into(), Value::Str(label.into())),
("emb".into(), vec_val(v)),
])
.unwrap();
}
fn seed(col: &taladb::Collection, n: usize, tag: &str) {
for i in 0..n {
let mut v = [0.0f32; 8];
v[i % 8] = 1.0;
v[(i + 3) % 8] = (i as f32) * 0.01;
insert_vec(col, &format!("{tag}-{i}"), &v);
}
}
fn make_index(col: &taladb::Collection) {
col.create_vector_index(
"emb",
8,
Some(VectorMetric::Cosine),
Some(HnswOptions {
m: 8,
ef_construction: 64,
}),
)
.unwrap();
}
fn labels(r: taladb::VectorQueryResult) -> Vec<String> {
r.hits
.into_iter()
.map(|h| match h.document.get("label") {
Some(Value::Str(s)) => s.clone(),
_ => String::new(),
})
.collect()
}
fn ann(col: &taladb::Collection, q: &[f32]) -> Vec<String> {
let opts = VectorQueryOptions {
mode: VectorSearchMode::Ann,
ef_search: Some(200),
..Default::default()
};
labels(col.search_vectors("emb", q, 5, None, &opts).unwrap())
}
fn exact(col: &taladb::Collection, q: &[f32]) -> Vec<String> {
let opts = VectorQueryOptions {
mode: VectorSearchMode::Exact,
..Default::default()
};
labels(col.search_vectors("emb", q, 5, None, &opts).unwrap())
}
const Q: [f32; 8] = [1.0, 0.0, 0.0, 0.2, 0.0, 0.0, 0.0, 0.0];
#[test]
fn repeated_ann_queries_are_consistent() {
let db = Database::open_in_memory().unwrap();
let col = db.collection("docs").unwrap();
seed(&col, 40, "a");
make_index(&col);
let first = ann(&col, &Q);
let second = ann(&col, &Q);
let third = ann(&col, &Q);
assert_eq!(first, second, "second query diverged from the first");
assert_eq!(second, third, "third query diverged");
assert!(!first.is_empty());
}
#[test]
fn a_rebuild_does_not_serve_stale_graph_nodes() {
let db = Database::open_in_memory().unwrap();
let col = db.collection("docs").unwrap();
seed(&col, 40, "a");
make_index(&col);
let before = ann(&col, &Q);
assert!(!before.is_empty());
col.upgrade_vector_index("emb").unwrap();
let after = ann(&col, &Q);
assert_eq!(
after,
exact(&col, &Q),
"ANN disagreed with exact after a rebuild — stale cached nodes"
);
assert_eq!(
before, after,
"rebuild changed the answer on unchanged data"
);
}
#[test]
fn deletes_then_rebuild_are_visible_through_the_cache() {
let db = Database::open_in_memory().unwrap();
let col = db.collection("docs").unwrap();
seed(&col, 40, "a");
make_index(&col);
let before = ann(&col, &Q);
assert!(!before.is_empty());
let victim = before[0].clone();
col.delete_one(taladb::query::Filter::Eq(
"label".into(),
Value::Str(victim.clone()),
))
.unwrap();
col.upgrade_vector_index("emb").unwrap();
let after = ann(&col, &Q);
assert!(
!after.contains(&victim),
"deleted document {victim} came back from the node cache: {after:?}"
);
assert_eq!(after, exact(&col, &Q), "ANN disagreed with exact");
}
#[test]
fn dropping_and_recreating_the_index_does_not_serve_stale_graph_nodes() {
let db = Database::open_in_memory().unwrap();
let col = db.collection("docs").unwrap();
seed(&col, 40, "old");
make_index(&col);
let before = ann(&col, &Q);
assert!(before.iter().all(|l| l.starts_with("old")));
col.drop_vector_index("emb").unwrap();
for d in col.find(taladb::query::Filter::All).unwrap() {
col.delete_one(taladb::query::Filter::Eq(
"_id".into(),
Value::Str(d.id.to_string()),
))
.unwrap();
}
seed(&col, 40, "new");
make_index(&col);
let after = ann(&col, &Q);
assert!(
after.iter().all(|l| l.starts_with("new")),
"stale 'old' documents survived a drop/recreate: {after:?}"
);
assert_eq!(after, exact(&col, &Q));
}
#[test]
fn writes_after_a_cached_query_are_not_answered_from_the_stale_graph() {
let db = Database::open_in_memory().unwrap();
let col = db.collection("docs").unwrap();
seed(&col, 40, "a");
make_index(&col);
let _warm = ann(&col, &Q);
insert_vec(&col, "needle", &Q);
let opts = VectorQueryOptions::default(); let auto = labels(col.search_vectors("emb", &Q, 5, None, &opts).unwrap());
assert_eq!(
auto[0], "needle",
"a stale graph answered instead of falling back to exact: {auto:?}"
);
}
#[test]
fn two_handles_from_one_database_share_invalidation() {
let db = Database::open_in_memory().unwrap();
let a = db.collection("docs").unwrap();
seed(&a, 40, "a");
make_index(&a);
let _warm = ann(&a, &Q);
let b = db.collection("docs").unwrap();
b.upgrade_vector_index("emb").unwrap();
assert_eq!(
ann(&a, &Q),
exact(&a, &Q),
"handle A served nodes invalidated by a rebuild through handle B"
);
}
#[test]
fn other_collections_are_not_invalidated_or_confused() {
let db = Database::open_in_memory().unwrap();
let docs = db.collection("docs").unwrap();
let other = db.collection("other").unwrap();
seed(&docs, 40, "d");
seed(&other, 40, "o");
make_index(&docs);
make_index(&other);
let docs_before = ann(&docs, &Q);
let other_before = ann(&other, &Q);
other.upgrade_vector_index("emb").unwrap();
assert_eq!(
ann(&docs, &Q),
docs_before,
"docs disturbed by other's rebuild"
);
assert!(ann(&docs, &Q).iter().all(|l| l.starts_with("d-")));
assert_eq!(ann(&other, &Q), exact(&other, &Q));
assert!(other_before.iter().all(|l| l.starts_with("o-")));
}