mirror of
https://github.com/meilisearch/meilisearch.git
synced 2025-09-05 20:26:31 +00:00
Add geo bounding box filter
This commit is contained in:
@ -522,6 +522,26 @@ pub async fn shared_index_with_geo_documents() -> &'static Index<'static, Shared
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn shared_index_geojson_documents() -> &'static Index<'static, Shared> {
|
||||
static INDEX: OnceCell<Index<'static, Shared>> = OnceCell::const_new();
|
||||
INDEX
|
||||
.get_or_init(|| async {
|
||||
// Retrieved from https://gitlab-forge.din.developpement-durable.gouv.fr/pub/geomatique/descartes/d-map/-/blob/main/demo/examples/commons/countries.geojson?ref_type=heads
|
||||
let server = Server::new_shared();
|
||||
let index = server._index("SHARED_GEOJSON_DOCUMENTS").to_shared();
|
||||
let countries = include_str!("../documents/geojson/assets/countries.geojson");
|
||||
let lille = serde_json::from_str::<serde_json::Value>(countries).unwrap();
|
||||
let (response, _code) = index._add_documents(Value(lille), Some("name")).await;
|
||||
server.wait_task(response.uid()).await.succeeded();
|
||||
|
||||
let (response, _code) =
|
||||
index._update_settings(json!({"filterableAttributes": ["_geojson"]})).await;
|
||||
server.wait_task(response.uid()).await.succeeded();
|
||||
index
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn shared_index_for_fragments() -> Index<'static, Shared> {
|
||||
static INDEX: OnceCell<(Server<Shared>, String)> = OnceCell::const_new();
|
||||
let (server, uid) = INDEX
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,4 +1,7 @@
|
||||
use crate::{common::Server, json};
|
||||
use crate::{
|
||||
common::{shared_index_geojson_documents, Server},
|
||||
json,
|
||||
};
|
||||
use meili_snap::{json_string, snapshot};
|
||||
|
||||
const LILLE: &str = include_str!("assets/lille.geojson");
|
||||
@ -281,3 +284,99 @@ async fn partial_update_geojson() {
|
||||
index.search_get("?filter=_geoPolygon([0.9,0.9],[2,0.9],[2,2],[0.9,2])").await;
|
||||
assert_eq!(response.get("hits").unwrap().as_array().unwrap().len(), 0);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn geo_bounding_box() {
|
||||
let index = shared_index_geojson_documents().await;
|
||||
|
||||
// The bounding box is a polygon over middle Europe
|
||||
let (response, code) =
|
||||
index.search_get("?filter=_geoBoundingBox([21.43443989912143,50.53987503447863],[0.54979129195425,43.76393151539099])&attributesToRetrieve=name").await;
|
||||
snapshot!(code, @"200 OK");
|
||||
snapshot!(response, @r#"
|
||||
{
|
||||
"hits": [
|
||||
{
|
||||
"name": "Austria"
|
||||
},
|
||||
{
|
||||
"name": "Belgium"
|
||||
},
|
||||
{
|
||||
"name": "Bosnia_and_Herzegovina"
|
||||
},
|
||||
{
|
||||
"name": "Switzerland"
|
||||
},
|
||||
{
|
||||
"name": "Czech_Republic"
|
||||
},
|
||||
{
|
||||
"name": "Germany"
|
||||
},
|
||||
{
|
||||
"name": "France"
|
||||
},
|
||||
{
|
||||
"name": "Croatia"
|
||||
},
|
||||
{
|
||||
"name": "Hungary"
|
||||
},
|
||||
{
|
||||
"name": "Italy"
|
||||
},
|
||||
{
|
||||
"name": "Luxembourg"
|
||||
},
|
||||
{
|
||||
"name": "Netherlands"
|
||||
},
|
||||
{
|
||||
"name": "Poland"
|
||||
},
|
||||
{
|
||||
"name": "Romania"
|
||||
},
|
||||
{
|
||||
"name": "Republic_of_Serbia"
|
||||
},
|
||||
{
|
||||
"name": "Slovakia"
|
||||
},
|
||||
{
|
||||
"name": "Slovenia"
|
||||
}
|
||||
],
|
||||
"query": "",
|
||||
"processingTimeMs": "[duration]",
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"estimatedTotalHits": 17
|
||||
}
|
||||
"#);
|
||||
|
||||
// Between Russia and Alaska
|
||||
// WARNING: This test doesn't pass, the countries are those I imagine being found but maybe there is also Canada or something
|
||||
let (response, code) = index
|
||||
.search_get("?filter=_geoBoundingBox([70,-148],[63,152])&attributesToRetrieve=name")
|
||||
.await;
|
||||
snapshot!(code, @"200 OK");
|
||||
snapshot!(response, @r#"
|
||||
{
|
||||
"hits": [
|
||||
{
|
||||
"name": "Russia"
|
||||
},
|
||||
{
|
||||
"name": "United_States_of_America"
|
||||
}
|
||||
],
|
||||
"query": "",
|
||||
"processingTimeMs": "[duration]",
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"estimatedTotalHits": 2
|
||||
}
|
||||
"#);
|
||||
}
|
||||
|
@ -1031,7 +1031,7 @@ impl Index {
|
||||
}
|
||||
|
||||
/// Returns true if the geo sorting feature is enabled.
|
||||
pub fn is_geojson_enabled(&self, rtxn: &RoTxn<'_>) -> Result<bool> {
|
||||
pub fn is_geojson_filtering_enabled(&self, rtxn: &RoTxn<'_>) -> Result<bool> {
|
||||
let geojson_filter =
|
||||
self.filterable_attributes_rules(rtxn)?.iter().any(|field| field.has_geojson());
|
||||
Ok(geojson_filter)
|
||||
|
@ -51,14 +51,17 @@ impl Display for BadGeoError {
|
||||
}
|
||||
Self::Lat(lat) => write!(
|
||||
f,
|
||||
"Bad latitude `{}`. Latitude must be contained between -90 and 90 degrees. ",
|
||||
"Bad latitude `{}`. Latitude must be contained between -90 and 90 degrees.",
|
||||
lat
|
||||
),
|
||||
Self::Lng(lng) => write!(
|
||||
f,
|
||||
"Bad longitude `{}`. Longitude must be contained between -180 and 180 degrees. ",
|
||||
lng
|
||||
),
|
||||
Self::Lng(lng) => {
|
||||
let normalized = (lng + 180.0).rem_euclid(360.0) - 180.0;
|
||||
write!(
|
||||
f,
|
||||
"Bad longitude `{}`. Longitude must be contained between -180 and 180 degrees. Hint: try using `{normalized}` instead.",
|
||||
lng
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -686,39 +689,42 @@ impl<'a> Filter<'a> {
|
||||
}
|
||||
}
|
||||
FilterCondition::GeoBoundingBox { top_right_point, bottom_left_point } => {
|
||||
if index.is_geo_filtering_enabled(rtxn)? {
|
||||
let top_right: [f64; 2] = [
|
||||
top_right_point[0].parse_finite_float()?,
|
||||
top_right_point[1].parse_finite_float()?,
|
||||
];
|
||||
let bottom_left: [f64; 2] = [
|
||||
bottom_left_point[0].parse_finite_float()?,
|
||||
bottom_left_point[1].parse_finite_float()?,
|
||||
];
|
||||
if !(-90.0..=90.0).contains(&top_right[0]) {
|
||||
return Err(
|
||||
top_right_point[0].as_external_error(BadGeoError::Lat(top_right[0]))
|
||||
)?;
|
||||
}
|
||||
if !(-180.0..=180.0).contains(&top_right[1]) {
|
||||
return Err(
|
||||
top_right_point[1].as_external_error(BadGeoError::Lng(top_right[1]))
|
||||
)?;
|
||||
}
|
||||
if !(-90.0..=90.0).contains(&bottom_left[0]) {
|
||||
return Err(bottom_left_point[0]
|
||||
.as_external_error(BadGeoError::Lat(bottom_left[0])))?;
|
||||
}
|
||||
if !(-180.0..=180.0).contains(&bottom_left[1]) {
|
||||
return Err(bottom_left_point[1]
|
||||
.as_external_error(BadGeoError::Lng(bottom_left[1])))?;
|
||||
}
|
||||
if top_right[0] < bottom_left[0] {
|
||||
return Err(bottom_left_point[1].as_external_error(
|
||||
BadGeoError::BoundingBoxTopIsBelowBottom(top_right[0], bottom_left[0]),
|
||||
))?;
|
||||
}
|
||||
let top_right: [f64; 2] = [
|
||||
top_right_point[0].parse_finite_float()?,
|
||||
top_right_point[1].parse_finite_float()?,
|
||||
];
|
||||
let bottom_left: [f64; 2] = [
|
||||
bottom_left_point[0].parse_finite_float()?,
|
||||
bottom_left_point[1].parse_finite_float()?,
|
||||
];
|
||||
if !(-90.0..=90.0).contains(&top_right[0]) {
|
||||
return Err(
|
||||
top_right_point[0].as_external_error(BadGeoError::Lat(top_right[0]))
|
||||
)?;
|
||||
}
|
||||
if !(-180.0..=180.0).contains(&top_right[1]) {
|
||||
return Err(
|
||||
top_right_point[1].as_external_error(BadGeoError::Lng(top_right[1]))
|
||||
)?;
|
||||
}
|
||||
if !(-90.0..=90.0).contains(&bottom_left[0]) {
|
||||
return Err(
|
||||
bottom_left_point[0].as_external_error(BadGeoError::Lat(bottom_left[0]))
|
||||
)?;
|
||||
}
|
||||
if !(-180.0..=180.0).contains(&bottom_left[1]) {
|
||||
return Err(
|
||||
bottom_left_point[1].as_external_error(BadGeoError::Lng(bottom_left[1]))
|
||||
)?;
|
||||
}
|
||||
if top_right[0] < bottom_left[0] {
|
||||
return Err(bottom_left_point[1].as_external_error(
|
||||
BadGeoError::BoundingBoxTopIsBelowBottom(top_right[0], bottom_left[0]),
|
||||
))?;
|
||||
}
|
||||
|
||||
let mut r1 = None;
|
||||
if index.is_geo_filtering_enabled(rtxn)? {
|
||||
// Instead of writing a custom `GeoBoundingBox` filter we're simply going to re-use the range
|
||||
// filter to create the following filter;
|
||||
// `_geo.lat {top_right[0]} TO {bottom_left[0]} AND _geo.lng {top_right[1]} TO {bottom_left[1]}`
|
||||
@ -813,9 +819,34 @@ impl<'a> Filter<'a> {
|
||||
)?
|
||||
};
|
||||
|
||||
Ok(selected_lat & selected_lng)
|
||||
} else {
|
||||
Err(top_right_point[0].as_external_error(
|
||||
r1 = Some(selected_lat & selected_lng);
|
||||
}
|
||||
|
||||
let mut r2 = None;
|
||||
if index.is_geojson_filtering_enabled(rtxn)? {
|
||||
let polygon = geo_types::Polygon::new(
|
||||
geo_types::LineString(vec![
|
||||
geo_types::Coord { x: top_right[0], y: top_right[1] },
|
||||
geo_types::Coord { x: bottom_left[0], y: top_right[1] },
|
||||
geo_types::Coord { x: bottom_left[0], y: bottom_left[1] },
|
||||
geo_types::Coord { x: top_right[0], y: bottom_left[1] },
|
||||
]),
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
let result = index
|
||||
.cellulite
|
||||
.in_shape(rtxn, &polygon, &mut |_| ())
|
||||
.map_err(InternalError::CelluliteError)?;
|
||||
|
||||
r2 = Some(RoaringBitmap::from_iter(result)); // TODO: Remove once we update roaring
|
||||
}
|
||||
|
||||
match (r1, r2) {
|
||||
(Some(r1), Some(r2)) => Ok(r1 | r2),
|
||||
(Some(r1), None) => Ok(r1),
|
||||
(None, Some(r2)) => Ok(r2),
|
||||
(None, None) => Err(top_right_point[0].as_external_error(
|
||||
FilterError::AttributeNotFilterable {
|
||||
attribute: RESERVED_GEO_FIELD_NAME,
|
||||
filterable_patterns: filtered_matching_patterns(
|
||||
@ -823,11 +854,11 @@ impl<'a> Filter<'a> {
|
||||
&|features| features.is_filterable(),
|
||||
),
|
||||
},
|
||||
))?
|
||||
))?,
|
||||
}
|
||||
}
|
||||
FilterCondition::GeoPolygon { points } => {
|
||||
if index.is_geojson_enabled(rtxn)? {
|
||||
if index.is_geojson_filtering_enabled(rtxn)? {
|
||||
let polygon = geo_types::Polygon::new(
|
||||
geo_types::LineString(
|
||||
points
|
||||
@ -846,8 +877,9 @@ impl<'a> Filter<'a> {
|
||||
.cellulite
|
||||
.in_shape(rtxn, &polygon, &mut |_| ())
|
||||
.map_err(InternalError::CelluliteError)?;
|
||||
// TODO: Remove once we update roaring
|
||||
let result = roaring::RoaringBitmap::from_iter(result);
|
||||
|
||||
let result = roaring::RoaringBitmap::from_iter(result); // TODO: Remove once we update roaring
|
||||
|
||||
Ok(result)
|
||||
} else {
|
||||
Err(points[0][0].as_external_error(FilterError::AttributeNotFilterable {
|
||||
|
@ -31,7 +31,7 @@ impl GeoJsonExtractor {
|
||||
index: &Index,
|
||||
grenad_parameters: GrenadParameters,
|
||||
) -> Result<Option<Self>> {
|
||||
if index.is_geojson_enabled(rtxn)? {
|
||||
if index.is_geojson_filtering_enabled(rtxn)? {
|
||||
Ok(Some(GeoJsonExtractor { grenad_parameters }))
|
||||
} else {
|
||||
Ok(None)
|
||||
|
Reference in New Issue
Block a user