use luci::index::Index;
use luci::mapping::{FieldType, Mapping};
use luci::search::expression::parse_search;
use serde_json::json;
fn test_dir(name: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"luci_error_propagation_{}_{name}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
dir
}
fn cleanup(path: &std::path::Path) {
let _ = std::fs::remove_dir_all(path);
}
fn build_vector_index(name: &str) -> (std::path::PathBuf, Index) {
let path = test_dir(name);
let schema = Mapping::builder()
.field("title", FieldType::Text)
.field("tag", FieldType::Keyword)
.field("embedding", FieldType::dense_vector(4))
.build();
let index = Index::create_with_mapping(&path, schema).unwrap();
index
.bulk(vec![
json!({"title": "alpha", "tag": "a", "embedding": [1.0, 0.0, 0.0, 0.0]}),
json!({"title": "beta", "tag": "b", "embedding": [0.0, 1.0, 0.0, 0.0]}),
json!({"title": "gamma", "tag": "a", "embedding": [0.0, 0.0, 1.0, 0.0]}),
])
.unwrap();
(path, index)
}
fn build_multi_segment_vector_index(name: &str) -> (std::path::PathBuf, Index) {
let path = test_dir(name);
let schema = Mapping::builder()
.field("title", FieldType::Text)
.field("embedding", FieldType::dense_vector(4))
.build();
let index = Index::create_with_mapping(&path, schema).unwrap();
for batch_id in 0..3 {
index
.bulk(vec![
json!({"title": format!("doc_{batch_id}_a"), "embedding": [1.0, 0.0, 0.0, 0.0]}),
json!({"title": format!("doc_{batch_id}_b"), "embedding": [0.0, 1.0, 0.0, 0.0]}),
])
.unwrap();
}
(path, index)
}
#[test]
fn bind_error_propagates_to_search_caller() {
let (path, index) = build_vector_index("bind_error");
let expr = parse_search(
json!({"query": {"knn": {
"field": "embedding",
"query_vector": [1.0, 0.0], "k": 3,
}}}),
10,
)
.unwrap();
let err = match index.search(&expr) {
Ok(_) => panic!("expected bind error, got Ok"),
Err(e) => e,
};
let msg = err.to_string();
assert!(
msg.contains("2 dimensions") && msg.contains("embedding"),
"expected dim-mismatch message naming dims and field, got: {msg}"
);
cleanup(&path);
}
#[test]
fn per_segment_error_aborts_whole_query() {
let (path, index) = build_multi_segment_vector_index("per_segment");
let expr = parse_search(
json!({"query": {"knn": {
"field": "embedding",
"query_vector": [1.0, 0.0, 0.0], "k": 3,
}}}),
10,
)
.unwrap();
let err = match index.search(&expr) {
Ok(r) => panic!(
"expected error from multi-segment search; got Ok with {} hits",
r.total_hits().value
),
Err(e) => e,
};
assert!(err.to_string().contains("3 dimensions"));
cleanup(&path);
}
#[test]
fn knn_empty_vector_field_returns_ok_empty() {
let path = test_dir("empty_vec_field");
let schema = Mapping::builder()
.field("title", FieldType::Text)
.field("embedding", FieldType::dense_vector(4))
.build();
let index = Index::create_with_mapping(&path, schema).unwrap();
index
.bulk(vec![json!({"title": "no vector here"})])
.unwrap();
let expr = parse_search(
json!({"query": {"knn": {
"field": "embedding",
"query_vector": [1.0, 0.0, 0.0, 0.0],
"k": 3,
}}}),
10,
)
.unwrap();
let results = index
.search(&expr)
.expect("valid dense_vector field with no vectors is honest-empty, not an error");
assert_eq!(results.len(), 0);
cleanup(&path);
}
#[test]
fn rescore_bind_error_propagates() {
let (path, index) = build_vector_index("rescore_bind");
let expr = parse_search(
json!({
"query": {"match_all": {}},
"rescore": {
"window_size": 10,
"query": {
"rescore_query": {"knn": {
"field": "embedding",
"query_vector": [1.0, 2.0], "k": 5,
}}
}
}
}),
10,
)
.unwrap();
let err = match index.search(&expr) {
Ok(_) => panic!("expected rescore bind error to propagate"),
Err(e) => e,
};
assert!(err.to_string().contains("2 dimensions"));
cleanup(&path);
}
#[test]
fn collapse_bind_error_propagates() {
let (path, index) = build_vector_index("collapse_bind");
let expr = parse_search(
json!({
"query": {"knn": {
"field": "embedding",
"query_vector": [1.0], "k": 3,
}},
"collapse": {"field": "tag"},
}),
10,
)
.unwrap();
let err = match index.search(&expr) {
Ok(_) => panic!("expected collapsed bind error to propagate"),
Err(e) => e,
};
assert!(err.to_string().contains("1 dimensions"));
cleanup(&path);
}