lucisearch 0.8.1

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

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_m6_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);
}

fn build_geo_index(name: &str) -> (std::path::PathBuf, Index) {
    let path = test_dir(name);
    let schema = Mapping::builder()
        .field("name", FieldType::Text)
        .field("city", FieldType::Keyword)
        .field("location", FieldType::GeoPoint)
        .build();
    let index = Index::create_with_mapping(&path, schema).unwrap();

    index.bulk(vec![
        json!({"name": "Times Square", "city": "nyc", "location": {"lat": 40.7580, "lon": -73.9855}}),
        json!({"name": "Central Park", "city": "nyc", "location": {"lat": 40.7829, "lon": -73.9654}}),
        json!({"name": "Brooklyn Bridge", "city": "nyc", "location": {"lat": 40.7061, "lon": -73.9969}}),
        json!({"name": "Big Ben", "city": "london", "location": {"lat": 51.5007, "lon": -0.1246}}),
        json!({"name": "Tower Bridge", "city": "london", "location": {"lat": 51.5055, "lon": -0.0754}}),
        json!({"name": "Eiffel Tower", "city": "paris", "location": {"lat": 48.8584, "lon": 2.2945}}),
    ]).unwrap();

    (path, index)
}

#[test]
fn geo_distance_query() {
    let (path, mut index) = build_geo_index("distance");

    let results = search(
        &mut index,
        json!({
            "geo_distance": {
                "distance": "5km",
                "location": {"lat": 40.7580, "lon": -73.9855}
            }
        }),
        10,
    );

    assert!(results.total_hits().value >= 2);
    assert!(results.total_hits().value <= 3);

    for hit in results.iter() {
        let source = hit.source().unwrap();
        assert_eq!(source["city"], "nyc");
    }

    cleanup(&path);
}

#[test]
fn geo_distance_large_radius() {
    let (path, mut index) = build_geo_index("large_radius");

    let results = search(
        &mut index,
        json!({
            "geo_distance": {
                "distance": "1000km",
                "location": {"lat": 51.5, "lon": -0.1}
            }
        }),
        10,
    );

    assert!(results.total_hits().value >= 3);

    let cities: Vec<String> = results
        .iter()
        .filter_map(|h| h.source()?.get("city")?.as_str().map(String::from))
        .collect();
    assert!(cities.iter().any(|c| c == "london"));
    assert!(cities.iter().any(|c| c == "paris"));
    assert!(!cities.iter().any(|c| c == "nyc"));

    cleanup(&path);
}

#[test]
fn geo_bounding_box_query() {
    let (path, mut index) = build_geo_index("bbox");

    let results = search(
        &mut index,
        json!({
            "geo_bounding_box": {
                "location": {
                    "top_left": {"lat": 40.80, "lon": -74.02},
                    "bottom_right": {"lat": 40.70, "lon": -73.95}
                }
            }
        }),
        10,
    );

    assert!(results.total_hits().value >= 2);

    for hit in results.iter() {
        let source = hit.source().unwrap();
        assert_eq!(source["city"], "nyc");
    }

    cleanup(&path);
}

#[test]
fn geo_plus_text_query() {
    let (path, mut index) = build_geo_index("combined");

    let results = search(
        &mut index,
        json!({
            "bool": {
                "must": [{"match": {"name": "bridge"}}],
                "filter": [{
                    "geo_distance": {
                        "distance": "100km",
                        "location": {"lat": 51.5, "lon": -0.1}
                    }
                }]
            }
        }),
        10,
    );

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

    cleanup(&path);
}