use std::{collections::HashMap, fmt};
use serde::de::{self, Deserializer, MapAccess, Visitor};
use serde::ser::{SerializeMap, Serializer};
use serde::{Deserialize, Serialize};
#[cfg(feature = "graphql")]
use super::request::RequestInput as AggregationInput;
use super::{request::Request as Aggregation, response::Ty, types::*, ComputedResult, Response};
use crate::search::query::CompoundQuery;
#[cfg(feature = "graphql")]
impl Serialize for AggregationInput {
#[inline]
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry(&self.name, &SubAggregation::from(self.to_owned()))?;
map.end()
}
}
#[allow(clippy::missing_docs_in_private_items)]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub(super) struct SubAggregation {
#[serde(default, skip_serializing_if = "Option::is_none")]
avg: Option<InnerAggregation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
weighted_avg: Option<WeightedAverageAggregation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
cardinality: Option<InnerAggregation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
max: Option<InnerAggregation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
min: Option<InnerAggregation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
median_absolute_deviation: Option<InnerAggregation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
percentiles: Option<InnerAggregation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
percentile_ranks: Option<InnerAggregation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
stats: Option<InnerAggregation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
extended_stats: Option<InnerAggregation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
sum: Option<InnerAggregation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
value_count: Option<InnerAggregation>,
#[serde(default, rename = "filter", skip_serializing_if = "Option::is_none")]
filters: Option<CompoundQuery>,
#[serde(default, skip_serializing_if = "Option::is_none")]
terms: Option<TermsAggregation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
range: Option<RangeAggregation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
date_range: Option<DateRangeAggregation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
date_histogram: Option<DateHistogramAggregation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
auto_date_histogram: Option<AutoDateHistogramAggregation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
histogram: Option<HistogramAggregation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
variable_width_histogram: Option<VariableWidthHistogram>,
#[serde(default, skip_serializing_if = "Option::is_none")]
sampler: Option<SamplerAggregation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
significant_text: Option<SignificantTextAggregation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
bucket_script: Option<BucketScript>,
#[serde(default, skip_serializing_if = "Option::is_none")]
bucket_selector: Option<BucketSelector>,
#[serde(default, skip_serializing_if = "Option::is_none")]
bucket_sort: Option<BucketSort>,
#[serde(default, skip_serializing_if = "Option::is_none")]
nested: Option<NestedAggregation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
reverse_nested: Option<ReverseNestedAggregation>,
#[serde(default, rename = "meta", skip_serializing_if = "Option::is_none")]
metadata: Option<crate::scalars::Map>,
#[serde(
default,
rename = "aggs",
skip_serializing_if = "Option::is_none",
with = "serde_sub_aggregations"
)]
aggregations: Option<Vec<Aggregation>>,
}
#[cfg(feature = "graphql")]
impl From<AggregationInput> for SubAggregation {
#[inline]
fn from(aggregation: AggregationInput) -> SubAggregation {
SubAggregation {
avg: aggregation.avg.map(Into::into),
weighted_avg: aggregation.weighted_avg.map(Into::into),
cardinality: aggregation.cardinality.map(Into::into),
max: aggregation.max.map(Into::into),
min: aggregation.min.map(Into::into),
median_absolute_deviation: aggregation.median_absolute_deviation.map(Into::into),
percentiles: aggregation.percentiles.map(Into::into),
percentile_ranks: aggregation.percentile_ranks.map(Into::into),
stats: aggregation.stats.map(Into::into),
extended_stats: aggregation.extended_stats.map(Into::into),
sum: aggregation.sum.map(Into::into),
value_count: aggregation.value_count.map(Into::into),
filters: aggregation.filters.map(Into::into),
terms: aggregation.terms.map(Into::into),
range: aggregation.range.map(Into::into),
date_range: aggregation.date_range.map(Into::into),
date_histogram: aggregation.date_histogram.map(Into::into),
auto_date_histogram: aggregation.auto_date_histogram.map(Into::into),
histogram: aggregation.histogram.map(Into::into),
variable_width_histogram: aggregation.variable_width_histogram.map(Into::into),
sampler: aggregation.sampler.map(Into::into),
significant_text: aggregation.significant_text.map(Into::into),
bucket_script: aggregation.bucket_script.map(Into::into),
bucket_selector: aggregation.bucket_selector.map(Into::into),
bucket_sort: aggregation.bucket_sort.map(Into::into),
reverse_nested: aggregation.reverse_nested.map(Into::into),
nested: aggregation.nested.map(Into::into),
metadata: aggregation.metadata,
aggregations: aggregation
.aggregations
.map(|aggs| aggs.into_iter().map(Into::into).collect()),
}
}
}
impl From<Aggregation> for SubAggregation {
#[inline]
fn from(aggregation: Aggregation) -> SubAggregation {
SubAggregation {
avg: aggregation.avg.map(Into::into),
weighted_avg: aggregation.weighted_avg.map(Into::into),
cardinality: aggregation.cardinality.map(Into::into),
max: aggregation.max.map(Into::into),
min: aggregation.min.map(Into::into),
median_absolute_deviation: aggregation.median_absolute_deviation.map(Into::into),
percentiles: aggregation.percentiles.map(Into::into),
percentile_ranks: aggregation.percentile_ranks.map(Into::into),
stats: aggregation.stats.map(Into::into),
extended_stats: aggregation.extended_stats.map(Into::into),
sum: aggregation.sum.map(Into::into),
value_count: aggregation.value_count.map(Into::into),
filters: aggregation.filters.map(Into::into),
terms: aggregation.terms.map(Into::into),
range: aggregation.range.map(Into::into),
date_range: aggregation.date_range.map(Into::into),
date_histogram: aggregation.date_histogram.map(Into::into),
auto_date_histogram: aggregation.auto_date_histogram.map(Into::into),
histogram: aggregation.histogram.map(Into::into),
variable_width_histogram: aggregation.variable_width_histogram.map(Into::into),
sampler: aggregation.sampler.map(Into::into),
significant_text: aggregation.significant_text.map(Into::into),
bucket_script: aggregation.bucket_script.map(Into::into),
bucket_selector: aggregation.bucket_selector.map(Into::into),
bucket_sort: aggregation.bucket_sort.map(Into::into),
reverse_nested: aggregation.reverse_nested.map(Into::into),
nested: aggregation.nested.map(Into::into),
metadata: aggregation.metadata,
aggregations: aggregation
.aggregations
.map(|aggs| aggs.into_iter().map(Into::into).collect()),
}
}
}
impl Aggregation {
#[allow(clippy::missing_docs_in_private_items)]
pub(super) fn from_sub_aggregation(name: String, aggregation: SubAggregation) -> Aggregation {
Aggregation {
name,
avg: aggregation.avg.map(Into::into),
weighted_avg: aggregation.weighted_avg.map(Into::into),
cardinality: aggregation.cardinality.map(Into::into),
max: aggregation.max.map(Into::into),
min: aggregation.min.map(Into::into),
median_absolute_deviation: aggregation.median_absolute_deviation.map(Into::into),
percentiles: aggregation.percentiles.map(Into::into),
percentile_ranks: aggregation.percentile_ranks.map(Into::into),
stats: aggregation.stats.map(Into::into),
extended_stats: aggregation.extended_stats.map(Into::into),
sum: aggregation.sum.map(Into::into),
value_count: aggregation.value_count.map(Into::into),
filters: aggregation.filters.map(Into::into),
terms: aggregation.terms.map(Into::into),
range: aggregation.range.map(Into::into),
date_range: aggregation.date_range.map(Into::into),
date_histogram: aggregation.date_histogram.map(Into::into),
auto_date_histogram: aggregation.auto_date_histogram.map(Into::into),
histogram: aggregation.histogram.map(Into::into),
variable_width_histogram: aggregation.variable_width_histogram.map(Into::into),
sampler: aggregation.sampler.map(Into::into),
significant_text: aggregation.significant_text.map(Into::into),
bucket_script: aggregation.bucket_script.map(Into::into),
bucket_selector: aggregation.bucket_selector.map(Into::into),
bucket_sort: aggregation.bucket_sort.map(Into::into),
reverse_nested: aggregation.reverse_nested.map(Into::into),
nested: aggregation.nested.map(Into::into),
metadata: aggregation.metadata,
aggregations: aggregation
.aggregations
.map(|aggs| aggs.into_iter().map(Into::into).collect()),
}
}
}
impl Serialize for Aggregation {
#[inline]
#[inline]
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry(&self.name, &SubAggregation::from(self.to_owned()))?;
map.end()
}
}
impl<'de> serde::Deserialize<'de> for Aggregation {
#[inline]
fn deserialize<D>(deserializer: D) -> Result<Aggregation, D::Error>
where
D: Deserializer<'de>,
{
struct AggregationVisitor;
impl<'de> Visitor<'de> for AggregationVisitor {
type Value = Aggregation;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an `Aggregation`")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let name = map
.next_key::<String>()?
.ok_or_else(|| de::Error::missing_field("name"))?;
let agg: SubAggregation = map.next_value()?;
Ok(Aggregation::from_sub_aggregation(name, agg))
}
}
deserializer.deserialize_map(AggregationVisitor)
}
}
#[derive(Serialize, Deserialize, Debug)]
pub(crate) struct ElasticAggregationResponse {
#[serde(default, alias = "aggs")]
aggregations: HashMap<String, ElasticAggregationResult>,
}
impl From<ElasticAggregationResponse> for Response {
#[inline]
fn from(response: ElasticAggregationResponse) -> Self {
let aggs = response.aggregations;
let mut results: HashMap<(Option<&String>, String), ComputedResult> = HashMap::new();
let mut pending_aggs: Vec<(Option<&String>, _)> = vec![(None, &aggs)];
while let Some(curr) = pending_aggs.pop() {
let (parent, aggs) = curr;
for (ty_and_name, curr_agg) in aggs.iter() {
let (ty, name) = split_ty_and_name(ty_and_name);
let mut handle_leaf_agg = |agg: &ElasticAggregationResult| {
if let Some(value) = agg.value_or_doc_count() {
if !agg.should_skip() {
#[allow(clippy::clone_on_copy)] let result =
results
.entry((parent, name.to_string()))
.or_insert_with(|| ComputedResult {
parent: parent.map(|p| p.to_owned()),
name: name.to_string(),
type_: ty.clone(),
fields: vec![],
values: vec![],
metadata: agg.metadata.to_owned(),
});
if let Some(key) = agg.parent_key.as_ref().or_else(|| agg.key.as_ref())
{
result.fields.push(key.to_owned());
}
result.values.push(value);
}
}
};
handle_leaf_agg(curr_agg);
pending_aggs.push((None, &curr_agg.aggregations));
for bucket_agg in curr_agg.buckets.iter() {
if bucket_agg.aggregations.is_empty() {
handle_leaf_agg(bucket_agg);
} else {
pending_aggs.push((curr_agg.parent_key.as_ref(), &bucket_agg.aggregations));
}
}
}
}
Response {
aggregations: results.into_iter().map(|(_, agg)| agg).collect(),
}
}
}
#[doc(hidden)]
#[allow(clippy::indexing_slicing)]
fn split_ty_and_name(ty_and_name: &str) -> (Ty, String) {
let parts: Vec<&str> = ty_and_name.split('#').collect();
if parts.len() < 2 {
(Ty::Unknown, parts[0].to_owned())
} else {
(parts[0].into(), parts[1..].join(""))
}
}
#[derive(Serialize, Default, Debug)]
struct ElasticAggregationResult {
parent_key: Option<String>,
key: Option<String>,
doc_count: Option<u64>,
value: Option<f64>,
buckets: Vec<ElasticAggregationResult>,
metadata: Option<crate::scalars::Map>,
aggregations: HashMap<String, ElasticAggregationResult>,
}
impl ElasticAggregationResult {
fn value_or_doc_count(&self) -> Option<f64> {
self.value.or_else(|| {
if self.buckets.is_empty() {
#[allow(clippy::as_conversions)]
self.doc_count.map(|v| v as f64)
} else {
None
}
})
}
fn should_skip(&self) -> bool {
let mut skip = false;
if let Some(meta) = self.metadata.as_ref() {
if let Some(s) = meta.get("_skip") {
if let Some(b) = s.as_bool() {
skip = b;
}
}
}
skip
}
}
impl<'de> serde::Deserialize<'de> for ElasticAggregationResult {
fn deserialize<D>(deserializer: D) -> Result<ElasticAggregationResult, D::Error>
where
D: Deserializer<'de>,
{
#[doc(hidden)]
struct ElasticAggregationResultVisitor;
impl<'de> Visitor<'de> for ElasticAggregationResultVisitor {
type Value = ElasticAggregationResult;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an `ElasticAggregationResult`")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Value {
Null,
Bool(bool),
Int(u64),
Float(f64),
String(String),
Array(Vec<Value>),
Object(HashMap<String, Value>),
}
let mut result = ElasticAggregationResult::default();
while let Some(k) = map.next_key::<String>()? {
match k.as_str() {
"key" => match map.next_value()? {
Value::Bool(val) => {
result.key = Some(val.to_string());
}
Value::Int(val) => {
result.key = Some(val.to_string());
}
Value::Float(val) => {
result.key = Some(val.to_string());
}
Value::String(val) => {
result.key = Some(val);
}
_ => {}
},
"key_as_string" => result.key = Some(map.next_value()?),
"value" => result.value = Some(map.next_value()?),
"buckets" => result.buckets = map.next_value()?,
"doc_count" => result.doc_count = Some(map.next_value()?),
"doc_count_error_upper_bound" | "sum_other_doc_count" | "interval" => {
let _: Value = map.next_value()?;
}
"meta" | "metadata" => result.metadata = Some(map.next_value()?),
_ => match map.next_value::<ElasticAggregationResult>() {
Ok(val) => {
result.aggregations.insert(k.to_string(), val);
}
Err(_err) => {
}
},
}
}
let key = &result.key;
result.aggregations = result
.aggregations
.into_iter()
.map(|(name, mut agg)| {
agg.parent_key = key.clone();
(name, agg)
})
.collect();
Ok(result)
}
}
deserializer.deserialize_map(ElasticAggregationResultVisitor)
}
}
pub(super) mod serde_sub_aggregations {
use std::collections::HashMap;
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serializer};
use super::{Aggregation, SubAggregation};
#[inline]
pub(crate) fn serialize<S>(aggs: &Option<Vec<Aggregation>>, ser: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(aggs) = aggs {
let mut map = ser.serialize_map(Some(aggs.len()))?;
for agg in aggs.iter() {
map.serialize_entry(agg.name.as_str(), &SubAggregation::from(agg.to_owned()))?;
}
map.end()
} else {
ser.serialize_none()
}
}
#[inline]
#[cfg(not(test))]
pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<Aggregation>>, D::Error>
where
D: Deserializer<'de>,
{
Ok(
Option::deserialize(deserializer)?.map(|agg: HashMap<String, SubAggregation>| {
agg.into_iter()
.map(|(name, sub_agg)| Aggregation::from_sub_aggregation(name, sub_agg))
.collect()
}),
)
}
#[doc(hidden)]
#[cfg(test)]
pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<Aggregation>>, D::Error>
where
D: Deserializer<'de>,
{
Ok(
Option::deserialize(deserializer)?.map(|agg: HashMap<String, SubAggregation>| {
let mut aggs: Vec<Aggregation> = agg
.into_iter()
.map(|(name, sub_agg)| Aggregation::from_sub_aggregation(name, sub_agg))
.collect();
#[allow(clippy::expect_used)]
aggs.sort_by(|a, b| a.name.partial_cmp(&b.name).expect("invalid ordering"));
aggs
}),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use crate::search::query::TermsQuery;
#[test]
fn simple() {
let input = json!({ "aggregations": { "agg": { "value_count": { "field": "id" } } } });
let _: Aggregation = serde_json::from_value(input).unwrap();
let result = json!({ "aggregations": { "agg": { "value": 123_456_789 } } });
let _: Response = serde_json::from_value(result).unwrap();
}
mod aggregation_input {
use super::*;
#[test]
fn simple() {
let input = json!({ "aggs": { "AVG_AGG": { "avg": { "field": "duration" } } } });
let _: Aggregation = serde_json::from_value(input).unwrap();
}
macro_rules! test_case {
($name:ident : $agg:expr, $json_value:expr) => {
mod $name {
use super::*;
#[test]
fn can_serialize() {
let actual = serde_json::to_value($agg).unwrap();
let expected = $json_value;
let json_pretty_print = |d| serde_json::to_string_pretty(&d).unwrap();
assert_eq!(
&actual,
&expected,
"\nactual:\n{}\nexpected:\n{}",
json_pretty_print(&actual),
json_pretty_print(&expected)
);
}
#[test]
fn can_deserialize() {
let expected = $agg;
let actual: Aggregation = serde_json::from_value($json_value).unwrap();
assert_eq!(
actual, expected,
"\nactual:\n{:#?}\nexpected:\n{:#?}",
&actual, &expected
);
}
}
};
}
test_case!(
simple_with_metadata:
Aggregation::builder()
.name("hasMetadata")
.terms(Some("id".into()))
.metadata(Some([("test".to_string(), json!(true).into())].iter().cloned().collect()))
.build(),
json!({
"hasMetadata": {
"terms": { "field": "id" },
"meta": { "test": true },
},
})
);
test_case!(
simple_with_nest:
Aggregation::builder()
.name("SPECIFIC_AGENTS")
.filters(Some(TermsQuery::new("agents", vec!["123", "456", "789"]).into()))
.aggregations(vec![Aggregation::builder()
.name("PER_AGENT")
.terms(Some("agents".into()))
.aggregations(vec![Aggregation::builder()
.name("PER_TYPE")
.terms(Some("metadata.type".into()))
.aggregations(vec![
Aggregation::builder()
.name("AVG_OF_DURATION")
.avg(Some("duration".into()))
.build(),
Aggregation::builder()
.name("AVG_OF_SILENCE_DURATION")
.avg(Some("silenceDuration".into()))
.build(),
Aggregation::builder()
.name("COUNT_OF_CALLS")
.value_count(Some("id".into()))
.build(),
])
.build()])
.build()])
.build(),
json!({
"SPECIFIC_AGENTS": {
"filter": {
"bool": {
"filter": [{ "terms": { "agents": ["123", "456", "789"] } }]
}
},
"aggs": {
"PER_AGENT": {
"terms": { "field": "agents" },
"aggs": {
"PER_TYPE": {
"terms": { "field": "metadata.type" },
"aggs": {
"AVG_OF_DURATION": { "avg": { "field": "duration" } },
"AVG_OF_SILENCE_DURATION": {
"avg": { "field": "silenceDuration" }
},
"COUNT_OF_CALLS": { "value_count": { "field": "id" } },
}
}
}
}
}
}
})
);
test_case!(
date_range_with_nest:
Aggregation::builder()
.name("TIMESTAMP_DATE_RANGE")
.date_range(
DateRangeAggregation::builder()
.field("timestamp")
.format(Some("yyyy-MM-dd'T'HH:mm:ssX".into()))
.missing(Some("1970-01-01T00:00:00Z".into()))
.ranges(vec![DateRange::new(Some("now-10M/M"), Some("now-1d/d"))])
.build()
)
.aggregations(vec![
Aggregation::builder()
.name("ID_VALUE_COUNT")
.value_count(Some("id".into()))
.build(),
])
.build(),
json!({
"TIMESTAMP_DATE_RANGE": {
"aggs": {
"ID_VALUE_COUNT": { "value_count": { "field": "id" } }
},
"date_range": {
"field": "timestamp",
"format": "yyyy-MM-dd'T'HH:mm:ssX",
"missing": "1970-01-01T00:00:00Z",
"ranges": [{ "from": "now-10M/M", "to": "now-1d/d" }]
}
},
})
);
}
mod aggregation_results {
use super::*;
use std::cmp::Ordering;
use crate::aggregation::Response;
#[test]
fn simple() {
let result = json!({ "aggregations": { "AVG_DURATION": { "value": 353_964.312 } } });
let _: Response = serde_json::from_value(result).unwrap();
}
macro_rules! test_case {
($name:ident : $agg:expr, $json_value:expr) => {
mod $name {
use super::*;
#[test]
fn can_deserialize() {
let mut expected = $agg;
let mut actual: Response = serde_json::from_value($json_value).unwrap();
actual.aggregations.sort();
expected.aggregations.sort();
assert_eq!(
actual, expected,
"\nactual:\n{:#?}\nexpected:\n{:#?}",
&actual, &expected
);
}
}
};
}
test_case!(
simple_with_metadata:
Response {
aggregations: vec![ComputedResult {
parent: None,
name: "AVG_DURATION".to_string(),
fields: vec![],
values: vec![3.0, 4.0],
metadata: Some([("test".to_string(), json!(true).into())].iter().cloned().collect()),
type_: Ty::Avg,
}],
},
json!({
"aggregations": {
"avg#AVG_DURATION": { "value": 353_964.312_5 },
"metadata": { "test": true }
}
})
);
test_case!(
simple_with_skip:
Response {
aggregations: vec![ComputedResult {
parent: None,
name: "PERCENT_DEAD_AIR".to_string(),
fields: vec!["dallin".to_string(), "will".to_string()],
values: vec![0.009, 0.017],
metadata: None,
type_: Ty::Unknown,
}],
},
json!({
"aggregations": {
"PER_AGENT": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "dallin",
"doc_count": 7,
"sum#SUM_DURATION": {
"metadata": { "_skip": true },
"value": 3_237_014.0,
},
"sum#SUM_SILENCE_DURATION": {
"metadata": { "_skip": true },
"value": 31074.0
},
"PERCENT_DEAD_AIR": {
"value": 0.009,
}
},
{
"key": "will",
"doc_count": 7,
"sum#SUM_DURATION": {
"metadata": { "_skip": true },
"value": 2_426_214.0
},
"sum#SUM_SILENCE_DURATION": {
"metadata": { "_skip": true },
"value": 41074.0,
},
"PERCENT_DEAD_AIR": {
"value": 0.017,
}
}
]
}
}
})
);
test_case!(
simple_without_nest:
Response {
aggregations: vec![ComputedResult {
parent: None,
name: "AVG_DURATION".to_string(),
fields: vec![],
values: vec![3.0, 4.0],
metadata: None,
type_: Ty::Avg,
}],
},
json!({ "aggregations": { "avg#AVG_DURATION": { "value": 353_964.312_5 } } })
);
test_case!(
simple_with_nest:
Response {
aggregations: vec![
ComputedResult {
parent: None,
name: "AVG_DURATION".to_string(),
fields: vec!["dallin".to_string(), "will".to_string()],
values: vec![462_430.123, 346_602.0],
metadata: None,
type_: Ty::Avg,
},
]
},
json!({
"took": 2,
"timed_out": false,
"hits": {
"total": { "value": 20, "relation": "eq" },
"max_score": null,
"hits": []
},
"aggregations": {
"PER_AGENT": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "dallin",
"doc_count": 7,
"avg#AVG_DURATION": { "value": 462_430.123 }
},
{
"key": "will",
"doc_count": 7,
"avg#AVG_DURATION": { "value": 346_602 }
}
]
}
}
})
);
test_case!(
complex_with_nest:
Response {
aggregations: vec![
ComputedResult {
parent: Some("sales".to_string()),
name: "COUNT_OF_CALLS".to_string(),
fields: vec!["dallin".to_string(), "will".to_string()],
values: vec![3.0, 4.0],
metadata: None,
type_: Ty::ValueCount,
},
ComputedResult {
parent: Some("sales".to_string()),
name: "SUM_OF_DURATION".to_string(),
fields: vec!["dallin".to_string(), "will".to_string()],
values: vec![2997.0, 2196.0],
metadata: None,
type_: Ty::Sum,
},
ComputedResult {
parent: Some("sales".to_string()),
name: "AVG_OF_DURATION".to_string(),
fields: vec!["dallin".to_string(), "will".to_string()],
values: vec![999.0, 549.0],
metadata: None,
type_: Ty::Avg,
},
ComputedResult {
parent: Some("(missing)".to_string()),
name: "COUNT_OF_CALLS".to_string(),
fields: vec!["dallin".to_string(), "will".to_string()],
values: vec![4.0, 3.0],
metadata: None,
type_: Ty::ValueCount,
},
ComputedResult {
parent: Some("(missing)".to_string()),
name: "SUM_OF_DURATION".to_string(),
fields: vec!["dallin".to_string(), "will".to_string()],
values: vec![3_234_017.0, 2_424_018.0],
metadata: None,
type_: Ty::Sum,
},
ComputedResult {
parent: Some("(missing)".to_string()),
name: "AVG_OF_DURATION".to_string(),
fields: vec!["dallin".to_string(), "will".to_string()],
values: vec![808_504.25, 808_006.0],
metadata: None,
type_: Ty::Avg,
},
],
},
json!({
"took": 16,
"timed_out": false,
"_shards": { "total": 2, "successful": 2, "skipped": 0, "failed": 0 },
"hits": {
"total": { "value": 14, "relation": "eq" },
"max_score": null,
"hits": []
},
"aggregations": {
"ANY_CALL": {
"doc_count": 14,
"meta": { "_skip": true },
"PER_TYPE": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "(missing)",
"doc_count": 7,
"PER_AGENT": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "dallin",
"doc_count": 4,
"avg#AVG_OF_DURATION": { "value": 808_504.25 },
"value_count#COUNT_OF_CALLS": { "value": 4 },
"sum#SUM_OF_DURATION": { "value": 3_234_017 }
},
{
"key": "will",
"doc_count": 3,
"avg#AVG_OF_DURATION": { "value": 808_006 },
"value_count#COUNT_OF_CALLS": { "value": 3 },
"sum#SUM_OF_DURATION": { "value": 2_424_018 }
}
]
}
},
{
"key": "sales",
"doc_count": 7,
"PER_AGENT": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "will",
"doc_count": 4,
"avg#AVG_OF_DURATION": { "value": 549 },
"value_count#COUNT_OF_CALLS": { "value": 4 },
"sum#SUM_OF_DURATION": { "value": 2196 }
},
{
"key": "dallin",
"doc_count": 3,
"avg#AVG_OF_DURATION": { "value": 999 },
"value_count#COUNT_OF_CALLS": { "value": 3 },
"sum#SUM_OF_DURATION": { "value": 2997 }
}
]
}
}
]
}
}
}
})
);
test_case!(
date_range_with_nest:
Response {
aggregations: vec![
ComputedResult {
parent: None,
name: "ID_VALUE_COUNT".to_string(),
fields: vec![
"*-2018-12-01T00:00:00Z".to_string(),
"2018-12-01T00:00:00Z-*".to_string(),
],
values: vec![0.0, 30.0],
metadata: None,
type_: Ty::ValueCount,
},
]
},
json!({
"aggregations": {
"date_range#TIMESTAMP_DATE_RANGE": {
"buckets": [
{
"key": "*-2018-12-01T00:00:00Z",
"to": 1_543_622_400_000.0,
"to_as_string": "2018-12-01T00:00:00Z",
"doc_count": 0,
"value_count#ID_VALUE_COUNT": { "value": 0 }
},
{
"key": "2018-12-01T00:00:00Z-*",
"from": 1_543_622_400_000.0,
"from_as_string": "2018-12-01T00:00:00Z",
"doc_count": 30,
"value_count#ID_VALUE_COUNT": { "value": 30 }
}
]
}
}
})
);
test_case!(
date_histogram:
Response {
aggregations: vec![
ComputedResult {
parent: None,
name: "TIMESTAMP_HISTOGRAM".to_string(),
fields: vec![
"2020-01-01T00:00:00.000Z".to_string(),
"2020-01-02T00:00:00.000Z".to_string(),
"2020-01-03T00:00:00.000Z".to_string(),
"2020-01-04T00:00:00.000Z".to_string(),
"2020-01-05T00:00:00.000Z".to_string(),
],
values: vec![1.0, 2.0, 1.0, 1.0, 2.0],
metadata: None,
type_: Ty::DateHistogram,
},
]
},
json!({
"aggregations": {
"filter#PER_COMPANY": {
"doc_count": 7,
"meta": { "_skip": true },
"date_histogram#TIMESTAMP_HISTOGRAM": {
"buckets": [
{
"doc_count": 1,
"key": 1_577_836_800_000_u64,
"key_as_string": "2020-01-01T00:00:00.000Z"
},
{
"doc_count": 2,
"key": 1_577_923_200_000_u64,
"key_as_string": "2020-01-02T00:00:00.000Z"
},
{
"doc_count": 1,
"key": 1_578_009_600_000_u64,
"key_as_string": "2020-01-03T00:00:00.000Z"
},
{
"doc_count": 1,
"key": 1_578_096_000_000_u64,
"key_as_string": "2020-01-04T00:00:00.000Z"
},
{
"doc_count": 2,
"key": 1_578_182_400_000_u64,
"key_as_string": "2020-01-05T00:00:00.000Z"
}
]
},
}
},
})
);
test_case!(
auto_date_histogram:
Response {
aggregations: vec![
ComputedResult {
parent: None,
name: "TIMESTAMP_AUTO_DATE_HISTOGRAM".to_string(),
fields: vec![
"2020-01-01T00:00:00.000Z".to_string(),
"2020-01-01T12:00:00.000Z".to_string(),
"2020-01-02T00:00:00.000Z".to_string(),
"2020-01-02T12:00:00.000Z".to_string(),
"2020-01-03T00:00:00.000Z".to_string(),
"2020-01-03T12:00:00.000Z".to_string(),
"2020-01-04T00:00:00.000Z".to_string(),
"2020-01-04T12:00:00.000Z".to_string(),
"2020-01-05T00:00:00.000Z".to_string(),
],
values: vec![1.0, 0.0, 2.0, 0.0, 1.0, 0.0, 1.0, 0.0, 2.0],
metadata: None,
type_: Ty::AutoDateHistogram,
},
]
},
json!({
"aggregations": {
"filter#PER_COMPANY": {
"doc_count": 7,
"meta": { "_skip": true },
"auto_date_histogram#TIMESTAMP_AUTO_DATE_HISTOGRAM": {
"buckets": [
{
"doc_count": 1,
"key": 1_577_836_800_000_u64,
"key_as_string": "2020-01-01T00:00:00.000Z"
},
{
"doc_count": 0,
"key": 1_577_880_000_000_u64,
"key_as_string": "2020-01-01T12:00:00.000Z"
},
{
"doc_count": 2,
"key": 1_577_923_200_000_u64,
"key_as_string": "2020-01-02T00:00:00.000Z"
},
{
"doc_count": 0,
"key": 1_577_966_400_000_u64,
"key_as_string": "2020-01-02T12:00:00.000Z"
},
{
"doc_count": 1,
"key": 1_578_009_600_000_u64,
"key_as_string": "2020-01-03T00:00:00.000Z"
},
{
"doc_count": 0,
"key": 1_578_052_800_000_u64,
"key_as_string": "2020-01-03T12:00:00.000Z"
},
{
"doc_count": 1,
"key": 1_578_096_000_000_u64,
"key_as_string": "2020-01-04T00:00:00.000Z"
},
{
"doc_count": 0,
"key": 1_578_139_200_000_u64,
"key_as_string": "2020-01-04T12:00:00.000Z"
},
{
"doc_count": 2,
"key": 1_578_182_400_000_u64,
"key_as_string": "2020-01-05T00:00:00.000Z"
}
],
"interval": "12h"
}
}
},
})
);
test_case!(
bucketing_agg_as_leaf_node:
Response {
aggregations: vec![
ComputedResult {
parent: None,
name: "PER_AGENT".to_string(),
fields: vec!["Denmark".to_string()],
values: vec![1.0],
metadata: None,
type_: Ty::Unknown,
},
]
},
json!({
"aggregations": {
"filter#PER_COMPANY": {
"doc_count": 41,
"meta": { "_skip": true },
"PER_AGENT": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "Denmark",
"doc_count": 1
}
]
}
}
}
})
);
impl Ord for ComputedResult {
fn cmp(&self, other: &Self) -> Ordering {
self.parent
.cmp(&other.parent)
.then(self.name.cmp(&other.name))
.then_with(|| {
let mut self_fields = self.fields.clone();
self_fields.sort();
let mut other_fields = other.fields.clone();
other_fields.sort();
self_fields.cmp(&other_fields)
})
}
}
impl PartialOrd for ComputedResult {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for ComputedResult {
fn eq(&self, other: &Self) -> bool {
let mut self_fields = self.fields.clone();
self_fields.sort();
let mut other_fields = other.fields.clone();
other_fields.sort();
self.parent == other.parent
&& self.name == other.name
&& self_fields == other_fields
}
}
impl Eq for ComputedResult {}
}
}