elasticsearch_dsl/search/response/
search_response.rs

1use super::{ClusterStatistics, HitsMetadata, ShardStatistics, Suggest};
2use crate::{util::ShouldSkip, Map};
3use serde::de::DeserializeOwned;
4use serde_json::Value;
5
6/// Search response
7#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
8pub struct SearchResponse {
9    /// The time that it took Elasticsearch to process the query
10    pub took: u32,
11
12    /// The search has been cancelled and results are partial
13    pub timed_out: bool,
14
15    /// Indicates if search has been terminated early
16    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
17    pub terminated_early: Option<bool>,
18
19    /// Scroll Id
20    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
21    #[serde(rename = "_scroll_id")]
22    pub scroll_id: Option<String>,
23
24    /// Dynamically fetched fields
25    #[serde(skip_serializing_if = "ShouldSkip::should_skip", default)]
26    pub fields: Map<String, Value>,
27
28    /// Point in time Id
29    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
30    pub pit_id: Option<String>,
31
32    /// Number of reduce phases
33    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
34    pub num_reduce_phases: Option<u64>,
35
36    /// Maximum document score. [None] when documents are implicitly sorted
37    /// by a field other than `_score`
38    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
39    pub max_score: Option<f32>,
40
41    /// Number of clusters touched with their states
42    #[serde(skip_serializing_if = "ShouldSkip::should_skip", rename = "_clusters")]
43    pub clusters: Option<ClusterStatistics>,
44
45    /// Number of shards touched with their states
46    #[serde(rename = "_shards")]
47    pub shards: ShardStatistics,
48
49    /// Search hits
50    pub hits: HitsMetadata,
51
52    /// Search aggregations
53    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
54    pub aggregations: Option<Value>,
55
56    /// Suggest response
57    #[serde(skip_serializing_if = "ShouldSkip::should_skip", default)]
58    pub suggest: Map<String, Vec<Suggest>>,
59}
60
61impl SearchResponse {
62    /// A shorthand for retrieving the _source for each hit
63    pub fn documents<T>(&self) -> Result<Vec<T>, serde_json::Error>
64    where
65        T: DeserializeOwned,
66    {
67        self.hits.hits.iter().map(|hit| hit.source()).collect()
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::{
75        CompletionSuggestOption, Hit, PhraseSuggestOption, Source, SuggestOption,
76        TermSuggestOption, TotalHits, TotalHitsRelation,
77    };
78
79    #[test]
80    fn deserializes_successfully() {
81        let json = json!({
82          "took": 6,
83          "timed_out": false,
84          "_shards": {
85            "total": 10,
86            "successful": 5,
87            "skipped": 3,
88            "failed": 2
89          },
90          "hits": {
91            "total": {
92              "value": 10000,
93              "relation": "gte"
94            },
95            "max_score": 1.0,
96            "hits": [
97              {
98                "_index": "_index",
99                "_type": "_doc",
100                "_id": "123",
101                "_score": 1.0
102              }
103            ]
104          },
105          "suggest": {
106            "song-suggest": [
107              {
108                "text": "nir",
109                "offset": 0,
110                "length": 3,
111                "options": [
112                  {
113                    "text": "Nirvana",
114                    "_index": "music",
115                    "_type": "_doc",
116                    "_id": "1",
117                    "_score": 1.0,
118                    "_source": { "suggest": ["Nevermind", "Nirvana"] }
119                  }
120                ]
121              }
122            ],
123            "term#my-first-suggester": [
124              {
125                "text": "some",
126                "offset": 0,
127                "length": 4,
128                "options": []
129              },
130              {
131                "text": "test",
132                "offset": 5,
133                "length": 4,
134                "options": []
135              },
136              {
137                "text": "mssage",
138                "offset": 10,
139                "length": 6,
140                "options": [
141                  {
142                    "text": "message",
143                    "score": 0.8333333,
144                    "freq": 4
145                  }
146                ]
147              }
148            ],
149            "phrase#my-second-suggester": [
150              {
151                "text": "some test mssage",
152                "offset": 0,
153                "length": 16,
154                "options": [
155                  {
156                    "text": "some test message",
157                    "score": 0.030227963
158                  }
159                ]
160              }
161            ]
162          }
163        });
164
165        let actual: SearchResponse = serde_json::from_value(json).unwrap();
166
167        let expected = SearchResponse {
168            took: 6,
169            timed_out: false,
170            shards: ShardStatistics {
171                total: 10,
172                successful: 5,
173                skipped: 3,
174                failed: 2,
175                failures: Default::default(),
176            },
177            hits: HitsMetadata {
178                total: Some(TotalHits {
179                    value: 10_000,
180                    relation: TotalHitsRelation::GreaterThanOrEqualTo,
181                }),
182                max_score: Some(1.0),
183                hits: vec![Hit {
184                    explanation: None,
185                    nested: None,
186                    index: "_index".into(),
187                    id: "123".into(),
188                    score: Some(1.0),
189                    source: Source::from_string("null".to_string()).unwrap(),
190                    highlight: Default::default(),
191                    inner_hits: Default::default(),
192                    matched_queries: Default::default(),
193                    sort: Default::default(),
194                    fields: Default::default(),
195                }],
196            },
197            aggregations: None,
198            terminated_early: None,
199            scroll_id: None,
200            fields: Default::default(),
201            pit_id: None,
202            num_reduce_phases: None,
203            max_score: None,
204            clusters: None,
205            suggest: Map::from([
206                (
207                    "song-suggest".to_string(),
208                    vec![Suggest {
209                        text: "nir".to_string(),
210                        length: 3,
211                        offset: 0,
212                        options: vec![SuggestOption::Completion(CompletionSuggestOption {
213                            text: "Nirvana".to_string(),
214                            index: "music".to_string(),
215                            id: "1".to_string(),
216                            score: 1.0,
217                            source: Some(json!({ "suggest": ["Nevermind", "Nirvana"] })),
218                            contexts: Default::default(),
219                        })],
220                    }],
221                ),
222                (
223                    "term#my-first-suggester".to_string(),
224                    vec![
225                        Suggest {
226                            text: "some".to_string(),
227                            length: 4,
228                            offset: 0,
229                            options: vec![],
230                        },
231                        Suggest {
232                            text: "test".to_string(),
233                            length: 4,
234                            offset: 5,
235                            options: vec![],
236                        },
237                        Suggest {
238                            text: "mssage".to_string(),
239                            length: 6,
240                            offset: 10,
241                            options: vec![SuggestOption::Term(TermSuggestOption {
242                                text: "message".to_string(),
243                                score: 0.8333333,
244                                frequency: 4,
245                            })],
246                        },
247                    ],
248                ),
249                (
250                    "phrase#my-second-suggester".to_string(),
251                    vec![Suggest {
252                        text: "some test mssage".to_string(),
253                        length: 16,
254                        offset: 0,
255                        options: vec![SuggestOption::Phrase(PhraseSuggestOption {
256                            text: "some test message".to_string(),
257                            score: 0.030227963,
258                            collate_match: None,
259                            highlighted: None,
260                        })],
261                    }],
262                ),
263            ]),
264        };
265
266        assert_eq!(actual, expected);
267    }
268
269    #[test]
270    fn parses_documents() {
271        let json = json!({
272          "took": 6,
273          "timed_out": false,
274          "_shards": {
275            "total": 10,
276            "successful": 5,
277            "skipped": 3,
278            "failed": 2
279          },
280          "hits": {
281            "total": {
282              "value": 10000,
283              "relation": "gte"
284            },
285            "max_score": 1.0,
286            "hits": [
287              {
288                "_index": "_index",
289                "_type": "_doc",
290                "_id": "123",
291                "_score": 1.0,
292                "_source": {
293                    "id": 123,
294                    "title": "test",
295                    "user_id": 456,
296                }
297              }
298            ]
299          }
300        });
301
302        #[derive(Debug, PartialEq, Deserialize)]
303        struct Document {
304            id: i32,
305            title: String,
306            user_id: Option<i32>,
307        }
308
309        let subject: SearchResponse = serde_json::from_value(json).unwrap();
310        let subject = subject.documents::<Document>().unwrap();
311
312        let expectation = [Document {
313            id: 123,
314            title: "test".to_string(),
315            user_id: Some(456),
316        }];
317
318        assert_eq!(subject, expectation);
319    }
320}