lucisearch 0.8.1

Embeddable, in-process search engine — the SQLite/DuckDB of search
Documentation
//! Exit criteria integration tests for Milestone 7: Nested Documents.

use luci::index::Index;
use luci::mapping::{FieldType, Mapping};
use serde_json::json;

fn search(
    index: &mut Index,
    query: serde_json::Value,
    size: usize,
) -> luci::search::results::SearchResults {
    let expr = luci::search::expression::parse_search(query, size).unwrap();
    index.search(&expr).unwrap()
}

fn test_dir(name: &str) -> std::path::PathBuf {
    let dir =
        std::env::temp_dir().join(format!("luci_m7_integration_{}_{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);
}

#[test]
fn cross_object_matching_prevented() {
    let path = test_dir("cross_object");
    let schema = Mapping::builder()
        .field("product", FieldType::Text)
        .field("offers", FieldType::Nested)
        .field("offers.seller", FieldType::Keyword)
        .field("offers.price", FieldType::Keyword)
        .build();
    let mut index = Index::create_with_mapping(&path, schema).unwrap();

    index
        .bulk(vec![json!({
            "product": "laptop",
            "offers": [
                {"seller": "Alice", "price": "999"},
                {"seller": "Bob", "price": "1299"}
            ]
        })])
        .unwrap();

    let results = search(
        &mut index,
        json!({
            "nested": {
                "path": "offers",
                "query": {
                    "bool": {
                        "must": [
                            {"term": {"offers.seller": "Alice"}},
                            {"term": {"offers.price": "1299"}}
                        ]
                    }
                }
            }
        }),
        10,
    );

    assert_eq!(
        results.total_hits().value,
        0,
        "cross-object match should be prevented: Alice doesn't sell at 1299"
    );

    let results = search(
        &mut index,
        json!({
            "nested": {
                "path": "offers",
                "query": {
                    "bool": {
                        "must": [
                            {"term": {"offers.seller": "Alice"}},
                            {"term": {"offers.price": "999"}}
                        ]
                    }
                }
            }
        }),
        10,
    );

    assert_eq!(
        results.total_hits().value,
        1,
        "Alice selling at 999 should match"
    );

    cleanup(&path);
}

#[test]
fn nested_returns_parent() {
    let path = test_dir("parent_return");
    let schema = Mapping::builder()
        .field("title", FieldType::Text)
        .field("tags", FieldType::Nested)
        .field("tags.name", FieldType::Keyword)
        .field("tags.score", FieldType::Keyword)
        .build();
    let mut index = Index::create_with_mapping(&path, schema).unwrap();

    index
        .bulk(vec![
            json!({
                "title": "article one",
                "tags": [
                    {"name": "rust", "score": "high"},
                    {"name": "search", "score": "medium"}
                ]
            }),
            json!({
                "title": "article two",
                "tags": [
                    {"name": "python", "score": "high"}
                ]
            }),
        ])
        .unwrap();

    let results = search(
        &mut index,
        json!({
            "nested": {
                "path": "tags",
                "query": {"term": {"tags.name": "rust"}}
            }
        }),
        10,
    );

    assert_eq!(results.total_hits().value, 1);
    let source = results.hit(0).unwrap().source().unwrap();
    assert_eq!(source["title"], "article one");

    let results = search(
        &mut index,
        json!({
            "nested": {
                "path": "tags",
                "query": {"term": {"tags.name": "python"}}
            }
        }),
        10,
    );

    assert_eq!(results.total_hits().value, 1);
    let source = results.hit(0).unwrap().source().unwrap();
    assert_eq!(source["title"], "article two");

    cleanup(&path);
}

#[test]
fn nested_matches_correct_parent() {
    let path = test_dir("correct_parent");
    let schema = Mapping::builder()
        .field("product", FieldType::Text)
        .field("offers", FieldType::Nested)
        .field("offers.seller", FieldType::Keyword)
        .build();
    let mut index = Index::create_with_mapping(&path, schema).unwrap();

    index
        .bulk(vec![
            json!({"product": "laptop", "offers": [{"seller": "alice"}]}),
            json!({"product": "phone", "offers": [{"seller": "bob"}]}),
            json!({"product": "tablet", "offers": [{"seller": "charlie"}]}),
            json!({"product": "monitor", "offers": [{"seller": "diana"}, {"seller": "eve"}]}),
        ])
        .unwrap();

    let results = search(
        &mut index,
        json!({
            "nested": {
                "path": "offers",
                "query": {"term": {"offers.seller": "diana"}}
            }
        }),
        10,
    );

    assert_eq!(results.total_hits().value, 1);
    let source = results.hit(0).unwrap().source().unwrap();
    assert_eq!(source["product"], "monitor");

    let results = search(
        &mut index,
        json!({
            "nested": {
                "path": "offers",
                "query": {"term": {"offers.seller": "bob"}}
            }
        }),
        10,
    );

    assert_eq!(results.total_hits().value, 1);
    let source = results.hit(0).unwrap().source().unwrap();
    assert_eq!(source["product"], "phone");

    cleanup(&path);
}

#[test]
fn multiple_nested_objects() {
    let path = test_dir("multi_nested");
    let schema = Mapping::builder()
        .field("name", FieldType::Text)
        .field("skills", FieldType::Nested)
        .field("skills.name", FieldType::Keyword)
        .build();
    let mut index = Index::create_with_mapping(&path, schema).unwrap();

    index
        .bulk(vec![json!({
            "name": "developer",
            "skills": [
                {"name": "rust"},
                {"name": "python"},
                {"name": "javascript"}
            ]
        })])
        .unwrap();

    let results = search(
        &mut index,
        json!({
            "nested": {
                "path": "skills",
                "query": {"term": {"skills.name": "python"}}
            }
        }),
        10,
    );

    assert_eq!(results.total_hits().value, 1);

    let results = search(
        &mut index,
        json!({
            "nested": {
                "path": "skills",
                "query": {"term": {"skills.name": "go"}}
            }
        }),
        10,
    );

    assert_eq!(results.total_hits().value, 0);

    cleanup(&path);
}