1use super::{ClusterStatistics, HitsMetadata, ShardStatistics, Suggest};
2use crate::{util::ShouldSkip, Map};
3use serde::de::DeserializeOwned;
4use serde_json::Value;
5
6#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
8pub struct SearchResponse {
9 pub took: u32,
11
12 pub timed_out: bool,
14
15 #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
17 pub terminated_early: Option<bool>,
18
19 #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
21 #[serde(rename = "_scroll_id")]
22 pub scroll_id: Option<String>,
23
24 #[serde(skip_serializing_if = "ShouldSkip::should_skip", default)]
26 pub fields: Map<String, Value>,
27
28 #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
30 pub pit_id: Option<String>,
31
32 #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
34 pub num_reduce_phases: Option<u64>,
35
36 #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
39 pub max_score: Option<f32>,
40
41 #[serde(skip_serializing_if = "ShouldSkip::should_skip", rename = "_clusters")]
43 pub clusters: Option<ClusterStatistics>,
44
45 #[serde(rename = "_shards")]
47 pub shards: ShardStatistics,
48
49 pub hits: HitsMetadata,
51
52 #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
54 pub aggregations: Option<Value>,
55
56 #[serde(skip_serializing_if = "ShouldSkip::should_skip", default)]
58 pub suggest: Map<String, Vec<Suggest>>,
59}
60
61impl SearchResponse {
62 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}