use zyxdb::{Database, ErrorCode};
fn temp_db() -> (tempfile::TempDir, Database) {
let dir = tempfile::tempdir().unwrap();
let db = Database::open(dir.path().join("vec.zyx")).unwrap();
(dir, db)
}
fn vec_literal(v: &[f64]) -> String {
v.iter().map(|x| x.to_string()).collect::<Vec<_>>().join(",")
}
fn search(db: &Database, idx: &str, k: usize, query: &[f64]) -> Vec<(i64, f64)> {
let q = format!(
"CALL db.index.vector.queryNodes('{idx}', {k}, [{q}]) YIELD node, score RETURN node.id AS id, score",
idx = idx,
k = k,
q = vec_literal(query)
);
let mut r = db.execute(&q, None).expect("vector query failed");
let mut out = Vec::new();
while let Some(row) = r.next().unwrap() {
let id = row.get_i64(0).unwrap();
let score = row.get_f64(1).unwrap();
out.push((id, score));
}
out
}
fn insert_l2_fixture(db: &Database) {
db.execute("CREATE VECTOR INDEX vidx ON :V(embedding) OPTIONS {dimension: 2, metric: 'L2'}", None).unwrap();
db.execute("CREATE (:V {id: 1, embedding: [1.0, 0.0]})", None).unwrap();
db.execute("CREATE (:V {id: 2, embedding: [0.0, 1.0]})", None).unwrap();
db.execute("CREATE (:V {id: 3, embedding: [0.0, 0.0]})", None).unwrap();
}
#[test]
fn l2_search_returns_top_match_with_squared_distance() {
let (_dir, db) = temp_db();
insert_l2_fixture(&db);
let res = search(&db, "vidx", 1, &[0.9, 0.1]);
assert!(!res.is_empty());
assert_eq!(res[0].0, 1); assert!((res[0].1 - 0.02).abs() < 1e-2, "expected ~0.02, got {}", res[0].1);
}
#[test]
fn search_returns_up_to_k_sorted_ascending() {
let (_dir, db) = temp_db();
insert_l2_fixture(&db);
let res = search(&db, "vidx", 3, &[0.9, 0.1]);
assert_eq!(res.len(), 3);
assert!(res[0].1 <= res[1].1 && res[1].1 <= res[2].1);
}
#[test]
fn cosine_metric_returns_negative_inner_product() {
let (_dir, db) = temp_db();
db.execute("CREATE VECTOR INDEX vcos ON :V(embedding) OPTIONS {dimension: 2, metric: 'Cosine'}", None).unwrap();
db.execute("CREATE (:V {id: 1, embedding: [1.0, 0.0]})", None).unwrap();
db.execute("CREATE (:V {id: 2, embedding: [0.0, 1.0]})", None).unwrap();
let res = search(&db, "vcos", 1, &[1.0, 0.0]);
assert_eq!(res[0].0, 1);
assert!((res[0].1 - (-1.0)).abs() < 5e-2, "expected ~-1.0, got {}", res[0].1);
}
#[test]
fn uppercase_cosine_silently_uses_l2() {
let (_dir, db) = temp_db();
db.execute("CREATE VECTOR INDEX vup ON :V(embedding) OPTIONS {dimension: 2, metric: 'COSINE'}", None).unwrap();
db.execute("CREATE (:V {id: 1, embedding: [1.0, 0.0]})", None).unwrap();
let res = search(&db, "vup", 1, &[1.0, 0.0]);
assert_eq!(res[0].0, 1);
}
#[test]
fn manual_train_returns_status_row_and_search_still_works() {
let (_dir, db) = temp_db();
db.execute("CREATE VECTOR INDEX vtrain ON :V(embedding) OPTIONS {dimension: 2, metric: 'L2'}", None).unwrap();
for i in 0..10i64 {
let x = i as f64 / 10.0;
let y = 1.0 - x;
db.execute(&format!("CREATE (:V {{id: {i}, embedding: [{x}, {y}]}})", i = i, x = x, y = y), None).unwrap();
}
let mut r = db
.execute("CALL db.index.vector.train('vtrain') YIELD status RETURN status", None)
.expect("train call failed");
let row = r.next().unwrap().expect("expected a status row");
let status = row.get_str(0).expect("status string");
assert!(!status.is_empty());
let res = search(&db, "vtrain", 1, &[0.0, 1.0]);
assert!(!res.is_empty());
}
#[test]
fn train_on_empty_index_returns_skipped_status() {
let (_dir, db) = temp_db();
db.execute("CREATE VECTOR INDEX vempty ON :V(embedding) OPTIONS {dimension: 2, metric: 'L2'}", None).unwrap();
let mut r = db
.execute("CALL db.index.vector.train('vempty') YIELD status RETURN status", None)
.unwrap();
let row = r.next().unwrap().expect("expected a status row");
let status = row.get_str(0).unwrap().to_lowercase();
assert!(status.contains("skip"), "expected skip in status, got: {status}");
}
#[test]
fn train_on_nonexistent_index_errors() {
let (_dir, db) = temp_db();
let result = db.execute("CALL db.index.vector.train('does_not_exist') YIELD status RETURN status", None);
match result {
Ok(_) => panic!("expected training on a missing index to error"),
Err(e) => assert!(
matches!(e.code(), ErrorCode::NotFound | ErrorCode::ExecutionError),
"expected NotFound/ExecutionError, got {:?}",
e.code()
),
}
}
#[test]
fn dimension_mismatch_on_insert_is_tolerated_by_index() {
let (_dir, db) = temp_db();
db.execute("CREATE VECTOR INDEX vdim ON :V(embedding) OPTIONS {dimension: 2, metric: 'L2'}", None).unwrap();
db.execute("CREATE (:V {id: 1, embedding: [1.0, 0.0]})", None).unwrap();
db.execute("CREATE (:V {id: 2, embedding: [1.0, 0.0, 0.0, 0.0]})", None).unwrap();
let res = search(&db, "vdim", 5, &[1.0, 0.0]);
let ids: Vec<_> = res.iter().map(|(id, _)| *id).collect();
assert!(ids.contains(&1), "2-d node must be found");
assert!(!ids.contains(&2), "4-d node must be skipped from the index");
}