rs_es/query/
geo.rs

1/*
2 * Copyright 2016-2019 Ben Ashford
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! Geo queries
18
19use serde::ser::{SerializeMap, Serializer};
20use serde::Serialize;
21
22use crate::{
23    json::{serialize_map_optional_kv, MergeSerialize, NoOuter, ShouldSkip},
24    units::{Distance, DistanceType, GeoBox, Location},
25};
26
27use super::{common::FieldBasedQuery, Query};
28
29#[derive(Debug, Serialize)]
30pub enum ShapeOption {
31    #[serde(rename = "shape")]
32    Shape(Shape),
33    #[serde(rename = "indexed_shape")]
34    IndexedShape(IndexedShape),
35    #[cfg(feature = "geo")]
36    #[serde(rename = "shape")]
37    Geojson(geojson::Geometry),
38}
39
40from!(Shape, ShapeOption, Shape);
41from!(IndexedShape, ShapeOption, IndexedShape);
42
43/// GeoShape query
44#[derive(Debug, Serialize)]
45pub struct GeoShapeQuery(FieldBasedQuery<Option<ShapeOption>, NoOuter>);
46
47impl Query {
48    pub fn build_geo_shape<A>(field: A) -> GeoShapeQuery
49    where
50        A: Into<String>,
51    {
52        GeoShapeQuery(FieldBasedQuery::new(field.into(), None, NoOuter))
53    }
54}
55
56impl GeoShapeQuery {
57    pub fn with_shape<A>(mut self, shape: A) -> Self
58    where
59        A: Into<Shape>,
60    {
61        self.0.inner = Some(ShapeOption::Shape(shape.into()));
62        self
63    }
64
65    pub fn with_indexed_shape<A>(mut self, indexed_shape: A) -> Self
66    where
67        A: Into<IndexedShape>,
68    {
69        self.0.inner = Some(ShapeOption::IndexedShape(indexed_shape.into()));
70        self
71    }
72
73    #[cfg(feature = "geo")]
74    /// Use a geojson object as shape.
75    /// Require to enable the `geo` feature.
76    pub fn with_geojson<A>(mut self, shape: A) -> Self
77    where
78        A: Into<geojson::Geometry>,
79    {
80        self.0.inner = Some(ShapeOption::Geojson(shape.into()));
81        self
82    }
83
84    build!(GeoShape);
85}
86
87// Required for GeoShape
88#[derive(Debug, Serialize)]
89pub struct Shape {
90    #[serde(rename = "type")]
91    shape_type: String,
92    coordinates: Vec<(f64, f64)>,
93}
94
95impl Shape {
96    pub fn new<A: Into<String>>(shape_type: A, coordinates: Vec<(f64, f64)>) -> Shape {
97        Shape {
98            shape_type: shape_type.into(),
99            coordinates,
100        }
101    }
102}
103
104#[derive(Debug, Serialize)]
105pub struct IndexedShape {
106    id: String,
107    doc_type: String,
108    index: String,
109    path: String,
110}
111
112impl IndexedShape {
113    pub fn new<A, B, C, D>(id: A, doc_type: B, index: C, path: D) -> IndexedShape
114    where
115        A: Into<String>,
116        B: Into<String>,
117        C: Into<String>,
118        D: Into<String>,
119    {
120        IndexedShape {
121            id: id.into(),
122            doc_type: doc_type.into(),
123            index: index.into(),
124            path: path.into(),
125        }
126    }
127}
128
129/// Geo Bounding Box Query
130#[derive(Debug, Serialize)]
131pub struct GeoBoundingBoxQuery(FieldBasedQuery<GeoBoundingBoxQueryInner, NoOuter>);
132
133#[derive(Debug, Default, Serialize)]
134pub struct GeoBoundingBoxQueryInner {
135    geo_box: GeoBox,
136    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
137    coerce: Option<bool>,
138    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
139    ignore_malformed: Option<bool>,
140    #[serde(skip_serializing_if = "ShouldSkip::should_skip", rename = "type")]
141    filter_type: Option<Type>,
142}
143
144impl Query {
145    pub fn build_geo_bounding_box<A, B>(field: A, geo_box: B) -> GeoBoundingBoxQuery
146    where
147        A: Into<String>,
148        B: Into<GeoBox>,
149    {
150        GeoBoundingBoxQuery(FieldBasedQuery::new(
151            field.into(),
152            GeoBoundingBoxQueryInner {
153                geo_box: geo_box.into(),
154                ..Default::default()
155            },
156            NoOuter,
157        ))
158    }
159}
160
161impl GeoBoundingBoxQuery {
162    add_inner_field!(with_coerce, coerce, bool);
163    add_inner_field!(with_ignore_malformed, ignore_malformed, bool);
164    add_inner_field!(with_type, filter_type, Type);
165
166    build!(GeoBoundingBox);
167}
168
169/// Geo Bounding Box filter type
170#[derive(Debug)]
171pub enum Type {
172    Indexed,
173    Memory,
174}
175
176impl Serialize for Type {
177    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
178    where
179        S: Serializer,
180    {
181        use self::Type::*;
182        match self {
183            Indexed => "indexed",
184            Memory => "memory",
185        }
186        .serialize(serializer)
187    }
188}
189
190/// Geo Distance query
191///
192/// TODO: Specific full unit test for querying with a generated query from here
193#[derive(Debug, Serialize)]
194pub struct GeoDistanceQuery(FieldBasedQuery<Location, GeoDistanceQueryOuter>);
195
196#[derive(Debug, Default)]
197struct GeoDistanceQueryOuter {
198    distance: Distance,
199    distance_type: Option<DistanceType>,
200    optimize_bbox: Option<OptimizeBbox>,
201    coerce: Option<bool>,
202    ignore_malformed: Option<bool>,
203}
204
205impl MergeSerialize for GeoDistanceQueryOuter {
206    fn merge_serialize<S>(&self, serializer: &mut S) -> Result<(), S::Error>
207    where
208        S: SerializeMap,
209    {
210        serializer.serialize_entry("distance", &self.distance)?;
211        serialize_map_optional_kv(serializer, "distance_type", &self.distance_type)?;
212        serialize_map_optional_kv(serializer, "optimize_bbox", &self.optimize_bbox)?;
213        serialize_map_optional_kv(serializer, "coerce", &self.coerce)?;
214        serialize_map_optional_kv(serializer, "ignore_malformed", &self.ignore_malformed)?;
215        Ok(())
216    }
217}
218
219impl Query {
220    pub fn build_geo_distance<A, B, C>(field: A, location: B, distance: C) -> GeoDistanceQuery
221    where
222        A: Into<String>,
223        B: Into<Location>,
224        C: Into<Distance>,
225    {
226        let outer = GeoDistanceQueryOuter {
227            distance: distance.into(),
228            ..Default::default()
229        };
230        GeoDistanceQuery(FieldBasedQuery::new(field.into(), location.into(), outer))
231    }
232}
233
234impl GeoDistanceQuery {
235    add_outer_field!(with_distance_type, distance_type, DistanceType);
236    add_outer_field!(with_optimize_bbox, optimize_bbox, OptimizeBbox);
237    add_outer_field!(with_coerce, coerce, bool);
238    add_outer_field!(with_ignore_malformed, ignore_malformed, bool);
239
240    build!(GeoDistance);
241}
242
243/// Options for `optimize_bbox`
244#[derive(Debug)]
245pub enum OptimizeBbox {
246    Memory,
247    Indexed,
248    None,
249}
250
251impl Serialize for OptimizeBbox {
252    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
253    where
254        S: Serializer,
255    {
256        use self::OptimizeBbox::*;
257        match self {
258            Memory => "memory".serialize(serializer),
259            Indexed => "indexed".serialize(serializer),
260            None => "none".serialize(serializer),
261        }
262    }
263}
264
265/// Geo Polygon query
266#[derive(Debug, Serialize)]
267pub struct GeoPolygonQuery(FieldBasedQuery<GeoPolygonQueryInner, NoOuter>);
268
269#[derive(Debug, Default, Serialize)]
270pub struct GeoPolygonQueryInner {
271    points: Vec<Location>,
272    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
273    coerce: Option<bool>,
274    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
275    ignore_malformed: Option<bool>,
276}
277
278impl Query {
279    pub fn build_geo_polygon<A, B>(field: A, points: B) -> GeoPolygonQuery
280    where
281        A: Into<String>,
282        B: Into<Vec<Location>>,
283    {
284        GeoPolygonQuery(FieldBasedQuery::new(
285            field.into(),
286            GeoPolygonQueryInner {
287                points: points.into(),
288                ..Default::default()
289            },
290            NoOuter,
291        ))
292    }
293}
294
295impl GeoPolygonQuery {
296    add_inner_field!(with_coerce, coerce, bool);
297    add_inner_field!(with_ignore_malformed, ignore_malformed, bool);
298
299    build!(GeoPolygon);
300}
301
302/// Geohash cell query
303#[derive(Debug, Serialize)]
304pub struct GeohashCellQuery(FieldBasedQuery<Location, GeohashCellQueryOuter>);
305
306#[derive(Debug, Default)]
307pub struct GeohashCellQueryOuter {
308    precision: Option<Precision>,
309    neighbors: Option<bool>,
310}
311
312impl MergeSerialize for GeohashCellQueryOuter {
313    fn merge_serialize<S>(&self, serializer: &mut S) -> Result<(), S::Error>
314    where
315        S: SerializeMap,
316    {
317        serialize_map_optional_kv(serializer, "precision", &self.precision)?;
318        serialize_map_optional_kv(serializer, "neighbors", &self.neighbors)?;
319        Ok(())
320    }
321}
322
323impl Query {
324    pub fn build_geohash_cell<A, B>(field: A, location: B) -> GeohashCellQuery
325    where
326        A: Into<String>,
327        B: Into<Location>,
328    {
329        GeohashCellQuery(FieldBasedQuery::new(
330            field.into(),
331            location.into(),
332            Default::default(),
333        ))
334    }
335}
336
337impl GeohashCellQuery {
338    add_outer_field!(with_precision, precision, Precision);
339    add_outer_field!(with_neighbors, neighbors, bool);
340
341    build!(GeohashCell);
342}
343
344#[derive(Debug)]
345pub enum Precision {
346    Geohash(u64),
347    Distance(Distance),
348}
349
350impl Default for Precision {
351    fn default() -> Self {
352        Precision::Distance(Default::default())
353    }
354}
355
356from!(u64, Precision, Geohash);
357from!(Distance, Precision, Distance);
358
359impl Serialize for Precision {
360    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
361    where
362        S: Serializer,
363    {
364        use self::Precision::*;
365        match self {
366            Geohash(precision) => precision.serialize(serializer),
367            Distance(ref dist) => dist.serialize(serializer),
368        }
369    }
370}
371
372#[cfg(test)]
373#[cfg(feature = "geo")]
374pub mod tests {
375    use crate::operations::mapping::{Analysis, MappingOperation, Settings};
376    use crate::operations::search::SearchResult;
377    use crate::query::Query;
378    use crate::tests::{clean_db, make_client};
379    use crate::Client;
380    use serde::{Deserialize, Serialize};
381    use std::collections::HashMap;
382
383    #[derive(Debug, Serialize, Deserialize)]
384    pub struct GeoTestDocument {
385        pub str_field: String,
386        pub geojson_field: geojson::Geometry,
387    }
388
389    impl Default for GeoTestDocument {
390        fn default() -> GeoTestDocument {
391            GeoTestDocument {
392                str_field: "null island".to_owned(),
393                geojson_field: geojson::Geometry::new(geojson::Value::Point(vec![0.0, 0.0])),
394            }
395        }
396    }
397
398    impl GeoTestDocument {
399        pub fn with_str_field(mut self, s: &str) -> GeoTestDocument {
400            self.str_field = s.to_owned();
401            self
402        }
403
404        pub fn with_point(mut self, p: Vec<f64>) -> GeoTestDocument {
405            self.geojson_field = geojson::Geometry::new(geojson::Value::Point(p));
406            self
407        }
408    }
409
410    pub fn setup_test_data(mut client: &mut Client, index_name: &str) {
411        let mut mapping = HashMap::new();
412        let mut doc = HashMap::new();
413        let mut geo_field = HashMap::new();
414        let mut str_field = HashMap::new();
415        str_field.insert("type", "string");
416        geo_field.insert("type", "geo_shape");
417        doc.insert("str_field", str_field);
418        doc.insert("geojson_field", geo_field);
419        mapping.insert("geo_test_type", doc);
420
421        let settings = Settings {
422            number_of_shards: 1,
423            analysis: Analysis {
424                filter: serde_json::json!({}).as_object().unwrap().clone(),
425                analyzer: serde_json::json!({}).as_object().unwrap().clone(),
426                .. Default::default()
427            },
428        };
429
430        // TODO - this fails in many cases (specifically on TravisCI), but we ignore the
431        // failures anyway
432        let _ = client.delete_index(index_name);
433
434        let result = MappingOperation::new(&mut client, index_name)
435            .with_mapping(&mapping)
436            .with_settings(&settings)
437            .send();
438        result.unwrap();
439        let documents = vec![
440            GeoTestDocument::default(),
441            GeoTestDocument::default()
442                .with_str_field("p1")
443                .with_point(vec![1.0, 1.0]),
444            GeoTestDocument::default()
445                .with_str_field("p2")
446                .with_point(vec![5.0, 1.0]),
447        ];
448        for doc in documents.iter() {
449            client
450                .index(index_name, "geo_test_type")
451                .with_doc(doc)
452                .send()
453                .unwrap();
454        }
455        client.refresh().with_indexes(&[index_name]).send().unwrap();
456    }
457
458    #[test]
459    fn test_geoshape_search_point() {
460        let index_name = "test_geoshape_search_point";
461        let mut client = make_client();
462
463        clean_db(&mut client, index_name);
464        setup_test_data(&mut client, index_name);
465
466        let all_results: SearchResult<GeoTestDocument> = client
467            .search_query()
468            .with_indexes(&[index_name])
469            .with_query(
470                &Query::build_geo_shape("geojson_field")
471                    .with_geojson(geojson::Geometry::new(geojson::Value::Point(vec![
472                        0.0, 0.0,
473                    ])))
474                    .build(),
475            )
476            .send()
477            .unwrap();
478        assert_eq!(1, all_results.hits.total);
479    }
480
481    #[test]
482    fn test_geoshape_search_polygon() {
483        let index_name = "test_geoshape_search_polygon";
484        let mut client = make_client();
485
486        clean_db(&mut client, index_name);
487        setup_test_data(&mut client, index_name);
488
489        let all_results: SearchResult<GeoTestDocument> = client
490            .search_query()
491            .with_indexes(&[index_name])
492            .with_query(
493                &Query::build_geo_shape("geojson_field")
494                    .with_geojson(geojson::Geometry::new(geojson::Value::Polygon(vec![vec![
495                        vec![1.0, 1.0],
496                        vec![1.0, -1.0],
497                        vec![-1.0, -1.0],
498                        vec![-1.0, 1.0],
499                        vec![1.0, 1.0],
500                    ]])))
501                    .build(),
502            )
503            .send()
504            .unwrap();
505        assert_eq!(2, all_results.hits.total);
506    }
507}