1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
use crate::search::*;
use crate::util::*;
use serde::Serialize;

/// Queries documents that contain fields indexed using the `shape` type.
///
/// Requires the [`shape` Mapping](https://www.elastic.co/guide/en/elasticsearch/reference/current/shape.html).
///
/// <https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-shape-query.html>
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(remote = "Self")]
pub struct ShapeLookupQuery {
    #[serde(skip)]
    field: String,

    #[serde(skip)]
    shape: Shape,

    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
    ignore_unmapped: Option<bool>,

    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
    boost: Option<f32>,

    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
    _name: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Serialize)]
struct Shape {
    indexed_shape: IndexedShape,

    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
    relation: Option<SpatialRelation>,
}

#[derive(Debug, Clone, PartialEq, Serialize)]
struct IndexedShape {
    id: String,

    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
    index: Option<String>,

    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
    path: Option<String>,

    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
    routing: Option<String>,
}

impl Query {
    /// Creates an instance of [`ShapeLookupQuery`]
    ///
    /// - `field` - Field you wish to search
    /// - `id` - The ID of the document that containing the pre-indexed shape
    pub fn shape_lookup<S, T>(field: S, id: T) -> ShapeLookupQuery
    where
        S: ToString,
        T: ToString,
    {
        ShapeLookupQuery {
            field: field.to_string(),
            shape: Shape {
                indexed_shape: IndexedShape {
                    id: id.to_string(),
                    index: None,
                    path: None,
                    routing: None,
                },
                relation: None,
            },
            ignore_unmapped: None,
            boost: None,
            _name: None,
        }
    }
}

impl ShapeLookupQuery {
    /// Name of the index where the pre-indexed shape is. Defaults to shapes
    pub fn index<S>(mut self, index: S) -> Self
    where
        S: ToString,
    {
        self.shape.indexed_shape.index = Some(index.to_string());
        self
    }

    /// The field specified as path containing the pre-indexed shape. Defaults to shape
    pub fn path<S>(mut self, path: S) -> Self
    where
        S: ToString,
    {
        self.shape.indexed_shape.path = Some(path.to_string());
        self
    }

    /// The routing of the shape document
    pub fn routing<S>(mut self, routing: S) -> Self
    where
        S: ToString,
    {
        self.shape.indexed_shape.routing = Some(routing.to_string());
        self
    }

    /// The [shape strategy](https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-shape.html#spatial-strategy)
    /// mapping parameter determines which spatial relation operators may be
    /// used at search time.
    pub fn relation(mut self, relation: SpatialRelation) -> Self {
        self.shape.relation = Some(relation);
        self
    }

    /// When set to true the `ignore_unmapped` option will ignore an unmapped
    /// field and will not match any documents for this query. This can be
    /// useful when querying multiple indexes which might have different
    /// mappings. When set to `false` (the default value) the query will throw
    /// an exception if the field is not mapped.
    pub fn ignore_unmapped(mut self, ignore_unmapped: bool) -> Self {
        self.ignore_unmapped = Some(ignore_unmapped);
        self
    }

    add_boost_and_name!();
}

impl ShouldSkip for ShapeLookupQuery {}

serialize_with_root_key_value_pair!("shape": ShapeLookupQuery, field, shape);

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_serialization() {
        assert_serialize_query(
            Query::shape_lookup("pin.location", "id"),
            json!({
                "shape": {
                    "pin.location": {
                        "indexed_shape": {
                            "id": "id",
                        }
                    },
                }
            }),
        );

        assert_serialize_query(
            Query::shape_lookup("pin.location", "id")
                .boost(2)
                .name("test")
                .ignore_unmapped(true)
                .relation(SpatialRelation::Within)
                .routing("routing")
                .index("index")
                .path("path"),
            json!({
                "shape": {
                    "_name": "test",
                    "boost": 2.0,
                    "ignore_unmapped": true,
                    "pin.location": {
                        "indexed_shape": {
                            "id": "id",
                            "index": "index",
                            "path": "path",
                            "routing": "routing"
                        },
                        "relation": "WITHIN"
                    },
                }
            }),
        );
    }
}