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
|
.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> {
|
pub async fn shared_index_for_fragments() -> Index<'static, Shared> {
|
||||||
static INDEX: OnceCell<(Server<Shared>, String)> = OnceCell::const_new();
|
static INDEX: OnceCell<(Server<Shared>, String)> = OnceCell::const_new();
|
||||||
let (server, uid) = INDEX
|
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};
|
use meili_snap::{json_string, snapshot};
|
||||||
|
|
||||||
const LILLE: &str = include_str!("assets/lille.geojson");
|
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;
|
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);
|
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.
|
/// 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 =
|
let geojson_filter =
|
||||||
self.filterable_attributes_rules(rtxn)?.iter().any(|field| field.has_geojson());
|
self.filterable_attributes_rules(rtxn)?.iter().any(|field| field.has_geojson());
|
||||||
Ok(geojson_filter)
|
Ok(geojson_filter)
|
||||||
|
@ -51,14 +51,17 @@ impl Display for BadGeoError {
|
|||||||
}
|
}
|
||||||
Self::Lat(lat) => write!(
|
Self::Lat(lat) => write!(
|
||||||
f,
|
f,
|
||||||
"Bad latitude `{}`. Latitude must be contained between -90 and 90 degrees. ",
|
"Bad latitude `{}`. Latitude must be contained between -90 and 90 degrees.",
|
||||||
lat
|
lat
|
||||||
),
|
),
|
||||||
Self::Lng(lng) => write!(
|
Self::Lng(lng) => {
|
||||||
|
let normalized = (lng + 180.0).rem_euclid(360.0) - 180.0;
|
||||||
|
write!(
|
||||||
f,
|
f,
|
||||||
"Bad longitude `{}`. Longitude must be contained between -180 and 180 degrees. ",
|
"Bad longitude `{}`. Longitude must be contained between -180 and 180 degrees. Hint: try using `{normalized}` instead.",
|
||||||
lng
|
lng
|
||||||
),
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -686,7 +689,6 @@ impl<'a> Filter<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
FilterCondition::GeoBoundingBox { top_right_point, bottom_left_point } => {
|
FilterCondition::GeoBoundingBox { top_right_point, bottom_left_point } => {
|
||||||
if index.is_geo_filtering_enabled(rtxn)? {
|
|
||||||
let top_right: [f64; 2] = [
|
let top_right: [f64; 2] = [
|
||||||
top_right_point[0].parse_finite_float()?,
|
top_right_point[0].parse_finite_float()?,
|
||||||
top_right_point[1].parse_finite_float()?,
|
top_right_point[1].parse_finite_float()?,
|
||||||
@ -706,12 +708,14 @@ impl<'a> Filter<'a> {
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
if !(-90.0..=90.0).contains(&bottom_left[0]) {
|
if !(-90.0..=90.0).contains(&bottom_left[0]) {
|
||||||
return Err(bottom_left_point[0]
|
return Err(
|
||||||
.as_external_error(BadGeoError::Lat(bottom_left[0])))?;
|
bottom_left_point[0].as_external_error(BadGeoError::Lat(bottom_left[0]))
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
if !(-180.0..=180.0).contains(&bottom_left[1]) {
|
if !(-180.0..=180.0).contains(&bottom_left[1]) {
|
||||||
return Err(bottom_left_point[1]
|
return Err(
|
||||||
.as_external_error(BadGeoError::Lng(bottom_left[1])))?;
|
bottom_left_point[1].as_external_error(BadGeoError::Lng(bottom_left[1]))
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
if top_right[0] < bottom_left[0] {
|
if top_right[0] < bottom_left[0] {
|
||||||
return Err(bottom_left_point[1].as_external_error(
|
return Err(bottom_left_point[1].as_external_error(
|
||||||
@ -719,6 +723,8 @@ impl<'a> Filter<'a> {
|
|||||||
))?;
|
))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Instead of writing a custom `GeoBoundingBox` filter we're simply going to re-use the range
|
||||||
// filter to create the following filter;
|
// 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]}`
|
// `_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)
|
r1 = Some(selected_lat & selected_lng);
|
||||||
} else {
|
}
|
||||||
Err(top_right_point[0].as_external_error(
|
|
||||||
|
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 {
|
FilterError::AttributeNotFilterable {
|
||||||
attribute: RESERVED_GEO_FIELD_NAME,
|
attribute: RESERVED_GEO_FIELD_NAME,
|
||||||
filterable_patterns: filtered_matching_patterns(
|
filterable_patterns: filtered_matching_patterns(
|
||||||
@ -823,11 +854,11 @@ impl<'a> Filter<'a> {
|
|||||||
&|features| features.is_filterable(),
|
&|features| features.is_filterable(),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
))?
|
))?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FilterCondition::GeoPolygon { points } => {
|
FilterCondition::GeoPolygon { points } => {
|
||||||
if index.is_geojson_enabled(rtxn)? {
|
if index.is_geojson_filtering_enabled(rtxn)? {
|
||||||
let polygon = geo_types::Polygon::new(
|
let polygon = geo_types::Polygon::new(
|
||||||
geo_types::LineString(
|
geo_types::LineString(
|
||||||
points
|
points
|
||||||
@ -846,8 +877,9 @@ impl<'a> Filter<'a> {
|
|||||||
.cellulite
|
.cellulite
|
||||||
.in_shape(rtxn, &polygon, &mut |_| ())
|
.in_shape(rtxn, &polygon, &mut |_| ())
|
||||||
.map_err(InternalError::CelluliteError)?;
|
.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)
|
Ok(result)
|
||||||
} else {
|
} else {
|
||||||
Err(points[0][0].as_external_error(FilterError::AttributeNotFilterable {
|
Err(points[0][0].as_external_error(FilterError::AttributeNotFilterable {
|
||||||
|
@ -31,7 +31,7 @@ impl GeoJsonExtractor {
|
|||||||
index: &Index,
|
index: &Index,
|
||||||
grenad_parameters: GrenadParameters,
|
grenad_parameters: GrenadParameters,
|
||||||
) -> Result<Option<Self>> {
|
) -> Result<Option<Self>> {
|
||||||
if index.is_geojson_enabled(rtxn)? {
|
if index.is_geojson_filtering_enabled(rtxn)? {
|
||||||
Ok(Some(GeoJsonExtractor { grenad_parameters }))
|
Ok(Some(GeoJsonExtractor { grenad_parameters }))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
Reference in New Issue
Block a user