use serde::de::DeserializeOwned;
use serde_json::Value;
use super::{AggregationResponse, ClusterStatistics, HitsMetadata, ShardStatistics, Suggest};
use crate::{util::ShouldSkip, Map};
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq)]
pub struct SearchResponse {
pub took: u32,
pub timed_out: bool,
#[serde(default, skip_serializing_if = "ShouldSkip::should_skip")]
pub terminated_early: Option<bool>,
#[serde(default, skip_serializing_if = "ShouldSkip::should_skip")]
#[serde(rename = "_scroll_id")]
pub scroll_id: Option<String>,
#[serde(default, skip_serializing_if = "ShouldSkip::should_skip")]
pub fields: Map<String, Value>,
#[serde(default, skip_serializing_if = "ShouldSkip::should_skip")]
pub pit_id: Option<String>,
#[serde(default, skip_serializing_if = "ShouldSkip::should_skip")]
pub num_reduce_phases: Option<u64>,
#[serde(default, skip_serializing_if = "ShouldSkip::should_skip")]
pub max_score: Option<f32>,
#[serde(
default,
skip_serializing_if = "ShouldSkip::should_skip",
rename = "_clusters"
)]
pub clusters: Option<ClusterStatistics>,
#[serde(rename = "_shards")]
pub shards: ShardStatistics,
pub hits: HitsMetadata,
#[serde(default, skip_serializing_if = "ShouldSkip::should_skip")]
pub aggregations: Map<String, AggregationResponse>,
#[serde(default, skip_serializing_if = "ShouldSkip::should_skip")]
pub suggest: Map<String, Vec<Suggest>>,
}
impl SearchResponse {
pub fn documents<T>(&self) -> Result<Vec<T>, serde_json::Error>
where
T: DeserializeOwned,
{
self.hits.hits.iter().map(|hit| hit.source()).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
search::response::aggregation::{AggregationTrait, BucketAggregation},
CompletionSuggestOption, Hit, PhraseSuggestOption, Source, SuggestOption,
TermSuggestOption, TotalHits, TotalHitsRelation,
};
#[test]
fn deserializes_successfully() {
let json = json!({
"took": 6,
"timed_out": false,
"_shards": {
"total": 10,
"successful": 5,
"skipped": 3,
"failed": 2
},
"hits": {
"total": {
"value": 10000,
"relation": "gte"
},
"max_score": 1.0,
"hits": [
{
"_index": "_index",
"_type": "_doc",
"_id": "123",
"_score": 1.0
}
]
},
"suggest": {
"song-suggest": [
{
"text": "nir",
"offset": 0,
"length": 3,
"options": [
{
"text": "Nirvana",
"_index": "music",
"_type": "_doc",
"_id": "1",
"_score": 1.0,
"_source": { "suggest": ["Nevermind", "Nirvana"] }
}
]
}
],
"term#my-first-suggester": [
{
"text": "some",
"offset": 0,
"length": 4,
"options": []
},
{
"text": "test",
"offset": 5,
"length": 4,
"options": []
},
{
"text": "mssage",
"offset": 10,
"length": 6,
"options": [
{
"text": "message",
"score": 0.8333333,
"freq": 4
}
]
}
],
"phrase#my-second-suggester": [
{
"text": "some test mssage",
"offset": 0,
"length": 16,
"options": [
{
"text": "some test message",
"score": 0.030227963
}
]
}
]
}
});
let actual: SearchResponse = serde_json::from_value(json).unwrap();
let expected = SearchResponse {
took: 6,
timed_out: false,
shards: ShardStatistics {
total: 10,
successful: 5,
skipped: 3,
failed: 2,
failures: Default::default(),
},
hits: HitsMetadata {
total: Some(TotalHits {
value: 10_000,
relation: TotalHitsRelation::GreaterThanOrEqualTo,
}),
max_score: Some(1.0),
hits: vec![Hit {
explanation: None,
nested: None,
index: "_index".into(),
id: "123".into(),
score: Some(1.0),
source: Source::from_string("null".to_string()).unwrap(),
highlight: Default::default(),
inner_hits: Default::default(),
matched_queries: Default::default(),
sort: Default::default(),
fields: Default::default(),
}],
},
aggregations: Map::new(),
terminated_early: None,
scroll_id: None,
fields: Default::default(),
pit_id: None,
num_reduce_phases: None,
max_score: None,
clusters: None,
suggest: Map::from([
(
"song-suggest".to_string(),
vec![Suggest {
text: "nir".to_string(),
length: 3,
offset: 0,
options: vec![SuggestOption::Completion(CompletionSuggestOption {
text: "Nirvana".to_string(),
index: "music".to_string(),
id: "1".to_string(),
score: 1.0,
source: Some(json!({ "suggest": ["Nevermind", "Nirvana"] })),
contexts: Default::default(),
})],
}],
),
(
"term#my-first-suggester".to_string(),
vec![
Suggest {
text: "some".to_string(),
length: 4,
offset: 0,
options: vec![],
},
Suggest {
text: "test".to_string(),
length: 4,
offset: 5,
options: vec![],
},
Suggest {
text: "mssage".to_string(),
length: 6,
offset: 10,
options: vec![SuggestOption::Term(TermSuggestOption {
text: "message".to_string(),
score: 0.8333333,
frequency: 4,
})],
},
],
),
(
"phrase#my-second-suggester".to_string(),
vec![Suggest {
text: "some test mssage".to_string(),
length: 16,
offset: 0,
options: vec![SuggestOption::Phrase(PhraseSuggestOption {
text: "some test message".to_string(),
score: 0.030227963,
collate_match: None,
highlighted: None,
})],
}],
),
]),
};
assert_eq!(actual, expected);
}
#[test]
fn parses_documents() {
let json = json!({
"took": 6,
"timed_out": false,
"_shards": {
"total": 10,
"successful": 5,
"skipped": 3,
"failed": 2
},
"hits": {
"total": {
"value": 10000,
"relation": "gte"
},
"max_score": 1.0,
"hits": [
{
"_index": "_index",
"_type": "_doc",
"_id": "123",
"_score": 1.0,
"_source": {
"id": 123,
"title": "test",
"user_id": 456,
}
}
]
}
});
#[derive(Debug, PartialEq, Deserialize)]
struct Document {
id: i32,
title: String,
user_id: Option<i32>,
}
let subject: SearchResponse = serde_json::from_value(json).unwrap();
let subject = subject.documents::<Document>().unwrap();
let expectation = [Document {
id: 123,
title: "test".to_string(),
user_id: Some(456),
}];
assert_eq!(subject, expectation);
}
#[test]
fn test_bucket_aggregation_from_json() {
let json_str =
include_str!("../../../test/resources/result/aggregations/bucket_aggregation.json");
let aggregation_json: Value = serde_json::from_str(json_str).unwrap();
let bucket_agg: BucketAggregation = serde_json::from_value(aggregation_json).unwrap();
assert_eq!(bucket_agg.doc_count_error_upper_bound, 0);
assert_eq!(bucket_agg.sum_other_doc_count, 0);
assert_eq!(bucket_agg.buckets.len(), 5);
let first_bucket = &bucket_agg.buckets[0];
assert_eq!(first_bucket.key_to_string(), "PN04872576P");
assert_eq!(first_bucket.doc_count, 591);
assert!(first_bucket.sub_aggs.contains_key("vinto_totale"));
}
#[test]
fn test_top_hits_aggregation_from_json() {
let json_str =
include_str!("../../../test/resources/result/aggregations/top_hits_aggregation.json");
let aggregation_json: Value = serde_json::from_str(json_str).unwrap();
let bucket_agg: BucketAggregation = serde_json::from_value(aggregation_json).unwrap();
assert_eq!(bucket_agg.buckets.len(), 3);
let first_bucket = &bucket_agg.buckets[0];
assert_eq!(first_bucket.key_to_string(), "hat");
assert_eq!(first_bucket.doc_count, 3);
assert!(first_bucket.sub_aggs.contains_key("top_sales_hits"));
}
#[test]
fn test_just_aggregation_part() {
let json_str = r#"
{
"buckets": {
"daily": {
"doc_count": 1020,
"sdg_year": {
"doc_count_error_upper_bound": 23,
"sum_other_doc_count": 728,
"buckets": [
{
"key": 14,
"doc_count": 30,
"sum_2": {
"value": 4.54048102E8
},
"sum_1": {
"value": 5.164097E8
}
}
]
}
}
}
}
"#;
let aggregation: AggregationResponse = serde_json::from_str(json_str).unwrap();
match aggregation {
AggregationResponse::MultiBucket(_) => println!("Successfully parsed as MultiBucket"),
_ => println!(
"Parsed as something else: {:?}",
std::mem::discriminant(&aggregation)
),
}
}
#[test]
fn test_debug_aggregation_parsing() {
let json_str = include_str!("../../../test/resources/result/aggregations/sample001.json");
let search_response: SearchResponse = serde_json::from_str(json_str).unwrap();
if let Some(sdg_daily) = search_response.aggregations.get("sdg_daily") {
println!(
"sdg_daily aggregation type: {:?}",
std::mem::discriminant(sdg_daily)
);
match sdg_daily {
AggregationResponse::Bucket(_) => println!("It's a Bucket"),
AggregationResponse::MultiBucket(_) => println!("It's a MultiBucket"),
AggregationResponse::DocCount(_) => println!("It's a DocCount"),
AggregationResponse::Simple(_) => println!("It's a Simple"),
_ => println!("It's something else"),
}
}
assert!(!search_response.aggregations.is_empty());
}
#[test]
fn test_complete_search_response_with_aggregations() {
let json_str = include_str!("../../../test/resources/result/aggregations/sample001.json");
let search_response: SearchResponse = serde_json::from_str(json_str).unwrap();
assert_eq!(search_response.took, 7);
assert!(!search_response.timed_out);
assert_eq!(search_response.shards.total, 5);
assert_eq!(search_response.shards.successful, 5);
assert!(!search_response.aggregations.is_empty());
let aggregations = search_response.aggregations;
assert!(aggregations.contains_key("sdg_daily"));
let _sdg_daily = aggregations.get("sdg_daily").unwrap();
}
#[test]
fn test_aggregation_trait_methods() {
let json_str =
include_str!("../../../test/resources/result/aggregations/bucket_aggregation.json");
let aggregation_json: Value = serde_json::from_str(json_str).unwrap();
let bucket_agg: BucketAggregation = serde_json::from_value(aggregation_json).unwrap();
let agg_response = AggregationResponse::Bucket(bucket_agg);
assert!(!agg_response.is_empty());
assert!(agg_response.non_empty());
let (labels, values) = agg_response.extract_label_values();
assert_eq!(labels.len(), 5);
assert_eq!(values.len(), 5);
assert_eq!(labels[0], "PN04872576P");
assert_eq!(values[0], 591.0);
}
#[test]
fn test_comprehensive_aggregation_functionality() {
let bucket_json =
include_str!("../../../test/resources/result/aggregations/bucket_aggregation.json");
let bucket: BucketAggregation = serde_json::from_str(bucket_json).unwrap();
assert_eq!(bucket.buckets.len(), 5);
let top_hits_json =
include_str!("../../../test/resources/result/aggregations/top_hits_aggregation.json");
let top_hits: BucketAggregation = serde_json::from_str(top_hits_json).unwrap();
assert_eq!(top_hits.buckets.len(), 3);
assert!(top_hits.buckets[0].sub_aggs.contains_key("top_sales_hits"));
let search_json =
include_str!("../../../test/resources/result/aggregations/sample001.json");
let search_response: SearchResponse = serde_json::from_str(search_json).unwrap();
assert!(!search_response.aggregations.is_empty());
assert!(search_response.aggregations.contains_key("sdg_daily"));
}
}