/*! This module tests the `geo_sort` ranking rule */ use big_s::S; use heed::RoTxn; use maplit::hashset; use crate::index::tests::TempIndex; use crate::score_details::ScoreDetails; use crate::search::new::tests::collect_field_values; use crate::{AscDesc, Criterion, GeoSortStrategy, Member, Search, SearchResult}; fn create_index() -> TempIndex { let index = TempIndex::new(); index .update_settings(|s| { s.set_primary_key("id".to_owned()); s.set_sortable_fields(hashset! { S("_geo") }); s.set_criteria(vec![Criterion::Words, Criterion::Sort]); }) .unwrap(); index } #[track_caller] fn execute_iterative_and_rtree_returns_the_same<'a>( rtxn: &RoTxn<'a>, index: &TempIndex, search: &mut Search<'a>, ) -> (Vec, Vec>) { search.geo_sort_strategy(GeoSortStrategy::AlwaysIterative(2)); let SearchResult { documents_ids, document_scores: iterative_scores_bucketed, .. } = search.execute().unwrap(); let iterative_ids_bucketed = collect_field_values(index, rtxn, "id", &documents_ids); search.geo_sort_strategy(GeoSortStrategy::AlwaysIterative(1000)); let SearchResult { documents_ids, document_scores: iterative_scores, .. } = search.execute().unwrap(); let iterative_ids = collect_field_values(index, rtxn, "id", &documents_ids); assert_eq!(iterative_ids_bucketed, iterative_ids, "iterative bucket"); assert_eq!(iterative_scores_bucketed, iterative_scores, "iterative bucket score"); search.geo_sort_strategy(GeoSortStrategy::AlwaysRtree(2)); let SearchResult { documents_ids, document_scores: rtree_scores_bucketed, .. } = search.execute().unwrap(); let rtree_ids_bucketed = collect_field_values(index, rtxn, "id", &documents_ids); search.geo_sort_strategy(GeoSortStrategy::AlwaysRtree(1000)); let SearchResult { documents_ids, document_scores: rtree_scores, .. } = search.execute().unwrap(); let rtree_ids = collect_field_values(index, rtxn, "id", &documents_ids); assert_eq!(rtree_ids_bucketed, rtree_ids, "rtree bucket"); assert_eq!(rtree_scores_bucketed, rtree_scores, "rtree bucket score"); assert_eq!(iterative_ids, rtree_ids, "iterative vs rtree"); assert_eq!(iterative_scores, rtree_scores, "iterative vs rtree scores"); (iterative_ids.into_iter().map(|id| id.parse().unwrap()).collect(), iterative_scores) } #[test] fn test_geo_sort() { let index = create_index(); index .add_documents(documents!([ { "id": 2, "_geo": { "lat": 2, "lng": -1 } }, { "id": 3, "_geo": { "lat": -2, "lng": -2 } }, { "id": 5, "_geo": { "lat": 6, "lng": -5 } }, { "id": 4, "_geo": { "lat": 3, "lng": 5 } }, { "id": 0, "_geo": { "lat": 0, "lng": 0 } }, { "id": 1, "_geo": { "lat": 1, "lng": 1 } }, { "id": 6 }, { "id": 8 }, { "id": 7 }, { "id": 10 }, { "id": 9 }, ])) .unwrap(); let rtxn = index.read_txn().unwrap(); let mut s = Search::new(&rtxn, &index); s.scoring_strategy(crate::score_details::ScoringStrategy::Detailed); s.sort_criteria(vec![AscDesc::Asc(Member::Geo([0., 0.]))]); let (ids, scores) = execute_iterative_and_rtree_returns_the_same(&rtxn, &index, &mut s); insta::assert_snapshot!(format!("{ids:?}"), @"[0, 1, 2, 3, 4, 5, 6, 8, 7, 10, 9]"); insta::assert_snapshot!(format!("{scores:#?}")); s.sort_criteria(vec![AscDesc::Desc(Member::Geo([0., 0.]))]); let (ids, scores) = execute_iterative_and_rtree_returns_the_same(&rtxn, &index, &mut s); insta::assert_snapshot!(format!("{ids:?}"), @"[5, 4, 3, 2, 1, 0, 6, 8, 7, 10, 9]"); insta::assert_snapshot!(format!("{scores:#?}")); } #[test] fn test_geo_sort_around_the_edge_of_the_flat_earth() { let index = create_index(); index .add_documents(documents!([ { "id": 0, "_geo": { "lat": 0, "lng": 0 } }, { "id": 1, "_geo": { "lat": 88, "lng": 0 } }, { "id": 2, "_geo": { "lat": -89, "lng": 0 } }, { "id": 3, "_geo": { "lat": 0, "lng": 178 } }, { "id": 4, "_geo": { "lat": 0, "lng": -179 } }, ])) .unwrap(); let rtxn = index.read_txn().unwrap(); let mut s = Search::new(&rtxn, &index); s.scoring_strategy(crate::score_details::ScoringStrategy::Detailed); // --- asc s.sort_criteria(vec![AscDesc::Asc(Member::Geo([0., 0.]))]); let (ids, scores) = execute_iterative_and_rtree_returns_the_same(&rtxn, &index, &mut s); insta::assert_snapshot!(format!("{ids:?}"), @"[0, 1, 2, 3, 4]"); insta::assert_snapshot!(format!("{scores:#?}")); // ensuring the lat doesn't wrap around s.sort_criteria(vec![AscDesc::Asc(Member::Geo([85., 0.]))]); let (ids, scores) = execute_iterative_and_rtree_returns_the_same(&rtxn, &index, &mut s); insta::assert_snapshot!(format!("{ids:?}"), @"[1, 0, 3, 4, 2]"); insta::assert_snapshot!(format!("{scores:#?}")); s.sort_criteria(vec![AscDesc::Asc(Member::Geo([-85., 0.]))]); let (ids, scores) = execute_iterative_and_rtree_returns_the_same(&rtxn, &index, &mut s); insta::assert_snapshot!(format!("{ids:?}"), @"[2, 0, 3, 4, 1]"); insta::assert_snapshot!(format!("{scores:#?}")); // ensuring the lng does wrap around s.sort_criteria(vec![AscDesc::Asc(Member::Geo([0., 175.]))]); let (ids, scores) = execute_iterative_and_rtree_returns_the_same(&rtxn, &index, &mut s); insta::assert_snapshot!(format!("{ids:?}"), @"[3, 4, 2, 1, 0]"); insta::assert_snapshot!(format!("{scores:#?}")); s.sort_criteria(vec![AscDesc::Asc(Member::Geo([0., -175.]))]); let (ids, scores) = execute_iterative_and_rtree_returns_the_same(&rtxn, &index, &mut s); insta::assert_snapshot!(format!("{ids:?}"), @"[4, 3, 2, 1, 0]"); insta::assert_snapshot!(format!("{scores:#?}")); // --- desc s.sort_criteria(vec![AscDesc::Desc(Member::Geo([0., 0.]))]); let (ids, scores) = execute_iterative_and_rtree_returns_the_same(&rtxn, &index, &mut s); insta::assert_snapshot!(format!("{ids:?}"), @"[4, 3, 2, 1, 0]"); insta::assert_snapshot!(format!("{scores:#?}")); // ensuring the lat doesn't wrap around s.sort_criteria(vec![AscDesc::Desc(Member::Geo([85., 0.]))]); let (ids, scores) = execute_iterative_and_rtree_returns_the_same(&rtxn, &index, &mut s); insta::assert_snapshot!(format!("{ids:?}"), @"[2, 4, 3, 0, 1]"); insta::assert_snapshot!(format!("{scores:#?}")); s.sort_criteria(vec![AscDesc::Desc(Member::Geo([-85., 0.]))]); let (ids, scores) = execute_iterative_and_rtree_returns_the_same(&rtxn, &index, &mut s); insta::assert_snapshot!(format!("{ids:?}"), @"[1, 4, 3, 0, 2]"); insta::assert_snapshot!(format!("{scores:#?}")); // ensuring the lng does wrap around s.sort_criteria(vec![AscDesc::Desc(Member::Geo([0., 175.]))]); let (ids, scores) = execute_iterative_and_rtree_returns_the_same(&rtxn, &index, &mut s); insta::assert_snapshot!(format!("{ids:?}"), @"[0, 1, 2, 4, 3]"); insta::assert_snapshot!(format!("{scores:#?}")); s.sort_criteria(vec![AscDesc::Desc(Member::Geo([0., -175.]))]); let (ids, scores) = execute_iterative_and_rtree_returns_the_same(&rtxn, &index, &mut s); insta::assert_snapshot!(format!("{ids:?}"), @"[0, 1, 2, 3, 4]"); insta::assert_snapshot!(format!("{scores:#?}")); } #[test] fn geo_sort_mixed_with_words() { let index = create_index(); index .add_documents(documents!([ { "id": 0, "doggo": "jean", "_geo": { "lat": 0, "lng": 0 } }, { "id": 1, "doggo": "intel", "_geo": { "lat": 88, "lng": 0 } }, { "id": 2, "doggo": "jean bob", "_geo": { "lat": -89, "lng": 0 } }, { "id": 3, "doggo": "jean michel", "_geo": { "lat": 0, "lng": 178 } }, { "id": 4, "doggo": "bob marley", "_geo": { "lat": 0, "lng": -179 } }, ])) .unwrap(); let rtxn = index.read_txn().unwrap(); let mut s = Search::new(&rtxn, &index); s.scoring_strategy(crate::score_details::ScoringStrategy::Detailed); s.sort_criteria(vec![AscDesc::Asc(Member::Geo([0., 0.]))]); s.query("jean"); let (ids, scores) = execute_iterative_and_rtree_returns_the_same(&rtxn, &index, &mut s); insta::assert_snapshot!(format!("{ids:?}"), @"[0, 2, 3]"); insta::assert_snapshot!(format!("{scores:#?}")); s.query("bob"); let (ids, scores) = execute_iterative_and_rtree_returns_the_same(&rtxn, &index, &mut s); insta::assert_snapshot!(format!("{ids:?}"), @"[2, 4]"); insta::assert_snapshot!(format!("{scores:#?}"), @r###" [ [ Words( Words { matching_words: 1, max_matching_words: 1, }, ), GeoSort( GeoSort { target_point: [ 0.0, 0.0, ], ascending: true, value: Some( [ -89.0, 0.0, ], ), }, ), ], [ Words( Words { matching_words: 1, max_matching_words: 1, }, ), GeoSort( GeoSort { target_point: [ 0.0, 0.0, ], ascending: true, value: Some( [ 0.0, -179.0, ], ), }, ), ], ] "###); s.query("intel"); let (ids, scores) = execute_iterative_and_rtree_returns_the_same(&rtxn, &index, &mut s); insta::assert_snapshot!(format!("{ids:?}"), @"[1]"); insta::assert_snapshot!(format!("{scores:#?}"), @r###" [ [ Words( Words { matching_words: 1, max_matching_words: 1, }, ), GeoSort( GeoSort { target_point: [ 0.0, 0.0, ], ascending: true, value: Some( [ 88.0, 0.0, ], ), }, ), ], ] "###); } #[test] fn geo_sort_without_any_geo_faceted_documents() { let index = create_index(); index .add_documents(documents!([ { "id": 0, "doggo": "jean" }, { "id": 1, "doggo": "intel" }, { "id": 2, "doggo": "jean bob" }, { "id": 3, "doggo": "jean michel" }, { "id": 4, "doggo": "bob marley" }, ])) .unwrap(); let rtxn = index.read_txn().unwrap(); let mut s = Search::new(&rtxn, &index); s.scoring_strategy(crate::score_details::ScoringStrategy::Detailed); s.sort_criteria(vec![AscDesc::Asc(Member::Geo([0., 0.]))]); s.query("jean"); let (ids, scores) = execute_iterative_and_rtree_returns_the_same(&rtxn, &index, &mut s); insta::assert_snapshot!(format!("{ids:?}"), @"[0, 2, 3]"); insta::assert_snapshot!(format!("{scores:#?}")); }