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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
use crate::search::*;
use crate::util::*;

/// Wraps another query to search
/// [nested](https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html)
/// fields.
///
/// The `nested` query searches nested field objects as if they were indexed as
/// separate documents. If an object matches the search, the `nested` query
/// returns the root parent document.
///
/// To create nested query:
/// ```
/// # use elasticsearch_dsl::queries::*;
/// # use elasticsearch_dsl::queries::params::*;
/// # let query =
/// Query::nested("vehicles", Query::term("vehicles.license", "ABC123"))
///     .boost(3)
///     .name("test");
/// ```
/// To create multi-level nested query:
/// ```
/// # use elasticsearch_dsl::queries::*;
/// # use elasticsearch_dsl::queries::params::*;
/// # let query =
/// Query::nested("driver", Query::nested("driver.vehicle", Query::term("driver.vehicle.make", "toyota")))
///     .boost(3)
///     .name("test");
/// ```
/// <https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-nested-query.html>
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(remote = "Self")]
pub struct NestedQuery {
    path: String,

    query: Box<Query>,

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

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

    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
    inner_hits: Option<Box<InnerHits>>,

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

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

impl Query {
    /// Creates an instance of [`NestedQuery`]
    ///
    /// - `path` - Path to the nested object you wish to search.
    /// - `query` - Query you wish to run on nested objects in the `path`. If an object
    /// matches the search, the `nested` query returns the root parent document.<br/>
    /// You can search nested fields using dot notation that includes the
    /// complete path, such as `obj1.name`.<br/>
    /// Multi-level nesting is automatically supported, and detected,
    /// resulting in an inner nested query to automatically match the relevant
    /// nesting level, rather than root, if it exists within another nested
    /// query.<br/>
    /// Multi-level nested queries are also supported.
    pub fn nested<T, U>(path: T, query: U) -> NestedQuery
    where
        T: ToString,
        U: Into<Query>,
    {
        NestedQuery {
            path: path.to_string(),
            query: Box::new(query.into()),
            score_mode: None,
            ignore_unmapped: None,
            inner_hits: None,
            boost: None,
            _name: None,
        }
    }
}

impl NestedQuery {
    /// Indicates how scores for matching child objects affect the root parent
    /// document’s
    /// [relevance score](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html#relevance-scores).
    pub fn score_mode(mut self, score_mode: NestedQueryScoreMode) -> Self {
        self.score_mode = Some(score_mode);
        self
    }

    /// Indicates whether to ignore an unmapped `path` and not return any
    /// documents instead of an error. Defaults to `false`.
    ///
    /// If `false`, Elasticsearch returns an error if the `path` is an unmapped
    /// field.
    ///
    /// You can use this parameter to query multiple indices that may not
    /// contain the field `path`.
    pub fn ignore_unmapped(mut self, ignore_unmapped: bool) -> Self {
        self.ignore_unmapped = Some(ignore_unmapped);
        self
    }

    /// The [parent-join](https://www.elastic.co/guide/en/elasticsearch/reference/current/parent-join.html)
    /// and [nested](https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html)
    /// features allow the return of documents that have matches in a different scope. In the
    /// parent/child case, parent documents are returned based on matches in child documents or
    /// child documents are returned based on matches in parent documents. In the nested case,
    /// documents are returned based on matches in nested inner objects.
    ///
    /// In both cases, the actual matches in the different scopes that caused a document to be
    /// returned are hidden. In many cases, it’s very useful to know which inner nested objects
    /// (in the case of nested) or children/parent documents (in the case of parent/child) caused
    /// certain information to be returned. The inner hits feature can be used for this. This
    /// feature returns per search hit in the search response additional nested hits that caused a
    /// search hit to match in a different scope.
    ///
    /// Inner hits can be used by defining an `inner_hits` definition on a `nested`, `has_child`
    /// or `has_parent` query and filter.
    ///
    /// <https://www.elastic.co/guide/en/elasticsearch/reference/current/inner-hits.html>
    pub fn inner_hits(mut self, inner_hits: InnerHits) -> Self {
        self.inner_hits = Some(Box::new(inner_hits));
        self
    }

    add_boost_and_name!();
}

impl ShouldSkip for NestedQuery {
    fn should_skip(&self) -> bool {
        self.query.should_skip()
    }
}

serialize_with_root!("nested": NestedQuery);

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

    #[test]
    fn serialization() {
        assert_serialize_query(
            Query::nested("vehicles", Query::term("vehicles.license", "ABC123")),
            json!({
                "nested": {
                    "path": "vehicles",
                    "query": {
                        "term": {
                            "vehicles.license": {
                                "value": "ABC123"
                            }
                        }
                    }
                }
            }),
        );

        assert_serialize_query(
            Query::nested("vehicles", Query::term("vehicles.license", "ABC123"))
                .boost(3)
                .name("test"),
            json!({
                "nested": {
                    "path": "vehicles",
                    "query": {
                        "term": {
                            "vehicles.license": {
                                "value": "ABC123"
                            }
                        }
                    },
                    "boost": 3.0,
                    "_name": "test",
                }
            }),
        );

        assert_serialize_query(
            Query::nested(
                "driver",
                Query::nested(
                    "driver.vehicles",
                    Query::term("driver.vehicles.make.keyword", "toyota"),
                ),
            ),
            json!({
                "nested": {
                    "path": "driver",
                    "query": {
                        "nested": {
                            "path": "driver.vehicles",
                            "query": {
                                "term": {
                                    "driver.vehicles.make.keyword": {
                                        "value": "toyota"
                                    }
                                }
                            }
                        }
                    }
                }
            }),
        );
    }
}