From e2949d7df1c785ea62f7c68ca565a9f5b9047fa9 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Wed, 11 Feb 2015 12:31:02 +0000 Subject: [PATCH 01/68] make InternalAggregation.reduce(ReduceContext) use template pattern sub-classes of InternalAggregation now implement doReduce(ReduceContext) that is called from InternalAggregation.reduce(ReduceContext) which is now final --- .../search/aggregations/InternalAggregation.java | 7 +++++-- .../search/aggregations/InternalAggregations.java | 2 +- .../bucket/InternalSingleBucketAggregation.java | 2 +- .../aggregations/bucket/filters/InternalFilters.java | 2 +- .../aggregations/bucket/geogrid/InternalGeoHashGrid.java | 2 +- .../aggregations/bucket/histogram/InternalHistogram.java | 2 +- .../search/aggregations/bucket/range/InternalRange.java | 2 +- .../bucket/significant/InternalSignificantTerms.java | 2 +- .../bucket/significant/UnmappedSignificantTerms.java | 4 ++-- .../search/aggregations/bucket/terms/InternalTerms.java | 2 +- .../search/aggregations/bucket/terms/UnmappedTerms.java | 4 ++-- .../search/aggregations/metrics/avg/InternalAvg.java | 2 +- .../metrics/cardinality/InternalCardinality.java | 2 +- .../aggregations/metrics/geobounds/InternalGeoBounds.java | 2 +- .../search/aggregations/metrics/max/InternalMax.java | 2 +- .../search/aggregations/metrics/min/InternalMin.java | 2 +- .../metrics/percentiles/AbstractInternalPercentiles.java | 2 +- .../metrics/scripted/InternalScriptedMetric.java | 2 +- .../search/aggregations/metrics/stats/InternalStats.java | 2 +- .../metrics/stats/extended/InternalExtendedStats.java | 4 ++-- .../search/aggregations/metrics/sum/InternalSum.java | 2 +- .../aggregations/metrics/tophits/InternalTopHits.java | 2 +- .../metrics/valuecount/InternalValueCount.java | 2 +- 23 files changed, 30 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/InternalAggregation.java b/src/main/java/org/elasticsearch/search/aggregations/InternalAggregation.java index 6dcee411e92d1..456b1b391b6b3 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/InternalAggregation.java +++ b/src/main/java/org/elasticsearch/search/aggregations/InternalAggregation.java @@ -18,7 +18,6 @@ */ package org.elasticsearch.search.aggregations; -import org.elasticsearch.Version; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.StreamInput; @@ -146,7 +145,11 @@ public String getName() { * try reusing an existing get instance (typically the first in the given list) to save on redundant object * construction. */ - public abstract InternalAggregation reduce(ReduceContext reduceContext); + public final InternalAggregation reduce(ReduceContext reduceContext) { + return doReduce(reduceContext); + } + + public abstract InternalAggregation doReduce(ReduceContext reduceContext); public Object getProperty(String path) { AggregationPath aggPath = AggregationPath.parse(path); diff --git a/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java b/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java index 6a33c0312af28..ec4625e23874a 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java +++ b/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java @@ -165,7 +165,7 @@ public static InternalAggregations reduce(List aggregation for (Map.Entry> entry : aggByName.entrySet()) { List aggregations = entry.getValue(); InternalAggregation first = aggregations.get(0); // the list can't be empty as it's created on demand - reducedAggregations.add(first.reduce(new InternalAggregation.ReduceContext(aggregations, context.bigArrays(), context.scriptService()))); + reducedAggregations.add(first.doReduce(new InternalAggregation.ReduceContext(aggregations, context.bigArrays(), context.scriptService()))); } return new InternalAggregations(reducedAggregations); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/InternalSingleBucketAggregation.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/InternalSingleBucketAggregation.java index 31d105d5ead56..2bccb4234e309 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/InternalSingleBucketAggregation.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/InternalSingleBucketAggregation.java @@ -69,7 +69,7 @@ public InternalAggregations getAggregations() { protected abstract InternalSingleBucketAggregation newAggregation(String name, long docCount, InternalAggregations subAggregations); @Override - public InternalAggregation reduce(ReduceContext reduceContext) { + public InternalAggregation doReduce(ReduceContext reduceContext) { List aggregations = reduceContext.aggregations(); long docCount = 0L; List subAggregationsList = new ArrayList<>(aggregations.size()); diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/InternalFilters.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/InternalFilters.java index 2642c99a2def3..505547487a642 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/InternalFilters.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/InternalFilters.java @@ -191,7 +191,7 @@ public Bucket getBucketByKey(String key) { } @Override - public InternalAggregation reduce(ReduceContext reduceContext) { + public InternalAggregation doReduce(ReduceContext reduceContext) { List aggregations = reduceContext.aggregations(); List> bucketsList = null; for (InternalAggregation aggregation : aggregations) { diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoHashGrid.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoHashGrid.java index 0d09f05694d8d..c30935c5c3d76 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoHashGrid.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoHashGrid.java @@ -188,7 +188,7 @@ public List getBuckets() { } @Override - public InternalGeoHashGrid reduce(ReduceContext reduceContext) { + public InternalGeoHashGrid doReduce(ReduceContext reduceContext) { List aggregations = reduceContext.aggregations(); LongObjectPagedHashMap> buckets = null; diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java index c7909442016a6..544f06998ce86 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java @@ -412,7 +412,7 @@ private void addEmptyBuckets(List list) { } @Override - public InternalAggregation reduce(ReduceContext reduceContext) { + public InternalAggregation doReduce(ReduceContext reduceContext) { List reducedBuckets = reduceBuckets(reduceContext); // adding empty buckets if needed diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java index d436e13928746..0bb00d031221e 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java @@ -258,7 +258,7 @@ public List getBuckets() { } @Override - public InternalAggregation reduce(ReduceContext reduceContext) { + public InternalAggregation doReduce(ReduceContext reduceContext) { List aggregations = reduceContext.aggregations(); @SuppressWarnings("unchecked") List[] rangeList = new List[ranges.size()]; diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/InternalSignificantTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/InternalSignificantTerms.java index 199daecf5da16..53949937bbb19 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/InternalSignificantTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/InternalSignificantTerms.java @@ -156,7 +156,7 @@ public SignificantTerms.Bucket getBucketByKey(String term) { } @Override - public InternalAggregation reduce(ReduceContext reduceContext) { + public InternalAggregation doReduce(ReduceContext reduceContext) { List aggregations = reduceContext.aggregations(); long globalSubsetSize = 0; diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/UnmappedSignificantTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/UnmappedSignificantTerms.java index c457c1331b83f..bb81274191302 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/UnmappedSignificantTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/UnmappedSignificantTerms.java @@ -68,10 +68,10 @@ public Type type() { } @Override - public InternalAggregation reduce(ReduceContext reduceContext) { + public InternalAggregation doReduce(ReduceContext reduceContext) { for (InternalAggregation aggregation : reduceContext.aggregations()) { if (!(aggregation instanceof UnmappedSignificantTerms)) { - return aggregation.reduce(reduceContext); + return aggregation.doReduce(reduceContext); } } return this; diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalTerms.java index b8b45e20ce783..a6ca9d4400c8c 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalTerms.java @@ -160,7 +160,7 @@ public long getSumOfOtherDocCounts() { } @Override - public InternalAggregation reduce(ReduceContext reduceContext) { + public InternalAggregation doReduce(ReduceContext reduceContext) { List aggregations = reduceContext.aggregations(); Multimap buckets = ArrayListMultimap.create(); diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedTerms.java index a515596868e88..1fffb9508a884 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedTerms.java @@ -81,10 +81,10 @@ protected void doWriteTo(StreamOutput out) throws IOException { } @Override - public InternalAggregation reduce(ReduceContext reduceContext) { + public InternalAggregation doReduce(ReduceContext reduceContext) { for (InternalAggregation agg : reduceContext.aggregations()) { if (!(agg instanceof UnmappedTerms)) { - return agg.reduce(reduceContext); + return agg.doReduce(reduceContext); } } return this; diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/avg/InternalAvg.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/avg/InternalAvg.java index dcdecbe3b6776..8c795a55332f2 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/avg/InternalAvg.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/avg/InternalAvg.java @@ -78,7 +78,7 @@ public Type type() { } @Override - public InternalAvg reduce(ReduceContext reduceContext) { + public InternalAvg doReduce(ReduceContext reduceContext) { long count = 0; double sum = 0; for (InternalAggregation aggregation : reduceContext.aggregations()) { diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/cardinality/InternalCardinality.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/cardinality/InternalCardinality.java index 2fd964e5f1f0b..c8341135fb4fb 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/cardinality/InternalCardinality.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/cardinality/InternalCardinality.java @@ -99,7 +99,7 @@ protected void doWriteTo(StreamOutput out) throws IOException { } @Override - public InternalAggregation reduce(ReduceContext reduceContext) { + public InternalAggregation doReduce(ReduceContext reduceContext) { List aggregations = reduceContext.aggregations(); InternalCardinality reduced = null; for (InternalAggregation aggregation : aggregations) { diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/geobounds/InternalGeoBounds.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/geobounds/InternalGeoBounds.java index cdda6597c14fd..eb6a61c960d5b 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/geobounds/InternalGeoBounds.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/geobounds/InternalGeoBounds.java @@ -73,7 +73,7 @@ public Type type() { } @Override - public InternalAggregation reduce(ReduceContext reduceContext) { + public InternalAggregation doReduce(ReduceContext reduceContext) { double top = Double.NEGATIVE_INFINITY; double bottom = Double.POSITIVE_INFINITY; double posLeft = Double.POSITIVE_INFINITY; diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/max/InternalMax.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/max/InternalMax.java index 90486f3b62065..7cae1444c6353 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/max/InternalMax.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/max/InternalMax.java @@ -76,7 +76,7 @@ public Type type() { } @Override - public InternalMax reduce(ReduceContext reduceContext) { + public InternalMax doReduce(ReduceContext reduceContext) { double max = Double.NEGATIVE_INFINITY; for (InternalAggregation aggregation : reduceContext.aggregations()) { max = Math.max(max, ((InternalMax) aggregation).max); diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/min/InternalMin.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/min/InternalMin.java index 554152e486c6d..0974314826c6b 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/min/InternalMin.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/min/InternalMin.java @@ -77,7 +77,7 @@ public Type type() { } @Override - public InternalMin reduce(ReduceContext reduceContext) { + public InternalMin doReduce(ReduceContext reduceContext) { double min = Double.POSITIVE_INFINITY; for (InternalAggregation aggregation : reduceContext.aggregations()) { min = Math.min(min, ((InternalMin) aggregation).min); diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/AbstractInternalPercentiles.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/AbstractInternalPercentiles.java index 67f33934bf6ba..19d056e00cdc2 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/AbstractInternalPercentiles.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/AbstractInternalPercentiles.java @@ -60,7 +60,7 @@ public double value(String name) { public abstract double value(double key); @Override - public AbstractInternalPercentiles reduce(ReduceContext reduceContext) { + public AbstractInternalPercentiles doReduce(ReduceContext reduceContext) { List aggregations = reduceContext.aggregations(); TDigestState merged = null; for (InternalAggregation aggregation : aggregations) { diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java index f204f8d5478af..c7176e0e1e163 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java @@ -81,7 +81,7 @@ public Object aggregation() { } @Override - public InternalAggregation reduce(ReduceContext reduceContext) { + public InternalAggregation doReduce(ReduceContext reduceContext) { List aggregationObjects = new ArrayList<>(); for (InternalAggregation aggregation : reduceContext.aggregations()) { InternalScriptedMetric mapReduceAggregation = (InternalScriptedMetric) aggregation; diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/InternalStats.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/InternalStats.java index 86bda11cd8e13..7186fee979ca6 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/InternalStats.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/InternalStats.java @@ -148,7 +148,7 @@ public double value(String name) { } @Override - public InternalStats reduce(ReduceContext reduceContext) { + public InternalStats doReduce(ReduceContext reduceContext) { long count = 0; double min = Double.POSITIVE_INFINITY; double max = Double.NEGATIVE_INFINITY; diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/extended/InternalExtendedStats.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/extended/InternalExtendedStats.java index 9a7006905304f..9f88bf4f42977 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/extended/InternalExtendedStats.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/extended/InternalExtendedStats.java @@ -143,13 +143,13 @@ public String getStdDeviationBoundAsString(Bounds bound) { } @Override - public InternalExtendedStats reduce(ReduceContext reduceContext) { + public InternalExtendedStats doReduce(ReduceContext reduceContext) { double sumOfSqrs = 0; for (InternalAggregation aggregation : reduceContext.aggregations()) { InternalExtendedStats stats = (InternalExtendedStats) aggregation; sumOfSqrs += stats.getSumOfSquares(); } - final InternalStats stats = super.reduce(reduceContext); + final InternalStats stats = super.doReduce(reduceContext); return new InternalExtendedStats(name, stats.getCount(), stats.getSum(), stats.getMin(), stats.getMax(), sumOfSqrs, sigma, valueFormatter, getMetaData()); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/sum/InternalSum.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/sum/InternalSum.java index b16663db26a3a..f653c082c797c 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/sum/InternalSum.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/sum/InternalSum.java @@ -76,7 +76,7 @@ public Type type() { } @Override - public InternalSum reduce(ReduceContext reduceContext) { + public InternalSum doReduce(ReduceContext reduceContext) { double sum = 0; for (InternalAggregation aggregation : reduceContext.aggregations()) { sum += ((InternalSum) aggregation).sum; diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/InternalTopHits.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/InternalTopHits.java index e4fad4ef692eb..8c5eafa29617a 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/InternalTopHits.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/InternalTopHits.java @@ -91,7 +91,7 @@ public SearchHits getHits() { } @Override - public InternalAggregation reduce(ReduceContext reduceContext) { + public InternalAggregation doReduce(ReduceContext reduceContext) { List aggregations = reduceContext.aggregations(); TopDocs[] shardDocs = new TopDocs[aggregations.size()]; InternalSearchHits[] shardHits = new InternalSearchHits[aggregations.size()]; diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/valuecount/InternalValueCount.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/valuecount/InternalValueCount.java index 062e88fce5f9d..b8b675c2eee0f 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/valuecount/InternalValueCount.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/valuecount/InternalValueCount.java @@ -76,7 +76,7 @@ public Type type() { } @Override - public InternalAggregation reduce(ReduceContext reduceContext) { + public InternalAggregation doReduce(ReduceContext reduceContext) { long valueCount = 0; for (InternalAggregation aggregation : reduceContext.aggregations()) { valueCount += ((InternalValueCount) aggregation).value; From c60bb4d73bd2ff67247291a58a44eaa50269cd51 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Wed, 11 Feb 2015 16:19:48 +0000 Subject: [PATCH 02/68] Adds reducers list to InternalAggregation.reduce() The list of reducers is fed through from the AggregatorFactory --- .../search/aggregations/Aggregator.java | 2 +- .../aggregations/AggregatorFactory.java | 29 ++-- .../aggregations/InternalAggregation.java | 16 ++- .../InternalMultiBucketAggregation.java | 5 +- .../aggregations/NonCollectingAggregator.java | 12 +- .../bucket/BucketsAggregator.java | 14 +- .../InternalSingleBucketAggregation.java | 5 +- .../bucket/SingleBucketAggregator.java | 7 +- .../bucket/children/InternalChildren.java | 9 +- .../children/ParentToChildrenAggregator.java | 25 ++-- .../bucket/filter/FilterAggregator.java | 23 ++-- .../bucket/filter/InternalFilter.java | 8 +- .../bucket/filters/FiltersAggregator.java | 26 ++-- .../bucket/filters/InternalFilters.java | 7 +- .../bucket/geogrid/GeoHashGridAggregator.java | 12 +- .../bucket/geogrid/GeoHashGridParser.java | 17 ++- .../bucket/geogrid/InternalGeoHashGrid.java | 8 +- .../bucket/global/GlobalAggregator.java | 17 ++- .../bucket/global/InternalGlobal.java | 8 +- .../bucket/histogram/HistogramAggregator.java | 23 ++-- .../histogram/InternalDateHistogram.java | 7 +- .../bucket/histogram/InternalHistogram.java | 13 +- .../bucket/missing/InternalMissing.java | 8 +- .../bucket/missing/MissingAggregator.java | 22 +-- .../bucket/nested/InternalNested.java | 9 +- .../bucket/nested/InternalReverseNested.java | 9 +- .../bucket/nested/NestedAggregator.java | 107 ++++++++------- .../nested/ReverseNestedAggregator.java | 63 +++++---- .../bucket/range/InternalRange.java | 13 +- .../bucket/range/RangeAggregator.java | 125 +++++++++--------- .../bucket/range/date/InternalDateRange.java | 11 +- .../range/geodistance/GeoDistanceParser.java | 13 +- .../geodistance/InternalGeoDistance.java | 11 +- .../bucket/range/ipv4/InternalIPv4Range.java | 11 +- ...balOrdinalsSignificantTermsAggregator.java | 43 +++--- .../significant/InternalSignificantTerms.java | 6 +- .../significant/SignificantLongTerms.java | 10 +- .../SignificantLongTermsAggregator.java | 17 ++- .../significant/SignificantStringTerms.java | 10 +- .../SignificantStringTermsAggregator.java | 16 ++- .../SignificantTermsAggregatorFactory.java | 40 ++++-- .../significant/UnmappedSignificantTerms.java | 6 +- .../terms/AbstractStringTermsAggregator.java | 15 ++- .../bucket/terms/DoubleTerms.java | 14 +- .../bucket/terms/DoubleTermsAggregator.java | 13 +- .../GlobalOrdinalsStringTermsAggregator.java | 20 +-- .../bucket/terms/InternalTerms.java | 11 +- .../aggregations/bucket/terms/LongTerms.java | 14 +- .../bucket/terms/LongTermsAggregator.java | 66 +++++---- .../bucket/terms/StringTerms.java | 14 +- .../bucket/terms/StringTermsAggregator.java | 13 +- .../bucket/terms/TermsAggregator.java | 6 +- .../bucket/terms/TermsAggregatorFactory.java | 45 ++++--- .../bucket/terms/UnmappedTerms.java | 9 +- .../metrics/InternalMetricsAggregation.java | 6 +- .../InternalNumericMetricsAggregation.java | 13 +- .../metrics/MetricsAggregator.java | 7 +- .../metrics/NumericMetricsAggregator.java | 17 ++- .../metrics/avg/AvgAggregator.java | 37 +++--- .../aggregations/metrics/avg/InternalAvg.java | 9 +- .../cardinality/CardinalityAggregator.java | 10 +- .../CardinalityAggregatorFactory.java | 13 +- .../cardinality/InternalCardinality.java | 8 +- .../geobounds/GeoBoundsAggregator.java | 22 +-- .../metrics/geobounds/InternalGeoBounds.java | 8 +- .../aggregations/metrics/max/InternalMax.java | 8 +- .../metrics/max/MaxAggregator.java | 35 ++--- .../aggregations/metrics/min/InternalMin.java | 8 +- .../metrics/min/MinAggregator.java | 35 ++--- .../AbstractInternalPercentiles.java | 9 +- .../AbstractPercentilesAggregator.java | 7 +- .../percentiles/InternalPercentileRanks.java | 10 +- .../percentiles/InternalPercentiles.java | 10 +- .../PercentileRanksAggregator.java | 24 ++-- .../percentiles/PercentilesAggregator.java | 21 ++- .../scripted/InternalScriptedMetric.java | 11 +- .../scripted/ScriptedMetricAggregator.java | 16 ++- .../metrics/stats/InternalStats.java | 7 +- .../metrics/stats/StatsAggegator.java | 61 +++++---- .../extended/ExtendedStatsAggregator.java | 30 +++-- .../stats/extended/InternalExtendedStats.java | 10 +- .../aggregations/metrics/sum/InternalSum.java | 8 +- .../metrics/sum/SumAggregator.java | 37 +++--- .../metrics/tophits/TopHitsAggregator.java | 17 ++- .../valuecount/InternalValueCount.java | 9 +- .../valuecount/ValueCountAggregator.java | 27 ++-- .../search/aggregations/reducers/Reducer.java | 63 +++++++++ .../aggregations/reducers/ReducerFactory.java | 88 ++++++++++++ .../ValuesSourceAggregatorFactory.java | 21 ++- .../SignificanceHeuristicTests.java | 24 +++- 90 files changed, 1185 insertions(+), 644 deletions(-) create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java diff --git a/src/main/java/org/elasticsearch/search/aggregations/Aggregator.java b/src/main/java/org/elasticsearch/search/aggregations/Aggregator.java index fd9519499a87c..bce1f9bc196ff 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/Aggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/Aggregator.java @@ -105,7 +105,7 @@ public static boolean descendsFromBucketAggregator(Aggregator parent) { * Build an empty aggregation. */ public abstract InternalAggregation buildEmptyAggregation(); - + /** Aggregation mode for sub aggregations. */ public enum SubAggCollectionMode { diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactory.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactory.java index 256700bada583..f49a328fd16db 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactory.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactory.java @@ -23,10 +23,13 @@ import org.elasticsearch.common.lease.Releasables; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.ObjectArray; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.internal.SearchContext.Lifetime; import java.io.IOException; +import java.util.Collections; +import java.util.List; import java.util.Map; /** @@ -38,6 +41,7 @@ public abstract class AggregatorFactory { protected String type; protected AggregatorFactory parent; protected AggregatorFactories factories = AggregatorFactories.EMPTY; + protected List reducers = Collections.emptyList(); protected Map metaData; /** @@ -79,7 +83,8 @@ public AggregatorFactory parent() { return parent; } - protected abstract Aggregator createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException; + protected abstract Aggregator createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, + List reducers, Map metaData) throws IOException; /** * Creates the aggregator @@ -92,7 +97,7 @@ public AggregatorFactory parent() { * @return The created aggregator */ public final Aggregator create(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket) throws IOException { - return createInternal(context, parent, collectsFromSingleBucket, this.metaData); + return createInternal(context, parent, collectsFromSingleBucket, this.reducers, this.metaData); } public void doValidate() { @@ -102,16 +107,18 @@ public void setMetaData(Map metaData) { this.metaData = metaData; } + + public void setReducers(List reducers) { + this.reducers = reducers; + } + + /** * Utility method. Given an {@link AggregatorFactory} that creates {@link Aggregator}s that only know how * to collect bucket 0, this returns an aggregator that can collect any bucket. */ protected static Aggregator asMultiBucketAggregator(final AggregatorFactory factory, final AggregationContext context, final Aggregator parent) throws IOException { - final Aggregator first = factory.create(context, parent, true); - final BigArrays bigArrays = context.bigArrays(); - return new Aggregator() { - - ObjectArray aggregators; + final Aggregator first = factory.create(context, parent, truegator> aggregators; ObjectArray collectors; { @@ -187,9 +194,9 @@ public void collect(int doc, long bucket) throws IOException { LeafBucketCollector collector = collectors.get(bucket); if (collector == null) { Aggregator aggregator = aggregators.get(bucket); - if (aggregator == null) { - aggregator = factory.create(context, parent, true); - aggregator.preCollection(); + if (aggregator == null) { + aggregator = factory.create(context, parent, true); + aggregator.preCollection(); aggregators.set(bucket, aggregator); } collector = aggregator.getLeafCollector(ctx); @@ -197,7 +204,7 @@ public void collect(int doc, long bucket) throws IOException { collectors.set(bucket, collector); } collector.collect(doc, 0); - } + } }; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/InternalAggregation.java b/src/main/java/org/elasticsearch/search/aggregations/InternalAggregation.java index 456b1b391b6b3..828a1a7ee0f89 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/InternalAggregation.java +++ b/src/main/java/org/elasticsearch/search/aggregations/InternalAggregation.java @@ -28,6 +28,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentBuilderString; import org.elasticsearch.script.ScriptService; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationPath; import java.io.IOException; @@ -116,6 +117,8 @@ public ScriptService scriptService() { protected Map metaData; + private List reducers; + /** Constructs an un initialized addAggregation (used for serialization) **/ protected InternalAggregation() {} @@ -124,8 +127,9 @@ protected InternalAggregation() {} * * @param name The name of the get. */ - protected InternalAggregation(String name, Map metaData) { + protected InternalAggregation(String name, List reducers, Map metaData) { this.name = name; + this.reducers = reducers; this.metaData = metaData; } @@ -146,7 +150,11 @@ public String getName() { * construction. */ public final InternalAggregation reduce(ReduceContext reduceContext) { - return doReduce(reduceContext); + InternalAggregation aggResult = doReduce(reduceContext); + for (Reducer reducer : reducers) { + aggResult = reducer.reduce(aggResult, reduceContext); + } + return aggResult; } public abstract InternalAggregation doReduce(ReduceContext reduceContext); @@ -180,6 +188,10 @@ public Map getMetaData() { return metaData; } + public List reducers() { + return reducers; + } + @Override public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(name); diff --git a/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java b/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java index bd6d8d2728c07..ebd2637ac5693 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java +++ b/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java @@ -21,6 +21,7 @@ import org.elasticsearch.ElasticsearchIllegalArgumentException; import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; import java.util.List; import java.util.Map; @@ -30,8 +31,8 @@ public abstract class InternalMultiBucketAggregation extends InternalAggregation public InternalMultiBucketAggregation() { } - public InternalMultiBucketAggregation(String name, Map metaData) { - super(name, metaData); + public InternalMultiBucketAggregation(String name, List reducers, Map metaData) { + super(name, reducers, metaData); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/NonCollectingAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/NonCollectingAggregator.java index 33c4215e27a51..9b64c647b381c 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/NonCollectingAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/NonCollectingAggregator.java @@ -20,9 +20,11 @@ package org.elasticsearch.search.aggregations; import org.apache.lucene.index.LeafReaderContext; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -31,12 +33,14 @@ */ public abstract class NonCollectingAggregator extends AggregatorBase { - protected NonCollectingAggregator(String name, AggregationContext context, Aggregator parent, AggregatorFactories subFactories, Map metaData) throws IOException { - super(name, subFactories, context, parent, metaData); + protected NonCollectingAggregator(String name, AggregationContext context, Aggregator parent, AggregatorFactories subFactories, + List reducers, Map metaData) throws IOException { + super(name, subFactories, context, parent, reducers, metaData); } - protected NonCollectingAggregator(String name, AggregationContext context, Aggregator parent, Map metaData) throws IOException { - this(name, context, parent, AggregatorFactories.EMPTY, metaData); + protected NonCollectingAggregator(String name, AggregationContext context, Aggregator parent, List reducers, + Map metaData) throws IOException { + this(name, context, parent, AggregatorFactories.EMPTY, reducers, metaData); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/BucketsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/BucketsAggregator.java index e4d0260cf933f..b7c8fe7ccfca0 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/BucketsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/BucketsAggregator.java @@ -21,6 +21,7 @@ import org.elasticsearch.common.lease.Releasable; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.IntArray; +import org.elasticsearch.search.aggregations.Aggregation; import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorBase; import org.elasticsearch.search.aggregations.AggregatorFactories; @@ -31,6 +32,7 @@ import java.io.IOException; import java.util.Arrays; +import java.util.List; import java.util.Map; /** @@ -42,9 +44,9 @@ public abstract class BucketsAggregator extends AggregatorBase { private IntArray docCounts; public BucketsAggregator(String name, AggregatorFactories factories, - AggregationContext context, Aggregator parent, Map metaData) throws IOException { - super(name, factories, context, parent, metaData); - bigArrays = context.bigArrays(); + AggregationContext context, Aggregator parent, + List reducers, Map metaData) throws IOException { + super(name, factories, context, parent, reducers, metaData); docCounts = bigArrays.newIntArray(1, true); } @@ -110,11 +112,11 @@ public final int bucketDocCount(long bucketOrd) { */ protected final InternalAggregations bucketAggregations(long bucket) throws IOException { final InternalAggregation[] aggregations = new InternalAggregation[subAggregators.length]; - for (int i = 0; i < subAggregators.length; i++) { + for (int i = 0; i < subAggregators.length; i++) { aggregations[i] = subAggregators[i].buildAggregation(bucket); - } + } return new InternalAggregations(Arrays.asList(aggregations)); - } + } /** * Utility method to build empty aggregations of the sub aggregators. diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/InternalSingleBucketAggregation.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/InternalSingleBucketAggregation.java index 2bccb4234e309..95157da9e77f1 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/InternalSingleBucketAggregation.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/InternalSingleBucketAggregation.java @@ -24,6 +24,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregations; +import org.elasticsearch.search.aggregations.reducers.Reducer; import java.io.IOException; import java.util.ArrayList; @@ -47,8 +48,8 @@ protected InternalSingleBucketAggregation() {} // for serialization * @param docCount The document count in the single bucket. * @param aggregations The already built sub-aggregations that are associated with the bucket. */ - protected InternalSingleBucketAggregation(String name, long docCount, InternalAggregations aggregations, Map metaData) { - super(name, metaData); + protected InternalSingleBucketAggregation(String name, long docCount, InternalAggregations aggregations, List reducers, Map metaData) { + super(name, reducers, metaData); this.docCount = docCount; this.aggregations = aggregations; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/SingleBucketAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/SingleBucketAggregator.java index d8b884a88e458..202f02c4a22e9 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/SingleBucketAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/SingleBucketAggregator.java @@ -20,9 +20,11 @@ import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorFactories; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -31,8 +33,9 @@ public abstract class SingleBucketAggregator extends BucketsAggregator { protected SingleBucketAggregator(String name, AggregatorFactories factories, - AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { - super(name, factories, aggregationContext, parent, metaData); + AggregationContext aggregationContext, Aggregator parent, + List reducers, Map metaData) throws IOException { + super(name, factories, aggregationContext, parent, reducers, metaData); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/children/InternalChildren.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/children/InternalChildren.java index 427637b9da7b8..cfac7f834bc8a 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/children/InternalChildren.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/children/InternalChildren.java @@ -23,8 +23,10 @@ import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.InternalSingleBucketAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -49,8 +51,9 @@ public static void registerStream() { public InternalChildren() { } - public InternalChildren(String name, long docCount, InternalAggregations aggregations, Map metaData) { - super(name, docCount, aggregations, metaData); + public InternalChildren(String name, long docCount, InternalAggregations aggregations, List reducers, + Map metaData) { + super(name, docCount, aggregations, reducers, metaData); } @Override @@ -60,6 +63,6 @@ public Type type() { @Override protected InternalSingleBucketAggregation newAggregation(String name, long docCount, InternalAggregations subAggregations) { - return new InternalChildren(name, docCount, subAggregations, getMetaData()); + return new InternalChildren(name, docCount, subAggregations, reducers(), getMetaData()); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/children/ParentToChildrenAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/children/ParentToChildrenAggregator.java index b4769f05bafd0..eefbd8534445b 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/children/ParentToChildrenAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/children/ParentToChildrenAggregator.java @@ -36,6 +36,7 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.NonCollectingAggregator; import org.elasticsearch.search.aggregations.bucket.SingleBucketAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; @@ -70,8 +71,9 @@ public class ParentToChildrenAggregator extends SingleBucketAggregator { public ParentToChildrenAggregator(String name, AggregatorFactories factories, AggregationContext aggregationContext, Aggregator parent, String parentType, Filter childFilter, Filter parentFilter, - ValuesSource.Bytes.WithOrdinals.ParentChild valuesSource, long maxOrd, Map metaData) throws IOException { - super(name, factories, aggregationContext, parent, metaData); + ValuesSource.Bytes.WithOrdinals.ParentChild valuesSource, + long maxOrd, List reducers, Map metaData) throws IOException { + super(name, factories, aggregationContext, parent, reducers, metaData); this.parentType = parentType; // these two filters are cached in the parser this.childFilter = childFilter; @@ -84,12 +86,13 @@ public ParentToChildrenAggregator(String name, AggregatorFactories factories, Ag @Override public InternalAggregation buildAggregation(long owningBucketOrdinal) throws IOException { - return new InternalChildren(name, bucketDocCount(owningBucketOrdinal), bucketAggregations(owningBucketOrdinal), metaData()); + return new InternalChildren(name, bucketDocCount(owningBucketOrdinal), bucketAggregations(owningBucketOrdinal), reducers(), + metaData()); } @Override public InternalAggregation buildEmptyAggregation() { - return new InternalChildren(name, 0, buildEmptySubAggregations(), metaData()); + return new InternalChildren(name, 0, buildEmptySubAggregations(), reducers(), metaData()); } @Override @@ -199,21 +202,25 @@ public Factory(String name, ValuesSourceConfig metaData) throws IOException { - return new NonCollectingAggregator(name, aggregationContext, parent, metaData) { + protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException { + return new NonCollectingAggregator(name, aggregationContext, parent, reducers, metaData) { @Override public InternalAggregation buildEmptyAggregation() { - return new InternalChildren(name, 0, buildEmptySubAggregations(), metaData()); + return new InternalChildren(name, 0, buildEmptySubAggregations(), reducers(), metaData()); } }; } @Override - protected Aggregator doCreateInternal(ValuesSource.Bytes.WithOrdinals.ParentChild valuesSource, AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { + protected Aggregator doCreateInternal(ValuesSource.Bytes.WithOrdinals.ParentChild valuesSource, + AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, List reducers, + Map metaData) throws IOException { long maxOrd = valuesSource.globalMaxOrd(aggregationContext.searchContext().searcher(), parentType); - return new ParentToChildrenAggregator(name, factories, aggregationContext, parent, parentType, childFilter, parentFilter, valuesSource, maxOrd, metaData); + return new ParentToChildrenAggregator(name, factories, aggregationContext, parent, parentType, childFilter, parentFilter, + valuesSource, maxOrd, reducers, metaData); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterAggregator.java index d5b15dba1cac7..da728f1ee044f 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterAggregator.java @@ -22,6 +22,7 @@ import org.apache.lucene.search.Filter; import org.apache.lucene.util.Bits; import org.elasticsearch.common.lucene.docset.DocIdSets; +import org.elasticsearch.search.aggregations.AggregationExecutionException; import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.aggregations.AggregatorFactory; @@ -29,9 +30,11 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.bucket.SingleBucketAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -45,9 +48,9 @@ public FilterAggregator(String name, org.apache.lucene.search.Filter filter, AggregatorFactories factories, AggregationContext aggregationContext, - Aggregator parent, + Aggregator parent, List reducers, Map metaData) throws IOException { - super(name, factories, aggregationContext, parent, metaData); + super(name, factories, aggregationContext, parent, reducers, metaData); this.filter = filter; } @@ -58,23 +61,24 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, // no need to provide deleted docs to the filter final Bits bits = DocIdSets.asSequentialAccessBits(ctx.reader().maxDoc(), filter.getDocIdSet(ctx, null)); return new LeafBucketCollectorBase(sub, null) { - @Override + @Override public void collect(int doc, long bucket) throws IOException { - if (bits.get(doc)) { + if (bits.get(doc)) { collectBucket(sub, doc, bucket); } - } + } }; } @Override public InternalAggregation buildAggregation(long owningBucketOrdinal) throws IOException { - return new InternalFilter(name, bucketDocCount(owningBucketOrdinal), bucketAggregations(owningBucketOrdinal), metaData()); + return new InternalFilter(name, bucketDocCount(owningBucketOrdinal), bucketAggregations(owningBucketOrdinal), reducers(), + metaData()); } @Override public InternalAggregation buildEmptyAggregation() { - return new InternalFilter(name, 0, buildEmptySubAggregations(), metaData()); + return new InternalFilter(name, 0, buildEmptySubAggregations(), reducers(), metaData()); } public static class Factory extends AggregatorFactory { @@ -87,8 +91,9 @@ public Factory(String name, Filter filter) { } @Override - public Aggregator createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { - return new FilterAggregator(name, filter, factories, context, parent, metaData); + public Aggregator createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, + List reducers, Map metaData) throws IOException { + return new FilterAggregator(name, filter, factories, context, parent, reducers, metaData); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/InternalFilter.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/InternalFilter.java index c3d84b9fe5106..0429ea20a59fb 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/InternalFilter.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/InternalFilter.java @@ -22,8 +22,10 @@ import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.InternalSingleBucketAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -48,8 +50,8 @@ public static void registerStreams() { InternalFilter() {} // for serialization - InternalFilter(String name, long docCount, InternalAggregations subAggregations, Map metaData) { - super(name, docCount, subAggregations, metaData); + InternalFilter(String name, long docCount, InternalAggregations subAggregations, List reducers, Map metaData) { + super(name, docCount, subAggregations, reducers, metaData); } @Override @@ -59,6 +61,6 @@ public Type type() { @Override protected InternalSingleBucketAggregation newAggregation(String name, long docCount, InternalAggregations subAggregations) { - return new InternalFilter(name, docCount, subAggregations, getMetaData()); + return new InternalFilter(name, docCount, subAggregations, reducers(), getMetaData()); } } \ No newline at end of file diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/FiltersAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/FiltersAggregator.java index b97a5442cedf9..931ead734fb1f 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/FiltersAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/FiltersAggregator.java @@ -25,6 +25,7 @@ import org.apache.lucene.search.Filter; import org.apache.lucene.util.Bits; import org.elasticsearch.common.lucene.docset.DocIdSets; +import org.elasticsearch.search.aggregations.AggregationExecutionException; import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.aggregations.AggregatorFactory; @@ -33,6 +34,7 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.bucket.BucketsAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import java.io.IOException; @@ -59,8 +61,9 @@ static class KeyedFilter { private final boolean keyed; public FiltersAggregator(String name, AggregatorFactories factories, List filters, boolean keyed, AggregationContext aggregationContext, - Aggregator parent, Map metaData) throws IOException { - super(name, factories, aggregationContext, parent, metaData); + Aggregator parent, List reducers, Map metaData) + throws IOException { + super(name, factories, aggregationContext, parent, reducers, metaData); this.keyed = keyed; this.filters = filters.toArray(new KeyedFilter[filters.size()]); } @@ -73,16 +76,16 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final Bits[] bits = new Bits[filters.length]; for (int i = 0; i < filters.length; ++i) { bits[i] = DocIdSets.asSequentialAccessBits(ctx.reader().maxDoc(), filters[i].filter.getDocIdSet(ctx, null)); - } + } return new LeafBucketCollectorBase(sub, null) { - @Override + @Override public void collect(int doc, long bucket) throws IOException { - for (int i = 0; i < bits.length; i++) { - if (bits[i].get(doc)) { + for (int i = 0; i < bits.length; i++) { + if (bits[i].get(doc)) { collectBucket(sub, doc, bucketOrd(bucket, i)); } - } } + } }; } @@ -95,7 +98,7 @@ public InternalAggregation buildAggregation(long owningBucketOrdinal) throws IOE InternalFilters.Bucket bucket = new InternalFilters.Bucket(filter.key, bucketDocCount(bucketOrd), bucketAggregations(bucketOrd), keyed); buckets.add(bucket); } - return new InternalFilters(name, buckets, keyed, metaData()); + return new InternalFilters(name, buckets, keyed, reducers(), metaData()); } @Override @@ -106,7 +109,7 @@ public InternalAggregation buildEmptyAggregation() { InternalFilters.Bucket bucket = new InternalFilters.Bucket(filters[i].key, 0, subAggs, keyed); buckets.add(bucket); } - return new InternalFilters(name, buckets, keyed, metaData()); + return new InternalFilters(name, buckets, keyed, reducers(), metaData()); } final long bucketOrd(long owningBucketOrdinal, int filterOrd) { @@ -125,8 +128,9 @@ public Factory(String name, List filters, boolean keyed) { } @Override - public Aggregator createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { - return new FiltersAggregator(name, factories, filters, keyed, context, parent, metaData); + public Aggregator createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, + List reducers, Map metaData) throws IOException { + return new FiltersAggregator(name, factories, filters, keyed, context, parent, reducers, metaData); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/InternalFilters.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/InternalFilters.java index 505547487a642..1e4c882ef5f1b 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/InternalFilters.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/InternalFilters.java @@ -31,6 +31,7 @@ import org.elasticsearch.search.aggregations.InternalMultiBucketAggregation; import org.elasticsearch.search.aggregations.bucket.BucketStreamContext; import org.elasticsearch.search.aggregations.bucket.BucketStreams; +import org.elasticsearch.search.aggregations.reducers.Reducer; import java.io.IOException; import java.util.ArrayList; @@ -163,8 +164,8 @@ public void writeTo(StreamOutput out) throws IOException { public InternalFilters() {} // for serialization - public InternalFilters(String name, List buckets, boolean keyed, Map metaData) { - super(name, metaData); + public InternalFilters(String name, List buckets, boolean keyed, List reducers, Map metaData) { + super(name, reducers, metaData); this.buckets = buckets; this.keyed = keyed; } @@ -211,7 +212,7 @@ public InternalAggregation doReduce(ReduceContext reduceContext) { } } - InternalFilters reduced = new InternalFilters(name, new ArrayList(bucketsList.size()), keyed, getMetaData()); + InternalFilters reduced = new InternalFilters(name, new ArrayList(bucketsList.size()), keyed, reducers(), getMetaData()); for (List sameRangeList : bucketsList) { reduced.buckets.add((sameRangeList.get(0)).reduce(sameRangeList, reduceContext)); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridAggregator.java index 7e9f468220725..c2c646f570259 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridAggregator.java @@ -28,12 +28,14 @@ import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.bucket.BucketsAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import java.io.IOException; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Map; /** @@ -49,8 +51,10 @@ public class GeoHashGridAggregator extends BucketsAggregator { private final LongHash bucketOrds; public GeoHashGridAggregator(String name, AggregatorFactories factories, ValuesSource.Numeric valuesSource, - int requiredSize, int shardSize, AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { - super(name, factories, aggregationContext, parent, metaData); + int requiredSize, + int shardSize, AggregationContext aggregationContext, Aggregator parent, List reducers, Map metaData) + throws IOException { + super(name, factories, aggregationContext, parent, reducers, metaData); this.valuesSource = valuesSource; this.requiredSize = requiredSize; this.shardSize = shardSize; @@ -126,12 +130,12 @@ public InternalGeoHashGrid buildAggregation(long owningBucketOrdinal) throws IOE bucket.aggregations = bucketAggregations(bucket.bucketOrd); list[i] = bucket; } - return new InternalGeoHashGrid(name, requiredSize, Arrays.asList(list), metaData()); + return new InternalGeoHashGrid(name, requiredSize, Arrays.asList(list), reducers(), metaData()); } @Override public InternalGeoHashGrid buildEmptyAggregation() { - return new InternalGeoHashGrid(name, requiredSize, Collections.emptyList(), metaData()); + return new InternalGeoHashGrid(name, requiredSize, Collections. emptyList(), reducers(), metaData()); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridParser.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridParser.java index e1ce0a38c13a6..24b6d490c9ff0 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridParser.java @@ -34,6 +34,7 @@ import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.NonCollectingAggregator; import org.elasticsearch.search.aggregations.bucket.BucketUtils; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; @@ -43,6 +44,7 @@ import java.io.IOException; import java.util.Collections; +import java.util.List; import java.util.Map; /** @@ -123,9 +125,11 @@ public GeoGridFactory(String name, ValuesSourceConfig con } @Override - protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { - final InternalAggregation aggregation = new InternalGeoHashGrid(name, requiredSize, Collections.emptyList(), metaData); - return new NonCollectingAggregator(name, aggregationContext, parent, metaData) { + protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException { + final InternalAggregation aggregation = new InternalGeoHashGrid(name, requiredSize, + Collections. emptyList(), reducers, metaData); + return new NonCollectingAggregator(name, aggregationContext, parent, reducers, metaData) { public InternalAggregation buildEmptyAggregation() { return aggregation; } @@ -133,12 +137,15 @@ public InternalAggregation buildEmptyAggregation() { } @Override - protected Aggregator doCreateInternal(final ValuesSource.GeoPoint valuesSource, AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { + protected Aggregator doCreateInternal(final ValuesSource.GeoPoint valuesSource, AggregationContext aggregationContext, + Aggregator parent, boolean collectsFromSingleBucket, List reducers, Map metaData) + throws IOException { if (collectsFromSingleBucket == false) { return asMultiBucketAggregator(this, aggregationContext, parent); } ValuesSource.Numeric cellIdSource = new CellIdSource(valuesSource, precision); - return new GeoHashGridAggregator(name, factories, cellIdSource, requiredSize, shardSize, aggregationContext, parent, metaData); + return new GeoHashGridAggregator(name, factories, cellIdSource, requiredSize, shardSize, aggregationContext, parent, reducers, + metaData); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoHashGrid.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoHashGrid.java index c30935c5c3d76..83428f8c2092e 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoHashGrid.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoHashGrid.java @@ -32,6 +32,7 @@ import org.elasticsearch.search.aggregations.InternalMultiBucketAggregation; import org.elasticsearch.search.aggregations.bucket.BucketStreamContext; import org.elasticsearch.search.aggregations.bucket.BucketStreams; +import org.elasticsearch.search.aggregations.reducers.Reducer; import java.io.IOException; import java.util.ArrayList; @@ -170,8 +171,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws InternalGeoHashGrid() { } // for serialization - public InternalGeoHashGrid(String name, int requiredSize, Collection buckets, Map metaData) { - super(name, metaData); + public InternalGeoHashGrid(String name, int requiredSize, Collection buckets, List reducers, + Map metaData) { + super(name, reducers, metaData); this.requiredSize = requiredSize; this.buckets = buckets; } @@ -218,7 +220,7 @@ public InternalGeoHashGrid doReduce(ReduceContext reduceContext) { for (int i = ordered.size() - 1; i >= 0; i--) { list[i] = ordered.pop(); } - return new InternalGeoHashGrid(getName(), requiredSize, Arrays.asList(list), getMetaData()); + return new InternalGeoHashGrid(getName(), requiredSize, Arrays.asList(list), reducers(), getMetaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/global/GlobalAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/global/GlobalAggregator.java index 7862eade5d6ff..edecdd749ddf3 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/global/GlobalAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/global/GlobalAggregator.java @@ -28,9 +28,11 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.bucket.SingleBucketAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -38,8 +40,9 @@ */ public class GlobalAggregator extends SingleBucketAggregator { - public GlobalAggregator(String name, AggregatorFactories subFactories, AggregationContext aggregationContext, Map metaData) throws IOException { - super(name, subFactories, aggregationContext, null, metaData); + public GlobalAggregator(String name, AggregatorFactories subFactories, AggregationContext aggregationContext, List reducers, + Map metaData) throws IOException { + super(name, subFactories, aggregationContext, null, reducers, metaData); } @Override @@ -50,14 +53,15 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, public void collect(int doc, long bucket) throws IOException { assert bucket == 0 : "global aggregator can only be a top level aggregator"; collectBucket(sub, doc, bucket); - } + } }; } @Override public InternalAggregation buildAggregation(long owningBucketOrdinal) throws IOException { assert owningBucketOrdinal == 0 : "global aggregator can only be a top level aggregator"; - return new InternalGlobal(name, bucketDocCount(owningBucketOrdinal), bucketAggregations(owningBucketOrdinal), metaData()); + return new InternalGlobal(name, bucketDocCount(owningBucketOrdinal), bucketAggregations(owningBucketOrdinal), reducers(), + metaData()); } @Override @@ -72,7 +76,8 @@ public Factory(String name) { } @Override - public Aggregator createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { + public Aggregator createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, + List reducers, Map metaData) throws IOException { if (parent != null) { throw new AggregationExecutionException("Aggregation [" + parent.name() + "] cannot have a global " + "sub-aggregation [" + name + "]. Global aggregations can only be defined as top level aggregations"); @@ -80,7 +85,7 @@ public Aggregator createInternal(AggregationContext context, Aggregator parent, if (collectsFromSingleBucket == false) { throw new ElasticsearchIllegalStateException(); } - return new GlobalAggregator(name, factories, context, metaData); + return new GlobalAggregator(name, factories, context, reducers, metaData); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/global/InternalGlobal.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/global/InternalGlobal.java index 6e317f2695286..157d2c5c7f968 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/global/InternalGlobal.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/global/InternalGlobal.java @@ -22,8 +22,10 @@ import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.InternalSingleBucketAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -49,8 +51,8 @@ public static void registerStreams() { InternalGlobal() {} // for serialization - InternalGlobal(String name, long docCount, InternalAggregations aggregations, Map metaData) { - super(name, docCount, aggregations, metaData); + InternalGlobal(String name, long docCount, InternalAggregations aggregations, List reducers, Map metaData) { + super(name, docCount, aggregations, reducers, metaData); } @Override @@ -60,6 +62,6 @@ public Type type() { @Override protected InternalSingleBucketAggregation newAggregation(String name, long docCount, InternalAggregations subAggregations) { - return new InternalGlobal(name, docCount, subAggregations, getMetaData()); + return new InternalGlobal(name, docCount, subAggregations, reducers(), getMetaData()); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/HistogramAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/HistogramAggregator.java index a39a488a615ce..0a6a8bce73296 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/HistogramAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/HistogramAggregator.java @@ -31,6 +31,7 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.bucket.BucketsAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; @@ -62,9 +63,10 @@ public HistogramAggregator(String name, AggregatorFactories factories, Rounding boolean keyed, long minDocCount, @Nullable ExtendedBounds extendedBounds, @Nullable ValuesSource.Numeric valuesSource, @Nullable ValueFormatter formatter, InternalHistogram.Factory histogramFactory, - AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { + AggregationContext aggregationContext, + Aggregator parent, List reducers, Map metaData) throws IOException { - super(name, factories, aggregationContext, parent, metaData); + super(name, factories, aggregationContext, parent, reducers, metaData); this.rounding = rounding; this.order = order; this.keyed = keyed; @@ -130,13 +132,14 @@ public InternalAggregation buildAggregation(long owningBucketOrdinal) throws IOE // value source will be null for unmapped fields InternalHistogram.EmptyBucketInfo emptyBucketInfo = minDocCount == 0 ? new InternalHistogram.EmptyBucketInfo(rounding, buildEmptySubAggregations(), extendedBounds) : null; - return histogramFactory.create(name, buckets, order, minDocCount, emptyBucketInfo, formatter, keyed, metaData()); + return histogramFactory.create(name, buckets, order, minDocCount, emptyBucketInfo, formatter, keyed, reducers(), metaData()); } @Override public InternalAggregation buildEmptyAggregation() { InternalHistogram.EmptyBucketInfo emptyBucketInfo = minDocCount == 0 ? new InternalHistogram.EmptyBucketInfo(rounding, buildEmptySubAggregations(), extendedBounds) : null; - return histogramFactory.create(name, Collections.emptyList(), order, minDocCount, emptyBucketInfo, formatter, keyed, metaData()); + return histogramFactory.create(name, Collections.emptyList(), order, minDocCount, emptyBucketInfo, formatter, keyed, reducers(), + metaData()); } @Override @@ -167,12 +170,15 @@ public Factory(String name, ValuesSourceConfig config, } @Override - protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { - return new HistogramAggregator(name, factories, rounding, order, keyed, minDocCount, null, null, config.formatter(), histogramFactory, aggregationContext, parent, metaData); + protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException { + return new HistogramAggregator(name, factories, rounding, order, keyed, minDocCount, null, null, config.formatter(), + histogramFactory, aggregationContext, parent, reducers, metaData); } @Override - protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { + protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, + boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException { if (collectsFromSingleBucket == false) { return asMultiBucketAggregator(this, aggregationContext, parent); } @@ -185,7 +191,8 @@ protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, Aggrega extendedBounds.processAndValidate(name, aggregationContext.searchContext(), config.parser()); roundedBounds = extendedBounds.round(rounding); } - return new HistogramAggregator(name, factories, rounding, order, keyed, minDocCount, roundedBounds, valuesSource, config.formatter(), histogramFactory, aggregationContext, parent, metaData); + return new HistogramAggregator(name, factories, rounding, order, keyed, minDocCount, roundedBounds, valuesSource, + config.formatter(), histogramFactory, aggregationContext, parent, reducers, metaData); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java index 63cab59ad6b20..9f9ad81c9530e 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java @@ -22,6 +22,7 @@ import org.elasticsearch.search.aggregations.InternalAggregation.Type; import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram.EmptyBucketInfo; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; @@ -75,8 +76,10 @@ public String type() { @Override public InternalHistogram create(String name, List buckets, InternalOrder order, - long minDocCount, EmptyBucketInfo emptyBucketInfo, @Nullable ValueFormatter formatter, boolean keyed, Map metaData) { - return new InternalHistogram(name, buckets, order, minDocCount, emptyBucketInfo, formatter, keyed, this, metaData); + long minDocCount, + EmptyBucketInfo emptyBucketInfo, @Nullable ValueFormatter formatter, boolean keyed, List reducers, + Map metaData) { + return new InternalHistogram(name, buckets, order, minDocCount, emptyBucketInfo, formatter, keyed, this, reducers, metaData); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java index 544f06998ce86..a33cdb49b3ca3 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java @@ -37,6 +37,7 @@ import org.elasticsearch.search.aggregations.InternalMultiBucketAggregation; import org.elasticsearch.search.aggregations.bucket.BucketStreamContext; import org.elasticsearch.search.aggregations.bucket.BucketStreams; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; @@ -233,8 +234,9 @@ public String type() { } public InternalHistogram create(String name, List buckets, InternalOrder order, long minDocCount, - EmptyBucketInfo emptyBucketInfo, @Nullable ValueFormatter formatter, boolean keyed, Map metaData) { - return new InternalHistogram<>(name, buckets, order, minDocCount, emptyBucketInfo, formatter, keyed, this, metaData); + EmptyBucketInfo emptyBucketInfo, @Nullable ValueFormatter formatter, boolean keyed, List reducers, + Map metaData) { + return new InternalHistogram<>(name, buckets, order, minDocCount, emptyBucketInfo, formatter, keyed, this, reducers, metaData); } public B createBucket(long key, long docCount, InternalAggregations aggregations, boolean keyed, @Nullable ValueFormatter formatter) { @@ -259,8 +261,8 @@ protected B createEmptyBucket(boolean keyed, @Nullable ValueFormatter formatter) InternalHistogram(String name, List buckets, InternalOrder order, long minDocCount, EmptyBucketInfo emptyBucketInfo, - @Nullable ValueFormatter formatter, boolean keyed, Factory factory, Map metaData) { - super(name, metaData); + @Nullable ValueFormatter formatter, boolean keyed, Factory factory, List reducers, Map metaData) { + super(name, reducers, metaData); this.buckets = buckets; this.order = order; assert (minDocCount == 0) == (emptyBucketInfo != null); @@ -432,7 +434,8 @@ public InternalAggregation doReduce(ReduceContext reduceContext) { CollectionUtil.introSort(reducedBuckets, order.comparator()); } - return getFactory().create(getName(), reducedBuckets, order, minDocCount, emptyBucketInfo, formatter, keyed, getMetaData()); + return getFactory().create(getName(), reducedBuckets, order, minDocCount, emptyBucketInfo, formatter, keyed, reducers(), + getMetaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/missing/InternalMissing.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/missing/InternalMissing.java index d314e44e90122..0245f117835a5 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/missing/InternalMissing.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/missing/InternalMissing.java @@ -22,8 +22,10 @@ import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.InternalSingleBucketAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -50,8 +52,8 @@ public static void registerStreams() { InternalMissing() { } - InternalMissing(String name, long docCount, InternalAggregations aggregations, Map metaData) { - super(name, docCount, aggregations, metaData); + InternalMissing(String name, long docCount, InternalAggregations aggregations, List reducers, Map metaData) { + super(name, docCount, aggregations, reducers, metaData); } @Override @@ -61,6 +63,6 @@ public Type type() { @Override protected InternalSingleBucketAggregation newAggregation(String name, long docCount, InternalAggregations subAggregations) { - return new InternalMissing(name, docCount, subAggregations, getMetaData()); + return new InternalMissing(name, docCount, subAggregations, reducers(), getMetaData()); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/missing/MissingAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/missing/MissingAggregator.java index 1b65bde990402..eb81c6a5ec119 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/missing/MissingAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/missing/MissingAggregator.java @@ -26,12 +26,14 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.bucket.SingleBucketAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -42,8 +44,9 @@ public class MissingAggregator extends SingleBucketAggregator { private final ValuesSource valuesSource; public MissingAggregator(String name, AggregatorFactories factories, ValuesSource valuesSource, - AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { - super(name, factories, aggregationContext, parent, metaData); + AggregationContext aggregationContext, + Aggregator parent, List reducers, Map metaData) throws IOException { + super(name, factories, aggregationContext, parent, reducers, metaData); this.valuesSource = valuesSource; } @@ -69,12 +72,13 @@ public void collect(int doc, long bucket) throws IOException { @Override public InternalAggregation buildAggregation(long owningBucketOrdinal) throws IOException { - return new InternalMissing(name, bucketDocCount(owningBucketOrdinal), bucketAggregations(owningBucketOrdinal), metaData()); + return new InternalMissing(name, bucketDocCount(owningBucketOrdinal), bucketAggregations(owningBucketOrdinal), reducers(), + metaData()); } @Override public InternalAggregation buildEmptyAggregation() { - return new InternalMissing(name, 0, buildEmptySubAggregations(), metaData()); + return new InternalMissing(name, 0, buildEmptySubAggregations(), reducers(), metaData()); } public static class Factory extends ValuesSourceAggregatorFactory { @@ -84,13 +88,15 @@ public Factory(String name, ValuesSourceConfig valueSourceConfig) { } @Override - protected MissingAggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { - return new MissingAggregator(name, factories, null, aggregationContext, parent, metaData); + protected MissingAggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException { + return new MissingAggregator(name, factories, null, aggregationContext, parent, reducers, metaData); } @Override - protected MissingAggregator doCreateInternal(ValuesSource valuesSource, AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { - return new MissingAggregator(name, factories, valuesSource, aggregationContext, parent, metaData); + protected MissingAggregator doCreateInternal(ValuesSource valuesSource, AggregationContext aggregationContext, Aggregator parent, + boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException { + return new MissingAggregator(name, factories, valuesSource, aggregationContext, parent, reducers, metaData); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/InternalNested.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/InternalNested.java index 8b434a3fd245d..86ad26edab38e 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/InternalNested.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/InternalNested.java @@ -22,8 +22,10 @@ import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.InternalSingleBucketAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -49,8 +51,9 @@ public static void registerStream() { public InternalNested() { } - public InternalNested(String name, long docCount, InternalAggregations aggregations, Map metaData) { - super(name, docCount, aggregations, metaData); + public InternalNested(String name, long docCount, InternalAggregations aggregations, List reducers, + Map metaData) { + super(name, docCount, aggregations, reducers, metaData); } @Override @@ -60,6 +63,6 @@ public Type type() { @Override protected InternalSingleBucketAggregation newAggregation(String name, long docCount, InternalAggregations subAggregations) { - return new InternalNested(name, docCount, subAggregations, getMetaData()); + return new InternalNested(name, docCount, subAggregations, reducers(), getMetaData()); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/InternalReverseNested.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/InternalReverseNested.java index eec7345d317dd..6dfaad42b03f6 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/InternalReverseNested.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/InternalReverseNested.java @@ -22,8 +22,10 @@ import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.InternalSingleBucketAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -49,8 +51,9 @@ public static void registerStream() { public InternalReverseNested() { } - public InternalReverseNested(String name, long docCount, InternalAggregations aggregations, Map metaData) { - super(name, docCount, aggregations, metaData); + public InternalReverseNested(String name, long docCount, InternalAggregations aggregations, List reducers, + Map metaData) { + super(name, docCount, aggregations, reducers, metaData); } @Override @@ -60,6 +63,6 @@ public Type type() { @Override protected InternalSingleBucketAggregation newAggregation(String name, long docCount, InternalAggregations subAggregations) { - return new InternalReverseNested(name, docCount, subAggregations, getMetaData()); + return new InternalReverseNested(name, docCount, subAggregations, reducers(), getMetaData()); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregator.java index 3fa459525f205..459802f62a3b0 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregator.java @@ -39,9 +39,11 @@ import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.NonCollectingAggregator; import org.elasticsearch.search.aggregations.bucket.SingleBucketAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -55,8 +57,8 @@ public class NestedAggregator extends SingleBucketAggregator { private DocIdSetIterator childDocs; private BitSet parentDocs; - public NestedAggregator(String name, AggregatorFactories factories, ObjectMapper objectMapper, AggregationContext aggregationContext, Aggregator parentAggregator, Map metaData, FilterCachingPolicy filterCachingPolicy) throws IOException { - super(name, factories, aggregationContext, parentAggregator, metaData); + public NestedAggregator(String name, AggregatorFactories factories, ObjectMapper objectMapper, AggregationContext aggregationContext, Aggregator parentAggregator, List reducers, Map metaData, FilterCachingPolicy filterCachingPolicy) throws IOException { + super(name, factories, aggregationContext, parentAggregator, reducers, metaData); childFilter = aggregationContext.searchContext().filterCache().cache(objectMapper.nestedTypeFilter(), null, filterCachingPolicy); } @@ -64,68 +66,69 @@ public NestedAggregator(String name, AggregatorFactories factories, ObjectMapper public LeafBucketCollector getLeafCollector(final LeafReaderContext ctx, final LeafBucketCollector sub) throws IOException { // Reset parentFilter, so we resolve the parentDocs for each new segment being searched this.parentFilter = null; - // In ES if parent is deleted, then also the children are deleted. Therefore acceptedDocs can also null here. + // In ES if parent is deleted, then also the children are deleted. Therefore acceptedDocs can also null here. DocIdSet childDocIdSet = childFilter.getDocIdSet(ctx, null); - if (DocIdSets.isEmpty(childDocIdSet)) { - childDocs = null; - } else { - childDocs = childDocIdSet.iterator(); - } + if (DocIdSets.isEmpty(childDocIdSet)) { + childDocs = null; + } else { + childDocs = childDocIdSet.iterator(); + } return new LeafBucketCollectorBase(sub, null) { - @Override + @Override public void collect(int parentDoc, long bucket) throws IOException { - // here we translate the parent doc to a list of its nested docs, and then call super.collect for evey one of them so they'll be collected + // here we translate the parent doc to a list of its nested docs, and then call super.collect for evey one of them so they'll be collected - // if parentDoc is 0 then this means that this parent doesn't have child docs (b/c these appear always before the parent doc), so we can skip: - if (parentDoc == 0 || childDocs == null) { - return; - } - if (parentFilter == null) { - // The aggs are instantiated in reverse, first the most inner nested aggs and lastly the top level aggs - // So at the time a nested 'nested' aggs is parsed its closest parent nested aggs hasn't been constructed. - // So the trick is to set at the last moment just before needed and we can use its child filter as the - // parent filter. - - // Additional NOTE: Before this logic was performed in the setNextReader(...) method, but the the assumption - // that aggs instances are constructed in reverse doesn't hold when buckets are constructed lazily during - // aggs execution + // if parentDoc is 0 then this means that this parent doesn't have child docs (b/c these appear always before the parent doc), so we can skip: + if (parentDoc == 0 || childDocs == null) { + return; + } + if (parentFilter == null) { + // The aggs are instantiated in reverse, first the most inner nested aggs and lastly the top level aggs + // So at the time a nested 'nested' aggs is parsed its closest parent nested aggs hasn't been constructed. + // So the trick is to set at the last moment just before needed and we can use its child filter as the + // parent filter. + + // Additional NOTE: Before this logic was performed in the setNextReader(...) method, but the the assumption + // that aggs instances are constructed in reverse doesn't hold when buckets are constructed lazily during + // aggs execution Filter parentFilterNotCached = findClosestNestedPath(parent()); - if (parentFilterNotCached == null) { - parentFilterNotCached = NonNestedDocsFilter.INSTANCE; - } - parentFilter = context.searchContext().bitsetFilterCache().getBitDocIdSetFilter(parentFilterNotCached); + if (parentFilterNotCached == null) { + parentFilterNotCached = NonNestedDocsFilter.INSTANCE; + } + parentFilter = context.searchContext().bitsetFilterCache().getBitDocIdSetFilter(parentFilterNotCached); BitDocIdSet parentSet = parentFilter.getDocIdSet(ctx); - if (DocIdSets.isEmpty(parentSet)) { - // There are no parentDocs in the segment, so return and set childDocs to null, so we exit early for future invocations. - childDocs = null; - return; - } else { - parentDocs = parentSet.bits(); - } - } + if (DocIdSets.isEmpty(parentSet)) { + // There are no parentDocs in the segment, so return and set childDocs to null, so we exit early for future invocations. + childDocs = null; + return; + } else { + parentDocs = parentSet.bits(); + } + } - final int prevParentDoc = parentDocs.prevSetBit(parentDoc - 1); - int childDocId = childDocs.docID(); - if (childDocId <= prevParentDoc) { - childDocId = childDocs.advance(prevParentDoc + 1); - } + final int prevParentDoc = parentDocs.prevSetBit(parentDoc - 1); + int childDocId = childDocs.docID(); + if (childDocId <= prevParentDoc) { + childDocId = childDocs.advance(prevParentDoc + 1); + } - for (; childDocId < parentDoc; childDocId = childDocs.nextDoc()) { + for (; childDocId < parentDoc; childDocId = childDocs.nextDoc()) { collectBucket(sub, childDocId, bucket); } - } + } }; } @Override public InternalAggregation buildAggregation(long owningBucketOrdinal) throws IOException { - return new InternalNested(name, bucketDocCount(owningBucketOrdinal), bucketAggregations(owningBucketOrdinal), metaData()); + return new InternalNested(name, bucketDocCount(owningBucketOrdinal), bucketAggregations(owningBucketOrdinal), reducers(), + metaData()); } @Override public InternalAggregation buildEmptyAggregation() { - return new InternalNested(name, 0, buildEmptySubAggregations(), metaData()); + return new InternalNested(name, 0, buildEmptySubAggregations(), reducers(), metaData()); } private static Filter findClosestNestedPath(Aggregator parent) { @@ -151,33 +154,35 @@ public Factory(String name, String path, FilterCachingPolicy filterCachingPolicy } @Override - public Aggregator createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { + public Aggregator createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, + List reducers, Map metaData) throws IOException { if (collectsFromSingleBucket == false) { return asMultiBucketAggregator(this, context, parent); } MapperService.SmartNameObjectMapper mapper = context.searchContext().smartNameObjectMapper(path); if (mapper == null) { - return new Unmapped(name, context, parent, metaData); + return new Unmapped(name, context, parent, reducers, metaData); } ObjectMapper objectMapper = mapper.mapper(); if (objectMapper == null) { - return new Unmapped(name, context, parent, metaData); + return new Unmapped(name, context, parent, reducers, metaData); } if (!objectMapper.nested().isNested()) { throw new AggregationExecutionException("[nested] nested path [" + path + "] is not nested"); } - return new NestedAggregator(name, factories, objectMapper, context, parent, metaData, filterCachingPolicy); + return new NestedAggregator(name, factories, objectMapper, context, parent, reducers, metaData, filterCachingPolicy); } private final static class Unmapped extends NonCollectingAggregator { - public Unmapped(String name, AggregationContext context, Aggregator parent, Map metaData) throws IOException { - super(name, context, parent, metaData); + public Unmapped(String name, AggregationContext context, Aggregator parent, List reducers, Map metaData) + throws IOException { + super(name, context, parent, reducers, metaData); } @Override public InternalAggregation buildEmptyAggregation() { - return new InternalNested(name, 0, buildEmptySubAggregations(), metaData()); + return new InternalNested(name, 0, buildEmptySubAggregations(), reducers(), metaData()); } } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/ReverseNestedAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/ReverseNestedAggregator.java index 4dbeec5898f0b..b64abf55b109a 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/ReverseNestedAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/ReverseNestedAggregator.java @@ -40,9 +40,11 @@ import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.NonCollectingAggregator; import org.elasticsearch.search.aggregations.bucket.SingleBucketAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -52,8 +54,10 @@ public class ReverseNestedAggregator extends SingleBucketAggregator { private final BitDocIdSetFilter parentFilter; - public ReverseNestedAggregator(String name, AggregatorFactories factories, ObjectMapper objectMapper, AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { - super(name, factories, aggregationContext, parent, metaData); + public ReverseNestedAggregator(String name, AggregatorFactories factories, ObjectMapper objectMapper, + AggregationContext aggregationContext, Aggregator parent, List reducers, Map metaData) + throws IOException { + super(name, factories, aggregationContext, parent, reducers, metaData); if (objectMapper == null) { parentFilter = context.searchContext().bitsetFilterCache().getBitDocIdSetFilter(NonNestedDocsFilter.INSTANCE); } else { @@ -64,33 +68,33 @@ public ReverseNestedAggregator(String name, AggregatorFactories factories, Objec @Override protected LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final LeafBucketCollector sub) throws IOException { - // In ES if parent is deleted, then also the children are deleted, so the child docs this agg receives - // must belong to parent docs that is alive. For this reason acceptedDocs can be null here. + // In ES if parent is deleted, then also the children are deleted, so the child docs this agg receives + // must belong to parent docs that is alive. For this reason acceptedDocs can be null here. BitDocIdSet docIdSet = parentFilter.getDocIdSet(ctx); final BitSet parentDocs; - if (DocIdSets.isEmpty(docIdSet)) { + if (DocIdSets.isEmpty(docIdSet)) { return LeafBucketCollector.NO_OP_COLLECTOR; - } else { - parentDocs = docIdSet.bits(); - } + } else { + parentDocs = docIdSet.bits(); + } final LongIntOpenHashMap bucketOrdToLastCollectedParentDoc = new LongIntOpenHashMap(32); return new LeafBucketCollectorBase(sub, null) { - @Override + @Override public void collect(int childDoc, long bucket) throws IOException { - // fast forward to retrieve the parentDoc this childDoc belongs to - final int parentDoc = parentDocs.nextSetBit(childDoc); - assert childDoc <= parentDoc && parentDoc != DocIdSetIterator.NO_MORE_DOCS; + // fast forward to retrieve the parentDoc this childDoc belongs to + final int parentDoc = parentDocs.nextSetBit(childDoc); + assert childDoc <= parentDoc && parentDoc != DocIdSetIterator.NO_MORE_DOCS; if (bucketOrdToLastCollectedParentDoc.containsKey(bucket)) { - int lastCollectedParentDoc = bucketOrdToLastCollectedParentDoc.lget(); - if (parentDoc > lastCollectedParentDoc) { + int lastCollectedParentDoc = bucketOrdToLastCollectedParentDoc.lget(); + if (parentDoc > lastCollectedParentDoc) { collectBucket(sub, parentDoc, bucket); - bucketOrdToLastCollectedParentDoc.lset(parentDoc); - } - } else { + bucketOrdToLastCollectedParentDoc.lset(parentDoc); + } + } else { collectBucket(sub, parentDoc, bucket); bucketOrdToLastCollectedParentDoc.put(bucket, parentDoc); - } - } + } + } }; } @@ -105,12 +109,13 @@ private static NestedAggregator findClosestNestedAggregator(Aggregator parent) { @Override public InternalAggregation buildAggregation(long owningBucketOrdinal) throws IOException { - return new InternalReverseNested(name, bucketDocCount(owningBucketOrdinal), bucketAggregations(owningBucketOrdinal), metaData()); + return new InternalReverseNested(name, bucketDocCount(owningBucketOrdinal), bucketAggregations(owningBucketOrdinal), reducers(), + metaData()); } @Override public InternalAggregation buildEmptyAggregation() { - return new InternalReverseNested(name, 0, buildEmptySubAggregations(), metaData()); + return new InternalReverseNested(name, 0, buildEmptySubAggregations(), reducers(), metaData()); } Filter getParentFilter() { @@ -127,7 +132,8 @@ public Factory(String name, String path) { } @Override - public Aggregator createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { + public Aggregator createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, + List reducers, Map metaData) throws IOException { // Early validation NestedAggregator closestNestedAggregator = findClosestNestedAggregator(parent); if (closestNestedAggregator == null) { @@ -138,11 +144,11 @@ public Aggregator createInternal(AggregationContext context, Aggregator parent, if (path != null) { MapperService.SmartNameObjectMapper mapper = context.searchContext().smartNameObjectMapper(path); if (mapper == null) { - return new Unmapped(name, context, parent, metaData); + return new Unmapped(name, context, parent, reducers, metaData); } objectMapper = mapper.mapper(); if (objectMapper == null) { - return new Unmapped(name, context, parent, metaData); + return new Unmapped(name, context, parent, reducers, metaData); } if (!objectMapper.nested().isNested()) { throw new AggregationExecutionException("[reverse_nested] nested path [" + path + "] is not nested"); @@ -150,18 +156,19 @@ public Aggregator createInternal(AggregationContext context, Aggregator parent, } else { objectMapper = null; } - return new ReverseNestedAggregator(name, factories, objectMapper, context, parent, metaData); + return new ReverseNestedAggregator(name, factories, objectMapper, context, parent, reducers, metaData); } private final static class Unmapped extends NonCollectingAggregator { - public Unmapped(String name, AggregationContext context, Aggregator parent, Map metaData) throws IOException { - super(name, context, parent, metaData); + public Unmapped(String name, AggregationContext context, Aggregator parent, List reducers, Map metaData) + throws IOException { + super(name, context, parent, reducers, metaData); } @Override public InternalAggregation buildEmptyAggregation() { - return new InternalReverseNested(name, 0, buildEmptySubAggregations(), metaData()); + return new InternalReverseNested(name, 0, buildEmptySubAggregations(), reducers(), metaData()); } } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java index 0bb00d031221e..59277b9a42f82 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java @@ -31,6 +31,7 @@ import org.elasticsearch.search.aggregations.InternalMultiBucketAggregation; import org.elasticsearch.search.aggregations.bucket.BucketStreamContext; import org.elasticsearch.search.aggregations.bucket.BucketStreams; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; @@ -219,8 +220,9 @@ public String type() { return TYPE.name(); } - public R create(String name, List ranges, @Nullable ValueFormatter formatter, boolean keyed, Map metaData) { - return (R) new InternalRange<>(name, ranges, formatter, keyed, metaData); + public R create(String name, List ranges, @Nullable ValueFormatter formatter, boolean keyed, List reducers, + Map metaData) { + return (R) new InternalRange<>(name, ranges, formatter, keyed, reducers, metaData); } @@ -236,8 +238,9 @@ public B createBucket(String key, double from, double to, long docCount, Interna public InternalRange() {} // for serialization - public InternalRange(String name, List ranges, @Nullable ValueFormatter formatter, boolean keyed, Map metaData) { - super(name, metaData); + public InternalRange(String name, List ranges, @Nullable ValueFormatter formatter, boolean keyed, List reducers, + Map metaData) { + super(name, reducers, metaData); this.ranges = ranges; this.formatter = formatter; this.keyed = keyed; @@ -277,7 +280,7 @@ public InternalAggregation doReduce(ReduceContext reduceContext) { for (int i = 0; i < this.ranges.size(); ++i) { ranges.add((B) rangeList[i].get(0).reduce(rangeList[i], reduceContext)); } - return getFactory().create(name, ranges, formatter, keyed, getMetaData()); + return getFactory().create(name, ranges, formatter, keyed, reducers(), getMetaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/RangeAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/RangeAggregator.java index 47011b8dc4905..14fe9ddd3bca4 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/RangeAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/RangeAggregator.java @@ -33,6 +33,7 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.NonCollectingAggregator; import org.elasticsearch.search.aggregations.bucket.BucketsAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; @@ -104,10 +105,10 @@ public RangeAggregator(String name, List ranges, boolean keyed, AggregationContext aggregationContext, - Aggregator parent, + Aggregator parent, List reducers, Map metaData) throws IOException { - super(name, factories, aggregationContext, parent, metaData); + super(name, factories, aggregationContext, parent, reducers, metaData); assert valuesSource != null; this.valuesSource = valuesSource; this.formatter = format != null ? format.formatter() : null; @@ -139,64 +140,64 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final LeafBucketCollector sub) throws IOException { final SortedNumericDoubleValues values = valuesSource.doubleValues(ctx); return new LeafBucketCollectorBase(sub, values) { - @Override + @Override public void collect(int doc, long bucket) throws IOException { - values.setDocument(doc); - final int valuesCount = values.count(); - for (int i = 0, lo = 0; i < valuesCount; ++i) { - final double value = values.valueAt(i); + values.setDocument(doc); + final int valuesCount = values.count(); + for (int i = 0, lo = 0; i < valuesCount; ++i) { + final double value = values.valueAt(i); lo = collect(doc, value, bucket, lo); - } - } + } + } - private int collect(int doc, double value, long owningBucketOrdinal, int lowBound) throws IOException { - int lo = lowBound, hi = ranges.length - 1; // all candidates are between these indexes - int mid = (lo + hi) >>> 1; - while (lo <= hi) { - if (value < ranges[mid].from) { - hi = mid - 1; - } else if (value >= maxTo[mid]) { - lo = mid + 1; - } else { - break; - } - mid = (lo + hi) >>> 1; - } - if (lo > hi) return lo; // no potential candidate - - // binary search the lower bound - int startLo = lo, startHi = mid; - while (startLo <= startHi) { - final int startMid = (startLo + startHi) >>> 1; - if (value >= maxTo[startMid]) { - startLo = startMid + 1; - } else { - startHi = startMid - 1; - } - } + private int collect(int doc, double value, long owningBucketOrdinal, int lowBound) throws IOException { + int lo = lowBound, hi = ranges.length - 1; // all candidates are between these indexes + int mid = (lo + hi) >>> 1; + while (lo <= hi) { + if (value < ranges[mid].from) { + hi = mid - 1; + } else if (value >= maxTo[mid]) { + lo = mid + 1; + } else { + break; + } + mid = (lo + hi) >>> 1; + } + if (lo > hi) return lo; // no potential candidate + + // binary search the lower bound + int startLo = lo, startHi = mid; + while (startLo <= startHi) { + final int startMid = (startLo + startHi) >>> 1; + if (value >= maxTo[startMid]) { + startLo = startMid + 1; + } else { + startHi = startMid - 1; + } + } - // binary search the upper bound - int endLo = mid, endHi = hi; - while (endLo <= endHi) { - final int endMid = (endLo + endHi) >>> 1; - if (value < ranges[endMid].from) { - endHi = endMid - 1; - } else { - endLo = endMid + 1; - } - } + // binary search the upper bound + int endLo = mid, endHi = hi; + while (endLo <= endHi) { + final int endMid = (endLo + endHi) >>> 1; + if (value < ranges[endMid].from) { + endHi = endMid - 1; + } else { + endLo = endMid + 1; + } + } - assert startLo == lowBound || value >= maxTo[startLo - 1]; - assert endHi == ranges.length - 1 || value < ranges[endHi + 1].from; + assert startLo == lowBound || value >= maxTo[startLo - 1]; + assert endHi == ranges.length - 1 || value < ranges[endHi + 1].from; - for (int i = startLo; i <= endHi; ++i) { - if (ranges[i].matches(value)) { + for (int i = startLo; i <= endHi; ++i) { + if (ranges[i].matches(value)) { collectBucket(sub, doc, subBucketOrdinal(owningBucketOrdinal, i)); - } - } - - return endHi + 1; } + } + + return endHi + 1; + } }; } @@ -215,7 +216,7 @@ public InternalAggregation buildAggregation(long owningBucketOrdinal) throws IOE buckets.add(bucket); } // value source can be null in the case of unmapped fields - return rangeFactory.create(name, buckets, formatter, keyed, metaData()); + return rangeFactory.create(name, buckets, formatter, keyed, reducers(), metaData()); } @Override @@ -229,7 +230,7 @@ public InternalAggregation buildEmptyAggregation() { buckets.add(bucket); } // value source can be null in the case of unmapped fields - return rangeFactory.create(name, buckets, formatter, keyed, metaData()); + return rangeFactory.create(name, buckets, formatter, keyed, reducers(), metaData()); } private static final void sortRanges(final Range[] ranges) { @@ -266,10 +267,10 @@ public Unmapped(String name, ValueFormat format, AggregationContext context, Aggregator parent, - InternalRange.Factory factory, + InternalRange.Factory factory, List reducers, Map metaData) throws IOException { - super(name, context, parent, metaData); + super(name, context, parent, reducers, metaData); this.ranges = ranges; ValueParser parser = format != null ? format.parser() : ValueParser.RAW; for (Range range : this.ranges) { @@ -287,7 +288,7 @@ public InternalAggregation buildEmptyAggregation() { for (RangeAggregator.Range range : ranges) { buckets.add(factory.createBucket(range.key, range.from, range.to, 0, subAggs, keyed, formatter)); } - return factory.create(name, buckets, formatter, keyed, metaData()); + return factory.create(name, buckets, formatter, keyed, reducers(), metaData()); } } @@ -305,13 +306,15 @@ public Factory(String name, ValuesSourceConfig valueSource } @Override - protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { - return new Unmapped(name, ranges, keyed, config.format(), aggregationContext, parent, rangeFactory, metaData); + protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException { + return new Unmapped(name, ranges, keyed, config.format(), aggregationContext, parent, rangeFactory, reducers, metaData); } @Override - protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { - return new RangeAggregator(name, factories, valuesSource, config.format(), rangeFactory, ranges, keyed, aggregationContext, parent, metaData); + protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, + boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException { + return new RangeAggregator(name, factories, valuesSource, config.format(), rangeFactory, ranges, keyed, aggregationContext, parent, reducers, metaData); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/date/InternalDateRange.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/date/InternalDateRange.java index 785df76e82488..b679a6bc3d5a5 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/date/InternalDateRange.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/date/InternalDateRange.java @@ -26,6 +26,7 @@ import org.elasticsearch.search.aggregations.bucket.BucketStreamContext; import org.elasticsearch.search.aggregations.bucket.BucketStreams; import org.elasticsearch.search.aggregations.bucket.range.InternalRange; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; @@ -120,8 +121,9 @@ public String type() { } @Override - public InternalDateRange create(String name, List ranges, ValueFormatter formatter, boolean keyed, Map metaData) { - return new InternalDateRange(name, ranges, formatter, keyed, metaData); + public InternalDateRange create(String name, List ranges, ValueFormatter formatter, boolean keyed, + List reducers, Map metaData) { + return new InternalDateRange(name, ranges, formatter, keyed, reducers, metaData); } @Override @@ -132,8 +134,9 @@ public Bucket createBucket(String key, double from, double to, long docCount, In InternalDateRange() {} // for serialization - InternalDateRange(String name, List ranges, @Nullable ValueFormatter formatter, boolean keyed, Map metaData) { - super(name, ranges, formatter, keyed, metaData); + InternalDateRange(String name, List ranges, @Nullable ValueFormatter formatter, boolean keyed, + List reducers, Map metaData) { + super(name, ranges, formatter, keyed, reducers, metaData); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/GeoDistanceParser.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/GeoDistanceParser.java index 713b94595f5e2..fdaabb075cdd5 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/GeoDistanceParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/GeoDistanceParser.java @@ -35,6 +35,7 @@ import org.elasticsearch.search.aggregations.bucket.range.InternalRange; import org.elasticsearch.search.aggregations.bucket.range.RangeAggregator; import org.elasticsearch.search.aggregations.bucket.range.RangeAggregator.Unmapped; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.GeoPointParser; import org.elasticsearch.search.aggregations.support.ValuesSource; @@ -179,14 +180,18 @@ public GeoDistanceFactory(String name, ValuesSourceConfig } @Override - protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { - return new Unmapped(name, ranges, keyed, null, aggregationContext, parent, rangeFactory, metaData); + protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException { + return new Unmapped(name, ranges, keyed, null, aggregationContext, parent, rangeFactory, reducers, metaData); } @Override - protected Aggregator doCreateInternal(final ValuesSource.GeoPoint valuesSource, AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { + protected Aggregator doCreateInternal(final ValuesSource.GeoPoint valuesSource, AggregationContext aggregationContext, + Aggregator parent, boolean collectsFromSingleBucket, List reducers, Map metaData) + throws IOException { DistanceSource distanceSource = new DistanceSource(valuesSource, distanceType, origin, unit); - return new RangeAggregator(name, factories, distanceSource, null, rangeFactory, ranges, keyed, aggregationContext, parent, metaData); + return new RangeAggregator(name, factories, distanceSource, null, rangeFactory, ranges, keyed, aggregationContext, parent, + reducers, metaData); } private static class DistanceSource extends ValuesSource.Numeric { diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/InternalGeoDistance.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/InternalGeoDistance.java index da2c41d5233bb..0fef2e2ba0016 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/InternalGeoDistance.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/InternalGeoDistance.java @@ -26,6 +26,7 @@ import org.elasticsearch.search.aggregations.bucket.BucketStreamContext; import org.elasticsearch.search.aggregations.bucket.BucketStreams; import org.elasticsearch.search.aggregations.bucket.range.InternalRange; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.io.IOException; @@ -108,8 +109,9 @@ public String type() { } @Override - public InternalGeoDistance create(String name, List ranges, @Nullable ValueFormatter formatter, boolean keyed, Map metaData) { - return new InternalGeoDistance(name, ranges, formatter, keyed, metaData); + public InternalGeoDistance create(String name, List ranges, @Nullable ValueFormatter formatter, boolean keyed, + List reducers, Map metaData) { + return new InternalGeoDistance(name, ranges, formatter, keyed, reducers, metaData); } @Override @@ -120,8 +122,9 @@ public Bucket createBucket(String key, double from, double to, long docCount, In InternalGeoDistance() {} // for serialization - public InternalGeoDistance(String name, List ranges, @Nullable ValueFormatter formatter, boolean keyed, Map metaData) { - super(name, ranges, formatter, keyed, metaData); + public InternalGeoDistance(String name, List ranges, @Nullable ValueFormatter formatter, boolean keyed, List reducers, + Map metaData) { + super(name, ranges, formatter, keyed, reducers, metaData); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ipv4/InternalIPv4Range.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ipv4/InternalIPv4Range.java index 9b608aa42d49d..be2f8e52f8f4c 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ipv4/InternalIPv4Range.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ipv4/InternalIPv4Range.java @@ -26,6 +26,7 @@ import org.elasticsearch.search.aggregations.bucket.BucketStreamContext; import org.elasticsearch.search.aggregations.bucket.BucketStreams; import org.elasticsearch.search.aggregations.bucket.range.InternalRange; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.io.IOException; @@ -117,8 +118,9 @@ public String type() { } @Override - public InternalIPv4Range create(String name, List ranges, @Nullable ValueFormatter formatter, boolean keyed, Map metaData) { - return new InternalIPv4Range(name, ranges, keyed, metaData); + public InternalIPv4Range create(String name, List ranges, @Nullable ValueFormatter formatter, boolean keyed, + List reducers, Map metaData) { + return new InternalIPv4Range(name, ranges, keyed, reducers, metaData); } @Override @@ -129,8 +131,9 @@ public Bucket createBucket(String key, double from, double to, long docCount, In public InternalIPv4Range() {} // for serialization - public InternalIPv4Range(String name, List ranges, boolean keyed, Map metaData) { - super(name, ranges, ValueFormatter.IPv4, keyed, metaData); + public InternalIPv4Range(String name, List ranges, boolean keyed, List reducers, + Map metaData) { + super(name, ranges, ValueFormatter.IPv4, keyed, reducers, metaData); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/GlobalOrdinalsSignificantTermsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/GlobalOrdinalsSignificantTermsAggregator.java index fc8e5e4b7f743..c7e260faf63e3 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/GlobalOrdinalsSignificantTermsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/GlobalOrdinalsSignificantTermsAggregator.java @@ -29,6 +29,7 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.bucket.terms.GlobalOrdinalsStringTermsAggregator; import org.elasticsearch.search.aggregations.bucket.terms.support.IncludeExclude; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.internal.ContextIndexSearcher; @@ -36,6 +37,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Map; /** @@ -49,9 +51,10 @@ public class GlobalOrdinalsSignificantTermsAggregator extends GlobalOrdinalsStri public GlobalOrdinalsSignificantTermsAggregator(String name, AggregatorFactories factories, ValuesSource.Bytes.WithOrdinals.FieldData valuesSource, BucketCountThresholds bucketCountThresholds, IncludeExclude includeExclude, AggregationContext aggregationContext, Aggregator parent, - SignificantTermsAggregatorFactory termsAggFactory, Map metaData) throws IOException { + SignificantTermsAggregatorFactory termsAggFactory, List reducers, Map metaData) throws IOException { - super(name, factories, valuesSource, null, bucketCountThresholds, includeExclude, aggregationContext, parent, SubAggCollectionMode.DEPTH_FIRST, false, metaData); + super(name, factories, valuesSource, maxOrd, null, bucketCountThresholds, includeExclude, aggregationContext, parent, + SubAggCollectionMode.DEPTH_FIRST, false, reducers, metaData); this.termsAggFactory = termsAggFactory; } @@ -62,8 +65,8 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, @Override public void collect(int doc, long bucket) throws IOException { super.collect(doc, bucket); - numCollectedDocs++; - } + numCollectedDocs++; + } }; } @@ -124,7 +127,9 @@ public SignificantStringTerms buildAggregation(long owningBucketOrdinal) throws list[i] = bucket; } - return new SignificantStringTerms(subsetSize, supersetSize, name, bucketCountThresholds.getRequiredSize(), bucketCountThresholds.getMinDocCount(), termsAggFactory.getSignificanceHeuristic(), Arrays.asList(list), metaData()); + return new SignificantStringTerms(subsetSize, supersetSize, name, bucketCountThresholds.getRequiredSize(), + bucketCountThresholds.getMinDocCount(), termsAggFactory.getSignificanceHeuristic(), Arrays.asList(list), reducers(), + metaData()); } @Override @@ -133,7 +138,9 @@ public SignificantStringTerms buildEmptyAggregation() { ContextIndexSearcher searcher = context.searchContext().searcher(); IndexReader topReader = searcher.getIndexReader(); int supersetSize = topReader.numDocs(); - return new SignificantStringTerms(0, supersetSize, name, bucketCountThresholds.getRequiredSize(), bucketCountThresholds.getMinDocCount(), termsAggFactory.getSignificanceHeuristic(), Collections.emptyList(), metaData()); + return new SignificantStringTerms(0, supersetSize, name, bucketCountThresholds.getRequiredSize(), + bucketCountThresholds.getMinDocCount(), termsAggFactory.getSignificanceHeuristic(), + Collections. emptyList(), reducers(), metaData()); } @Override @@ -145,8 +152,8 @@ public static class WithHash extends GlobalOrdinalsSignificantTermsAggregator { private final LongHash bucketOrds; - public WithHash(String name, AggregatorFactories factories, ValuesSource.Bytes.WithOrdinals.FieldData valuesSource, BucketCountThresholds bucketCountThresholds, IncludeExclude includeExclude, AggregationContext aggregationContext, Aggregator parent, SignificantTermsAggregatorFactory termsAggFactory, Map metaData) throws IOException { - super(name, factories, valuesSource, bucketCountThresholds, includeExclude, aggregationContext, parent, termsAggFactory, metaData); + public WithHash(String name, AggregatorFactories factories, ValuesSource.Bytes.WithOrdinals.FieldData valuesSource, BucketCountThresholds bucketCountThresholds, IncludeExclude includeExclude, AggregationContext aggregationContext, Aggregator parent, SignificantTermsAggregatorFactory termsAggFactory, List reducers, Map metaData) throws IOException { + super(name, factories, valuesSource, bucketCountThresholds, includeExclude, aggregationContext, parent, termsAggFactory, reducers, metaData); bucketOrds = new LongHash(1, aggregationContext.bigArrays()); } @@ -157,20 +164,20 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, @Override public void collect(int doc, long bucket) throws IOException { assert bucket == 0; - numCollectedDocs++; - globalOrds.setDocument(doc); - final int numOrds = globalOrds.cardinality(); - for (int i = 0; i < numOrds; i++) { - final long globalOrd = globalOrds.ordAt(i); - long bucketOrd = bucketOrds.add(globalOrd); - if (bucketOrd < 0) { - bucketOrd = -1 - bucketOrd; + numCollectedDocs++; + globalOrds.setDocument(doc); + final int numOrds = globalOrds.cardinality(); + for (int i = 0; i < numOrds; i++) { + final long globalOrd = globalOrds.ordAt(i); + long bucketOrd = bucketOrds.add(globalOrd); + if (bucketOrd < 0) { + bucketOrd = -1 - bucketOrd; collectExistingBucket(sub, doc, bucketOrd); - } else { + } else { collectBucket(sub, doc, bucketOrd); } - } } + } }; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/InternalSignificantTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/InternalSignificantTerms.java index 53949937bbb19..91ad85364e723 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/InternalSignificantTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/InternalSignificantTerms.java @@ -27,6 +27,7 @@ import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.InternalMultiBucketAggregation; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristic; +import org.elasticsearch.search.aggregations.reducers.Reducer; import java.util.ArrayList; import java.util.Arrays; @@ -122,8 +123,9 @@ public double getSignificanceScore() { } } - protected InternalSignificantTerms(long subsetSize, long supersetSize, String name, int requiredSize, long minDocCount, SignificanceHeuristic significanceHeuristic, List buckets, Map metaData) { - super(name, metaData); + protected InternalSignificantTerms(long subsetSize, long supersetSize, String name, int requiredSize, long minDocCount, + SignificanceHeuristic significanceHeuristic, List buckets, List reducers, Map metaData) { + super(name, reducers, metaData); this.requiredSize = requiredSize; this.minDocCount = minDocCount; this.buckets = buckets; diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTerms.java index c4f97942ef2bf..bfb0b70458bf2 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTerms.java @@ -28,6 +28,7 @@ import org.elasticsearch.search.aggregations.bucket.BucketStreams; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristic; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristicStreams; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; @@ -159,9 +160,11 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } // for serialization public SignificantLongTerms(long subsetSize, long supersetSize, String name, @Nullable ValueFormatter formatter, - int requiredSize, long minDocCount, SignificanceHeuristic significanceHeuristic, List buckets, Map metaData) { + int requiredSize, + long minDocCount, SignificanceHeuristic significanceHeuristic, List buckets, + List reducers, Map metaData) { - super(subsetSize, supersetSize, name, requiredSize, minDocCount, significanceHeuristic, buckets, metaData); + super(subsetSize, supersetSize, name, requiredSize, minDocCount, significanceHeuristic, buckets, reducers, metaData); this.formatter = formatter; } @@ -173,7 +176,8 @@ public Type type() { @Override InternalSignificantTerms newAggregation(long subsetSize, long supersetSize, List buckets) { - return new SignificantLongTerms(subsetSize, supersetSize, getName(), formatter, requiredSize, minDocCount, significanceHeuristic, buckets, getMetaData()); + return new SignificantLongTerms(subsetSize, supersetSize, getName(), formatter, requiredSize, minDocCount, significanceHeuristic, + buckets, reducers(), getMetaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTermsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTermsAggregator.java index 0b8d5813721ff..f67c533956c40 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTermsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTermsAggregator.java @@ -28,6 +28,7 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.bucket.terms.LongTermsAggregator; import org.elasticsearch.search.aggregations.bucket.terms.support.IncludeExclude; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.format.ValueFormat; @@ -36,6 +37,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Map; /** @@ -45,9 +47,12 @@ public class SignificantLongTermsAggregator extends LongTermsAggregator { public SignificantLongTermsAggregator(String name, AggregatorFactories factories, ValuesSource.Numeric valuesSource, @Nullable ValueFormat format, BucketCountThresholds bucketCountThresholds, - AggregationContext aggregationContext, Aggregator parent, SignificantTermsAggregatorFactory termsAggFactory, IncludeExclude.LongFilter includeExclude, Map metaData) throws IOException { + AggregationContext aggregationContext, + Aggregator parent, SignificantTermsAggregatorFactory termsAggFactory, IncludeExclude.LongFilter includeExclude, + List reducers, Map metaData) throws IOException { - super(name, factories, valuesSource, format, null, bucketCountThresholds, aggregationContext, parent, SubAggCollectionMode.DEPTH_FIRST, false, includeExclude, metaData); + super(name, factories, valuesSource, format, null, bucketCountThresholds, aggregationContext, parent, + SubAggCollectionMode.DEPTH_FIRST, false, includeExclude, reducers, metaData); this.termsAggFactory = termsAggFactory; } @@ -102,7 +107,9 @@ public SignificantLongTerms buildAggregation(long owningBucketOrdinal) throws IO bucket.aggregations = bucketAggregations(bucket.bucketOrd); list[i] = bucket; } - return new SignificantLongTerms(subsetSize, supersetSize, name, formatter, bucketCountThresholds.getRequiredSize(), bucketCountThresholds.getMinDocCount(), termsAggFactory.getSignificanceHeuristic(), Arrays.asList(list), metaData()); + return new SignificantLongTerms(subsetSize, supersetSize, name, formatter, bucketCountThresholds.getRequiredSize(), + bucketCountThresholds.getMinDocCount(), termsAggFactory.getSignificanceHeuristic(), Arrays.asList(list), reducers(), + metaData()); } @Override @@ -111,7 +118,9 @@ public SignificantLongTerms buildEmptyAggregation() { ContextIndexSearcher searcher = context.searchContext().searcher(); IndexReader topReader = searcher.getIndexReader(); int supersetSize = topReader.numDocs(); - return new SignificantLongTerms(0, supersetSize, name, formatter, bucketCountThresholds.getRequiredSize(), bucketCountThresholds.getMinDocCount(), termsAggFactory.getSignificanceHeuristic(), Collections.emptyList(), metaData()); + return new SignificantLongTerms(0, supersetSize, name, formatter, bucketCountThresholds.getRequiredSize(), + bucketCountThresholds.getMinDocCount(), termsAggFactory.getSignificanceHeuristic(), + Collections. emptyList(), reducers(), metaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTerms.java index ff4d5c94e05a4..295fadd41b971 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTerms.java @@ -29,6 +29,7 @@ import org.elasticsearch.search.aggregations.bucket.BucketStreams; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristic; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristicStreams; +import org.elasticsearch.search.aggregations.reducers.Reducer; import java.io.IOException; import java.util.ArrayList; @@ -152,8 +153,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws SignificantStringTerms() {} // for serialization public SignificantStringTerms(long subsetSize, long supersetSize, String name, int requiredSize, - long minDocCount, SignificanceHeuristic significanceHeuristic, List buckets, Map metaData) { - super(subsetSize, supersetSize, name, requiredSize, minDocCount, significanceHeuristic, buckets, metaData); + long minDocCount, + SignificanceHeuristic significanceHeuristic, List buckets, List reducers, + Map metaData) { + super(subsetSize, supersetSize, name, requiredSize, minDocCount, significanceHeuristic, buckets, reducers, metaData); } @Override @@ -164,7 +167,8 @@ public Type type() { @Override InternalSignificantTerms newAggregation(long subsetSize, long supersetSize, List buckets) { - return new SignificantStringTerms(subsetSize, supersetSize, getName(), requiredSize, minDocCount, significanceHeuristic, buckets, getMetaData()); + return new SignificantStringTerms(subsetSize, supersetSize, getName(), requiredSize, minDocCount, significanceHeuristic, buckets, + reducers(), getMetaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTermsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTermsAggregator.java index fb65fd7d6f8e6..2638e82c6073c 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTermsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTermsAggregator.java @@ -28,6 +28,7 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.bucket.terms.StringTermsAggregator; import org.elasticsearch.search.aggregations.bucket.terms.support.IncludeExclude; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.internal.ContextIndexSearcher; @@ -35,6 +36,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Map; /** @@ -48,9 +50,11 @@ public class SignificantStringTermsAggregator extends StringTermsAggregator { public SignificantStringTermsAggregator(String name, AggregatorFactories factories, ValuesSource valuesSource, BucketCountThresholds bucketCountThresholds, IncludeExclude includeExclude, AggregationContext aggregationContext, Aggregator parent, - SignificantTermsAggregatorFactory termsAggFactory, Map metaData) throws IOException { + SignificantTermsAggregatorFactory termsAggFactory, List reducers, Map metaData) + throws IOException { - super(name, factories, valuesSource, null, bucketCountThresholds, includeExclude, aggregationContext, parent, SubAggCollectionMode.DEPTH_FIRST, false, metaData); + super(name, factories, valuesSource, null, bucketCountThresholds, includeExclude, aggregationContext, parent, + SubAggCollectionMode.DEPTH_FIRST, false, reducers, metaData); this.termsAggFactory = termsAggFactory; } @@ -107,7 +111,9 @@ public SignificantStringTerms buildAggregation(long owningBucketOrdinal) throws list[i] = bucket; } - return new SignificantStringTerms(subsetSize, supersetSize, name, bucketCountThresholds.getRequiredSize(), bucketCountThresholds.getMinDocCount(), termsAggFactory.getSignificanceHeuristic(), Arrays.asList(list), metaData()); + return new SignificantStringTerms(subsetSize, supersetSize, name, bucketCountThresholds.getRequiredSize(), + bucketCountThresholds.getMinDocCount(), termsAggFactory.getSignificanceHeuristic(), Arrays.asList(list), reducers(), + metaData()); } @Override @@ -116,7 +122,9 @@ public SignificantStringTerms buildEmptyAggregation() { ContextIndexSearcher searcher = context.searchContext().searcher(); IndexReader topReader = searcher.getIndexReader(); int supersetSize = topReader.numDocs(); - return new SignificantStringTerms(0, supersetSize, name, bucketCountThresholds.getRequiredSize(), bucketCountThresholds.getMinDocCount(), termsAggFactory.getSignificanceHeuristic(), Collections.emptyList(), metaData()); + return new SignificantStringTerms(0, supersetSize, name, bucketCountThresholds.getRequiredSize(), + bucketCountThresholds.getMinDocCount(), termsAggFactory.getSignificanceHeuristic(), + Collections. emptyList(), reducers(), metaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsAggregatorFactory.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsAggregatorFactory.java index 7536bd05b69ef..7b85a76b21f38 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsAggregatorFactory.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsAggregatorFactory.java @@ -39,6 +39,7 @@ import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristic; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregator; import org.elasticsearch.search.aggregations.bucket.terms.support.IncludeExclude; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; @@ -46,6 +47,7 @@ import org.elasticsearch.search.internal.SearchContext; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -64,8 +66,10 @@ public enum ExecutionMode { @Override Aggregator create(String name, AggregatorFactories factories, ValuesSource valuesSource, TermsAggregator.BucketCountThresholds bucketCountThresholds, IncludeExclude includeExclude, - AggregationContext aggregationContext, Aggregator parent, SignificantTermsAggregatorFactory termsAggregatorFactory, Map metaData) throws IOException { - return new SignificantStringTermsAggregator(name, factories, valuesSource, bucketCountThresholds, includeExclude, aggregationContext, parent, termsAggregatorFactory, metaData); + AggregationContext aggregationContext, Aggregator parent, SignificantTermsAggregatorFactory termsAggregatorFactory, + List reducers, Map metaData) throws IOException { + return new SignificantStringTermsAggregator(name, factories, valuesSource, bucketCountThresholds, includeExclude, + aggregationContext, parent, termsAggregatorFactory, reducers, metaData); } }, @@ -74,10 +78,11 @@ Aggregator create(String name, AggregatorFactories factories, ValuesSource value @Override Aggregator create(String name, AggregatorFactories factories, ValuesSource valuesSource, TermsAggregator.BucketCountThresholds bucketCountThresholds, IncludeExclude includeExclude, - AggregationContext aggregationContext, Aggregator parent, SignificantTermsAggregatorFactory termsAggregatorFactory, Map metaData) throws IOException { + AggregationContext aggregationContext, Aggregator parent, SignificantTermsAggregatorFactory termsAggregatorFactory, + List reducers, Map metaData) throws IOException { ValuesSource.Bytes.WithOrdinals valueSourceWithOrdinals = (ValuesSource.Bytes.WithOrdinals) valuesSource; IndexSearcher indexSearcher = aggregationContext.searchContext().searcher(); - return new GlobalOrdinalsSignificantTermsAggregator(name, factories, (ValuesSource.Bytes.WithOrdinals.FieldData) valuesSource, bucketCountThresholds, includeExclude, aggregationContext, parent, termsAggregatorFactory, metaData); + return new GlobalOrdinalsSignificantTermsAggregator(name, factories, (ValuesSource.Bytes.WithOrdinals.FieldData) valuesSource, bucketCountThresholds, includeExclude, aggregationContext, parent, termsAggregatorFactory, reducers, metaData); } }, @@ -86,8 +91,11 @@ Aggregator create(String name, AggregatorFactories factories, ValuesSource value @Override Aggregator create(String name, AggregatorFactories factories, ValuesSource valuesSource, TermsAggregator.BucketCountThresholds bucketCountThresholds, IncludeExclude includeExclude, - AggregationContext aggregationContext, Aggregator parent, SignificantTermsAggregatorFactory termsAggregatorFactory, Map metaData) throws IOException { - return new GlobalOrdinalsSignificantTermsAggregator.WithHash(name, factories, (ValuesSource.Bytes.WithOrdinals.FieldData) valuesSource, bucketCountThresholds, includeExclude, aggregationContext, parent, termsAggregatorFactory, metaData); + AggregationContext aggregationContext, Aggregator parent, SignificantTermsAggregatorFactory termsAggregatorFactory, + List reducers, Map metaData) throws IOException { + return new GlobalOrdinalsSignificantTermsAggregator.WithHash(name, factories, + (ValuesSource.Bytes.WithOrdinals.FieldData) valuesSource, bucketCountThresholds, includeExclude, + aggregationContext, parent, termsAggregatorFactory, reducers, metaData); } }; @@ -108,7 +116,8 @@ public static ExecutionMode fromString(String value) { abstract Aggregator create(String name, AggregatorFactories factories, ValuesSource valuesSource, TermsAggregator.BucketCountThresholds bucketCountThresholds, IncludeExclude includeExclude, - AggregationContext aggregationContext, Aggregator parent, SignificantTermsAggregatorFactory termsAggregatorFactory, Map metaData) throws IOException; + AggregationContext aggregationContext, Aggregator parent, SignificantTermsAggregatorFactory termsAggregatorFactory, + List reducers, Map metaData) throws IOException; @Override public String toString() { @@ -145,9 +154,11 @@ public SignificantTermsAggregatorFactory(String name, ValuesSourceConfig valueSo } @Override - protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { - final InternalAggregation aggregation = new UnmappedSignificantTerms(name, bucketCountThresholds.getRequiredSize(), bucketCountThresholds.getMinDocCount(), metaData); - return new NonCollectingAggregator(name, aggregationContext, parent, metaData) { + protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException { + final InternalAggregation aggregation = new UnmappedSignificantTerms(name, bucketCountThresholds.getRequiredSize(), + bucketCountThresholds.getMinDocCount(), reducers, metaData); + return new NonCollectingAggregator(name, aggregationContext, parent, reducers, metaData) { @Override public InternalAggregation buildEmptyAggregation() { return aggregation; @@ -156,7 +167,8 @@ public InternalAggregation buildEmptyAggregation() { } @Override - protected Aggregator doCreateInternal(ValuesSource valuesSource, AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { + protected Aggregator doCreateInternal(ValuesSource valuesSource, AggregationContext aggregationContext, Aggregator parent, + boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException { if (collectsFromSingleBucket == false) { return asMultiBucketAggregator(this, aggregationContext, parent); } @@ -179,7 +191,8 @@ protected Aggregator doCreateInternal(ValuesSource valuesSource, AggregationCont } } assert execution != null; - return execution.create(name, factories, valuesSource, bucketCountThresholds, includeExclude, aggregationContext, parent, this, metaData); + return execution.create(name, factories, valuesSource, bucketCountThresholds, includeExclude, aggregationContext, parent, this, + reducers, metaData); } @@ -197,7 +210,8 @@ protected Aggregator doCreateInternal(ValuesSource valuesSource, AggregationCont if (includeExclude != null) { longFilter = includeExclude.convertToLongFilter(); } - return new SignificantLongTermsAggregator(name, factories, (ValuesSource.Numeric) valuesSource, config.format(), bucketCountThresholds, aggregationContext, parent, this, longFilter, metaData); + return new SignificantLongTermsAggregator(name, factories, (ValuesSource.Numeric) valuesSource, config.format(), + bucketCountThresholds, aggregationContext, parent, this, longFilter, reducers, metaData); } throw new AggregationExecutionException("sigfnificant_terms aggregation cannot be applied to field [" + config.fieldContext().field() + diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/UnmappedSignificantTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/UnmappedSignificantTerms.java index bb81274191302..f382237dacfa3 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/UnmappedSignificantTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/UnmappedSignificantTerms.java @@ -24,9 +24,9 @@ import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.JLHScore; +import org.elasticsearch.search.aggregations.reducers.Reducer; import java.io.IOException; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; @@ -56,10 +56,10 @@ public static void registerStreams() { UnmappedSignificantTerms() {} // for serialization - public UnmappedSignificantTerms(String name, int requiredSize, long minDocCount, Map metaData) { + public UnmappedSignificantTerms(String name, int requiredSize, long minDocCount, List reducers, Map metaData) { //We pass zero for index/subset sizes because for the purpose of significant term analysis // we assume an unmapped index's size is irrelevant to the proceedings. - super(0, 0, name, requiredSize, minDocCount, JLHScore.INSTANCE, BUCKETS, metaData); + super(0, 0, name, requiredSize, minDocCount, JLHScore.INSTANCE, BUCKETS, reducers, metaData); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/AbstractStringTermsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/AbstractStringTermsAggregator.java index e87821e4e3885..363895c5a3980 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/AbstractStringTermsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/AbstractStringTermsAggregator.java @@ -22,27 +22,30 @@ import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import java.io.IOException; import java.util.Collections; +import java.util.List; import java.util.Map; abstract class AbstractStringTermsAggregator extends TermsAggregator { protected final boolean showTermDocCountError; - public AbstractStringTermsAggregator(String name, AggregatorFactories factories, - AggregationContext context, Aggregator parent, - Terms.Order order, BucketCountThresholds bucketCountThresholds, - SubAggCollectionMode subAggCollectMode, boolean showTermDocCountError, Map metaData) throws IOException { - super(name, factories, context, parent, bucketCountThresholds, order, subAggCollectMode, metaData); + public AbstractStringTermsAggregator(String name, AggregatorFactories factories, AggregationContext context, Aggregator parent, + Terms.Order order, BucketCountThresholds bucketCountThresholds, SubAggCollectionMode subAggCollectMode, + boolean showTermDocCountError, List reducers, Map metaData) throws IOException { + super(name, factories, context, parent, bucketCountThresholds, order, subAggCollectMode, reducers, metaData); this.showTermDocCountError = showTermDocCountError; } @Override public InternalAggregation buildEmptyAggregation() { - return new StringTerms(name, order, bucketCountThresholds.getRequiredSize(), bucketCountThresholds.getShardSize(), bucketCountThresholds.getMinDocCount(), Collections.emptyList(), showTermDocCountError, 0, 0, metaData()); + return new StringTerms(name, order, bucketCountThresholds.getRequiredSize(), bucketCountThresholds.getShardSize(), + bucketCountThresholds.getMinDocCount(), Collections. emptyList(), showTermDocCountError, 0, 0, + reducers(), metaData()); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTerms.java index c004f6e1e9006..0e6ca403407ef 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTerms.java @@ -27,6 +27,7 @@ import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.BucketStreamContext; import org.elasticsearch.search.aggregations.bucket.BucketStreams; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; @@ -156,8 +157,11 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws DoubleTerms() {} // for serialization - public DoubleTerms(String name, Terms.Order order, @Nullable ValueFormatter formatter, int requiredSize, int shardSize, long minDocCount, List buckets, boolean showTermDocCountError, long docCountError, long otherDocCount, Map metaData) { - super(name, order, requiredSize, shardSize, minDocCount, buckets, showTermDocCountError, docCountError, otherDocCount, metaData); + public DoubleTerms(String name, Terms.Order order, @Nullable ValueFormatter formatter, int requiredSize, int shardSize, + long minDocCount, List buckets, boolean showTermDocCountError, long docCountError, long otherDocCount, + List reducers, Map metaData) { + super(name, order, requiredSize, shardSize, minDocCount, buckets, showTermDocCountError, docCountError, otherDocCount, reducers, + metaData); this.formatter = formatter; } @@ -167,8 +171,10 @@ public Type type() { } @Override - protected InternalTerms newAggregation(String name, List buckets, boolean showTermDocCountError, long docCountError, long otherDocCount, Map metaData) { - return new DoubleTerms(name, order, formatter, requiredSize, shardSize, minDocCount, buckets, showTermDocCountError, docCountError, otherDocCount, metaData); + protected InternalTerms newAggregation(String name, List buckets, boolean showTermDocCountError, + long docCountError, long otherDocCount, List reducers, Map metaData) { + return new DoubleTerms(name, order, formatter, requiredSize, shardSize, minDocCount, buckets, showTermDocCountError, docCountError, + otherDocCount, reducers, metaData); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTermsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTermsAggregator.java index e71be14dc5bf9..ea98734b94e1a 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTermsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTermsAggregator.java @@ -26,6 +26,7 @@ import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.aggregations.bucket.terms.support.IncludeExclude; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSource.Numeric; @@ -33,6 +34,7 @@ import java.io.IOException; import java.util.Arrays; +import java.util.List; import java.util.Map; /** @@ -41,8 +43,11 @@ public class DoubleTermsAggregator extends LongTermsAggregator { public DoubleTermsAggregator(String name, AggregatorFactories factories, ValuesSource.Numeric valuesSource, @Nullable ValueFormat format, - Terms.Order order, BucketCountThresholds bucketCountThresholds, AggregationContext aggregationContext, Aggregator parent, SubAggCollectionMode collectionMode, boolean showTermDocCountError, IncludeExclude.LongFilter longFilter, Map metaData) throws IOException { - super(name, factories, valuesSource, format, order, bucketCountThresholds, aggregationContext, parent, collectionMode, showTermDocCountError, longFilter, metaData); + Terms.Order order, BucketCountThresholds bucketCountThresholds, + AggregationContext aggregationContext, Aggregator parent, SubAggCollectionMode collectionMode, boolean showTermDocCountError, + IncludeExclude.LongFilter longFilter, List reducers, Map metaData) throws IOException { + super(name, factories, valuesSource, format, order, bucketCountThresholds, aggregationContext, parent, collectionMode, + showTermDocCountError, longFilter, reducers, metaData); } @Override @@ -73,7 +78,9 @@ private static DoubleTerms convertToDouble(LongTerms terms) { for (int i = 0; i < buckets.length; ++i) { buckets[i] = convertToDouble(buckets[i]); } - return new DoubleTerms(terms.getName(), terms.order, terms.formatter, terms.requiredSize, terms.shardSize, terms.minDocCount, Arrays.asList(buckets), terms.showTermDocCountError, terms.docCountError, terms.otherDocCount, terms.getMetaData()); + return new DoubleTerms(terms.getName(), terms.order, terms.formatter, terms.requiredSize, terms.shardSize, terms.minDocCount, + Arrays.asList(buckets), terms.showTermDocCountError, terms.docCountError, terms.otherDocCount, terms.reducers(), + terms.getMetaData()); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/GlobalOrdinalsStringTermsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/GlobalOrdinalsStringTermsAggregator.java index 5b0ad6082b820..bff09e07e4a84 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/GlobalOrdinalsStringTermsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/GlobalOrdinalsStringTermsAggregator.java @@ -44,11 +44,13 @@ import org.elasticsearch.search.aggregations.bucket.terms.InternalTerms.Bucket; import org.elasticsearch.search.aggregations.bucket.terms.support.BucketPriorityQueue; import org.elasticsearch.search.aggregations.bucket.terms.support.IncludeExclude; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import java.io.IOException; import java.util.Arrays; +import java.util.List; import java.util.Map; /** @@ -71,8 +73,8 @@ public class GlobalOrdinalsStringTermsAggregator extends AbstractStringTermsAggr public GlobalOrdinalsStringTermsAggregator(String name, AggregatorFactories factories, ValuesSource.Bytes.WithOrdinals.FieldData valuesSource, Terms.Order order, BucketCountThresholds bucketCountThresholds, - IncludeExclude includeExclude, AggregationContext aggregationContext, Aggregator parent, SubAggCollectionMode collectionMode, boolean showTermDocCountError, Map metaData) throws IOException { - super(name, factories, aggregationContext, parent, order, bucketCountThresholds, collectionMode, showTermDocCountError, metaData); + IncludeExclude includeExclude, AggregationContext aggregationContext, Aggregator parent, SubAggCollectionMode collectionMode, boolean showTermDocCountError, List reducers, Map metaData) throws IOException { + super(name, factories, aggregationContext, parent, order, bucketCountThresholds, collectionMode, showTermDocCountError, reducers, reducers, metaData); this.valuesSource = valuesSource; this.includeExclude = includeExclude; } @@ -196,7 +198,9 @@ public InternalAggregation buildAggregation(long owningBucketOrdinal) throws IOE bucket.docCountError = 0; } - return new StringTerms(name, order, bucketCountThresholds.getRequiredSize(), bucketCountThresholds.getShardSize(), bucketCountThresholds.getMinDocCount(), Arrays.asList(list), showTermDocCountError, 0, otherDocCount, metaData()); + return new StringTerms(name, order, bucketCountThresholds.getRequiredSize(), bucketCountThresholds.getShardSize(), + bucketCountThresholds.getMinDocCount(), Arrays.asList(list), showTermDocCountError, 0, otherDocCount, reducers(), + metaData()); } /** @@ -261,8 +265,8 @@ public static class WithHash extends GlobalOrdinalsStringTermsAggregator { public WithHash(String name, AggregatorFactories factories, ValuesSource.Bytes.WithOrdinals.FieldData valuesSource, Terms.Order order, BucketCountThresholds bucketCountThresholds, IncludeExclude includeExclude, AggregationContext aggregationContext, - Aggregator parent, SubAggCollectionMode collectionMode, boolean showTermDocCountError, Map metaData) throws IOException { - super(name, factories, valuesSource, order, bucketCountThresholds, includeExclude, aggregationContext, parent, collectionMode, showTermDocCountError, metaData); + Aggregator parent, SubAggCollectionMode collectionMode, boolean showTermDocCountError, List reducers, Map metaData) throws IOException { + super(name, factories, valuesSource, order, bucketCountThresholds, includeExclude, aggregationContext, parent, collectionMode, showTermDocCountError, reducers, metaData); bucketOrds = new LongHash(1, aggregationContext.bigArrays()); } @@ -329,8 +333,8 @@ public static class LowCardinality extends GlobalOrdinalsStringTermsAggregator { private RandomAccessOrds segmentOrds; public LowCardinality(String name, AggregatorFactories factories, ValuesSource.Bytes.WithOrdinals.FieldData valuesSource, - Terms.Order order, BucketCountThresholds bucketCountThresholds, AggregationContext aggregationContext, Aggregator parent, SubAggCollectionMode collectionMode, boolean showTermDocCountError, Map metaData) throws IOException { - super(name, factories, valuesSource, order, bucketCountThresholds, null, aggregationContext, parent, collectionMode, showTermDocCountError, metaData); + Terms.Order order, BucketCountThresholds bucketCountThresholds, AggregationContext aggregationContext, Aggregator parent, SubAggCollectionMode collectionMode, boolean showTermDocCountError, List reducers, Map metaData) throws IOException { + super(name, factories, valuesSource, order, bucketCountThresholds, null, aggregationContext, parent, collectionMode, showTermDocCountError, reducers, metaData); assert factories == null || factories.count() == 0; this.segmentDocCounts = context.bigArrays().newIntArray(1, true); } @@ -409,7 +413,7 @@ private void mapSegmentCountsToGlobalCounts() { } final long ord = i - 1; // remember we do +1 when counting final long globalOrd = mapping == null ? ord : mapping.getGlobalOrd(ord); - incrementBucketDocCount(globalOrd, inc); + incrementBucketDocCount(globalOrd, inc); } } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalTerms.java index a6ca9d4400c8c..75b82d4778c9e 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalTerms.java @@ -31,6 +31,7 @@ import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.InternalMultiBucketAggregation; import org.elasticsearch.search.aggregations.bucket.terms.support.BucketPriorityQueue; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.util.ArrayList; @@ -121,8 +122,9 @@ public Bucket reduce(List buckets, ReduceContext context) { protected InternalTerms() {} // for serialization - protected InternalTerms(String name, Terms.Order order, int requiredSize, int shardSize, long minDocCount, List buckets, boolean showTermDocCountError, long docCountError, long otherDocCount, Map metaData) { - super(name, metaData); + protected InternalTerms(String name, Terms.Order order, int requiredSize, int shardSize, long minDocCount, List buckets, + boolean showTermDocCountError, long docCountError, long otherDocCount, List reducers, Map metaData) { + super(name, reducers, metaData); this.order = order; this.requiredSize = requiredSize; this.shardSize = shardSize; @@ -220,9 +222,10 @@ public InternalAggregation doReduce(ReduceContext reduceContext) { } else { docCountError = aggregations.size() == 1 ? 0 : sumDocCountError; } - return newAggregation(name, Arrays.asList(list), showTermDocCountError, docCountError, otherDocCount, getMetaData()); + return newAggregation(name, Arrays.asList(list), showTermDocCountError, docCountError, otherDocCount, reducers(), getMetaData()); } - protected abstract InternalTerms newAggregation(String name, List buckets, boolean showTermDocCountError, long docCountError, long otherDocCount, Map metaData); + protected abstract InternalTerms newAggregation(String name, List buckets, boolean showTermDocCountError, long docCountError, + long otherDocCount, List reducers, Map metaData); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTerms.java index 1a7c2b4d0eeb6..b8edad21dd98e 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTerms.java @@ -26,6 +26,7 @@ import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.BucketStreamContext; import org.elasticsearch.search.aggregations.bucket.BucketStreams; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; @@ -155,8 +156,11 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws LongTerms() {} // for serialization - public LongTerms(String name, Terms.Order order, @Nullable ValueFormatter formatter, int requiredSize, int shardSize, long minDocCount, List buckets, boolean showTermDocCountError, long docCountError, long otherDocCount, Map metaData) { - super(name, order, requiredSize, shardSize, minDocCount, buckets, showTermDocCountError, docCountError, otherDocCount, metaData); + public LongTerms(String name, Terms.Order order, @Nullable ValueFormatter formatter, int requiredSize, int shardSize, long minDocCount, + List buckets, boolean showTermDocCountError, long docCountError, long otherDocCount, + List reducers, Map metaData) { + super(name, order, requiredSize, shardSize, minDocCount, buckets, showTermDocCountError, docCountError, otherDocCount, reducers, + metaData); this.formatter = formatter; } @@ -166,8 +170,10 @@ public Type type() { } @Override - protected InternalTerms newAggregation(String name, List buckets, boolean showTermDocCountError, long docCountError, long otherDocCount, Map metaData) { - return new LongTerms(name, order, formatter, requiredSize, shardSize, minDocCount, buckets, showTermDocCountError, docCountError, otherDocCount, metaData); + protected InternalTerms newAggregation(String name, List buckets, boolean showTermDocCountError, + long docCountError, long otherDocCount, List reducers, Map metaData) { + return new LongTerms(name, order, formatter, requiredSize, shardSize, minDocCount, buckets, showTermDocCountError, docCountError, + otherDocCount, reducers, metaData); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTermsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTermsAggregator.java index a570b06360f3f..ef1150f1d7efa 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTermsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTermsAggregator.java @@ -31,6 +31,7 @@ import org.elasticsearch.search.aggregations.bucket.terms.support.BucketPriorityQueue; import org.elasticsearch.search.aggregations.bucket.terms.support.IncludeExclude; import org.elasticsearch.search.aggregations.bucket.terms.support.IncludeExclude.LongFilter; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.format.ValueFormat; @@ -39,6 +40,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Map; /** @@ -53,15 +55,17 @@ public class LongTermsAggregator extends TermsAggregator { private LongFilter longFilter; public LongTermsAggregator(String name, AggregatorFactories factories, ValuesSource.Numeric valuesSource, @Nullable ValueFormat format, - Terms.Order order, BucketCountThresholds bucketCountThresholds, AggregationContext aggregationContext, Aggregator parent, SubAggCollectionMode subAggCollectMode, boolean showTermDocCountError, IncludeExclude.LongFilter longFilter, Map metaData) throws IOException { - super(name, factories, aggregationContext, parent, bucketCountThresholds, order, subAggCollectMode, metaData); + Terms.Order order, BucketCountThresholds bucketCountThresholds, AggregationContext aggregationContext, Aggregator parent, + SubAggCollectionMode subAggCollectMode, boolean showTermDocCountError, IncludeExclude.LongFilter longFilter, + List reducers, Map metaData) throws IOException { + super(name, factories, aggregationContext, parent, bucketCountThresholds, order, subAggCollectMode, reducers, metaData); this.valuesSource = valuesSource; this.showTermDocCountError = showTermDocCountError; this.formatter = format != null ? format.formatter() : null; this.longFilter = longFilter; bucketOrds = new LongHash(1, aggregationContext.bigArrays()); } - + @Override public boolean needsScores() { return (valuesSource != null && valuesSource.needsScores()) || super.needsScores(); @@ -76,30 +80,30 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final LeafBucketCollector sub) throws IOException { final SortedNumericDocValues values = getValues(valuesSource, ctx); return new LeafBucketCollectorBase(sub, values) { - @Override - public void collect(int doc, long owningBucketOrdinal) throws IOException { - assert owningBucketOrdinal == 0; - values.setDocument(doc); - final int valuesCount = values.count(); - - long previous = Long.MAX_VALUE; - for (int i = 0; i < valuesCount; ++i) { - final long val = values.valueAt(i); - if (previous != val || i == 0) { - if ((longFilter == null) || (longFilter.accept(val))) { - long bucketOrdinal = bucketOrds.add(val); - if (bucketOrdinal < 0) { // already seen - bucketOrdinal = - 1 - bucketOrdinal; + @Override + public void collect(int doc, long owningBucketOrdinal) throws IOException { + assert owningBucketOrdinal == 0; + values.setDocument(doc); + final int valuesCount = values.count(); + + long previous = Long.MAX_VALUE; + for (int i = 0; i < valuesCount; ++i) { + final long val = values.valueAt(i); + if (previous != val || i == 0) { + if ((longFilter == null) || (longFilter.accept(val))) { + long bucketOrdinal = bucketOrds.add(val); + if (bucketOrdinal < 0) { // already seen + bucketOrdinal = - 1 - bucketOrdinal; collectExistingBucket(sub, doc, bucketOrdinal); - } else { + } else { collectBucket(sub, doc, bucketOrdinal); - } - } - - previous = val; - } + } } + + previous = val; } + } + } }; } @@ -148,7 +152,7 @@ public InternalAggregation buildAggregation(long owningBucketOrdinal) throws IOE list[i] = bucket; otherDocCount -= bucket.docCount; } - + runDeferredCollections(survivingBucketOrds); //Now build the aggs @@ -156,14 +160,18 @@ public InternalAggregation buildAggregation(long owningBucketOrdinal) throws IOE list[i].aggregations = bucketAggregations(list[i].bucketOrd); list[i].docCountError = 0; } - - return new LongTerms(name, order, formatter, bucketCountThresholds.getRequiredSize(), bucketCountThresholds.getShardSize(), bucketCountThresholds.getMinDocCount(), Arrays.asList(list), showTermDocCountError, 0, otherDocCount, metaData()); + + return new LongTerms(name, order, formatter, bucketCountThresholds.getRequiredSize(), bucketCountThresholds.getShardSize(), + bucketCountThresholds.getMinDocCount(), Arrays.asList(list), showTermDocCountError, 0, otherDocCount, reducers(), + metaData()); } - - + + @Override public InternalAggregation buildEmptyAggregation() { - return new LongTerms(name, order, formatter, bucketCountThresholds.getRequiredSize(), bucketCountThresholds.getShardSize(), bucketCountThresholds.getMinDocCount(), Collections.emptyList(), showTermDocCountError, 0, 0, metaData()); + return new LongTerms(name, order, formatter, bucketCountThresholds.getRequiredSize(), bucketCountThresholds.getShardSize(), + bucketCountThresholds.getMinDocCount(), Collections. emptyList(), showTermDocCountError, 0, 0, + reducers(), metaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTerms.java index 7caec199df3ca..ef9ec91e80c5f 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTerms.java @@ -27,6 +27,7 @@ import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.BucketStreamContext; import org.elasticsearch.search.aggregations.bucket.BucketStreams; +import org.elasticsearch.search.aggregations.reducers.Reducer; import java.io.IOException; import java.util.ArrayList; @@ -150,8 +151,11 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws StringTerms() {} // for serialization - public StringTerms(String name, Terms.Order order, int requiredSize, int shardSize, long minDocCount, List buckets, boolean showTermDocCountError, long docCountError, long otherDocCount, Map metaData) { - super(name, order, requiredSize, shardSize, minDocCount, buckets, showTermDocCountError, docCountError, otherDocCount, metaData); + public StringTerms(String name, Terms.Order order, int requiredSize, int shardSize, long minDocCount, + List buckets, boolean showTermDocCountError, long docCountError, long otherDocCount, + List reducers, Map metaData) { + super(name, order, requiredSize, shardSize, minDocCount, buckets, showTermDocCountError, docCountError, otherDocCount, reducers, + metaData); } @Override @@ -160,8 +164,10 @@ public Type type() { } @Override - protected InternalTerms newAggregation(String name, List buckets, boolean showTermDocCountError, long docCountError, long otherDocCount, Map metaData) { - return new StringTerms(name, order, requiredSize, shardSize, minDocCount, buckets, showTermDocCountError, docCountError, otherDocCount, metaData); + protected InternalTerms newAggregation(String name, List buckets, boolean showTermDocCountError, + long docCountError, long otherDocCount, List reducers, Map metaData) { + return new StringTerms(name, order, requiredSize, shardSize, minDocCount, buckets, showTermDocCountError, docCountError, + otherDocCount, reducers, metaData); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTermsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTermsAggregator.java index 9d731a2552906..4d5310b4c19a5 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTermsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTermsAggregator.java @@ -31,11 +31,13 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.bucket.terms.support.BucketPriorityQueue; import org.elasticsearch.search.aggregations.bucket.terms.support.IncludeExclude; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import java.io.IOException; import java.util.Arrays; +import java.util.List; import java.util.Map; /** @@ -49,9 +51,12 @@ public class StringTermsAggregator extends AbstractStringTermsAggregator { public StringTermsAggregator(String name, AggregatorFactories factories, ValuesSource valuesSource, Terms.Order order, BucketCountThresholds bucketCountThresholds, - IncludeExclude includeExclude, AggregationContext aggregationContext, Aggregator parent, SubAggCollectionMode collectionMode, boolean showTermDocCountError, Map metaData) throws IOException { + IncludeExclude includeExclude, AggregationContext aggregationContext, + Aggregator parent, SubAggCollectionMode collectionMode, boolean showTermDocCountError, List reducers, + Map metaData) throws IOException { - super(name, factories, aggregationContext, parent, order, bucketCountThresholds, collectionMode, showTermDocCountError, metaData); + super(name, factories, aggregationContext, parent, order, bucketCountThresholds, collectionMode, showTermDocCountError, reducers, + metaData); this.valuesSource = valuesSource; this.includeExclude = includeExclude; bucketOrds = new BytesRefHash(1, aggregationContext.bigArrays()); @@ -158,7 +163,9 @@ public InternalAggregation buildAggregation(long owningBucketOrdinal) throws IOE bucket.docCountError = 0; } - return new StringTerms(name, order, bucketCountThresholds.getRequiredSize(), bucketCountThresholds.getShardSize(), bucketCountThresholds.getMinDocCount(), Arrays.asList(list), showTermDocCountError, 0, otherDocCount, metaData()); + return new StringTerms(name, order, bucketCountThresholds.getRequiredSize(), bucketCountThresholds.getShardSize(), + bucketCountThresholds.getMinDocCount(), Arrays.asList(list), showTermDocCountError, 0, otherDocCount, reducers(), + metaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregator.java index ef254bb059453..4c4ad7ee31c8d 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregator.java @@ -28,11 +28,13 @@ import org.elasticsearch.search.aggregations.bucket.BucketsAggregator; import org.elasticsearch.search.aggregations.bucket.terms.InternalOrder.Aggregation; import org.elasticsearch.search.aggregations.bucket.terms.InternalOrder.CompoundOrder; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.AggregationPath; import java.io.IOException; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -135,8 +137,8 @@ public void toXContent(XContentBuilder builder) throws IOException { protected final Set aggsUsedForSorting = new HashSet<>(); protected final SubAggCollectionMode collectMode; - public TermsAggregator(String name, AggregatorFactories factories, AggregationContext context, Aggregator parent, BucketCountThresholds bucketCountThresholds, Terms.Order order, SubAggCollectionMode collectMode, Map metaData) throws IOException { - super(name, factories, context, parent, metaData); + public TermsAggregator(String name, AggregatorFactories factories, AggregationContext context, Aggregator parent, BucketCountThresholds bucketCountThresholds, Terms.Order order, SubAggCollectionMode collectMode, List reducers, Map metaData) throws IOException { + super(name, factories, context, parent, reducers, metaData); this.bucketCountThresholds = bucketCountThresholds; this.order = InternalOrder.validate(order, this); this.collectMode = collectMode; diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorFactory.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorFactory.java index 6fbbd3064115f..a9cb4ea19cb9a 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorFactory.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorFactory.java @@ -1,4 +1,4 @@ -/* +List metaData) throws IOException { - return new StringTermsAggregator(name, factories, valuesSource, order, bucketCountThresholds, includeExclude, aggregationContext, parent, subAggCollectMode, showTermDocCountError, metaData); + AggregationContext aggregationContext, Aggregator parent, SubAggCollectionMode subAggCollectMode, + boolean showTermDocCountError, List reducers, Map metaData) throws IOException { + return new StringTermsAggregator(name, factories, valuesSource, order, bucketCountThresholds, includeExclude, + aggregationContext, parent, subAggCollectMode, showTermDocCountError, reducers, metaData); } @Override @@ -64,8 +68,8 @@ boolean needsGlobalOrdinals() { @Override Aggregator create(String name, AggregatorFactories factories, ValuesSource valuesSource, Terms.Order order, TermsAggregator.BucketCountThresholds bucketCountThresholds, IncludeExclude includeExclude, - AggregationContext aggregationContext, Aggregator parent, SubAggCollectionMode subAggCollectMode, boolean showTermDocCountError, Map metaData) throws IOException { - return new GlobalOrdinalsStringTermsAggregator(name, factories, (ValuesSource.Bytes.WithOrdinals.FieldData) valuesSource, order, bucketCountThresholds, includeExclude, aggregationContext, parent, subAggCollectMode, showTermDocCountError, metaData); + AggregationContext aggregationContext, Aggregator parent, SubAggCollectionMode subAggCollectMode, boolean showTermDocCountError, List reducers, Map metaData) throws IOException { + return new GlobalOrdinalsStringTermsAggregator(name, factories, (ValuesSource.Bytes.WithOrdinals.FieldData) valuesSource, order, bucketCountThresholds, includeExclude, aggregationContext, parent, subAggCollectMode, showTermDocCountError, reducers, metaData); } @Override @@ -79,8 +83,8 @@ boolean needsGlobalOrdinals() { @Override Aggregator create(String name, AggregatorFactories factories, ValuesSource valuesSource, Terms.Order order, TermsAggregator.BucketCountThresholds bucketCountThresholds, IncludeExclude includeExclude, - AggregationContext aggregationContext, Aggregator parent, SubAggCollectionMode subAggCollectMode, boolean showTermDocCountError, Map metaData) throws IOException { - return new GlobalOrdinalsStringTermsAggregator.WithHash(name, factories, (ValuesSource.Bytes.WithOrdinals.FieldData) valuesSource, order, bucketCountThresholds, includeExclude, aggregationContext, parent, subAggCollectMode, showTermDocCountError, metaData); + AggregationContext aggregationContext, Aggregator parent, SubAggCollectionMode subAggCollectMode, boolean showTermDocCountError, List reducers, Map metaData) throws IOException { + return new GlobalOrdinalsStringTermsAggregator.WithHash(name, factories, (ValuesSource.Bytes.WithOrdinals.FieldData) valuesSource, order, bucketCountThresholds, includeExclude, aggregationContext, parent, subAggCollectMode, showTermDocCountError, reducers, metaData); } @Override @@ -93,11 +97,12 @@ boolean needsGlobalOrdinals() { @Override Aggregator create(String name, AggregatorFactories factories, ValuesSource valuesSource, Terms.Order order, TermsAggregator.BucketCountThresholds bucketCountThresholds, IncludeExclude includeExclude, - AggregationContext aggregationContext, Aggregator parent, SubAggCollectionMode subAggCollectMode, boolean showTermDocCountError, Map metaData) throws IOException { + AggregationContext aggregationContext, Aggregator parent, SubAggCollectionMode subAggCollectMode, + boolean showTermDocCountError, List reducers, Map metaData) throws IOException { if (includeExclude != null || factories.count() > 0) { - return GLOBAL_ORDINALS.create(name, factories, valuesSource, order, bucketCountThresholds, includeExclude, aggregationContext, parent, subAggCollectMode, showTermDocCountError, metaData); + return GLOBAL_ORDINALS.create(name, factories, valuesSource, order, bucketCountThresholds, includeExclude, aggregationContext, parent, subAggCollectMode, showTermDocCountError, reducers, metaData); } - return new GlobalOrdinalsStringTermsAggregator.LowCardinality(name, factories, (ValuesSource.Bytes.WithOrdinals.FieldData) valuesSource, order, bucketCountThresholds, aggregationContext, parent, subAggCollectMode, showTermDocCountError, metaData); + return new GlobalOrdinalsStringTermsAggregator.LowCardinality(name, factories, (ValuesSource.Bytes.WithOrdinals.FieldData) valuesSource, order, bucketCountThresholds, aggregationContext, parent, subAggCollectMode, showTermDocCountError, reducers, metaData); } @Override @@ -124,7 +129,7 @@ public static ExecutionMode fromString(String value) { abstract Aggregator create(String name, AggregatorFactories factories, ValuesSource valuesSource, Terms.Order order, TermsAggregator.BucketCountThresholds bucketCountThresholds, IncludeExclude includeExclude, AggregationContext aggregationContext, Aggregator parent, - SubAggCollectionMode subAggCollectMode, boolean showTermDocCountError, Map metaData) throws IOException; + SubAggCollectionMode subAggCollectMode, boolean showTermDocCountError, List reducers, Map metaData) throws IOException; abstract boolean needsGlobalOrdinals(); @@ -152,9 +157,11 @@ public TermsAggregatorFactory(String name, ValuesSourceConfig config, Terms.Orde } @Override - protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { - final InternalAggregation aggregation = new UnmappedTerms(name, order, bucketCountThresholds.getRequiredSize(), bucketCountThresholds.getShardSize(), bucketCountThresholds.getMinDocCount(), metaData); - return new NonCollectingAggregator(name, aggregationContext, parent, factories, metaData) { + protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException { + final InternalAggregation aggregation = new UnmappedTerms(name, order, bucketCountThresholds.getRequiredSize(), + bucketCountThresholds.getShardSize(), bucketCountThresholds.getMinDocCount(), reducers, metaData); + return new NonCollectingAggregator(name, aggregationContext, parent, factories, reducers, metaData) { { // even in the case of an unmapped aggregator, validate the order InternalOrder.validate(order, this); @@ -167,7 +174,8 @@ public InternalAggregation buildEmptyAggregation() { } @Override - protected Aggregator doCreateInternal(ValuesSource valuesSource, AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { + protected Aggregator doCreateInternal(ValuesSource valuesSource, AggregationContext aggregationContext, Aggregator parent, + boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException { if (collectsFromSingleBucket == false) { return asMultiBucketAggregator(this, aggregationContext, parent); } @@ -217,7 +225,7 @@ protected Aggregator doCreateInternal(ValuesSource valuesSource, AggregationCont } assert execution != null; - return execution.create(name, factories, valuesSource, order, bucketCountThresholds, includeExclude, aggregationContext, parent, collectMode, showTermDocCountError, metaData); + return execution.create(name, factories, valuesSource, order, bucketCountThresholds, includeExclude, aggregationContext, parent, collectMode, showTermDocCountError, reducers, metaData); } if ((includeExclude != null) && (includeExclude.isRegexBased())) { @@ -233,13 +241,14 @@ protected Aggregator doCreateInternal(ValuesSource valuesSource, AggregationCont } return new DoubleTermsAggregator(name, factories, (ValuesSource.Numeric) valuesSource, config.format(), order, bucketCountThresholds, aggregationContext, parent, collectMode, - showTermDocCountError, longFilter, metaData); + showTermDocCountError, longFilter, reducers, + metaData); } if (includeExclude != null) { longFilter = includeExclude.convertToLongFilter(); } return new LongTermsAggregator(name, factories, (ValuesSource.Numeric) valuesSource, config.format(), - order, bucketCountThresholds, aggregationContext, parent, collectMode, showTermDocCountError, longFilter, metaData); + order, bucketCountThresholds, aggregationContext, parent, collectMode, showTermDocCountError, longFilter, reducers, metaData); } throw new AggregationExecutionException("terms aggregation cannot be applied to field [" + config.fieldContext().field() + diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedTerms.java index 1fffb9508a884..82c850bcac787 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedTerms.java @@ -23,6 +23,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; import java.io.IOException; import java.util.Collections; @@ -54,8 +55,9 @@ public static void registerStreams() { UnmappedTerms() {} // for serialization - public UnmappedTerms(String name, Terms.Order order, int requiredSize, int shardSize, long minDocCount, Map metaData) { - super(name, order, requiredSize, shardSize, minDocCount, BUCKETS, false, 0, 0, metaData); + public UnmappedTerms(String name, Terms.Order order, int requiredSize, int shardSize, long minDocCount, List reducers, + Map metaData) { + super(name, order, requiredSize, shardSize, minDocCount, BUCKETS, false, 0, 0, reducers, metaData); } @Override @@ -91,7 +93,8 @@ public InternalAggregation doReduce(ReduceContext reduceContext) { } @Override - protected InternalTerms newAggregation(String name, List buckets, boolean showTermDocCountError, long docCountError, long otherDocCount, Map metaData) { + protected InternalTerms newAggregation(String name, List buckets, boolean showTermDocCountError, long docCountError, + long otherDocCount, List reducers, Map metaData) { throw new UnsupportedOperationException("How did you get there?"); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalMetricsAggregation.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalMetricsAggregation.java index e3a9476e56a77..8facf4c1ae51e 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalMetricsAggregation.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalMetricsAggregation.java @@ -20,14 +20,16 @@ package org.elasticsearch.search.aggregations.metrics; import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; +import java.util.List; import java.util.Map; public abstract class InternalMetricsAggregation extends InternalAggregation { protected InternalMetricsAggregation() {} // for serialization - protected InternalMetricsAggregation(String name, Map metaData) { - super(name, metaData); + protected InternalMetricsAggregation(String name, List reducers, Map metaData) { + super(name, reducers, metaData); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalNumericMetricsAggregation.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalNumericMetricsAggregation.java index 0c301e30bde1c..e9323615fc3ff 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalNumericMetricsAggregation.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalNumericMetricsAggregation.java @@ -19,6 +19,7 @@ package org.elasticsearch.search.aggregations.metrics; import org.elasticsearch.ElasticsearchIllegalArgumentException; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.util.List; @@ -35,8 +36,8 @@ public static abstract class SingleValue extends InternalNumericMetricsAggregati protected SingleValue() {} - protected SingleValue(String name, Map metaData) { - super(name, metaData); + protected SingleValue(String name, List reducers, Map metaData) { + super(name, reducers, metaData); } public String getValueAsString() { @@ -64,8 +65,8 @@ public static abstract class MultiValue extends InternalNumericMetricsAggregatio protected MultiValue() {} - protected MultiValue(String name, Map metaData) { - super(name, metaData); + protected MultiValue(String name, List reducers, Map metaData) { + super(name, reducers, metaData); } public abstract double value(String name); @@ -92,8 +93,8 @@ public Object getProperty(List path) { private InternalNumericMetricsAggregation() {} // for serialization - private InternalNumericMetricsAggregation(String name, Map metaData) { - super(name, metaData); + private InternalNumericMetricsAggregation(String name, List reducers, Map metaData) { + super(name, reducers, metaData); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/MetricsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/MetricsAggregator.java index f29e063d61ae6..f3160cf464cde 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/MetricsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/MetricsAggregator.java @@ -22,14 +22,17 @@ import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorBase; import org.elasticsearch.search.aggregations.AggregatorFactories; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import java.io.IOException; +import java.util.List; import java.util.Map; public abstract class MetricsAggregator extends AggregatorBase { - protected MetricsAggregator(String name, AggregationContext context, Aggregator parent, Map metaData) throws IOException { - super(name, AggregatorFactories.EMPTY, context, parent, metaData); + protected MetricsAggregator(String name, AggregationContext context, Aggregator parent, List reducers, + Map metaData) throws IOException { + super(name, AggregatorFactories.EMPTY, context, parent, reducers, metaData); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/NumericMetricsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/NumericMetricsAggregator.java index 66adf3ed74efe..6342df383ed47 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/NumericMetricsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/NumericMetricsAggregator.java @@ -19,9 +19,11 @@ package org.elasticsearch.search.aggregations.metrics; import org.elasticsearch.search.aggregations.Aggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -29,14 +31,16 @@ */ public abstract class NumericMetricsAggregator extends MetricsAggregator { - private NumericMetricsAggregator(String name, AggregationContext context, Aggregator parent, Map metaData) throws IOException { - super(name, context, parent, metaData); + private NumericMetricsAggregator(String name, AggregationContext context, Aggregator parent, List reducers, + Map metaData) throws IOException { + super(name, context, parent, reducers, metaData); } public static abstract class SingleValue extends NumericMetricsAggregator { - protected SingleValue(String name, AggregationContext context, Aggregator parent, Map metaData) throws IOException { - super(name, context, parent, metaData); + protected SingleValue(String name, AggregationContext context, Aggregator parent, List reducers, + Map metaData) throws IOException { + super(name, context, parent, reducers, metaData); } public abstract double metric(long owningBucketOrd); @@ -44,8 +48,9 @@ protected SingleValue(String name, AggregationContext context, Aggregator parent public static abstract class MultiValue extends NumericMetricsAggregator { - protected MultiValue(String name, AggregationContext context, Aggregator parent, Map metaData) throws IOException { - super(name, context, parent, metaData); + protected MultiValue(String name, AggregationContext context, Aggregator parent, List reducers, + Map metaData) throws IOException { + super(name, context, parent, reducers, metaData); } public abstract boolean hasMetric(String name); diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/avg/AvgAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/avg/AvgAggregator.java index 94a2e26c7b802..3f0035330b892 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/avg/AvgAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/avg/AvgAggregator.java @@ -30,6 +30,7 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; @@ -37,6 +38,7 @@ import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -51,8 +53,9 @@ public class AvgAggregator extends NumericMetricsAggregator.SingleValue { ValueFormatter formatter; public AvgAggregator(String name, ValuesSource.Numeric valuesSource, @Nullable ValueFormatter formatter, - AggregationContext context, Aggregator parent, Map metaData) throws IOException { - super(name,context, parent, metaData); + AggregationContext context, + Aggregator parent, List reducers, Map metaData) throws IOException { + super(name, context, parent, reducers, metaData); this.valuesSource = valuesSource; this.formatter = formatter; if (valuesSource != null) { @@ -72,22 +75,22 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final LeafBucketCollector sub) throws IOException { if (valuesSource == null) { return LeafBucketCollector.NO_OP_COLLECTOR; - } + } final BigArrays bigArrays = context.bigArrays(); final SortedNumericDoubleValues values = valuesSource.doubleValues(ctx); return new LeafBucketCollectorBase(sub, values) { - @Override + @Override public void collect(int doc, long bucket) throws IOException { counts = bigArrays.grow(counts, bucket + 1); sums = bigArrays.grow(sums, bucket + 1); - values.setDocument(doc); - final int valueCount = values.count(); + values.setDocument(doc); + final int valueCount = values.count(); counts.increment(bucket, valueCount); - double sum = 0; - for (int i = 0; i < valueCount; i++) { - sum += values.valueAt(i); - } + double sum = 0; + for (int i = 0; i < valueCount; i++) { + sum += values.valueAt(i); + } sums.increment(bucket, sum); } }; @@ -103,12 +106,12 @@ public InternalAggregation buildAggregation(long bucket) { if (valuesSource == null || bucket >= sums.size()) { return buildEmptyAggregation(); } - return new InternalAvg(name, sums.get(bucket), counts.get(bucket), formatter, metaData()); + return new InternalAvg(name, sums.get(bucket), counts.get(bucket), formatter, reducers(), metaData()); } @Override public InternalAggregation buildEmptyAggregation() { - return new InternalAvg(name, 0.0, 0l, formatter, metaData()); + return new InternalAvg(name, 0.0, 0l, formatter, reducers(), metaData()); } public static class Factory extends ValuesSourceAggregatorFactory.LeafOnly { @@ -118,13 +121,15 @@ public Factory(String name, String type, ValuesSourceConfig metaData) throws IOException { - return new AvgAggregator(name, null, config.formatter(), aggregationContext, parent, metaData); + protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException { + return new AvgAggregator(name, null, config.formatter(), aggregationContext, parent, reducers, metaData); } @Override - protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { - return new AvgAggregator(name, valuesSource, config.formatter(), aggregationContext, parent, metaData); + protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, + boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException { + return new AvgAggregator(name, valuesSource, config.formatter(), aggregationContext, parent, reducers, metaData); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/avg/InternalAvg.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/avg/InternalAvg.java index 8c795a55332f2..f30cee32b313d 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/avg/InternalAvg.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/avg/InternalAvg.java @@ -25,10 +25,12 @@ import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -56,8 +58,9 @@ public static void registerStreams() { InternalAvg() {} // for serialization - public InternalAvg(String name, double sum, long count, @Nullable ValueFormatter formatter, Map metaData) { - super(name, metaData); + public InternalAvg(String name, double sum, long count, @Nullable ValueFormatter formatter, List reducers, + Map metaData) { + super(name, reducers, metaData); this.sum = sum; this.count = count; this.valueFormatter = formatter; @@ -85,7 +88,7 @@ public InternalAvg doReduce(ReduceContext reduceContext) { count += ((InternalAvg) aggregation).count; sum += ((InternalAvg) aggregation).sum; } - return new InternalAvg(getName(), sum, count, valueFormatter, getMetaData()); + return new InternalAvg(getName(), sum, count, valueFormatter, reducers(), getMetaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/cardinality/CardinalityAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/cardinality/CardinalityAggregator.java index e4c2acce93c74..98c911c20256f 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/cardinality/CardinalityAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/cardinality/CardinalityAggregator.java @@ -42,11 +42,13 @@ import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -66,8 +68,8 @@ public class CardinalityAggregator extends NumericMetricsAggregator.SingleValue private ValueFormatter formatter; public CardinalityAggregator(String name, ValuesSource valuesSource, boolean rehash, int precision, @Nullable ValueFormatter formatter, - AggregationContext context, Aggregator parent, Map metaData) throws IOException { - super(name, context, parent, metaData); + AggregationContext context, Aggregator parent, List reducers, Map metaData) throws IOException { + super(name, context, parent, reducers, metaData); this.valuesSource = valuesSource; this.rehash = rehash; this.precision = precision; @@ -156,12 +158,12 @@ public InternalAggregation buildAggregation(long owningBucketOrdinal) { // this Aggregator (and its HLL++ counters) is released. HyperLogLogPlusPlus copy = new HyperLogLogPlusPlus(precision, BigArrays.NON_RECYCLING_INSTANCE, 1); copy.merge(0, counts, owningBucketOrdinal); - return new InternalCardinality(name, copy, formatter, metaData()); + return new InternalCardinality(name, copy, formatter, reducers(), metaData()); } @Override public InternalAggregation buildEmptyAggregation() { - return new InternalCardinality(name, null, formatter, metaData()); + return new InternalCardinality(name, null, formatter, reducers(), metaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/cardinality/CardinalityAggregatorFactory.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/cardinality/CardinalityAggregatorFactory.java index 2d063dd5bd92e..d2341bb264752 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/cardinality/CardinalityAggregatorFactory.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/cardinality/CardinalityAggregatorFactory.java @@ -22,12 +22,14 @@ import org.elasticsearch.search.aggregations.AggregationExecutionException; import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.bucket.SingleBucketAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; import java.io.IOException; +import java.util.List; import java.util.Map; final class CardinalityAggregatorFactory extends ValuesSourceAggregatorFactory { @@ -46,16 +48,19 @@ private int precision(Aggregator parent) { } @Override - protected Aggregator createUnmapped(AggregationContext context, Aggregator parent, Map metaData) throws IOException { - return new CardinalityAggregator(name, null, true, precision(parent), config.formatter(), context, parent, metaData); + protected Aggregator createUnmapped(AggregationContext context, Aggregator parent, List reducers, Map metaData) + throws IOException { + return new CardinalityAggregator(name, null, true, precision(parent), config.formatter(), context, parent, reducers, metaData); } @Override - protected Aggregator doCreateInternal(ValuesSource valuesSource, AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { + protected Aggregator doCreateInternal(ValuesSource valuesSource, AggregationContext context, Aggregator parent, + boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException { if (!(valuesSource instanceof ValuesSource.Numeric) && !rehash) { throw new AggregationExecutionException("Turning off rehashing for cardinality aggregation [" + name + "] on non-numeric values in not allowed"); } - return new CardinalityAggregator(name, valuesSource, rehash, precision(parent), config.formatter(), context, parent, metaData); + return new CardinalityAggregator(name, valuesSource, rehash, precision(parent), config.formatter(), context, parent, reducers, + metaData); } /* diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/cardinality/InternalCardinality.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/cardinality/InternalCardinality.java index c8341135fb4fb..434140e74f68b 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/cardinality/InternalCardinality.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/cardinality/InternalCardinality.java @@ -27,6 +27,7 @@ import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; @@ -53,8 +54,9 @@ public static void registerStreams() { private HyperLogLogPlusPlus counts; - InternalCardinality(String name, HyperLogLogPlusPlus counts, @Nullable ValueFormatter formatter, Map metaData) { - super(name, metaData); + InternalCardinality(String name, HyperLogLogPlusPlus counts, @Nullable ValueFormatter formatter, List reducers, + Map metaData) { + super(name, reducers, metaData); this.counts = counts; this.valueFormatter = formatter; } @@ -107,7 +109,7 @@ public InternalAggregation doReduce(ReduceContext reduceContext) { if (cardinality.counts != null) { if (reduced == null) { reduced = new InternalCardinality(name, new HyperLogLogPlusPlus(cardinality.counts.precision(), - BigArrays.NON_RECYCLING_INSTANCE, 1), this.valueFormatter, getMetaData()); + BigArrays.NON_RECYCLING_INSTANCE, 1), this.valueFormatter, reducers(), getMetaData()); } reduced.merge(cardinality); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/geobounds/GeoBoundsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/geobounds/GeoBoundsAggregator.java index 44e7fd195c082..53e5c534094c8 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/geobounds/GeoBoundsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/geobounds/GeoBoundsAggregator.java @@ -30,12 +30,14 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.metrics.MetricsAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; import java.io.IOException; +import java.util.List; import java.util.Map; public final class GeoBoundsAggregator extends MetricsAggregator { @@ -50,8 +52,10 @@ public final class GeoBoundsAggregator extends MetricsAggregator { DoubleArray negRights; protected GeoBoundsAggregator(String name, AggregationContext aggregationContext, - Aggregator parent, ValuesSource.GeoPoint valuesSource, boolean wrapLongitude, Map metaData) throws IOException { - super(name, aggregationContext, parent, metaData); + Aggregator parent, + ValuesSource.GeoPoint valuesSource, boolean wrapLongitude, List reducers, Map metaData) + throws IOException { + super(name, aggregationContext, parent, reducers, metaData); this.valuesSource = valuesSource; this.wrapLongitude = wrapLongitude; if (valuesSource != null) { @@ -149,13 +153,13 @@ public InternalAggregation buildAggregation(long owningBucketOrdinal) { double posRight = posRights.get(owningBucketOrdinal); double negLeft = negLefts.get(owningBucketOrdinal); double negRight = negRights.get(owningBucketOrdinal); - return new InternalGeoBounds(name, top, bottom, posLeft, posRight, negLeft, negRight, wrapLongitude, metaData()); + return new InternalGeoBounds(name, top, bottom, posLeft, posRight, negLeft, negRight, wrapLongitude, reducers(), metaData()); } @Override public InternalAggregation buildEmptyAggregation() { return new InternalGeoBounds(name, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, - Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, wrapLongitude, metaData()); + Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, wrapLongitude, reducers(), metaData()); } @Override @@ -173,14 +177,16 @@ protected Factory(String name, ValuesSourceConfig config, } @Override - protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { - return new GeoBoundsAggregator(name, aggregationContext, parent, null, wrapLongitude, metaData); + protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException { + return new GeoBoundsAggregator(name, aggregationContext, parent, null, wrapLongitude, reducers, metaData); } @Override protected Aggregator doCreateInternal(ValuesSource.GeoPoint valuesSource, AggregationContext aggregationContext, - Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { - return new GeoBoundsAggregator(name, aggregationContext, parent, valuesSource, wrapLongitude, metaData); + Aggregator parent, + boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException { + return new GeoBoundsAggregator(name, aggregationContext, parent, valuesSource, wrapLongitude, reducers, metaData); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/geobounds/InternalGeoBounds.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/geobounds/InternalGeoBounds.java index eb6a61c960d5b..f67734bdd09a4 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/geobounds/InternalGeoBounds.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/geobounds/InternalGeoBounds.java @@ -27,6 +27,7 @@ import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.metrics.InternalMetricsAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; import java.io.IOException; import java.util.List; @@ -56,8 +57,9 @@ public InternalGeoBounds readResult(StreamInput in) throws IOException { } InternalGeoBounds(String name, double top, double bottom, double posLeft, double posRight, - double negLeft, double negRight, boolean wrapLongitude, Map metaData) { - super(name, metaData); + double negLeft, double negRight, + boolean wrapLongitude, List reducers, Map metaData) { + super(name, reducers, metaData); this.top = top; this.bottom = bottom; this.posLeft = posLeft; @@ -103,7 +105,7 @@ public InternalAggregation doReduce(ReduceContext reduceContext) { negRight = bounds.negRight; } } - return new InternalGeoBounds(name, top, bottom, posLeft, posRight, negLeft, negRight, wrapLongitude, getMetaData()); + return new InternalGeoBounds(name, top, bottom, posLeft, posRight, negLeft, negRight, wrapLongitude, reducers(), getMetaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/max/InternalMax.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/max/InternalMax.java index 7cae1444c6353..a181db30d9807 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/max/InternalMax.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/max/InternalMax.java @@ -25,10 +25,12 @@ import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -55,8 +57,8 @@ public static void registerStreams() { InternalMax() {} // for serialization - public InternalMax(String name, double max, @Nullable ValueFormatter formatter, Map metaData) { - super(name, metaData); + public InternalMax(String name, double max, @Nullable ValueFormatter formatter, List reducers, Map metaData) { + super(name, reducers, metaData); this.valueFormatter = formatter; this.max = max; } @@ -81,7 +83,7 @@ public InternalMax doReduce(ReduceContext reduceContext) { for (InternalAggregation aggregation : reduceContext.aggregations()) { max = Math.max(max, ((InternalMax) aggregation).max); } - return new InternalMax(name, max, valueFormatter, getMetaData()); + return new InternalMax(name, max, valueFormatter, reducers(), getMetaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/max/MaxAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/max/MaxAggregator.java index 88edddc286c9c..0c97ba38ac3d3 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/max/MaxAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/max/MaxAggregator.java @@ -31,6 +31,7 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; @@ -38,6 +39,7 @@ import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -51,8 +53,9 @@ public class MaxAggregator extends NumericMetricsAggregator.SingleValue { DoubleArray maxes; public MaxAggregator(String name, ValuesSource.Numeric valuesSource, @Nullable ValueFormatter formatter, - AggregationContext context, Aggregator parent, Map metaData) throws IOException { - super(name, context, parent, metaData); + AggregationContext context, + Aggregator parent, List reducers, Map metaData) throws IOException { + super(name, context, parent, reducers, metaData); this.valuesSource = valuesSource; this.formatter = formatter; if (valuesSource != null) { @@ -71,22 +74,22 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final LeafBucketCollector sub) throws IOException { if (valuesSource == null) { return LeafBucketCollector.NO_OP_COLLECTOR; - } + } final BigArrays bigArrays = context.bigArrays(); final SortedNumericDoubleValues allValues = valuesSource.doubleValues(ctx); final NumericDoubleValues values = MultiValueMode.MAX.select(allValues, Double.NEGATIVE_INFINITY); return new LeafBucketCollectorBase(sub, allValues) { - @Override + @Override public void collect(int doc, long bucket) throws IOException { if (bucket >= maxes.size()) { - long from = maxes.size(); + long from = maxes.size(); maxes = bigArrays.grow(maxes, bucket + 1); - maxes.fill(from, maxes.size(), Double.NEGATIVE_INFINITY); - } - final double value = values.get(doc); + maxes.fill(from, maxes.size(), Double.NEGATIVE_INFINITY); + } + final double value = values.get(doc); double max = maxes.get(bucket); - max = Math.max(max, value); + max = Math.max(max, value); maxes.set(bucket, max); } @@ -103,12 +106,12 @@ public InternalAggregation buildAggregation(long bucket) { if (valuesSource == null || bucket >= maxes.size()) { return buildEmptyAggregation(); } - return new InternalMax(name, maxes.get(bucket), formatter, metaData()); + return new InternalMax(name, maxes.get(bucket), formatter, reducers(), metaData()); } @Override public InternalAggregation buildEmptyAggregation() { - return new InternalMax(name, Double.NEGATIVE_INFINITY, formatter, metaData()); + return new InternalMax(name, Double.NEGATIVE_INFINITY, formatter, reducers(), metaData()); } public static class Factory extends ValuesSourceAggregatorFactory.LeafOnly { @@ -118,13 +121,15 @@ public Factory(String name, ValuesSourceConfig valuesSourc } @Override - protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { - return new MaxAggregator(name, null, config.formatter(), aggregationContext, parent, metaData); + protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException { + return new MaxAggregator(name, null, config.formatter(), aggregationContext, parent, reducers, metaData); } @Override - protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { - return new MaxAggregator(name, valuesSource, config.formatter(), aggregationContext, parent, metaData); + protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, + boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException { + return new MaxAggregator(name, valuesSource, config.formatter(), aggregationContext, parent, reducers, metaData); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/min/InternalMin.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/min/InternalMin.java index 0974314826c6b..9917f96640300 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/min/InternalMin.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/min/InternalMin.java @@ -25,10 +25,12 @@ import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -56,8 +58,8 @@ public static void registerStreams() { InternalMin() {} // for serialization - public InternalMin(String name, double min, @Nullable ValueFormatter formatter, Map metaData) { - super(name, metaData); + public InternalMin(String name, double min, @Nullable ValueFormatter formatter, List reducers, Map metaData) { + super(name, reducers, metaData); this.min = min; this.valueFormatter = formatter; } @@ -82,7 +84,7 @@ public InternalMin doReduce(ReduceContext reduceContext) { for (InternalAggregation aggregation : reduceContext.aggregations()) { min = Math.min(min, ((InternalMin) aggregation).min); } - return new InternalMin(getName(), min, this.valueFormatter, getMetaData()); + return new InternalMin(getName(), min, this.valueFormatter, reducers(), getMetaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/min/MinAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/min/MinAggregator.java index 438272e2bc138..c80b7b8f064df 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/min/MinAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/min/MinAggregator.java @@ -31,6 +31,7 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; @@ -38,6 +39,7 @@ import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -51,8 +53,9 @@ public class MinAggregator extends NumericMetricsAggregator.SingleValue { DoubleArray mins; public MinAggregator(String name, ValuesSource.Numeric valuesSource, @Nullable ValueFormatter formatter, - AggregationContext context, Aggregator parent, Map metaData) throws IOException { - super(name, context, parent, metaData); + AggregationContext context, + Aggregator parent, List reducers, Map metaData) throws IOException { + super(name, context, parent, reducers, metaData); this.valuesSource = valuesSource; if (valuesSource != null) { mins = context.bigArrays().newDoubleArray(1, false); @@ -71,22 +74,22 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final LeafBucketCollector sub) throws IOException { if (valuesSource == null) { return LeafBucketCollector.NO_OP_COLLECTOR; - } + } final BigArrays bigArrays = context.bigArrays(); final SortedNumericDoubleValues allValues = valuesSource.doubleValues(ctx); final NumericDoubleValues values = MultiValueMode.MIN.select(allValues, Double.POSITIVE_INFINITY); return new LeafBucketCollectorBase(sub, allValues) { - @Override + @Override public void collect(int doc, long bucket) throws IOException { if (bucket >= mins.size()) { - long from = mins.size(); + long from = mins.size(); mins = bigArrays.grow(mins, bucket + 1); - mins.fill(from, mins.size(), Double.POSITIVE_INFINITY); - } - final double value = values.get(doc); + mins.fill(from, mins.size(), Double.POSITIVE_INFINITY); + } + final double value = values.get(doc); double min = mins.get(bucket); - min = Math.min(min, value); + min = Math.min(min, value); mins.set(bucket, min); } @@ -103,12 +106,12 @@ public InternalAggregation buildAggregation(long bucket) { if (valuesSource == null || bucket >= mins.size()) { return buildEmptyAggregation(); } - return new InternalMin(name, mins.get(bucket), formatter, metaData()); + return new InternalMin(name, mins.get(bucket), formatter, reducers(), metaData()); } @Override public InternalAggregation buildEmptyAggregation() { - return new InternalMin(name, Double.POSITIVE_INFINITY, formatter, metaData()); + return new InternalMin(name, Double.POSITIVE_INFINITY, formatter, reducers(), metaData()); } public static class Factory extends ValuesSourceAggregatorFactory.LeafOnly { @@ -118,13 +121,15 @@ public Factory(String name, ValuesSourceConfig valuesSourc } @Override - protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { - return new MinAggregator(name, null, config.formatter(), aggregationContext, parent, metaData); + protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException { + return new MinAggregator(name, null, config.formatter(), aggregationContext, parent, reducers, metaData); } @Override - protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { - return new MinAggregator(name, valuesSource, config.formatter(), aggregationContext, parent, metaData); + protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, + boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException { + return new MinAggregator(name, valuesSource, config.formatter(), aggregationContext, parent, reducers, metaData); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/AbstractInternalPercentiles.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/AbstractInternalPercentiles.java index 19d056e00cdc2..7ae2ad9ec600d 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/AbstractInternalPercentiles.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/AbstractInternalPercentiles.java @@ -28,6 +28,7 @@ import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; import org.elasticsearch.search.aggregations.metrics.percentiles.tdigest.TDigestState; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; @@ -44,8 +45,9 @@ abstract class AbstractInternalPercentiles extends InternalNumericMetricsAggrega AbstractInternalPercentiles() {} // for serialization public AbstractInternalPercentiles(String name, double[] keys, TDigestState state, boolean keyed, @Nullable ValueFormatter formatter, + List reducers, Map metaData) { - super(name, metaData); + super(name, reducers, metaData); this.keys = keys; this.state = state; this.keyed = keyed; @@ -70,10 +72,11 @@ public AbstractInternalPercentiles doReduce(ReduceContext reduceContext) { } merged.add(percentiles.state); } - return createReduced(getName(), keys, merged, keyed, getMetaData()); + return createReduced(getName(), keys, merged, keyed, reducers(), getMetaData()); } - protected abstract AbstractInternalPercentiles createReduced(String name, double[] keys, TDigestState merged, boolean keyed, Map metaData); + protected abstract AbstractInternalPercentiles createReduced(String name, double[] keys, TDigestState merged, boolean keyed, + List reducers, Map metaData); @Override protected void doReadFrom(StreamInput in) throws IOException { diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/AbstractPercentilesAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/AbstractPercentilesAggregator.java index 31a097f0b473c..8dd75b5911064 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/AbstractPercentilesAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/AbstractPercentilesAggregator.java @@ -31,11 +31,13 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregator; import org.elasticsearch.search.aggregations.metrics.percentiles.tdigest.TDigestState; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.io.IOException; +import java.util.List; import java.util.Map; public abstract class AbstractPercentilesAggregator extends NumericMetricsAggregator.MultiValue { @@ -53,8 +55,9 @@ private static int indexOfKey(double[] keys, double key) { public AbstractPercentilesAggregator(String name, ValuesSource.Numeric valuesSource, AggregationContext context, Aggregator parent, double[] keys, double compression, boolean keyed, - @Nullable ValueFormatter formatter, Map metaData) throws IOException { - super(name, context, parent, metaData); + @Nullable ValueFormatter formatter, List reducers, + Map metaData) throws IOException { + super(name, context, parent, reducers, metaData); this.valuesSource = valuesSource; this.keyed = keyed; this.formatter = formatter; diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/InternalPercentileRanks.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/InternalPercentileRanks.java index 190ca363ed3de..687e1822b645a 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/InternalPercentileRanks.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/InternalPercentileRanks.java @@ -24,10 +24,12 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.metrics.percentiles.tdigest.TDigestState; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.io.IOException; import java.util.Iterator; +import java.util.List; import java.util.Map; /** @@ -53,8 +55,9 @@ public static void registerStreams() { InternalPercentileRanks() {} // for serialization public InternalPercentileRanks(String name, double[] cdfValues, TDigestState state, boolean keyed, @Nullable ValueFormatter formatter, + List reducers, Map metaData) { - super(name, cdfValues, state, keyed, formatter, metaData); + super(name, cdfValues, state, keyed, formatter, reducers, metaData); } @Override @@ -77,8 +80,9 @@ public double value(double key) { return percent(key); } - protected AbstractInternalPercentiles createReduced(String name, double[] keys, TDigestState merged, boolean keyed, Map metaData) { - return new InternalPercentileRanks(name, keys, merged, keyed, valueFormatter, metaData); + protected AbstractInternalPercentiles createReduced(String name, double[] keys, TDigestState merged, boolean keyed, + List reducers, Map metaData) { + return new InternalPercentileRanks(name, keys, merged, keyed, valueFormatter, reducers, metaData); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/InternalPercentiles.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/InternalPercentiles.java index 5e7d47803d835..357921aeb91e5 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/InternalPercentiles.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/InternalPercentiles.java @@ -24,10 +24,12 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.metrics.percentiles.tdigest.TDigestState; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.io.IOException; import java.util.Iterator; +import java.util.List; import java.util.Map; /** @@ -53,8 +55,9 @@ public static void registerStreams() { InternalPercentiles() {} // for serialization public InternalPercentiles(String name, double[] percents, TDigestState state, boolean keyed, @Nullable ValueFormatter formatter, + List reducers, Map metaData) { - super(name, percents, state, keyed, formatter, metaData); + super(name, percents, state, keyed, formatter, reducers, metaData); } @Override @@ -77,8 +80,9 @@ public double value(double key) { return percentile(key); } - protected AbstractInternalPercentiles createReduced(String name, double[] keys, TDigestState merged, boolean keyed, Map metaData) { - return new InternalPercentiles(name, keys, merged, keyed, valueFormatter, metaData); + protected AbstractInternalPercentiles createReduced(String name, double[] keys, TDigestState merged, boolean keyed, + List reducers, Map metaData) { + return new InternalPercentiles(name, keys, merged, keyed, valueFormatter, reducers, metaData); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/PercentileRanksAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/PercentileRanksAggregator.java index 0383e33e8a7dd..9d14e3b70c375 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/PercentileRanksAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/PercentileRanksAggregator.java @@ -22,6 +22,7 @@ import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.metrics.percentiles.tdigest.TDigestState; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSource.Numeric; @@ -30,6 +31,7 @@ import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -37,10 +39,10 @@ */ public class PercentileRanksAggregator extends AbstractPercentilesAggregator { - public PercentileRanksAggregator(String name, Numeric valuesSource, AggregationContext context, - Aggregator parent, double[] percents, double compression, boolean keyed, @Nullable ValueFormatter formatter, - Map metaData) throws IOException { - super(name, valuesSource, context, parent, percents, compression, keyed, formatter, metaData); + public PercentileRanksAggregator(String name, Numeric valuesSource, AggregationContext context, Aggregator parent, double[] percents, + double compression, boolean keyed, @Nullable ValueFormatter formatter, List reducers, Map metaData) + throws IOException { + super(name, valuesSource, context, parent, percents, compression, keyed, formatter, reducers, metaData); } @Override @@ -49,13 +51,13 @@ public InternalAggregation buildAggregation(long owningBucketOrdinal) { if (state == null) { return buildEmptyAggregation(); } else { - return new InternalPercentileRanks(name, keys, state, keyed, formatter, metaData()); + return new InternalPercentileRanks(name, keys, state, keyed, formatter, reducers(), metaData()); } } @Override public InternalAggregation buildEmptyAggregation() { - return new InternalPercentileRanks(name, keys, new TDigestState(compression), keyed, formatter, metaData()); + return new InternalPercentileRanks(name, keys, new TDigestState(compression), keyed, formatter, reducers(), metaData()); } @Override @@ -83,15 +85,19 @@ public Factory(String name, ValuesSourceConfig valuesSourc } @Override - protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { + protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException { return new PercentileRanksAggregator(name, null, aggregationContext, parent, values, compression, keyed, config.formatter(), + reducers, metaData); } @Override - protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { + protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, + boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException { return new PercentileRanksAggregator(name, valuesSource, aggregationContext, parent, values, compression, - keyed, config.formatter(), metaData); + keyed, + config.formatter(), reducers, metaData); } } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/PercentilesAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/PercentilesAggregator.java index 4dd99b73cd93d..1a9a839bb757b 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/PercentilesAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/PercentilesAggregator.java @@ -22,6 +22,7 @@ import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.metrics.percentiles.tdigest.TDigestState; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSource.Numeric; @@ -30,6 +31,7 @@ import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -38,9 +40,10 @@ public class PercentilesAggregator extends AbstractPercentilesAggregator { public PercentilesAggregator(String name, Numeric valuesSource, AggregationContext context, - Aggregator parent, double[] percents, double compression, boolean keyed, @Nullable ValueFormatter formatter, + Aggregator parent, double[] percents, + double compression, boolean keyed, @Nullable ValueFormatter formatter, List reducers, Map metaData) throws IOException { - super(name, valuesSource, context, parent, percents, compression, keyed, formatter, metaData); + super(name, valuesSource, context, parent, percents, compression, keyed, formatter, reducers, metaData); } @Override @@ -49,7 +52,7 @@ public InternalAggregation buildAggregation(long owningBucketOrdinal) { if (state == null) { return buildEmptyAggregation(); } else { - return new InternalPercentiles(name, keys, state, keyed, formatter, metaData()); + return new InternalPercentiles(name, keys, state, keyed, formatter, reducers(), metaData()); } } @@ -65,7 +68,7 @@ public double metric(String name, long bucketOrd) { @Override public InternalAggregation buildEmptyAggregation() { - return new InternalPercentiles(name, keys, new TDigestState(compression), keyed, formatter, metaData()); + return new InternalPercentiles(name, keys, new TDigestState(compression), keyed, formatter, reducers(), metaData()); } public static class Factory extends ValuesSourceAggregatorFactory.LeafOnly { @@ -83,15 +86,19 @@ public Factory(String name, ValuesSourceConfig valuesSourc } @Override - protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { + protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException { return new PercentilesAggregator(name, null, aggregationContext, parent, percents, compression, keyed, config.formatter(), + reducers, metaData); } @Override - protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { + protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, + boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException { return new PercentilesAggregator(name, valuesSource, aggregationContext, parent, percents, compression, - keyed, config.formatter(), metaData); + keyed, + config.formatter(), reducers, metaData); } } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java index c7176e0e1e163..2a3900afc46e7 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java @@ -28,6 +28,7 @@ import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.metrics.InternalMetricsAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; import java.io.IOException; import java.util.ArrayList; @@ -61,13 +62,13 @@ public static void registerStreams() { private InternalScriptedMetric() { } - private InternalScriptedMetric(String name, Map metaData) { - super(name, metaData); + private InternalScriptedMetric(String name, List reducers, Map metaData) { + super(name, reducers, metaData); } public InternalScriptedMetric(String name, Object aggregation, String scriptLang, ScriptType scriptType, String reduceScript, - Map reduceParams, Map metaData) { - this(name, metaData); + Map reduceParams, List reducers, Map metaData) { + this(name, reducers, metaData); this.aggregation = aggregation; this.scriptType = scriptType; this.reduceScript = reduceScript; @@ -104,7 +105,7 @@ public InternalAggregation doReduce(ReduceContext reduceContext) { aggregation = aggregationObjects; } return new InternalScriptedMetric(firstAggregation.getName(), aggregation, firstAggregation.scriptLang, firstAggregation.scriptType, - firstAggregation.reduceScript, firstAggregation.reduceParams, getMetaData()); + firstAggregation.reduceScript, firstAggregation.reduceParams, reducers(), getMetaData()); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregator.java index e9260d852ad07..22781a1861244 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregator.java @@ -31,6 +31,7 @@ import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.metrics.MetricsAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.internal.SearchContext; @@ -57,8 +58,9 @@ public class ScriptedMetricAggregator extends MetricsAggregator { protected ScriptedMetricAggregator(String name, String scriptLang, ScriptType initScriptType, String initScript, ScriptType mapScriptType, String mapScript, ScriptType combineScriptType, String combineScript, ScriptType reduceScriptType, - String reduceScript, Map params, Map reduceParams, AggregationContext context, Aggregator parent, Map metaData) throws IOException { - super(name, context, parent, metaData); + String reduceScript, Map params, Map reduceParams, AggregationContext context, + Aggregator parent, List reducers, Map metaData) throws IOException { + super(name, context, parent, reducers, metaData); this.scriptService = context.searchContext().scriptService(); this.scriptLang = scriptLang; this.reduceScriptType = reduceScriptType; @@ -112,12 +114,13 @@ public InternalAggregation buildAggregation(long owningBucketOrdinal) { } else { aggregation = params.get("_agg"); } - return new InternalScriptedMetric(name, aggregation, scriptLang, reduceScriptType, reduceScript, reduceParams, metaData()); + return new InternalScriptedMetric(name, aggregation, scriptLang, reduceScriptType, reduceScript, reduceParams, reducers(), + metaData()); } @Override public InternalAggregation buildEmptyAggregation() { - return new InternalScriptedMetric(name, null, scriptLang, reduceScriptType, reduceScript, reduceParams, metaData()); + return new InternalScriptedMetric(name, null, scriptLang, reduceScriptType, reduceScript, reduceParams, reducers(), metaData()); } public static class Factory extends AggregatorFactory { @@ -151,7 +154,8 @@ public Factory(String name, String scriptLang, ScriptType initScriptType, String } @Override - public Aggregator createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { + public Aggregator createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, + List reducers, Map metaData) throws IOException { if (collectsFromSingleBucket == false) { return asMultiBucketAggregator(this, context, parent); } @@ -164,7 +168,7 @@ public Aggregator createInternal(AggregationContext context, Aggregator parent, reduceParams = deepCopyParams(this.reduceParams, context.searchContext()); } return new ScriptedMetricAggregator(name, scriptLang, initScriptType, initScript, mapScriptType, mapScript, combineScriptType, - combineScript, reduceScriptType, reduceScript, params, reduceParams, context, parent, metaData); + combineScript, reduceScriptType, reduceScript, params, reduceParams, context, parent, reducers, metaData); } @SuppressWarnings({ "unchecked" }) diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/InternalStats.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/InternalStats.java index 7186fee979ca6..5133012aabd5f 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/InternalStats.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/InternalStats.java @@ -26,10 +26,12 @@ import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -69,8 +71,9 @@ public static Metrics resolve(String name) { protected InternalStats() {} // for serialization public InternalStats(String name, long count, double sum, double min, double max, @Nullable ValueFormatter formatter, + List reducers, Map metaData) { - super(name, metaData); + super(name, reducers, metaData); this.count = count; this.sum = sum; this.min = min; @@ -160,7 +163,7 @@ public InternalStats doReduce(ReduceContext reduceContext) { max = Math.max(max, stats.getMax()); sum += stats.getSum(); } - return new InternalStats(name, count, sum, min, max, valueFormatter, getMetaData()); + return new InternalStats(name, count, sum, min, max, valueFormatter, reducers(), getMetaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/StatsAggegator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/StatsAggegator.java index 8f431578fef5f..8a454b6cb734d 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/StatsAggegator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/StatsAggegator.java @@ -31,6 +31,7 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; @@ -38,6 +39,7 @@ import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -55,8 +57,9 @@ public class StatsAggegator extends NumericMetricsAggregator.MultiValue { public StatsAggegator(String name, ValuesSource.Numeric valuesSource, @Nullable ValueFormatter formatter, - AggregationContext context, Aggregator parent, Map metaData) throws IOException { - super(name, context, parent, metaData); + AggregationContext context, + Aggregator parent, List reducers, Map metaData) throws IOException { + super(name, context, parent, reducers, metaData); this.valuesSource = valuesSource; if (valuesSource != null) { final BigArrays bigArrays = context.bigArrays(); @@ -80,35 +83,35 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final LeafBucketCollector sub) throws IOException { if (valuesSource == null) { return LeafBucketCollector.NO_OP_COLLECTOR; - } + } final BigArrays bigArrays = context.bigArrays(); final SortedNumericDoubleValues values = valuesSource.doubleValues(ctx); return new LeafBucketCollectorBase(sub, values) { - @Override + @Override public void collect(int doc, long bucket) throws IOException { if (bucket >= counts.size()) { - final long from = counts.size(); + final long from = counts.size(); final long overSize = BigArrays.overSize(bucket + 1); - counts = bigArrays.resize(counts, overSize); - sums = bigArrays.resize(sums, overSize); - mins = bigArrays.resize(mins, overSize); - maxes = bigArrays.resize(maxes, overSize); - mins.fill(from, overSize, Double.POSITIVE_INFINITY); - maxes.fill(from, overSize, Double.NEGATIVE_INFINITY); - } - - values.setDocument(doc); - final int valuesCount = values.count(); + counts = bigArrays.resize(counts, overSize); + sums = bigArrays.resize(sums, overSize); + mins = bigArrays.resize(mins, overSize); + maxes = bigArrays.resize(maxes, overSize); + mins.fill(from, overSize, Double.POSITIVE_INFINITY); + maxes.fill(from, overSize, Double.NEGATIVE_INFINITY); + } + + values.setDocument(doc); + final int valuesCount = values.count(); counts.increment(bucket, valuesCount); - double sum = 0; + double sum = 0; double min = mins.get(bucket); double max = maxes.get(bucket); - for (int i = 0; i < valuesCount; i++) { - double value = values.valueAt(i); - sum += value; - min = Math.min(min, value); - max = Math.max(max, value); - } + for (int i = 0; i < valuesCount; i++) { + double value = values.valueAt(i); + sum += value; + min = Math.min(min, value); + max = Math.max(max, value); + } sums.increment(bucket, sum); mins.set(bucket, min); maxes.set(bucket, max); @@ -145,12 +148,12 @@ public InternalAggregation buildAggregation(long bucket) { return buildEmptyAggregation(); } return new InternalStats(name, counts.get(bucket), sums.get(bucket), mins.get(bucket), - maxes.get(bucket), formatter, metaData()); + maxes.get(bucket), formatter, reducers(), metaData()); } @Override public InternalAggregation buildEmptyAggregation() { - return new InternalStats(name, 0, 0, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, formatter, metaData()); + return new InternalStats(name, 0, 0, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, formatter, reducers(), metaData()); } public static class Factory extends ValuesSourceAggregatorFactory.LeafOnly { @@ -160,13 +163,15 @@ public Factory(String name, ValuesSourceConfig valuesSourc } @Override - protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { - return new StatsAggegator(name, null, config.formatter(), aggregationContext, parent, metaData); + protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException { + return new StatsAggegator(name, null, config.formatter(), aggregationContext, parent, reducers, metaData); } @Override - protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { - return new StatsAggegator(name, valuesSource, config.formatter(), aggregationContext, parent, metaData); + protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, + boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException { + return new StatsAggegator(name, valuesSource, config.formatter(), aggregationContext, parent, reducers, metaData); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/extended/ExtendedStatsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/extended/ExtendedStatsAggregator.java index 75dc354f8745b..ae1bc68965d22 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/extended/ExtendedStatsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/extended/ExtendedStatsAggregator.java @@ -31,6 +31,7 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; @@ -38,6 +39,7 @@ import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -55,10 +57,10 @@ public class ExtendedStatsAggregator extends NumericMetricsAggregator.MultiValue DoubleArray maxes; DoubleArray sumOfSqrs; - public ExtendedStatsAggregator(String name, ValuesSource.Numeric valuesSource, - @Nullable ValueFormatter formatter, AggregationContext context, - Aggregator parent, double sigma, Map metaData) throws IOException { - super(name, context, parent, metaData); + public ExtendedStatsAggregator(String name, ValuesSource.Numeric valuesSource, @Nullable ValueFormatter formatter, + AggregationContext context, Aggregator parent, double sigma, List reducers, Map metaData) + throws IOException { + super(name, context, parent, reducers, metaData); this.valuesSource = valuesSource; this.formatter = formatter; this.sigma = sigma; @@ -167,16 +169,19 @@ private double variance(long owningBucketOrd) { @Override public InternalAggregation buildAggregation(long owningBucketOrdinal) { if (valuesSource == null) { - return new InternalExtendedStats(name, 0, 0d, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 0d, 0d, formatter, metaData()); + return new InternalExtendedStats(name, 0, 0d, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 0d, 0d, formatter, + reducers(), metaData()); } assert owningBucketOrdinal < counts.size(); return new InternalExtendedStats(name, counts.get(owningBucketOrdinal), sums.get(owningBucketOrdinal), - mins.get(owningBucketOrdinal), maxes.get(owningBucketOrdinal), sumOfSqrs.get(owningBucketOrdinal), sigma, formatter, metaData()); + mins.get(owningBucketOrdinal), maxes.get(owningBucketOrdinal), sumOfSqrs.get(owningBucketOrdinal), sigma, formatter, + reducers(), metaData()); } @Override public InternalAggregation buildEmptyAggregation() { - return new InternalExtendedStats(name, 0, 0d, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 0d, 0d, formatter, metaData()); + return new InternalExtendedStats(name, 0, 0d, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 0d, 0d, formatter, reducers(), + metaData()); } @Override @@ -195,13 +200,16 @@ public Factory(String name, ValuesSourceConfig valuesSourc } @Override - protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { - return new ExtendedStatsAggregator(name, null, config.formatter(), aggregationContext, parent, sigma, metaData); + protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException { + return new ExtendedStatsAggregator(name, null, config.formatter(), aggregationContext, parent, sigma, reducers, metaData); } @Override - protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { - return new ExtendedStatsAggregator(name, valuesSource, config.formatter(), aggregationContext, parent, sigma, metaData); + protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, + boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException { + return new ExtendedStatsAggregator(name, valuesSource, config.formatter(), aggregationContext, parent, sigma, reducers, + metaData); } } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/extended/InternalExtendedStats.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/extended/InternalExtendedStats.java index 9f88bf4f42977..f5d9c7d198376 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/extended/InternalExtendedStats.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/extended/InternalExtendedStats.java @@ -27,9 +27,11 @@ import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.metrics.stats.InternalStats; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -67,8 +69,9 @@ public static Metrics resolve(String name) { InternalExtendedStats() {} // for serialization public InternalExtendedStats(String name, long count, double sum, double min, double max, double sumOfSqrs, - double sigma, @Nullable ValueFormatter formatter, Map metaData) { - super(name, count, sum, min, max, formatter, metaData); + double sigma, + @Nullable ValueFormatter formatter, List reducers, Map metaData) { + super(name, count, sum, min, max, formatter, reducers, metaData); this.sumOfSqrs = sumOfSqrs; this.sigma = sigma; } @@ -150,7 +153,8 @@ public InternalExtendedStats doReduce(ReduceContext reduceContext) { sumOfSqrs += stats.getSumOfSquares(); } final InternalStats stats = super.doReduce(reduceContext); - return new InternalExtendedStats(name, stats.getCount(), stats.getSum(), stats.getMin(), stats.getMax(), sumOfSqrs, sigma, valueFormatter, getMetaData()); + return new InternalExtendedStats(name, stats.getCount(), stats.getSum(), stats.getMin(), stats.getMax(), sumOfSqrs, sigma, + valueFormatter, reducers(), getMetaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/sum/InternalSum.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/sum/InternalSum.java index f653c082c797c..7f98d6cc4e808 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/sum/InternalSum.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/sum/InternalSum.java @@ -25,10 +25,12 @@ import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -55,8 +57,8 @@ public static void registerStreams() { InternalSum() {} // for serialization - InternalSum(String name, double sum, @Nullable ValueFormatter formatter, Map metaData) { - super(name, metaData); + InternalSum(String name, double sum, @Nullable ValueFormatter formatter, List reducers, Map metaData) { + super(name, reducers, metaData); this.sum = sum; this.valueFormatter = formatter; } @@ -81,7 +83,7 @@ public InternalSum doReduce(ReduceContext reduceContext) { for (InternalAggregation aggregation : reduceContext.aggregations()) { sum += ((InternalSum) aggregation).sum; } - return new InternalSum(name, sum, valueFormatter, getMetaData()); + return new InternalSum(name, sum, valueFormatter, reducers(), getMetaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/sum/SumAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/sum/SumAggregator.java index ab6b565a62be0..af834af7f7b5a 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/sum/SumAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/sum/SumAggregator.java @@ -29,6 +29,7 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; @@ -36,6 +37,7 @@ import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -49,8 +51,9 @@ public class SumAggregator extends NumericMetricsAggregator.SingleValue { DoubleArray sums; public SumAggregator(String name, ValuesSource.Numeric valuesSource, @Nullable ValueFormatter formatter, - AggregationContext context, Aggregator parent, Map metaData) throws IOException { - super(name, context, parent, metaData); + AggregationContext context, + Aggregator parent, List reducers, Map metaData) throws IOException { + super(name, context, parent, reducers, metaData); this.valuesSource = valuesSource; this.formatter = formatter; if (valuesSource != null) { @@ -68,19 +71,19 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final LeafBucketCollector sub) throws IOException { if (valuesSource == null) { return LeafBucketCollector.NO_OP_COLLECTOR; - } + } final BigArrays bigArrays = context.bigArrays(); final SortedNumericDoubleValues values = valuesSource.doubleValues(ctx); return new LeafBucketCollectorBase(sub, values) { - @Override + @Override public void collect(int doc, long bucket) throws IOException { sums = bigArrays.grow(sums, bucket + 1); - values.setDocument(doc); - final int valuesCount = values.count(); - double sum = 0; - for (int i = 0; i < valuesCount; i++) { - sum += values.valueAt(i); - } + values.setDocument(doc); + final int valuesCount = values.count(); + double sum = 0; + for (int i = 0; i < valuesCount; i++) { + sum += values.valueAt(i); + } sums.increment(bucket, sum); } }; @@ -96,12 +99,12 @@ public InternalAggregation buildAggregation(long bucket) { if (valuesSource == null || bucket >= sums.size()) { return buildEmptyAggregation(); } - return new InternalSum(name, sums.get(bucket), formatter, metaData()); + return new InternalSum(name, sums.get(bucket), formatter, reducers(), metaData()); } @Override public InternalAggregation buildEmptyAggregation() { - return new InternalSum(name, 0.0, formatter, metaData()); + return new InternalSum(name, 0.0, formatter, reducers(), metaData()); } public static class Factory extends ValuesSourceAggregatorFactory.LeafOnly { @@ -111,13 +114,15 @@ public Factory(String name, ValuesSourceConfig valuesSourc } @Override - protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { - return new SumAggregator(name, null, config.formatter(), aggregationContext, parent, metaData); + protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException { + return new SumAggregator(name, null, config.formatter(), aggregationContext, parent, reducers, metaData); } @Override - protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { - return new SumAggregator(name, valuesSource, config.formatter(), aggregationContext, parent, metaData); + protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, + boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException { + return new SumAggregator(name, valuesSource, config.formatter(), aggregationContext, parent, reducers, metaData); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregator.java index e841ded7d9199..20aeaae2f5a2f 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregator.java @@ -36,10 +36,12 @@ import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.aggregations.AggregatorFactory; +import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.metrics.MetricsAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.fetch.FetchPhase; import org.elasticsearch.search.fetch.FetchSearchResult; @@ -48,6 +50,7 @@ import org.elasticsearch.search.internal.SubSearchContext; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -68,8 +71,9 @@ private static class TopDocsAndLeafCollector { final SubSearchContext subSearchContext; final LongObjectPagedHashMap topDocsCollectors; - public TopHitsAggregator(FetchPhase fetchPhase, SubSearchContext subSearchContext, String name, AggregationContext context, Aggregator parent, Map metaData) throws IOException { - super(name, context, parent, metaData); + public TopHitsAggregator(FetchPhase fetchPhase, SubSearchContext subSearchContext, String name, AggregationContext context, + Aggregator parent, List reducers, Map metaData) throws IOException { + super(name, context, parent, reducers, metaData); this.fetchPhase = fetchPhase; topDocsCollectors = new LongObjectPagedHashMap<>(1, context.bigArrays()); this.subSearchContext = subSearchContext; @@ -82,8 +86,8 @@ public boolean needsScores() { return sort.needsScores() || subSearchContext.trackScores(); } else { // sort by score - return true; - } + return true; + } } @Override @@ -180,8 +184,9 @@ public Factory(String name, FetchPhase fetchPhase, SubSearchContext subSearchCon } @Override - public Aggregator createInternal(AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { - return new TopHitsAggregator(fetchPhase, subSearchContext, name, aggregationContext, parent, metaData); + public Aggregator createInternal(AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, + List reducers, Map metaData) throws IOException { + return new TopHitsAggregator(fetchPhase, subSearchContext, name, aggregationContext, parent, reducers, metaData); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/valuecount/InternalValueCount.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/valuecount/InternalValueCount.java index b8b675c2eee0f..935eb5e19330e 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/valuecount/InternalValueCount.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/valuecount/InternalValueCount.java @@ -25,9 +25,11 @@ import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -54,8 +56,9 @@ public static void registerStreams() { InternalValueCount() {} // for serialization - public InternalValueCount(String name, long value, @Nullable ValueFormatter formatter, Map metaData) { - super(name, metaData); + public InternalValueCount(String name, long value, @Nullable ValueFormatter formatter, List reducers, + Map metaData) { + super(name, reducers, metaData); this.value = value; this.valueFormatter = formatter; } @@ -81,7 +84,7 @@ public InternalAggregation doReduce(ReduceContext reduceContext) { for (InternalAggregation aggregation : reduceContext.aggregations()) { valueCount += ((InternalValueCount) aggregation).value; } - return new InternalValueCount(name, valueCount, valueFormatter, getMetaData()); + return new InternalValueCount(name, valueCount, valueFormatter, reducers(), getMetaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/valuecount/ValueCountAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/valuecount/ValueCountAggregator.java index a74ec061b8e53..2bd7b50513598 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/valuecount/ValueCountAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/valuecount/ValueCountAggregator.java @@ -29,6 +29,7 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; @@ -36,6 +37,7 @@ import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -53,8 +55,9 @@ public class ValueCountAggregator extends NumericMetricsAggregator.SingleValue { LongArray counts; public ValueCountAggregator(String name, ValuesSource valuesSource, @Nullable ValueFormatter formatter, - AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { - super(name, aggregationContext, parent, metaData); + AggregationContext aggregationContext, Aggregator parent, List reducers, Map metaData) + throws IOException { + super(name, aggregationContext, parent, reducers, metaData); this.valuesSource = valuesSource; this.formatter = formatter; if (valuesSource != null) { @@ -67,17 +70,17 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final LeafBucketCollector sub) throws IOException { if (valuesSource == null) { return LeafBucketCollector.NO_OP_COLLECTOR; - } + } final BigArrays bigArrays = context.bigArrays(); final SortedBinaryDocValues values = valuesSource.bytesValues(ctx); return new LeafBucketCollectorBase(sub, values) { - @Override + @Override public void collect(int doc, long bucket) throws IOException { counts = bigArrays.grow(counts, bucket + 1); values.setDocument(doc); counts.increment(bucket, values.count()); - } + } }; } @@ -92,12 +95,12 @@ public InternalAggregation buildAggregation(long bucket) { if (valuesSource == null || bucket >= counts.size()) { return buildEmptyAggregation(); } - return new InternalValueCount(name, counts.get(bucket), formatter, metaData()); + return new InternalValueCount(name, counts.get(bucket), formatter, reducers(), metaData()); } @Override public InternalAggregation buildEmptyAggregation() { - return new InternalValueCount(name, 0l, formatter, metaData()); + return new InternalValueCount(name, 0l, formatter, reducers(), metaData()); } @Override @@ -112,13 +115,15 @@ public Factory(String name, ValuesSourceConfig config) { } @Override - protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException { - return new ValueCountAggregator(name, null, config.formatter(), aggregationContext, parent, metaData); + protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException { + return new ValueCountAggregator(name, null, config.formatter(), aggregationContext, parent, reducers, metaData); } @Override - protected Aggregator doCreateInternal(VS valuesSource, AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { - return new ValueCountAggregator(name, valuesSource, config.formatter(), aggregationContext, parent, + protected Aggregator doCreateInternal(VS valuesSource, AggregationContext aggregationContext, Aggregator parent, + boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException { + return new ValueCountAggregator(name, valuesSource, config.formatter(), aggregationContext, parent, reducers, metaData); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java new file mode 100644 index 0000000000000..c74f6f0b0f22e --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; +import org.elasticsearch.search.internal.SearchContext; + +import java.io.IOException; + +public abstract class Reducer { + + /** + * Parses the reducer request and creates the appropriate reducer factory + * for it. + * + * @see {@link ReducerFactory} + */ + public static interface Parser { + + /** + * @return The reducer type this parser is associated with. + */ + String type(); + + /** + * Returns the reducer factory with which this parser is associated. + * + * @param reducerName + * The name of the reducer + * @param parser + * The xcontent parser + * @param context + * The search context + * @return The resolved reducer factory + * @throws java.io.IOException + * When parsing fails + */ + ReducerFactory parse(String reducerName, XContentParser parser, SearchContext context) throws IOException; + + } + + public abstract InternalAggregation reduce(InternalAggregation aggregation, ReduceContext reduceContext); + +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java new file mode 100644 index 0000000000000..64e7d1c7baf5b --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java @@ -0,0 +1,88 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.search.aggregations.reducers; + +import org.elasticsearch.common.io.stream.Streamable; +import org.elasticsearch.search.aggregations.Aggregator; +import org.elasticsearch.search.aggregations.support.AggregationContext; + +import java.io.IOException; +import java.util.Map; + +/** + * A factory that knows how to create an {@link Aggregator} of a specific type. + */ +public abstract class ReducerFactory implements Streamable { + + protected String name; + protected String type; + protected Map metaData; + + /** + * Constructs a new reducer factory. + * + * @param name + * The aggregation name + * @param type + * The aggregation type + */ + public ReducerFactory(String name, String type) { + this.name = name; + this.type = type; + } + + /** + * Validates the state of this factory (makes sure the factory is properly configured) + */ + public final void validate() { + doValidate(); + } + + protected abstract Reducer createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, + Map metaData) throws IOException; + + /** + * Creates the reducer + * + * @param context + * The aggregation context + * @param parent + * The parent aggregator (if this is a top level factory, the + * parent will be {@code null}) + * @param collectsFromSingleBucket + * If true then the created aggregator will only be collected + * with 0 as a bucket ordinal. Some factories can take + * advantage of this in order to return more optimized + * implementations. + * + * @return The created aggregator + */ + public final Reducer create(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket) throws IOException { + Reducer aggregator = createInternal(context, parent, collectsFromSingleBucket, this.metaData); + return aggregator; + } + + public void doValidate() { + } + + public void setMetaData(Map metaData) { + this.metaData = metaData; + } + +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSourceAggregatorFactory.java b/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSourceAggregatorFactory.java index d88f95642c319..dbefc2e261242 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSourceAggregatorFactory.java +++ b/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSourceAggregatorFactory.java @@ -18,10 +18,16 @@ */ package org.elasticsearch.search.aggregations.support; -import org.elasticsearch.search.aggregations.*; +import org.elasticsearch.search.aggregations.AggregationExecutionException; +import org.elasticsearch.search.aggregations.AggregationInitializationException; +import org.elasticsearch.search.aggregations.Aggregator; +import org.elasticsearch.search.aggregations.AggregatorFactories; +import org.elasticsearch.search.aggregations.AggregatorFactory; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormat; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -49,12 +55,13 @@ protected ValuesSourceAggregatorFactory(String name, String type, ValuesSourceCo } @Override - public Aggregator createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { + public Aggregator createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, + List reducers, Map metaData) throws IOException { if (config.unmapped()) { - return createUnmapped(context, parent, metaData); + return createUnmapped(context, parent, reducers, metaData); } VS vs = context.valuesSource(config); - return doCreateInternal(vs, context, parent, collectsFromSingleBucket, metaData); + return doCreateInternal(vs, context, parent, collectsFromSingleBucket, reducers, metaData); } @Override @@ -64,9 +71,11 @@ public void doValidate() { } } - protected abstract Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, Map metaData) throws IOException; + protected abstract Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException; - protected abstract Aggregator doCreateInternal(VS valuesSource, AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException; + protected abstract Aggregator doCreateInternal(VS valuesSource, AggregationContext aggregationContext, Aggregator parent, + boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException; private void resolveValuesSourceConfigFromAncestors(String aggName, AggregatorFactory parent, Class requiredValuesSourceType) { ValuesSourceConfig config; diff --git a/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificanceHeuristicTests.java b/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificanceHeuristicTests.java index ec3d17b929467..bdfec3154029c 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificanceHeuristicTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificanceHeuristicTests.java @@ -30,7 +30,16 @@ import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.search.SearchShardTarget; import org.elasticsearch.search.aggregations.InternalAggregations; -import org.elasticsearch.search.aggregations.bucket.significant.heuristics.*; +import org.elasticsearch.search.aggregations.bucket.significant.heuristics.ChiSquare; +import org.elasticsearch.search.aggregations.bucket.significant.heuristics.GND; +import org.elasticsearch.search.aggregations.bucket.significant.heuristics.JLHScore; +import org.elasticsearch.search.aggregations.bucket.significant.heuristics.MutualInformation; +import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristic; +import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristicBuilder; +import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristicParser; +import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristicParserMapper; +import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristicStreams; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.test.ElasticsearchIntegrationTest; import org.elasticsearch.test.ElasticsearchTestCase; @@ -41,11 +50,16 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.lessThan; +import static org.hamcrest.Matchers.lessThanOrEqualTo; /** * @@ -96,13 +110,15 @@ InternalSignificantTerms[] getRandomSignificantTerms(SignificanceHeuristic heuri if (randomBoolean()) { BytesRef term = new BytesRef("123.0"); buckets.add(new SignificantLongTerms.Bucket(1, 2, 3, 4, 123, InternalAggregations.EMPTY, null)); - sTerms[0] = new SignificantLongTerms(10, 20, "some_name", null, 1, 1, heuristic, buckets, null); + sTerms[0] = new SignificantLongTerms(10, 20, "some_name", null, 1, 1, heuristic, buckets, + (List) Collections.EMPTY_LIST, null); sTerms[1] = new SignificantLongTerms(); } else { BytesRef term = new BytesRef("someterm"); buckets.add(new SignificantStringTerms.Bucket(term, 1, 2, 3, 4, InternalAggregations.EMPTY)); - sTerms[0] = new SignificantStringTerms(10, 20, "some_name", 1, 1, heuristic, buckets, null); + sTerms[0] = new SignificantStringTerms(10, 20, "some_name", 1, 1, heuristic, buckets, (List) Collections.EMPTY_LIST, + null); sTerms[1] = new SignificantStringTerms(); } return sTerms; From ae76239b0aefe65991e50572c6d0b0039f2c1c0d Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Thu, 12 Feb 2015 08:41:21 +0000 Subject: [PATCH 03/68] AggregatorFactories now stores reducers as well as aggregators These reducers will be passed through from the AggregatorParser --- .../aggregations/AggregatorFactories.java | 20 ++++++++++++++++--- .../aggregations/AggregatorFactory.java | 8 +------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java index 10ea7f74c2c01..795d9b5724c96 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java @@ -19,6 +19,7 @@ package org.elasticsearch.search.aggregations; import org.elasticsearch.ElasticsearchIllegalArgumentException; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import java.io.IOException; @@ -35,13 +36,19 @@ public class AggregatorFactories { public static final AggregatorFactories EMPTY = new Empty(); private AggregatorFactory[] factories; + private List reducers; public static Builder builder() { return new Builder(); } - private AggregatorFactories(AggregatorFactory[] factories) { + private AggregatorFactories(AggregatorFactory[] factories, List reducers) { this.factories = factories; + this.reducers = reducers; + } + + public List reducers() { + return reducers; } private static Aggregator createAndRegisterContextAware(AggregationContext context, AggregatorFactory factory, Aggregator parent, boolean collectsFromSingleBucket) throws IOException { @@ -100,9 +107,10 @@ private final static class Empty extends AggregatorFactories { private static final AggregatorFactory[] EMPTY_FACTORIES = new AggregatorFactory[0]; private static final Aggregator[] EMPTY_AGGREGATORS = new Aggregator[0]; + private static final List EMPTY_REDUCERS = new ArrayList<>(); private Empty() { - super(EMPTY_FACTORIES); + super(EMPTY_FACTORIES, EMPTY_REDUCERS); } @Override @@ -121,6 +129,7 @@ public static class Builder { private final Set names = new HashSet<>(); private final List factories = new ArrayList<>(); + private List reducers = new ArrayList<>(); public Builder add(AggregatorFactory factory) { if (!names.add(factory.name)) { @@ -130,11 +139,16 @@ public Builder add(AggregatorFactory factory) { return this; } + public Builder setReducers(List reducers) { + this.reducers = reducers; + return this; + } + public AggregatorFactories build() { if (factories.isEmpty()) { return EMPTY; } - return new AggregatorFactories(factories.toArray(new AggregatorFactory[factories.size()])); + return new AggregatorFactories(factories.toArray(new AggregatorFactory[factories.size()]), this.reducers); } } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactory.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactory.java index f49a328fd16db..3db9e5ddd69c2 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactory.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactory.java @@ -28,7 +28,6 @@ import org.elasticsearch.search.internal.SearchContext.Lifetime; import java.io.IOException; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -41,7 +40,6 @@ public abstract class AggregatorFactory { protected String type; protected AggregatorFactory parent; protected AggregatorFactories factories = AggregatorFactories.EMPTY; - protected List reducers = Collections.emptyList(); protected Map metaData; /** @@ -97,7 +95,7 @@ protected abstract Aggregator createInternal(AggregationContext context, Aggrega * @return The created aggregator */ public final Aggregator create(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket) throws IOException { - return createInternal(context, parent, collectsFromSingleBucket, this.reducers, this.metaData); + return createInternal(context, parent, collectsFromSingleBucket, this.factories.reducers(), this.metaData); } public void doValidate() { @@ -108,10 +106,6 @@ public void setMetaData(Map metaData) { } - public void setReducers(List reducers) { - this.reducers = reducers; - } - /** * Utility method. Given an {@link AggregatorFactory} that creates {@link Aggregator}s that only know how From 1e947c8d1750498725d73b27aa87c65a768c83c0 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Thu, 12 Feb 2015 10:51:32 +0000 Subject: [PATCH 04/68] Reducers are now parsed in AggregatorParsers --- .../index/query/CommonTermsQueryBuilder.java | 2 +- .../aggregations/AggregationModule.java | 75 ++++++++------- .../aggregations/AggregatorFactories.java | 26 ++++-- .../aggregations/AggregatorFactory.java | 2 +- .../aggregations/AggregatorParsers.java | 91 +++++++++++++------ .../bucket/nested/NestedAggregatorTest.java | 2 +- 6 files changed, 125 insertions(+), 73 deletions(-) diff --git a/src/main/java/org/elasticsearch/index/query/CommonTermsQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/CommonTermsQueryBuilder.java index 9775b3f04d80a..e57dd0e0b4e53 100644 --- a/src/main/java/org/elasticsearch/index/query/CommonTermsQueryBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/CommonTermsQueryBuilder.java @@ -27,7 +27,7 @@ /** * CommonTermsQuery query is a query that executes high-frequency terms in a * optional sub-query to prevent slow queries due to "common" terms like - * stopwords. This query basically builds 2 queries off the {@link #add(Term) + * stopwords. This query basically builds 2 queries off the {@link #addAggregator(Term) * added} terms where low-frequency terms are added to a required boolean clause * and high-frequency terms are added to an optional boolean clause. The * optional clause is only executed if the required "low-frequency' clause diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregationModule.java b/src/main/java/org/elasticsearch/search/aggregations/AggregationModule.java index 2feaf112104ce..3910f0962466c 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregationModule.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregationModule.java @@ -20,6 +20,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; + import org.elasticsearch.common.inject.AbstractModule; import org.elasticsearch.common.inject.Module; import org.elasticsearch.common.inject.SpawnModules; @@ -54,6 +55,7 @@ import org.elasticsearch.search.aggregations.metrics.sum.SumParser; import org.elasticsearch.search.aggregations.metrics.tophits.TopHitsParser; import org.elasticsearch.search.aggregations.metrics.valuecount.ValueCountParser; +import org.elasticsearch.search.aggregations.reducers.Reducer; import java.util.List; @@ -62,39 +64,40 @@ */ public class AggregationModule extends AbstractModule implements SpawnModules{ - private List> parsers = Lists.newArrayList(); + private List> aggParsers = Lists.newArrayList(); + private List> reducerParsers = Lists.newArrayList(); public AggregationModule() { - parsers.add(AvgParser.class); - parsers.add(SumParser.class); - parsers.add(MinParser.class); - parsers.add(MaxParser.class); - parsers.add(StatsParser.class); - parsers.add(ExtendedStatsParser.class); - parsers.add(ValueCountParser.class); - parsers.add(PercentilesParser.class); - parsers.add(PercentileRanksParser.class); - parsers.add(CardinalityParser.class); + aggParsers.add(AvgParser.class); + aggParsers.add(SumParser.class); + aggParsers.add(MinParser.class); + aggParsers.add(MaxParser.class); + aggParsers.add(StatsParser.class); + aggParsers.add(ExtendedStatsParser.class); + aggParsers.add(ValueCountParser.class); + aggParsers.add(PercentilesParser.class); + aggParsers.add(PercentileRanksParser.class); + aggParsers.add(CardinalityParser.class); - parsers.add(GlobalParser.class); - parsers.add(MissingParser.class); - parsers.add(FilterParser.class); - parsers.add(FiltersParser.class); - parsers.add(TermsParser.class); - parsers.add(SignificantTermsParser.class); - parsers.add(RangeParser.class); - parsers.add(DateRangeParser.class); - parsers.add(IpRangeParser.class); - parsers.add(HistogramParser.class); - parsers.add(DateHistogramParser.class); - parsers.add(GeoDistanceParser.class); - parsers.add(GeoHashGridParser.class); - parsers.add(NestedParser.class); - parsers.add(ReverseNestedParser.class); - parsers.add(TopHitsParser.class); - parsers.add(GeoBoundsParser.class); - parsers.add(ScriptedMetricParser.class); - parsers.add(ChildrenParser.class); + aggParsers.add(GlobalParser.class); + aggParsers.add(MissingParser.class); + aggParsers.add(FilterParser.class); + aggParsers.add(FiltersParser.class); + aggParsers.add(TermsParser.class); + aggParsers.add(SignificantTermsParser.class); + aggParsers.add(RangeParser.class); + aggParsers.add(DateRangeParser.class); + aggParsers.add(IpRangeParser.class); + aggParsers.add(HistogramParser.class); + aggParsers.add(DateHistogramParser.class); + aggParsers.add(GeoDistanceParser.class); + aggParsers.add(GeoHashGridParser.class); + aggParsers.add(NestedParser.class); + aggParsers.add(ReverseNestedParser.class); + aggParsers.add(TopHitsParser.class); + aggParsers.add(GeoBoundsParser.class); + aggParsers.add(ScriptedMetricParser.class); + aggParsers.add(ChildrenParser.class); } /** @@ -103,14 +106,18 @@ public AggregationModule() { * @param parser The parser for the custom aggregator. */ public void addAggregatorParser(Class parser) { - parsers.add(parser); + aggParsers.add(parser); } @Override protected void configure() { - Multibinder multibinder = Multibinder.newSetBinder(binder(), Aggregator.Parser.class); - for (Class parser : parsers) { - multibinder.addBinding().to(parser); + Multibinder multibinderAggParser = Multibinder.newSetBinder(binder(), Aggregator.Parser.class); + for (Class parser : aggParsers) { + multibinderAggParser.addBinding().to(parser); + } + Multibinder multibinderReducerParser = Multibinder.newSetBinder(binder(), Reducer.Parser.class); + for (Class parser : reducerParsers) { + multibinderReducerParser.addBinding().to(parser); } bind(AggregatorParsers.class).asEagerSingleton(); bind(AggregationParseElement.class).asEagerSingleton(); diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java index 795d9b5724c96..5103f9c2b7a60 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java @@ -20,6 +20,7 @@ import org.elasticsearch.ElasticsearchIllegalArgumentException; import org.elasticsearch.search.aggregations.reducers.Reducer; +import org.elasticsearch.search.aggregations.reducers.ReducerFactory; import org.elasticsearch.search.aggregations.support.AggregationContext; import java.io.IOException; @@ -36,18 +37,22 @@ public class AggregatorFactories { public static final AggregatorFactories EMPTY = new Empty(); private AggregatorFactory[] factories; - private List reducers; + private List reducerFactories; public static Builder builder() { return new Builder(); } - private AggregatorFactories(AggregatorFactory[] factories, List reducers) { + private AggregatorFactories(AggregatorFactory[] factories, List reducers) { this.factories = factories; - this.reducers = reducers; + this.reducerFactories = reducers; } - public List reducers() { + public List createReducers() throws IOException { + List reducers = new ArrayList<>(); + for (ReducerFactory factory : this.reducerFactories) { + reducers.add(factory.create(null, null, false)); // NOCOMIT add context, parent etc. + } return reducers; } @@ -107,7 +112,7 @@ private final static class Empty extends AggregatorFactories { private static final AggregatorFactory[] EMPTY_FACTORIES = new AggregatorFactory[0]; private static final Aggregator[] EMPTY_AGGREGATORS = new Aggregator[0]; - private static final List EMPTY_REDUCERS = new ArrayList<>(); + private static final List EMPTY_REDUCERS = new ArrayList<>(); private Empty() { super(EMPTY_FACTORIES, EMPTY_REDUCERS); @@ -129,9 +134,9 @@ public static class Builder { private final Set names = new HashSet<>(); private final List factories = new ArrayList<>(); - private List reducers = new ArrayList<>(); + private final List reducerFactories = new ArrayList<>(); - public Builder add(AggregatorFactory factory) { + public Builder addAggregator(AggregatorFactory factory) { if (!names.add(factory.name)) { throw new ElasticsearchIllegalArgumentException("Two sibling aggregations cannot have the same name: [" + factory.name + "]"); } @@ -139,8 +144,8 @@ public Builder add(AggregatorFactory factory) { return this; } - public Builder setReducers(List reducers) { - this.reducers = reducers; + public Builder addReducer(ReducerFactory reducerFactory) { + this.reducerFactories.add(reducerFactory); return this; } @@ -148,7 +153,8 @@ public AggregatorFactories build() { if (factories.isEmpty()) { return EMPTY; } - return new AggregatorFactories(factories.toArray(new AggregatorFactory[factories.size()]), this.reducers); + // NOCOMMIT work out dependency order of reducer factories + return new AggregatorFactories(factories.toArray(new AggregatorFactory[factories.size()]), this.reducerFactories); } } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactory.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactory.java index 3db9e5ddd69c2..d22fed75a8c23 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactory.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactory.java @@ -95,7 +95,7 @@ protected abstract Aggregator createInternal(AggregationContext context, Aggrega * @return The created aggregator */ public final Aggregator create(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket) throws IOException { - return createInternal(context, parent, collectsFromSingleBucket, this.factories.reducers(), this.metaData); + return createInternal(context, parent, collectsFromSingleBucket, this.factories.createReducers(), this.metaData); } public void doValidate() { diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorParsers.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorParsers.java index b55f6a4f022d7..e23cf8ef228cf 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorParsers.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorParsers.java @@ -19,10 +19,13 @@ package org.elasticsearch.search.aggregations; import com.google.common.collect.ImmutableMap; + import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.SearchParseException; +import org.elasticsearch.search.aggregations.reducers.Reducer; +import org.elasticsearch.search.aggregations.reducers.ReducerFactory; import org.elasticsearch.search.internal.SearchContext; import java.io.IOException; @@ -37,21 +40,30 @@ public class AggregatorParsers { public static final Pattern VALID_AGG_NAME = Pattern.compile("[^\\[\\]>]+"); - private final ImmutableMap parsers; + private final ImmutableMap aggParsers; + private final ImmutableMap reducerParsers; /** * Constructs the AggregatorParsers out of all the given parsers - * - * @param parsers The available aggregator parsers (dynamically injected by the {@link org.elasticsearch.search.aggregations.AggregationModule}). + * + * @param aggParsers + * The available aggregator parsers (dynamically injected by the + * {@link org.elasticsearch.search.aggregations.AggregationModule} + * ). */ @Inject - public AggregatorParsers(Set parsers) { - MapBuilder builder = MapBuilder.newMapBuilder(); - for (Aggregator.Parser parser : parsers) { - builder.put(parser.type(), parser); + public AggregatorParsers(Set aggParsers, Set reducerParsers) { + MapBuilder aggParsersBuilder = MapBuilder.newMapBuilder(); + for (Aggregator.Parser parser : aggParsers) { + aggParsersBuilder.put(parser.type(), parser); } - this.parsers = builder.immutableMap(); + this.aggParsers = aggParsersBuilder.immutableMap(); + MapBuilder reducerParsersBuilder = MapBuilder.newMapBuilder(); + for (Reducer.Parser parser : reducerParsers) { + reducerParsersBuilder.put(parser.type(), parser); + } + this.reducerParsers = reducerParsersBuilder.immutableMap(); } /** @@ -61,7 +73,18 @@ public AggregatorParsers(Set parsers) { * @return The parser associated with the given aggregation type. */ public Aggregator.Parser parser(String type) { - return parsers.get(type); + return aggParsers.get(type); + } + + /** + * Returns the parser that is registered under the given reducer type. + * + * @param type + * The reducer type + * @return The parser associated with the given reducer type. + */ + public Reducer.Parser reducer(String type) { + return reducerParsers.get(type); } /** @@ -98,7 +121,8 @@ private AggregatorFactories parseAggregators(XContentParser parser, SearchContex throw new SearchParseException(context, "Aggregation definition for [" + aggregationName + " starts with a [" + token + "], expected a [" + XContentParser.Token.START_OBJECT + "]."); } - AggregatorFactory factory = null; + AggregatorFactory aggFactory = null; + ReducerFactory reducerFactory = null; AggregatorFactories subFactories = null; Map metaData = null; @@ -126,34 +150,49 @@ private AggregatorFactories parseAggregators(XContentParser parser, SearchContex subFactories = parseAggregators(parser, context, level+1); break; default: - if (factory != null) { - throw new SearchParseException(context, "Found two aggregation type definitions in [" + aggregationName + "]: [" + factory.type + "] and [" + fieldName + "]"); + if (aggFactory != null) { + throw new SearchParseException(context, "Found two aggregation type definitions in [" + aggregationName + "]: [" + + aggFactory.type + "] and [" + fieldName + "]"); } Aggregator.Parser aggregatorParser = parser(fieldName); if (aggregatorParser == null) { - throw new SearchParseException(context, "Could not find aggregator type [" + fieldName + "] in [" + aggregationName + "]"); + Reducer.Parser reducerParser = reducer(fieldName); + if (reducerParser == null) { + throw new SearchParseException(context, "Could not find aggregator type [" + fieldName + "] in [" + + aggregationName + "]"); + } else { + reducerFactory = reducerParser.parse(aggregationName, parser, context); } - factory = aggregatorParser.parse(aggregationName, parser, context); + } else { + aggFactory = aggregatorParser.parse(aggregationName, parser, context); + } } } - if (factory == null) { + if (aggFactory == null && reducerFactory == null) { throw new SearchParseException(context, "Missing definition for aggregation [" + aggregationName + "]"); - } + } else if (aggFactory != null) { + if (metaData != null) { + aggFactory.setMetaData(metaData); + } - if (metaData != null) { - factory.setMetaData(metaData); - } + if (subFactories != null) { + aggFactory.subFactories(subFactories); + } - if (subFactories != null) { - factory.subFactories(subFactories); - } + if (level == 0) { + aggFactory.validate(); + } - if (level == 0) { - factory.validate(); + factories.addAggregator(aggFactory); + } else if (reducerFactory != null) { + if (subFactories != null) { + throw new SearchParseException(context, "Aggregation [" + aggregationName + "] cannot define sub-aggregations"); + } + factories.addReducer(reducerFactory); + } else { + throw new SearchParseException(context, "Found two sub aggregation definitions under [" + aggregationName + "]"); } - - factories.add(factory); } return factories.build(); diff --git a/src/test/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregatorTest.java b/src/test/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregatorTest.java index 7cdff38d7c80f..2f9ffafac5379 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregatorTest.java +++ b/src/test/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregatorTest.java @@ -120,7 +120,7 @@ public void testResetRootDocId() throws Exception { AggregationContext context = new AggregationContext(searchContext); AggregatorFactories.Builder builder = AggregatorFactories.builder(); - builder.add(new NestedAggregator.Factory("test", "nested_field", FilterCachingPolicy.ALWAYS_CACHE)); + builder.addAggregator(new NestedAggregator.Factory("test", "nested_field", FilterCachingPolicy.ALWAYS_CACHE)); AggregatorFactories factories = builder.build(); searchContext.aggregations(new SearchContextAggregations(factories)); Aggregator[] aggs = factories.createTopLevelAggregators(context); From 55b82db34638fa15470da2bbf71b4e98861d1203 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Thu, 12 Feb 2015 12:32:54 +0000 Subject: [PATCH 05/68] Reducers are now wired end-to-end into the agg framework --- .../aggregations/AggregationModule.java | 2 + .../aggregations/InternalAggregation.java | 20 ++++ .../aggregations/InternalAggregations.java | 3 +- .../bucket/histogram/InternalHistogram.java | 12 +- .../significant/UnmappedSignificantTerms.java | 2 +- .../bucket/terms/UnmappedTerms.java | 2 +- .../metrics/tophits/InternalTopHits.java | 20 ++-- .../metrics/tophits/TopHitsAggregator.java | 6 +- .../reducers/InternalSimpleValue.java | 103 ++++++++++++++++++ .../search/aggregations/reducers/Reducer.java | 45 +++++++- .../aggregations/reducers/ReducerFactory.java | 3 +- .../aggregations/reducers/ReducerStreams.java | 68 ++++++++++++ .../aggregations/reducers/SimpleValue.java | 26 +++++ 13 files changed, 294 insertions(+), 18 deletions(-) create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/InternalSimpleValue.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerStreams.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/SimpleValue.java diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregationModule.java b/src/main/java/org/elasticsearch/search/aggregations/AggregationModule.java index 3910f0962466c..cb4bef6ca342f 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregationModule.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregationModule.java @@ -98,6 +98,8 @@ public AggregationModule() { aggParsers.add(GeoBoundsParser.class); aggParsers.add(ScriptedMetricParser.class); aggParsers.add(ChildrenParser.class); + + // NOCOMMIT reducerParsers.add(FooParser.class); } /** diff --git a/src/main/java/org/elasticsearch/search/aggregations/InternalAggregation.java b/src/main/java/org/elasticsearch/search/aggregations/InternalAggregation.java index 828a1a7ee0f89..fb621ea5103ba 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/InternalAggregation.java +++ b/src/main/java/org/elasticsearch/search/aggregations/InternalAggregation.java @@ -18,6 +18,9 @@ */ package org.elasticsearch.search.aggregations; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.StreamInput; @@ -29,6 +32,7 @@ import org.elasticsearch.common.xcontent.XContentBuilderString; import org.elasticsearch.script.ScriptService; import org.elasticsearch.search.aggregations.reducers.Reducer; +import org.elasticsearch.search.aggregations.reducers.ReducerStreams; import org.elasticsearch.search.aggregations.support.AggregationPath; import java.io.IOException; @@ -209,6 +213,11 @@ public final XContentBuilder toXContent(XContentBuilder builder, Params params) public final void writeTo(StreamOutput out) throws IOException { out.writeString(name); out.writeGenericValue(metaData); + out.writeVInt(reducers.size()); + for (Reducer reducer : reducers) { + out.writeBytesReference(reducer.type().stream()); + reducer.writeTo(out); + } doWriteTo(out); } @@ -217,6 +226,17 @@ public final void writeTo(StreamOutput out) throws IOException { public final void readFrom(StreamInput in) throws IOException { name = in.readString(); metaData = in.readMap(); + int size = in.readVInt(); + if (size == 0) { + reducers = ImmutableList.of(); + } else { + reducers = Lists.newArrayListWithCapacity(size); + for (int i = 0; i < size; i++) { + BytesReference type = in.readBytesReference(); + Reducer reducer = ReducerStreams.stream(type).readResult(in); + reducers.add(reducer); + } + } doReadFrom(in); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java b/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java index ec4625e23874a..c41e8a4ff77ef 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java +++ b/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java @@ -165,7 +165,8 @@ public static InternalAggregations reduce(List aggregation for (Map.Entry> entry : aggByName.entrySet()) { List aggregations = entry.getValue(); InternalAggregation first = aggregations.get(0); // the list can't be empty as it's created on demand - reducedAggregations.add(first.doReduce(new InternalAggregation.ReduceContext(aggregations, context.bigArrays(), context.scriptService()))); + reducedAggregations.add(first.reduce(new InternalAggregation.ReduceContext(aggregations, context.bigArrays(), context + .scriptService()))); } return new InternalAggregations(reducedAggregations); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java index a33cdb49b3ca3..5c945afddf0ff 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java @@ -186,6 +186,14 @@ public void writeTo(StreamOutput out) throws IOException { out.writeVLong(docCount); aggregations.writeTo(out); } + + public ValueFormatter getFormatter() { + return formatter; + } + + public boolean getKeyed() { + return keyed; + } } static class EmptyBucketInfo { @@ -224,7 +232,7 @@ public static void writeTo(EmptyBucketInfo info, StreamOutput out) throws IOExce } - static class Factory { + public static class Factory { protected Factory() { } @@ -283,7 +291,7 @@ public List getBuckets() { return buckets; } - protected Factory getFactory() { + public Factory getFactory() { return factory; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/UnmappedSignificantTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/UnmappedSignificantTerms.java index f382237dacfa3..0409900927280 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/UnmappedSignificantTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/UnmappedSignificantTerms.java @@ -71,7 +71,7 @@ public Type type() { public InternalAggregation doReduce(ReduceContext reduceContext) { for (InternalAggregation aggregation : reduceContext.aggregations()) { if (!(aggregation instanceof UnmappedSignificantTerms)) { - return aggregation.doReduce(reduceContext); + return aggregation.reduce(reduceContext); } } return this; diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedTerms.java index 82c850bcac787..89134a394ec67 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedTerms.java @@ -86,7 +86,7 @@ protected void doWriteTo(StreamOutput out) throws IOException { public InternalAggregation doReduce(ReduceContext reduceContext) { for (InternalAggregation agg : reduceContext.aggregations()) { if (!(agg instanceof UnmappedTerms)) { - return agg.doReduce(reduceContext); + return agg.reduce(reduceContext); } } return this; diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/InternalTopHits.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/InternalTopHits.java index 8c5eafa29617a..b3e4c5cf4c92d 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/InternalTopHits.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/InternalTopHits.java @@ -18,9 +18,6 @@ */ package org.elasticsearch.search.aggregations.metrics.tophits; -import java.io.IOException; -import java.util.List; - import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.Sort; import org.apache.lucene.search.TopDocs; @@ -35,9 +32,14 @@ import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.metrics.InternalMetricsAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.internal.InternalSearchHit; import org.elasticsearch.search.internal.InternalSearchHits; +import java.io.IOException; +import java.util.List; +import java.util.Map; + /** */ public class InternalTopHits extends InternalMetricsAggregation implements TopHits { @@ -65,16 +67,17 @@ public static void registerStreams() { InternalTopHits() { } - public InternalTopHits(String name, int from, int size, TopDocs topDocs, InternalSearchHits searchHits) { - this.name = name; + public InternalTopHits(String name, int from, int size, TopDocs topDocs, InternalSearchHits searchHits, List reducers, + Map metaData) { + super(name, reducers, metaData); this.from = from; this.size = size; this.topDocs = topDocs; this.searchHits = searchHits; } - public InternalTopHits(String name, InternalSearchHits searchHits) { - this.name = name; + public InternalTopHits(String name, InternalSearchHits searchHits, List reducers, Map metaData) { + super(name, reducers, metaData); this.searchHits = searchHits; this.topDocs = Lucene.EMPTY_TOP_DOCS; } @@ -123,7 +126,8 @@ public InternalAggregation doReduce(ReduceContext reduceContext) { } while (shardDocs[scoreDoc.shardIndex].scoreDocs[position] != scoreDoc); hits[i] = (InternalSearchHit) shardHits[scoreDoc.shardIndex].getAt(position); } - return new InternalTopHits(name, new InternalSearchHits(hits, reducedTopDocs.totalHits, reducedTopDocs.getMaxScore())); + return new InternalTopHits(name, new InternalSearchHits(hits, reducedTopDocs.totalHits, reducedTopDocs.getMaxScore()), + reducers(), getMetaData()); } catch (IOException e) { throw ExceptionsHelper.convertToElastic(e); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregator.java index 20aeaae2f5a2f..6abf1917c1750 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregator.java @@ -158,13 +158,15 @@ public InternalAggregation buildAggregation(long owningBucketOrdinal) { searchHitFields.sortValues(fieldDoc.fields); } } - return new InternalTopHits(name, subSearchContext.from(), subSearchContext.size(), topDocs, fetchResult.hits()); + return new InternalTopHits(name, subSearchContext.from(), subSearchContext.size(), topDocs, fetchResult.hits(), reducers(), + metaData()); } } @Override public InternalAggregation buildEmptyAggregation() { - return new InternalTopHits(name, subSearchContext.from(), subSearchContext.size(), Lucene.EMPTY_TOP_DOCS, InternalSearchHits.empty()); + return new InternalTopHits(name, subSearchContext.from(), subSearchContext.size(), Lucene.EMPTY_TOP_DOCS, + InternalSearchHits.empty(), reducers(), metaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/InternalSimpleValue.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/InternalSimpleValue.java new file mode 100644 index 0000000000000..7d204c007c6b3 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/InternalSimpleValue.java @@ -0,0 +1,103 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers; + +import org.elasticsearch.common.inject.internal.Nullable; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.search.aggregations.AggregationStreams; +import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; +import org.elasticsearch.search.aggregations.metrics.max.InternalMax; +import org.elasticsearch.search.aggregations.support.format.ValueFormatter; +import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public class InternalSimpleValue extends InternalNumericMetricsAggregation.SingleValue implements SimpleValue { + + public final static Type TYPE = new Type("simple_value"); + + public final static AggregationStreams.Stream STREAM = new AggregationStreams.Stream() { + @Override + public InternalSimpleValue readResult(StreamInput in) throws IOException { + InternalSimpleValue result = new InternalSimpleValue(); + result.readFrom(in); + return result; + } + }; + + public static void registerStreams() { + AggregationStreams.registerStream(STREAM, TYPE.stream()); + } + + private double value; + + InternalSimpleValue() {} // for serialization + + public InternalSimpleValue(String name, double value, @Nullable ValueFormatter formatter, List reducers, Map metaData) { + super(name, reducers, metaData); + this.valueFormatter = formatter; + this.value = value; + } + + @Override + public double value() { + return value; + } + + public double getValue() { + return value; + } + + @Override + public Type type() { + return TYPE; + } + + @Override + public InternalMax doReduce(ReduceContext reduceContext) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + protected void doReadFrom(StreamInput in) throws IOException { + valueFormatter = ValueFormatterStreams.readOptional(in); + value = in.readDouble(); + } + + @Override + protected void doWriteTo(StreamOutput out) throws IOException { + ValueFormatterStreams.writeOptional(valueFormatter, out); + out.writeDouble(value); + } + + @Override + public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { + boolean hasValue = !Double.isInfinite(value); + builder.field(CommonFields.VALUE, hasValue ? value : null); + if (hasValue && valueFormatter != null) { + builder.field(CommonFields.VALUE_AS_STRING, valueFormatter.format(value)); + } + return builder; + } +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java index c74f6f0b0f22e..d87d9fa72e1e6 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java @@ -19,14 +19,19 @@ package org.elasticsearch.search.aggregations.reducers; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Streamable; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; +import org.elasticsearch.search.aggregations.InternalAggregation.Type; import org.elasticsearch.search.internal.SearchContext; import java.io.IOException; +import java.util.Map; -public abstract class Reducer { +public abstract class Reducer implements Streamable { /** * Parses the reducer request and creates the appropriate reducer factory @@ -58,6 +63,44 @@ public static interface Parser { } + protected String name; + protected Map metaData; + + protected Reducer() { // for Serialisation + } + + protected Reducer(String name, Map metaData) { + this.name = name; + this.metaData = metaData; + } + + public String name() { + return name; + } + + public Map metaData() { + return metaData; + } + + public abstract Type type(); + public abstract InternalAggregation reduce(InternalAggregation aggregation, ReduceContext reduceContext); + @Override + public final void writeTo(StreamOutput out) throws IOException { + out.writeString(name); + out.writeMap(metaData); + doWriteTo(out); + } + + protected abstract void doWriteTo(StreamOutput out) throws IOException; + + @Override + public final void readFrom(StreamInput in) throws IOException { + name = in.readString(); + metaData = in.readMap(); + doReadFrom(in); + } + + protected abstract void doReadFrom(StreamInput in) throws IOException; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java index 64e7d1c7baf5b..4249cde2dc3ae 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java @@ -18,7 +18,6 @@ */ package org.elasticsearch.search.aggregations.reducers; -import org.elasticsearch.common.io.stream.Streamable; import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.support.AggregationContext; @@ -28,7 +27,7 @@ /** * A factory that knows how to create an {@link Aggregator} of a specific type. */ -public abstract class ReducerFactory implements Streamable { +public abstract class ReducerFactory { protected String name; protected String type; diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerStreams.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerStreams.java new file mode 100644 index 0000000000000..7a4319e0a2ba0 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerStreams.java @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.search.aggregations.reducers; + +import com.google.common.collect.ImmutableMap; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.MapBuilder; +import org.elasticsearch.common.io.stream.StreamInput; + +import java.io.IOException; + +/** + * A registry for all the dedicated streams in the aggregation module. This is to support dynamic addAggregation that + * know how to stream themselves. + */ +public class ReducerStreams { + + private static ImmutableMap streams = ImmutableMap.of(); + + /** + * A stream that knows how to read an aggregation from the input. + */ + public static interface Stream { + Reducer readResult(StreamInput in) throws IOException; + } + + /** + * Registers the given stream and associate it with the given types. + * + * @param stream The streams to register + * @param types The types associated with the streams + */ + public static synchronized void registerStream(Stream stream, BytesReference... types) { + MapBuilder uStreams = MapBuilder.newMapBuilder(streams); + for (BytesReference type : types) { + uStreams.put(type, stream); + } + streams = uStreams.immutableMap(); + } + + /** + * Returns the stream that is registered for the given type + * + * @param type The given type + * @return The associated stream + */ + public static Stream stream(BytesReference type) { + return streams.get(type); + } + +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/SimpleValue.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/SimpleValue.java new file mode 100644 index 0000000000000..e1c510e1a29c2 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/SimpleValue.java @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers; + +import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation; + +public interface SimpleValue extends NumericMetricsAggregation.SingleValue { + +} From 9cfa6c6af7141bb662de64e979aeb79d385f4a69 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Thu, 12 Feb 2015 13:22:22 +0000 Subject: [PATCH 06/68] Basic derivative reducer --- .../aggregations/AggregationModule.java | 3 +- .../TransportAggregationModule.java | 6 +- .../reducers/derivative/DerivativeParser.java | 69 +++++++++ .../derivative/DerivativeReducer.java | 138 ++++++++++++++++++ 4 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregationModule.java b/src/main/java/org/elasticsearch/search/aggregations/AggregationModule.java index cb4bef6ca342f..d1cb6d968005b 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregationModule.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregationModule.java @@ -56,6 +56,7 @@ import org.elasticsearch.search.aggregations.metrics.tophits.TopHitsParser; import org.elasticsearch.search.aggregations.metrics.valuecount.ValueCountParser; import org.elasticsearch.search.aggregations.reducers.Reducer; +import org.elasticsearch.search.aggregations.reducers.derivative.DerivativeParser; import java.util.List; @@ -99,7 +100,7 @@ public AggregationModule() { aggParsers.add(ScriptedMetricParser.class); aggParsers.add(ChildrenParser.class); - // NOCOMMIT reducerParsers.add(FooParser.class); + reducerParsers.add(DerivativeParser.class); } /** diff --git a/src/main/java/org/elasticsearch/search/aggregations/TransportAggregationModule.java b/src/main/java/org/elasticsearch/search/aggregations/TransportAggregationModule.java index ce09d1e5c6972..c99f885462ccd 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/TransportAggregationModule.java +++ b/src/main/java/org/elasticsearch/search/aggregations/TransportAggregationModule.java @@ -57,6 +57,7 @@ import org.elasticsearch.search.aggregations.metrics.sum.InternalSum; import org.elasticsearch.search.aggregations.metrics.tophits.InternalTopHits; import org.elasticsearch.search.aggregations.metrics.valuecount.InternalValueCount; +import org.elasticsearch.search.aggregations.reducers.derivative.DerivativeReducer; /** * A module that registers all the transport streams for the addAggregation @@ -89,7 +90,7 @@ protected void configure() { SignificantStringTerms.registerStreams(); SignificantLongTerms.registerStreams(); UnmappedSignificantTerms.registerStreams(); - InternalGeoHashGrid.registerStreams(); + InternalGeoHashGrid.registerStreams(); DoubleTerms.registerStreams(); UnmappedTerms.registerStreams(); InternalRange.registerStream(); @@ -102,6 +103,9 @@ protected void configure() { InternalTopHits.registerStreams(); InternalGeoBounds.registerStream(); InternalChildren.registerStream(); + + // Reducers + DerivativeReducer.registerStreams(); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java new file mode 100644 index 0000000000000..0e9b1f7f41f86 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.derivative; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.search.SearchParseException; +import org.elasticsearch.search.aggregations.reducers.Reducer; +import org.elasticsearch.search.aggregations.reducers.ReducerFactory; +import org.elasticsearch.search.internal.SearchContext; + +import java.io.IOException; + +public class DerivativeParser implements Reducer.Parser { + + public static final ParseField BUCKETS_PATH = new ParseField("bucketsPath"); + + @Override + public String type() { + return DerivativeReducer.TYPE.name(); + } + + @Override + public ReducerFactory parse(String reducerName, XContentParser parser, SearchContext context) throws IOException { + XContentParser.Token token; + String currentFieldName = null; + String bucketsPath = null; + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.VALUE_STRING) { + if (BUCKETS_PATH.match(currentFieldName)) { + bucketsPath = parser.text(); + } else { + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + reducerName + "]: [" + + currentFieldName + "]."); + } + } else { + throw new SearchParseException(context, "Unexpected token " + token + " in [" + reducerName + "]."); + } + } + + if (bucketsPath == null) { + throw new SearchParseException(context, "Missing required field [" + BUCKETS_PATH.getPreferredName() + + "] for derivative aggregation [" + reducerName + "]"); + } + + return new DerivativeReducer.Factory(reducerName, bucketsPath); + } + +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java new file mode 100644 index 0000000000000..d2cfae6784bae --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java @@ -0,0 +1,138 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.derivative; + +import com.google.common.base.Function; +import com.google.common.collect.Lists; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.search.aggregations.Aggregation; +import org.elasticsearch.search.aggregations.Aggregator; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; +import org.elasticsearch.search.aggregations.InternalAggregation.Type; +import org.elasticsearch.search.aggregations.InternalAggregations; +import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; +import org.elasticsearch.search.aggregations.reducers.InternalSimpleValue; +import org.elasticsearch.search.aggregations.reducers.Reducer; +import org.elasticsearch.search.aggregations.reducers.ReducerFactory; +import org.elasticsearch.search.aggregations.reducers.ReducerStreams; +import org.elasticsearch.search.aggregations.support.AggregationContext; +import org.elasticsearch.search.aggregations.support.AggregationPath; +import org.joda.time.DateTime; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class DerivativeReducer extends Reducer { + + public final static Type TYPE = new Type("derivative"); + + public final static ReducerStreams.Stream STREAM = new ReducerStreams.Stream() { + @Override + public DerivativeReducer readResult(StreamInput in) throws IOException { + DerivativeReducer result = new DerivativeReducer(); + result.readFrom(in); + return result; + } + }; + + public static void registerStreams() { + ReducerStreams.registerStream(STREAM, TYPE.stream()); + } + + private String bucketsPath; + private static final Function FUNCTION = new Function() { + @Override + public InternalAggregation apply(Aggregation input) { + return (InternalAggregation) input; + } + }; + + public DerivativeReducer() { + } + + public DerivativeReducer(String name, String bucketsPath, Map metadata) { + super(name, metadata); + this.bucketsPath = bucketsPath; + } + + @Override + public Type type() { + return TYPE; + } + + @Override + public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext reduceContext) { + InternalHistogram histo = (InternalHistogram) aggregation; + List buckets = histo.getBuckets(); + InternalHistogram.Factory factory = histo.getFactory(); + List newBuckets = new ArrayList<>(); + Double lastBucketValue = null; + for (InternalHistogram.Bucket bucket : buckets) { + double thisBucketValue = (double) bucket.getProperty(histo.getName(), AggregationPath.parse(bucketsPath) + .getPathElementsAsStringList()); + if (lastBucketValue != null) { + double diff = thisBucketValue - lastBucketValue; + + List aggs = new ArrayList<>(Lists.transform(bucket.getAggregations().asList(), FUNCTION)); + aggs.add(new InternalSimpleValue(bucketsPath, diff, null, new ArrayList(), metaData())); // NOCOMMIT implement formatter for derivative reducer + InternalHistogram.Bucket newBucket = factory.createBucket(((DateTime) bucket.getKey()).getMillis(), bucket.getDocCount(), + new InternalAggregations(aggs), bucket.getKeyed(), bucket.getFormatter()); // NOCOMMIT fix key resolution for dates + newBuckets.add(newBucket); + } else { + newBuckets.add(bucket); + } + lastBucketValue = thisBucketValue; + } + return factory.create(histo.getName(), newBuckets, null, 1, null, null, false, new ArrayList(), histo.getMetaData()); // NOCOMMIT get order, minDocCount, emptyBucketInfo etc. from histo + } + + @Override + public void doReadFrom(StreamInput in) throws IOException { + bucketsPath = in.readString(); + + } + + @Override + public void doWriteTo(StreamOutput out) throws IOException { + out.writeString(bucketsPath); + } + + public static class Factory extends ReducerFactory { + + private String bucketsPath; + + public Factory(String name, String field) { + super(name, TYPE.name()); + this.bucketsPath = field; + } + + @Override + protected Reducer createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, + Map metaData) throws IOException { + return new DerivativeReducer(name, bucketsPath, metaData); + } + + } +} From d65e9a4a90deba1f749b073c2f303f0ea0f593ef Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Thu, 12 Feb 2015 13:52:56 +0000 Subject: [PATCH 07/68] Fixing compile issues after rebase with master Mostly due to @jpountz's leaf collector changes --- .../search/aggregations/AggregatorBase.java | 10 +++++++++- .../search/aggregations/AggregatorFactory.java | 14 +++++++++----- .../aggregations/bucket/BucketsAggregator.java | 3 ++- .../GlobalOrdinalsSignificantTermsAggregator.java | 10 +++++----- .../terms/GlobalOrdinalsStringTermsAggregator.java | 5 +++-- .../bucket/terms/TermsAggregatorFactory.java | 3 +-- 6 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorBase.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorBase.java index e23639352cf0d..661d975a41fb4 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorBase.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorBase.java @@ -20,6 +20,7 @@ import org.apache.lucene.index.LeafReaderContext; import org.elasticsearch.search.aggregations.bucket.DeferringBucketCollector; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.internal.SearchContext.Lifetime; import org.elasticsearch.search.query.QueryPhaseExecutionException; @@ -45,6 +46,7 @@ public abstract class AggregatorBase extends Aggregator { private Map subAggregatorbyName; private DeferringBucketCollector recordingWrapper; + private final List reducers; /** * Constructs a new Aggregator. @@ -55,8 +57,10 @@ public abstract class AggregatorBase extends Aggregator { * @param parent The parent aggregator (may be {@code null} for top level aggregators) * @param metaData The metaData associated with this aggregator */ - protected AggregatorBase(String name, AggregatorFactories factories, AggregationContext context, Aggregator parent, Map metaData) throws IOException { + protected AggregatorBase(String name, AggregatorFactories factories, AggregationContext context, Aggregator parent, + List reducers, Map metaData) throws IOException { this.name = name; + this.reducers = reducers; this.metaData = metaData; this.parent = parent; this.context = context; @@ -106,6 +110,10 @@ public Map metaData() { return this.metaData; } + public List reducers() { + return this.reducers; + } + /** * Get a {@link LeafBucketCollector} for the given ctx, which should * delegate to the given collector. diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactory.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactory.java index d22fed75a8c23..41aee8f931f3e 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactory.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactory.java @@ -112,7 +112,11 @@ public void setMetaData(Map metaData) { * to collect bucket 0, this returns an aggregator that can collect any bucket. */ protected static Aggregator asMultiBucketAggregator(final AggregatorFactory factory, final AggregationContext context, final Aggregator parent) throws IOException { - final Aggregator first = factory.create(context, parent, truegator> aggregators; + final Aggregator first = factory.create(context, parent, true); + final BigArrays bigArrays = context.bigArrays(); + return new Aggregator() { + + ObjectArray aggregators; ObjectArray collectors; { @@ -188,9 +192,9 @@ public void collect(int doc, long bucket) throws IOException { LeafBucketCollector collector = collectors.get(bucket); if (collector == null) { Aggregator aggregator = aggregators.get(bucket); - if (aggregator == null) { - aggregator = factory.create(context, parent, true); - aggregator.preCollection(); + if (aggregator == null) { + aggregator = factory.create(context, parent, true); + aggregator.preCollection(); aggregators.set(bucket, aggregator); } collector = aggregator.getLeafCollector(ctx); @@ -198,7 +202,7 @@ public void collect(int doc, long bucket) throws IOException { collectors.set(bucket, collector); } collector.collect(doc, 0); - } + } }; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/BucketsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/BucketsAggregator.java index b7c8fe7ccfca0..93fa360b113c6 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/BucketsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/BucketsAggregator.java @@ -21,13 +21,13 @@ import org.elasticsearch.common.lease.Releasable; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.IntArray; -import org.elasticsearch.search.aggregations.Aggregation; import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorBase; import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.LeafBucketCollector; +import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import java.io.IOException; @@ -47,6 +47,7 @@ public BucketsAggregator(String name, AggregatorFactories factories, AggregationContext context, Aggregator parent, List reducers, Map metaData) throws IOException { super(name, factories, context, parent, reducers, metaData); + bigArrays = context.bigArrays(); docCounts = bigArrays.newIntArray(1, true); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/GlobalOrdinalsSignificantTermsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/GlobalOrdinalsSignificantTermsAggregator.java index c7e260faf63e3..0d08c6d5efee8 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/GlobalOrdinalsSignificantTermsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/GlobalOrdinalsSignificantTermsAggregator.java @@ -25,8 +25,8 @@ import org.elasticsearch.common.util.LongHash; import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorFactories; -import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.LeafBucketCollector; +import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.bucket.terms.GlobalOrdinalsStringTermsAggregator; import org.elasticsearch.search.aggregations.bucket.terms.support.IncludeExclude; import org.elasticsearch.search.aggregations.reducers.Reducer; @@ -48,12 +48,12 @@ public class GlobalOrdinalsSignificantTermsAggregator extends GlobalOrdinalsStri protected long numCollectedDocs; protected final SignificantTermsAggregatorFactory termsAggFactory; - public GlobalOrdinalsSignificantTermsAggregator(String name, AggregatorFactories factories, ValuesSource.Bytes.WithOrdinals.FieldData valuesSource, - BucketCountThresholds bucketCountThresholds, - IncludeExclude includeExclude, AggregationContext aggregationContext, Aggregator parent, + public GlobalOrdinalsSignificantTermsAggregator(String name, AggregatorFactories factories, + ValuesSource.Bytes.WithOrdinals.FieldData valuesSource, BucketCountThresholds bucketCountThresholds, + IncludeExclude includeExclude, AggregationContext aggregationContext, Aggregator parent, SignificantTermsAggregatorFactory termsAggFactory, List reducers, Map metaData) throws IOException { - super(name, factories, valuesSource, maxOrd, null, bucketCountThresholds, includeExclude, aggregationContext, parent, + super(name, factories, valuesSource, null, bucketCountThresholds, includeExclude, aggregationContext, parent, SubAggCollectionMode.DEPTH_FIRST, false, reducers, metaData); this.termsAggFactory = termsAggFactory; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/GlobalOrdinalsStringTermsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/GlobalOrdinalsStringTermsAggregator.java index bff09e07e4a84..6f538b384d35c 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/GlobalOrdinalsStringTermsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/GlobalOrdinalsStringTermsAggregator.java @@ -37,10 +37,10 @@ import org.elasticsearch.index.fielddata.ordinals.GlobalOrdinalMapping; import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorFactories; -import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.LeafBucketCollector; +import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.bucket.terms.InternalTerms.Bucket; import org.elasticsearch.search.aggregations.bucket.terms.support.BucketPriorityQueue; import org.elasticsearch.search.aggregations.bucket.terms.support.IncludeExclude; @@ -74,7 +74,8 @@ public class GlobalOrdinalsStringTermsAggregator extends AbstractStringTermsAggr public GlobalOrdinalsStringTermsAggregator(String name, AggregatorFactories factories, ValuesSource.Bytes.WithOrdinals.FieldData valuesSource, Terms.Order order, BucketCountThresholds bucketCountThresholds, IncludeExclude includeExclude, AggregationContext aggregationContext, Aggregator parent, SubAggCollectionMode collectionMode, boolean showTermDocCountError, List reducers, Map metaData) throws IOException { - super(name, factories, aggregationContext, parent, order, bucketCountThresholds, collectionMode, showTermDocCountError, reducers, reducers, metaData); + super(name, factories, aggregationContext, parent, order, bucketCountThresholds, collectionMode, showTermDocCountError, reducers, + metaData); this.valuesSource = valuesSource; this.includeExclude = includeExclude; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorFactory.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorFactory.java index a9cb4ea19cb9a..59fa19366d696 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorFactory.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorFactory.java @@ -1,4 +1,4 @@ -List Date: Thu, 12 Feb 2015 14:01:34 +0000 Subject: [PATCH 08/68] fix to the name of the injected aggregation for derivatives --- .../aggregations/reducers/derivative/DerivativeReducer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java index d2cfae6784bae..975ad809adf94 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java @@ -96,9 +96,9 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext double diff = thisBucketValue - lastBucketValue; List aggs = new ArrayList<>(Lists.transform(bucket.getAggregations().asList(), FUNCTION)); - aggs.add(new InternalSimpleValue(bucketsPath, diff, null, new ArrayList(), metaData())); // NOCOMMIT implement formatter for derivative reducer + aggs.add(new InternalSimpleValue(name(), diff, null, new ArrayList(), metaData())); // NOCOMMIT implement formatter for derivative reducer InternalHistogram.Bucket newBucket = factory.createBucket(((DateTime) bucket.getKey()).getMillis(), bucket.getDocCount(), - new InternalAggregations(aggs), bucket.getKeyed(), bucket.getFormatter()); // NOCOMMIT fix key resolution for dates + new InternalAggregations(aggs), bucket.getKeyed(), bucket.getFormatter()); // NOCOMMIT fix key resolution to deal with numbers and dates newBuckets.add(newBucket); } else { newBuckets.add(bucket); From f00a9b85578cdfe5a40a2677d3af4be0960670b5 Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Thu, 12 Feb 2015 15:50:11 +0100 Subject: [PATCH 09/68] Minor indentation/validation fix in AggregatorParsers. --- .../aggregations/AggregatorParsers.java | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorParsers.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorParsers.java index e23cf8ef228cf..62caa385585fd 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorParsers.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorParsers.java @@ -150,28 +150,35 @@ private AggregatorFactories parseAggregators(XContentParser parser, SearchContex subFactories = parseAggregators(parser, context, level+1); break; default: - if (aggFactory != null) { - throw new SearchParseException(context, "Found two aggregation type definitions in [" + aggregationName + "]: [" + if (aggFactory != null) { + throw new SearchParseException(context, "Found two aggregation type definitions in [" + aggregationName + "]: [" + aggFactory.type + "] and [" + fieldName + "]"); } + if (reducerFactory != null) { + // TODO we would need a .type property on reducers too for this error message? + throw new SearchParseException(context, "Found two aggregation type definitions in [" + aggregationName + "]: [" + + reducerFactory + "] and [" + fieldName + "]"); + } + Aggregator.Parser aggregatorParser = parser(fieldName); if (aggregatorParser == null) { - Reducer.Parser reducerParser = reducer(fieldName); - if (reducerParser == null) { - throw new SearchParseException(context, "Could not find aggregator type [" + fieldName + "] in [" + Reducer.Parser reducerParser = reducer(fieldName); + if (reducerParser == null) { + throw new SearchParseException(context, "Could not find aggregator type [" + fieldName + "] in [" + aggregationName + "]"); + } else { + reducerFactory = reducerParser.parse(aggregationName, parser, context); + } } else { - reducerFactory = reducerParser.parse(aggregationName, parser, context); + aggFactory = aggregatorParser.parse(aggregationName, parser, context); } - } else { - aggFactory = aggregatorParser.parse(aggregationName, parser, context); - } } } if (aggFactory == null && reducerFactory == null) { throw new SearchParseException(context, "Missing definition for aggregation [" + aggregationName + "]"); } else if (aggFactory != null) { + assert reducerFactory == null; if (metaData != null) { aggFactory.setMetaData(metaData); } @@ -185,13 +192,13 @@ private AggregatorFactories parseAggregators(XContentParser parser, SearchContex } factories.addAggregator(aggFactory); - } else if (reducerFactory != null) { + } else { + assert reducerFactory != null; if (subFactories != null) { throw new SearchParseException(context, "Aggregation [" + aggregationName + "] cannot define sub-aggregations"); } + // TODO: should we validate here like aggs? factories.addReducer(reducerFactory); - } else { - throw new SearchParseException(context, "Found two sub aggregation definitions under [" + aggregationName + "]"); } } From 3a777545de9df45d688171c942c2234660c8a9b7 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Thu, 12 Feb 2015 15:36:02 +0000 Subject: [PATCH 10/68] derivative reducer now works with both date_histogram and histogram --- .../bucket/histogram/InternalDateHistogram.java | 12 ++++++++++-- .../bucket/histogram/InternalHistogram.java | 10 ++++++++-- .../search/aggregations/reducers/ReducerFactory.java | 2 +- .../reducers/derivative/DerivativeReducer.java | 7 ++++--- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java index 9f9ad81c9530e..0457ad9e92cde 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java @@ -19,6 +19,7 @@ package org.elasticsearch.search.aggregations.bucket.histogram; import org.elasticsearch.common.Nullable; +import org.elasticsearch.search.aggregations.AggregationExecutionException; import org.elasticsearch.search.aggregations.InternalAggregation.Type; import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram.EmptyBucketInfo; @@ -83,8 +84,15 @@ public InternalHistogram create(String name, List } @Override - public InternalDateHistogram.Bucket createBucket(long key, long docCount, InternalAggregations aggregations, boolean keyed, @Nullable ValueFormatter formatter) { - return new Bucket(key, docCount, aggregations, keyed, formatter, this); + public InternalDateHistogram.Bucket createBucket(Object key, long docCount, InternalAggregations aggregations, boolean keyed, + @Nullable ValueFormatter formatter) { + if (key instanceof Number) { + return new Bucket(((Number) key).longValue(), docCount, aggregations, keyed, formatter, this); + } else if (key instanceof DateTime) { + return new Bucket(((DateTime) key).getMillis(), docCount, aggregations, keyed, formatter, this); + } else { + throw new AggregationExecutionException("Expected key of type Number or DateTime but got [" + key + "]"); + } } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java index 5c945afddf0ff..d5b3a1384f1a4 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java @@ -30,6 +30,7 @@ import org.elasticsearch.common.text.StringText; import org.elasticsearch.common.text.Text; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.search.aggregations.AggregationExecutionException; import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.InternalAggregation; @@ -247,8 +248,13 @@ public InternalHistogram create(String name, List buckets, InternalOrder o return new InternalHistogram<>(name, buckets, order, minDocCount, emptyBucketInfo, formatter, keyed, this, reducers, metaData); } - public B createBucket(long key, long docCount, InternalAggregations aggregations, boolean keyed, @Nullable ValueFormatter formatter) { - return (B) new Bucket(key, docCount, keyed, formatter, this, aggregations); + public B createBucket(Object key, long docCount, InternalAggregations aggregations, boolean keyed, + @Nullable ValueFormatter formatter) { + if (key instanceof Number) { + return (B) new Bucket(((Number) key).longValue(), docCount, keyed, formatter, this, aggregations); + } else { + throw new AggregationExecutionException("Expected key of type Number but got [" + key + "]"); + } } protected B createEmptyBucket(boolean keyed, @Nullable ValueFormatter formatter) { diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java index 4249cde2dc3ae..c4c6b304ba853 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java @@ -49,7 +49,7 @@ public ReducerFactory(String name, String type) { /** * Validates the state of this factory (makes sure the factory is properly configured) */ - public final void validate() { + public final void validate() { // NOCOMMIT hook in validation doValidate(); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java index 975ad809adf94..76d3bd9b3acc5 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java @@ -37,7 +37,6 @@ import org.elasticsearch.search.aggregations.reducers.ReducerStreams; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.AggregationPath; -import org.joda.time.DateTime; import java.io.IOException; import java.util.ArrayList; @@ -89,6 +88,7 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext InternalHistogram.Factory factory = histo.getFactory(); List newBuckets = new ArrayList<>(); Double lastBucketValue = null; + // NOCOMMIT this needs to be improved so that the aggs are cloned correctly to ensure aggs are fully immutable. for (InternalHistogram.Bucket bucket : buckets) { double thisBucketValue = (double) bucket.getProperty(histo.getName(), AggregationPath.parse(bucketsPath) .getPathElementsAsStringList()); @@ -97,8 +97,9 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext List aggs = new ArrayList<>(Lists.transform(bucket.getAggregations().asList(), FUNCTION)); aggs.add(new InternalSimpleValue(name(), diff, null, new ArrayList(), metaData())); // NOCOMMIT implement formatter for derivative reducer - InternalHistogram.Bucket newBucket = factory.createBucket(((DateTime) bucket.getKey()).getMillis(), bucket.getDocCount(), - new InternalAggregations(aggs), bucket.getKeyed(), bucket.getFormatter()); // NOCOMMIT fix key resolution to deal with numbers and dates + InternalHistogram.Bucket newBucket = factory.createBucket(bucket.getKey(), bucket.getDocCount(), + new InternalAggregations( + aggs), bucket.getKeyed(), bucket.getFormatter()); newBuckets.add(newBucket); } else { newBuckets.add(bucket); From 9805b8359b408e5bde4e0507646b09c6c17515e0 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Thu, 12 Feb 2015 15:47:06 +0000 Subject: [PATCH 11/68] can now reference single value metrics directly instead of having to add '.value' to the path --- .../reducers/derivative/DerivativeReducer.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java index 76d3bd9b3acc5..9a707687cc243 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java @@ -25,12 +25,14 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.search.aggregations.Aggregation; +import org.elasticsearch.search.aggregations.AggregationExecutionException; import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; import org.elasticsearch.search.aggregations.InternalAggregation.Type; import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; +import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; import org.elasticsearch.search.aggregations.reducers.InternalSimpleValue; import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.reducers.ReducerFactory; @@ -90,8 +92,7 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext Double lastBucketValue = null; // NOCOMMIT this needs to be improved so that the aggs are cloned correctly to ensure aggs are fully immutable. for (InternalHistogram.Bucket bucket : buckets) { - double thisBucketValue = (double) bucket.getProperty(histo.getName(), AggregationPath.parse(bucketsPath) - .getPathElementsAsStringList()); + double thisBucketValue = resolveBucketValue(histo, bucket); if (lastBucketValue != null) { double diff = thisBucketValue - lastBucketValue; @@ -109,6 +110,19 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext return factory.create(histo.getName(), newBuckets, null, 1, null, null, false, new ArrayList(), histo.getMetaData()); // NOCOMMIT get order, minDocCount, emptyBucketInfo etc. from histo } + private double resolveBucketValue(InternalHistogram histo, InternalHistogram.Bucket bucket) { + Object propertyValue = bucket.getProperty(histo.getName(), AggregationPath.parse(bucketsPath) + .getPathElementsAsStringList()); + if (propertyValue instanceof Number) { + return ((Number) propertyValue).doubleValue(); + } else if (propertyValue instanceof InternalNumericMetricsAggregation.SingleValue) { + return ((InternalNumericMetricsAggregation.SingleValue) propertyValue).value(); + } else { + throw new AggregationExecutionException(DerivativeParser.BUCKETS_PATH.getPreferredName() + + "must reference either a number value or a single value numeric metric aggregation"); + } + } + @Override public void doReadFrom(StreamInput in) throws IOException { bucketsPath = in.readString(); From 0f22d7e65ea7eb37953d71bc6eee9b5ff2323db7 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Thu, 12 Feb 2015 17:13:59 +0000 Subject: [PATCH 12/68] Can now specify a format for the returned derivative values --- .../reducers/derivative/DerivativeParser.java | 13 +++++++++- .../derivative/DerivativeReducer.java | 24 ++++++++++++------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java index 0e9b1f7f41f86..55259102dfd74 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java @@ -24,6 +24,8 @@ import org.elasticsearch.search.SearchParseException; import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.reducers.ReducerFactory; +import org.elasticsearch.search.aggregations.support.format.ValueFormat; +import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.internal.SearchContext; import java.io.IOException; @@ -31,6 +33,7 @@ public class DerivativeParser implements Reducer.Parser { public static final ParseField BUCKETS_PATH = new ParseField("bucketsPath"); + public static final ParseField FORMAT = new ParseField("format"); @Override public String type() { @@ -42,6 +45,7 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon XContentParser.Token token; String currentFieldName = null; String bucketsPath = null; + String format = null; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { @@ -49,6 +53,8 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon } else if (token == XContentParser.Token.VALUE_STRING) { if (BUCKETS_PATH.match(currentFieldName)) { bucketsPath = parser.text(); + } else if (FORMAT.match(currentFieldName)) { + format = parser.text(); } else { throw new SearchParseException(context, "Unknown key for a " + token + " in [" + reducerName + "]: [" + currentFieldName + "]."); @@ -63,7 +69,12 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon + "] for derivative aggregation [" + reducerName + "]"); } - return new DerivativeReducer.Factory(reducerName, bucketsPath); + ValueFormatter formatter = null; + if (format != null) { + formatter = ValueFormat.Patternable.Number.format(format).formatter(); + } + + return new DerivativeReducer.Factory(reducerName, bucketsPath, formatter); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java index 9a707687cc243..26f40b2824d82 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java @@ -22,6 +22,7 @@ import com.google.common.base.Function; import com.google.common.collect.Lists; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.search.aggregations.Aggregation; @@ -39,6 +40,7 @@ import org.elasticsearch.search.aggregations.reducers.ReducerStreams; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.AggregationPath; +import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.io.IOException; import java.util.ArrayList; @@ -62,7 +64,6 @@ public static void registerStreams() { ReducerStreams.registerStream(STREAM, TYPE.stream()); } - private String bucketsPath; private static final Function FUNCTION = new Function() { @Override public InternalAggregation apply(Aggregation input) { @@ -70,12 +71,16 @@ public InternalAggregation apply(Aggregation input) { } }; + private ValueFormatter formatter; + private String bucketsPath; + public DerivativeReducer() { } - public DerivativeReducer(String name, String bucketsPath, Map metadata) { + public DerivativeReducer(String name, String bucketsPath, @Nullable ValueFormatter formatter, Map metadata) { super(name, metadata); this.bucketsPath = bucketsPath; + this.formatter = formatter; } @Override @@ -97,9 +102,8 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext double diff = thisBucketValue - lastBucketValue; List aggs = new ArrayList<>(Lists.transform(bucket.getAggregations().asList(), FUNCTION)); - aggs.add(new InternalSimpleValue(name(), diff, null, new ArrayList(), metaData())); // NOCOMMIT implement formatter for derivative reducer - InternalHistogram.Bucket newBucket = factory.createBucket(bucket.getKey(), bucket.getDocCount(), - new InternalAggregations( + aggs.add(new InternalSimpleValue(name(), diff, formatter, new ArrayList(), metaData())); + InternalHistogram.Bucket newBucket = factory.createBucket(bucket.getKey(), bucket.getDocCount(), new InternalAggregations( aggs), bucket.getKeyed(), bucket.getFormatter()); newBuckets.add(newBucket); } else { @@ -136,17 +140,19 @@ public void doWriteTo(StreamOutput out) throws IOException { public static class Factory extends ReducerFactory { - private String bucketsPath; + private final String bucketsPath; + private final ValueFormatter formatter; - public Factory(String name, String field) { + public Factory(String name, String bucketsPath, @Nullable ValueFormatter formatter) { super(name, TYPE.name()); - this.bucketsPath = field; + this.bucketsPath = bucketsPath; + this.formatter = formatter; } @Override protected Reducer createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, Map metaData) throws IOException { - return new DerivativeReducer(name, bucketsPath, metaData); + return new DerivativeReducer(name, bucketsPath, formatter, metaData); } } From 18c2cb64b78a6ba7ea6f9285b91dee675e59489a Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Fri, 13 Feb 2015 14:33:44 +0000 Subject: [PATCH 13/68] Validation of the reducer factories is now called from within the AggregatorFactories --- .../elasticsearch/search/aggregations/AggregatorFactories.java | 3 +++ .../search/aggregations/reducers/ReducerFactory.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java index 5103f9c2b7a60..a4f68b05efb47 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java @@ -106,6 +106,9 @@ public void validate() { for (AggregatorFactory factory : factories) { factory.validate(); } + for (ReducerFactory factory : reducerFactories) { + factory.validate(); + } } private final static class Empty extends AggregatorFactories { diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java index c4c6b304ba853..4249cde2dc3ae 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java @@ -49,7 +49,7 @@ public ReducerFactory(String name, String type) { /** * Validates the state of this factory (makes sure the factory is properly configured) */ - public final void validate() { // NOCOMMIT hook in validation + public final void validate() { doValidate(); } From 9357fc4f95f7f2a8a09a0e7fd8d6b695618e7d12 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Fri, 13 Feb 2015 15:43:39 +0000 Subject: [PATCH 14/68] bucketsPath is now in the Reducer class since every Reducer implementation will need it --- .../search/aggregations/reducers/Reducer.java | 14 +++++++--- .../aggregations/reducers/ReducerFactory.java | 4 ++- .../reducers/derivative/DerivativeParser.java | 26 ++++++++++++++----- .../derivative/DerivativeReducer.java | 22 +++++++--------- 4 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java index d87d9fa72e1e6..cfc0f76622b5f 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java @@ -63,14 +63,16 @@ public static interface Parser { } - protected String name; - protected Map metaData; + private String name; + private String[] bucketsPaths; + private Map metaData; protected Reducer() { // for Serialisation } - protected Reducer(String name, Map metaData) { + protected Reducer(String name, String[] bucketsPaths, Map metaData) { this.name = name; + this.bucketsPaths = bucketsPaths; this.metaData = metaData; } @@ -78,6 +80,10 @@ public String name() { return name; } + public String[] bucketsPaths() { + return bucketsPaths; + } + public Map metaData() { return metaData; } @@ -89,6 +95,7 @@ public Map metaData() { @Override public final void writeTo(StreamOutput out) throws IOException { out.writeString(name); + out.writeStringArray(bucketsPaths); out.writeMap(metaData); doWriteTo(out); } @@ -98,6 +105,7 @@ public final void writeTo(StreamOutput out) throws IOException { @Override public final void readFrom(StreamInput in) throws IOException { name = in.readString(); + bucketsPaths = in.readStringArray(); metaData = in.readMap(); doReadFrom(in); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java index 4249cde2dc3ae..f904a564dd26f 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java @@ -31,6 +31,7 @@ public abstract class ReducerFactory { protected String name; protected String type; + protected String[] bucketsPaths; protected Map metaData; /** @@ -41,9 +42,10 @@ public abstract class ReducerFactory { * @param type * The aggregation type */ - public ReducerFactory(String name, String type) { + public ReducerFactory(String name, String type, String[] bucketsPaths) { this.name = name; this.type = type; + this.bucketsPaths = bucketsPaths; } /** diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java index 55259102dfd74..edb416f875ad7 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java @@ -29,6 +29,8 @@ import org.elasticsearch.search.internal.SearchContext; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; public class DerivativeParser implements Reducer.Parser { @@ -44,17 +46,29 @@ public String type() { public ReducerFactory parse(String reducerName, XContentParser parser, SearchContext context) throws IOException { XContentParser.Token token; String currentFieldName = null; - String bucketsPath = null; + String[] bucketsPaths = null; String format = null; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { currentFieldName = parser.currentName(); } else if (token == XContentParser.Token.VALUE_STRING) { - if (BUCKETS_PATH.match(currentFieldName)) { - bucketsPath = parser.text(); - } else if (FORMAT.match(currentFieldName)) { + if (FORMAT.match(currentFieldName)) { format = parser.text(); + } else if (BUCKETS_PATH.match(currentFieldName)) { + bucketsPaths = new String[] { parser.text() }; + } else { + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + reducerName + "]: [" + + currentFieldName + "]."); + } + } else if (token == XContentParser.Token.START_ARRAY) { + if (BUCKETS_PATH.match(currentFieldName)) { + List paths = new ArrayList<>(); + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + String path = parser.text(); + paths.add(path); + } + bucketsPaths = paths.toArray(new String[paths.size()]); } else { throw new SearchParseException(context, "Unknown key for a " + token + " in [" + reducerName + "]: [" + currentFieldName + "]."); @@ -64,7 +78,7 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon } } - if (bucketsPath == null) { + if (bucketsPaths == null) { throw new SearchParseException(context, "Missing required field [" + BUCKETS_PATH.getPreferredName() + "] for derivative aggregation [" + reducerName + "]"); } @@ -74,7 +88,7 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon formatter = ValueFormat.Patternable.Number.format(format).formatter(); } - return new DerivativeReducer.Factory(reducerName, bucketsPath, formatter); + return new DerivativeReducer.Factory(reducerName, bucketsPaths, formatter); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java index 26f40b2824d82..2bd42164c46bd 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java @@ -41,6 +41,7 @@ import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.AggregationPath; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; +import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; import java.io.IOException; import java.util.ArrayList; @@ -72,14 +73,12 @@ public InternalAggregation apply(Aggregation input) { }; private ValueFormatter formatter; - private String bucketsPath; public DerivativeReducer() { } - public DerivativeReducer(String name, String bucketsPath, @Nullable ValueFormatter formatter, Map metadata) { - super(name, metadata); - this.bucketsPath = bucketsPath; + public DerivativeReducer(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter, Map metadata) { + super(name, bucketsPaths, metadata); this.formatter = formatter; } @@ -115,7 +114,7 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext } private double resolveBucketValue(InternalHistogram histo, InternalHistogram.Bucket bucket) { - Object propertyValue = bucket.getProperty(histo.getName(), AggregationPath.parse(bucketsPath) + Object propertyValue = bucket.getProperty(histo.getName(), AggregationPath.parse(bucketsPaths()[0]) .getPathElementsAsStringList()); if (propertyValue instanceof Number) { return ((Number) propertyValue).doubleValue(); @@ -129,30 +128,27 @@ private double resolveBucketValue(InternalHistogram metaData) throws IOException { - return new DerivativeReducer(name, bucketsPath, formatter, metaData); + return new DerivativeReducer(name, bucketsPaths, formatter, metaData); } } From 3ab3ffa98928abab3d05100a55729f82e3f4a572 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Mon, 16 Feb 2015 10:42:08 +0000 Subject: [PATCH 15/68] First (rough) pass at dependancy resolution for reducers uses the depth-first algorithm from http://en.wikipedia.org/wiki/Topological_sorting#Algorithms Needs some cleaning up --- .../aggregations/AggregatorFactories.java | 71 ++++++++++++++++++- .../aggregations/reducers/ReducerFactory.java | 8 +++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java index a4f68b05efb47..ad17c533cc049 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java @@ -19,14 +19,18 @@ package org.elasticsearch.search.aggregations; import org.elasticsearch.ElasticsearchIllegalArgumentException; +import org.elasticsearch.ElasticsearchIllegalStateException; import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.reducers.ReducerFactory; import org.elasticsearch.search.aggregations.support.AggregationContext; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -156,8 +160,73 @@ public AggregatorFactories build() { if (factories.isEmpty()) { return EMPTY; } + List orderedReducers = resolveReducerOrder(this.reducerFactories, this.factories); // NOCOMMIT work out dependency order of reducer factories - return new AggregatorFactories(factories.toArray(new AggregatorFactory[factories.size()]), this.reducerFactories); + return new AggregatorFactories(factories.toArray(new AggregatorFactory[factories.size()]), orderedReducers); + } + + /* + * L ← Empty list that will contain the sorted nodes + * while there are unmarked nodes do + * select an unmarked node n + * visit(n) + * function visit(node n) + * if n has a temporary mark then stop (not a DAG) + * if n is not marked (i.e. has not been visited yet) then + * mark n temporarily + * for each node m with an edge from n to m do + * visit(m) + * mark n permanently + * unmark n temporarily + * add n to head of L + */ + private List resolveReducerOrder(List reducerFactories, List aggFactories) { + Map reducerFactoriesMap = new HashMap<>(); + for (ReducerFactory factory : reducerFactories) { + reducerFactoriesMap.put(factory.getName(), factory); + } + Set aggFactoryNames = new HashSet<>(); + for (AggregatorFactory aggFactory : aggFactories) { + aggFactoryNames.add(aggFactory.name); + } + List orderedReducers = new LinkedList<>(); + List unmarkedFactories = new ArrayList(reducerFactories); + Set temporarilyMarked = new HashSet(); + while (!unmarkedFactories.isEmpty()) { + ReducerFactory factory = unmarkedFactories.get(0); + resolveReducerOrder(aggFactoryNames, reducerFactoriesMap, orderedReducers, unmarkedFactories, temporarilyMarked, factory); + } + List orderedReducerNames = new ArrayList<>(); + for (ReducerFactory reducerFactory : orderedReducers) { + orderedReducerNames.add(reducerFactory.getName()); + } + System.out.println("ORDERED REDUCERS: " + orderedReducerNames); + return orderedReducers; + } + + private void resolveReducerOrder(Set aggFactoryNames, Map reducerFactoriesMap, + List orderedReducers, List unmarkedFactories, Set temporarilyMarked, + ReducerFactory factory) { + if (temporarilyMarked.contains(factory)) { + throw new ElasticsearchIllegalStateException("Cyclical dependancy found with reducer [" + factory.getName() + "]"); // NOCOMMIT is this the right Exception to throw? + } else if (unmarkedFactories.contains(factory)) { + temporarilyMarked.add(factory); + String[] bucketsPaths = factory.getBucketsPaths(); + for (String bucketsPath : bucketsPaths) { + ReducerFactory matchingFactory = reducerFactoriesMap.get(bucketsPath); + if (aggFactoryNames.contains(bucketsPath)) { + continue; + } else if (matchingFactory != null) { + resolveReducerOrder(aggFactoryNames, reducerFactoriesMap, orderedReducers, unmarkedFactories, temporarilyMarked, + matchingFactory); + } else { + throw new ElasticsearchIllegalStateException("No reducer found for path [" + bucketsPath + "]"); // NOCOMMIT is this the right Exception to throw? + } + } + unmarkedFactories.remove(factory); + temporarilyMarked.remove(factory); + orderedReducers.add(factory); + } } } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java index f904a564dd26f..05cb6fbed48bf 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java @@ -86,4 +86,12 @@ public void setMetaData(Map metaData) { this.metaData = metaData; } + public String getName() { + return name; + } + + public String[] getBucketsPaths() { + return bucketsPaths; + } + } From f20dae85a9bbb0972b30ffdc94a34576ce039102 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Mon, 16 Feb 2015 10:51:31 +0000 Subject: [PATCH 16/68] getProperty method in the aggregations framework now throws a specific exception --- .../InternalMultiBucketAggregation.java | 8 ++--- .../InvalidAggregationPathException.java | 33 +++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/elasticsearch/search/aggregations/InvalidAggregationPathException.java diff --git a/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java b/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java index ebd2637ac5693..5efc2180229f2 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java +++ b/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java @@ -19,7 +19,6 @@ package org.elasticsearch.search.aggregations; -import org.elasticsearch.ElasticsearchIllegalArgumentException; import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation; import org.elasticsearch.search.aggregations.reducers.Reducer; @@ -58,18 +57,19 @@ public Object getProperty(String containingAggName, List path) { String aggName = path.get(0); if (aggName.equals("_count")) { if (path.size() > 1) { - throw new ElasticsearchIllegalArgumentException("_count must be the last element in the path"); + throw new InvalidAggregationPathException("_count must be the last element in the path"); } return getDocCount(); } else if (aggName.equals("_key")) { if (path.size() > 1) { - throw new ElasticsearchIllegalArgumentException("_key must be the last element in the path"); + throw new InvalidAggregationPathException("_key must be the last element in the path"); } return getKey(); } InternalAggregation aggregation = aggregations.get(aggName); if (aggregation == null) { - throw new ElasticsearchIllegalArgumentException("Cannot find an aggregation named [" + aggName + "] in [" + containingAggName + "]"); + throw new InvalidAggregationPathException("Cannot find an aggregation named [" + aggName + "] in [" + containingAggName + + "]"); } return aggregation.getProperty(path.subList(1, path.size())); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/InvalidAggregationPathException.java b/src/main/java/org/elasticsearch/search/aggregations/InvalidAggregationPathException.java new file mode 100644 index 0000000000000..e2ab1f6524572 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/InvalidAggregationPathException.java @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations; + +import org.elasticsearch.ElasticsearchException; + +public class InvalidAggregationPathException extends ElasticsearchException { + + public InvalidAggregationPathException(String msg) { + super(msg); + } + + public InvalidAggregationPathException(String msg, Throwable cause) { + super(msg, cause); + } +} From 58f2ceca12e9bdc40735142ddf8fa6def6f90e4d Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Mon, 16 Feb 2015 10:52:00 +0000 Subject: [PATCH 17/68] Derivative Reducer now supported nth order derivatives --- .../derivative/DerivativeReducer.java | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java index 2bd42164c46bd..a0a3d9cb4253e 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java @@ -22,6 +22,7 @@ import com.google.common.base.Function; import com.google.common.collect.Lists; +import org.elasticsearch.ElasticsearchIllegalStateException; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -32,6 +33,7 @@ import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; import org.elasticsearch.search.aggregations.InternalAggregation.Type; import org.elasticsearch.search.aggregations.InternalAggregations; +import org.elasticsearch.search.aggregations.InvalidAggregationPathException; import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; import org.elasticsearch.search.aggregations.reducers.InternalSimpleValue; @@ -92,12 +94,16 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext InternalHistogram histo = (InternalHistogram) aggregation; List buckets = histo.getBuckets(); InternalHistogram.Factory factory = histo.getFactory(); + List newBuckets = new ArrayList<>(); Double lastBucketValue = null; // NOCOMMIT this needs to be improved so that the aggs are cloned correctly to ensure aggs are fully immutable. for (InternalHistogram.Bucket bucket : buckets) { - double thisBucketValue = resolveBucketValue(histo, bucket); + Double thisBucketValue = resolveBucketValue(histo, bucket); if (lastBucketValue != null) { + if (thisBucketValue == null) { + throw new ElasticsearchIllegalStateException("FOUND GAP IN DATA"); // NOCOMMIT deal with gaps in data + } double diff = thisBucketValue - lastBucketValue; List aggs = new ArrayList<>(Lists.transform(bucket.getAggregations().asList(), FUNCTION)); @@ -113,16 +119,20 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext return factory.create(histo.getName(), newBuckets, null, 1, null, null, false, new ArrayList(), histo.getMetaData()); // NOCOMMIT get order, minDocCount, emptyBucketInfo etc. from histo } - private double resolveBucketValue(InternalHistogram histo, InternalHistogram.Bucket bucket) { - Object propertyValue = bucket.getProperty(histo.getName(), AggregationPath.parse(bucketsPaths()[0]) - .getPathElementsAsStringList()); - if (propertyValue instanceof Number) { - return ((Number) propertyValue).doubleValue(); - } else if (propertyValue instanceof InternalNumericMetricsAggregation.SingleValue) { - return ((InternalNumericMetricsAggregation.SingleValue) propertyValue).value(); - } else { - throw new AggregationExecutionException(DerivativeParser.BUCKETS_PATH.getPreferredName() - + "must reference either a number value or a single value numeric metric aggregation"); + private Double resolveBucketValue(InternalHistogram histo, InternalHistogram.Bucket bucket) { + try { + Object propertyValue = bucket.getProperty(histo.getName(), AggregationPath.parse(bucketsPaths()[0]) + .getPathElementsAsStringList()); + if (propertyValue instanceof Number) { + return ((Number) propertyValue).doubleValue(); + } else if (propertyValue instanceof InternalNumericMetricsAggregation.SingleValue) { + return ((InternalNumericMetricsAggregation.SingleValue) propertyValue).value(); + } else { + throw new AggregationExecutionException(DerivativeParser.BUCKETS_PATH.getPreferredName() + + " must reference either a number value or a single value numeric metric aggregation"); + } + } catch (InvalidAggregationPathException e) { + return null; } } From 247b6a7e13f2d782822519a1a0511659a7f30922 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Mon, 16 Feb 2015 10:54:37 +0000 Subject: [PATCH 18/68] removed obselete NOCOMMIT and left over sysout call --- .../elasticsearch/search/aggregations/AggregatorFactories.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java index ad17c533cc049..258b90c265396 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java @@ -161,7 +161,6 @@ public AggregatorFactories build() { return EMPTY; } List orderedReducers = resolveReducerOrder(this.reducerFactories, this.factories); - // NOCOMMIT work out dependency order of reducer factories return new AggregatorFactories(factories.toArray(new AggregatorFactory[factories.size()]), orderedReducers); } @@ -200,7 +199,6 @@ private List resolveReducerOrder(List reducerFac for (ReducerFactory reducerFactory : orderedReducers) { orderedReducerNames.add(reducerFactory.getName()); } - System.out.println("ORDERED REDUCERS: " + orderedReducerNames); return orderedReducers; } From e994044d28b5bba490257a70c3357ee859d1279d Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Mon, 16 Feb 2015 11:56:41 +0000 Subject: [PATCH 19/68] Added Builder classes for Reducers --- .../search/aggregations/reducers/Reducer.java | 3 + .../aggregations/reducers/ReducerBuilder.java | 95 +++++++++++++++++++ .../reducers/ReducerBuilders.java | 32 +++++++ 3 files changed, 130 insertions(+) create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilder.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilders.java diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java index cfc0f76622b5f..ed602b3175109 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java @@ -19,6 +19,7 @@ package org.elasticsearch.search.aggregations.reducers; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Streamable; @@ -41,6 +42,8 @@ public abstract class Reducer implements Streamable { */ public static interface Parser { + public static final ParseField BUCKETS_PATH = new ParseField("bucketsPath"); + /** * @return The reducer type this parser is associated with. */ diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilder.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilder.java new file mode 100644 index 0000000000000..49bba5a0ecbc6 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilder.java @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers; + +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * A base class for all reducer builders. + */ +public abstract class ReducerBuilder> implements ToXContent { + + private final String name; + protected final String type; + private List bucketsPaths; + private Map metaData; + + /** + * Sole constructor, typically used by sub-classes. + */ + protected ReducerBuilder(String name, String type) { + this.name = name; + this.type = type; + } + + /** + * Return the name of the reducer that is being built. + */ + public String getName() { + return name; + } + + /** + * Sets the paths to the buckets to use for this reducer + */ + public B setBucketsPaths(List bucketsPaths) { + this.bucketsPaths = bucketsPaths; + return (B) this; + } + + /** + * Sets the meta data to be included in the reducer's response + */ + public B setMetaData(Map metaData) { + this.metaData = metaData; + return (B)this; + } + + @Override + public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(getName()); + + if (this.metaData != null) { + builder.field("meta", this.metaData); + } + builder.startObject(type); + + if (bucketsPaths != null) { + builder.startArray(Reducer.Parser.BUCKETS_PATH.getPreferredName()); + for (String path : bucketsPaths) { + builder.value(path); + } + builder.endArray(); + } + + internalXContent(builder, params); + + builder.endObject(); + + return builder.endObject(); + } + + protected abstract XContentBuilder internalXContent(XContentBuilder builder, Params params) throws IOException; +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilders.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilders.java new file mode 100644 index 0000000000000..21c901af80d4d --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilders.java @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers; + +import org.elasticsearch.search.aggregations.reducers.derivative.DerivativeBuilder; + +public final class ReducerBuilders { + + private ReducerBuilders() { + } + + public static final DerivativeBuilder derivative(String name) { + return new DerivativeBuilder(name); + } +} From c97dd84badc08df6bb57fcbd0a1af4470775e319 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Mon, 16 Feb 2015 11:57:01 +0000 Subject: [PATCH 20/68] Added Builder for Derivatives Reducer --- .../derivative/DerivativeBuilder.java | 48 +++++++++++++++++++ .../reducers/derivative/DerivativeParser.java | 2 - 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeBuilder.java diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeBuilder.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeBuilder.java new file mode 100644 index 0000000000000..87165c32ac094 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeBuilder.java @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.derivative; + +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.search.aggregations.reducers.ReducerBuilder; + +import java.io.IOException; + +public class DerivativeBuilder extends ReducerBuilder { + + private String format; + + public DerivativeBuilder(String name) { + super(name, DerivativeReducer.TYPE.name()); + } + + public DerivativeBuilder format(String format) { + this.format = format; + return this; + } + + @Override + protected XContentBuilder internalXContent(XContentBuilder builder, Params params) throws IOException { + if (format != null) { + builder.field(DerivativeParser.FORMAT.getPreferredName(), format); + } + return builder; + } + +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java index edb416f875ad7..8a562050dcbc9 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java @@ -33,8 +33,6 @@ import java.util.List; public class DerivativeParser implements Reducer.Parser { - - public static final ParseField BUCKETS_PATH = new ParseField("bucketsPath"); public static final ParseField FORMAT = new ParseField("format"); @Override From 511e2758250a6f02de8eef87f3587b650179b662 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Mon, 16 Feb 2015 16:32:42 +0000 Subject: [PATCH 21/68] More update to support Reducer Builders --- .../aggregations/AggregationBuilder.java | 23 ++++++++++++++++++- .../TransportAggregationModule.java | 2 ++ .../bucket/histogram/InternalHistogram.java | 4 ++++ .../bucket/histogram/InternalOrder.java | 2 +- .../aggregations/reducers/ReducerBuilder.java | 5 ++-- 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregationBuilder.java b/src/main/java/org/elasticsearch/search/aggregations/AggregationBuilder.java index 5b9fab55aa482..cc3033e883fe5 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregationBuilder.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregationBuilder.java @@ -20,12 +20,14 @@ package org.elasticsearch.search.aggregations; import com.google.common.collect.Lists; + import org.elasticsearch.ElasticsearchGenerationException; import org.elasticsearch.client.Requests; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.search.aggregations.reducers.ReducerBuilder; import java.io.IOException; import java.util.List; @@ -37,6 +39,7 @@ public abstract class AggregationBuilder> extends AbstractAggregationBuilder { private List aggregations; + private List> reducers; private BytesReference aggregationsBinary; private Map metaData; @@ -59,6 +62,18 @@ public B subAggregation(AbstractAggregationBuilder aggregation) { return (B) this; } + /** + * Add a sub get to this bucket get. + */ + @SuppressWarnings("unchecked") + public B subAggregation(ReducerBuilder reducer) { + if (reducers == null) { + reducers = Lists.newArrayList(); + } + reducers.add(reducer); + return (B) this; + } + /** * Sets a raw (xcontent / json) sub addAggregation. */ @@ -120,7 +135,7 @@ public final XContentBuilder toXContent(XContentBuilder builder, Params params) builder.field(type); internalXContent(builder, params); - if (aggregations != null || aggregationsBinary != null) { + if (aggregations != null || aggregationsBinary != null || reducers != null) { builder.startObject("aggregations"); if (aggregations != null) { @@ -129,6 +144,12 @@ public final XContentBuilder toXContent(XContentBuilder builder, Params params) } } + if (reducers != null) { + for (ReducerBuilder subAgg : reducers) { + subAgg.toXContent(builder, params); + } + } + if (aggregationsBinary != null) { if (XContentFactory.xContentType(aggregationsBinary) == builder.contentType()) { builder.rawField("aggregations", aggregationsBinary); diff --git a/src/main/java/org/elasticsearch/search/aggregations/TransportAggregationModule.java b/src/main/java/org/elasticsearch/search/aggregations/TransportAggregationModule.java index c99f885462ccd..fe4542830cc5b 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/TransportAggregationModule.java +++ b/src/main/java/org/elasticsearch/search/aggregations/TransportAggregationModule.java @@ -57,6 +57,7 @@ import org.elasticsearch.search.aggregations.metrics.sum.InternalSum; import org.elasticsearch.search.aggregations.metrics.tophits.InternalTopHits; import org.elasticsearch.search.aggregations.metrics.valuecount.InternalValueCount; +import org.elasticsearch.search.aggregations.reducers.InternalSimpleValue; import org.elasticsearch.search.aggregations.reducers.derivative.DerivativeReducer; /** @@ -103,6 +104,7 @@ protected void configure() { InternalTopHits.registerStreams(); InternalGeoBounds.registerStream(); InternalChildren.registerStream(); + InternalSimpleValue.registerStreams(); // Reducers DerivativeReducer.registerStreams(); diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java index d5b3a1384f1a4..4171cc3f51449 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java @@ -301,6 +301,10 @@ public Factory getFactory() { return factory; } + public InternalOrder getOrder() { + return order; + } + private static class IteratorAndCurrent { private final Iterator iterator; diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalOrder.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalOrder.java index 9d503a8e90b87..1090206478620 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalOrder.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalOrder.java @@ -29,7 +29,7 @@ /** * An internal {@link Histogram.Order} strategy which is identified by a unique id. */ -class InternalOrder extends Histogram.Order { +public class InternalOrder extends Histogram.Order { final byte id; final String key; diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilder.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilder.java index 49bba5a0ecbc6..0f0f9225635f7 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilder.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilder.java @@ -23,7 +23,6 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import java.io.IOException; -import java.util.List; import java.util.Map; /** @@ -33,7 +32,7 @@ public abstract class ReducerBuilder> implements ToX private final String name; protected final String type; - private List bucketsPaths; + private String[] bucketsPaths; private Map metaData; /** @@ -54,7 +53,7 @@ public String getName() { /** * Sets the paths to the buckets to use for this reducer */ - public B setBucketsPaths(List bucketsPaths) { + public B setBucketsPaths(String... bucketsPaths) { this.bucketsPaths = bucketsPaths; return (B) this; } From f68bce51f1d7d83e05ddb3fdeb73a91bb9c5c419 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Tue, 17 Feb 2015 09:05:24 +0000 Subject: [PATCH 22/68] Tests for derivative reducer Most tests have been marked with @AwaitsFix since they require functionality to be implemented before they will pass --- .../derivative/DerivativeReducer.java | 3 +- .../reducers/DateDerivativeTests.java | 321 ++++++++++ .../reducers/DerivativeTests.java | 568 ++++++++++++++++++ 3 files changed, 891 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java create mode 100644 src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java index a0a3d9cb4253e..730a85a2d419b 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java @@ -116,7 +116,8 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext } lastBucketValue = thisBucketValue; } - return factory.create(histo.getName(), newBuckets, null, 1, null, null, false, new ArrayList(), histo.getMetaData()); // NOCOMMIT get order, minDocCount, emptyBucketInfo etc. from histo + return factory.create(histo.getName(), newBuckets, histo.getOrder(), 1, null, null, false, new ArrayList(), + histo.getMetaData()); // NOCOMMIT get order, minDocCount, emptyBucketInfo etc. from histo } private Double resolveBucketValue(InternalHistogram histo, InternalHistogram.Bucket bucket) { diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java new file mode 100644 index 0000000000000..ec131b3a60949 --- /dev/null +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java @@ -0,0 +1,321 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers; + +import org.apache.lucene.util.LuceneTestCase.AwaitsFix; +import org.elasticsearch.action.index.IndexRequestBuilder; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.index.mapper.core.DateFieldMapper; +import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; +import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; +import org.elasticsearch.search.aggregations.bucket.histogram.Histogram.Bucket; +import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; +import org.elasticsearch.search.aggregations.metrics.sum.Sum; +import org.elasticsearch.search.aggregations.support.AggregationPath; +import org.elasticsearch.test.ElasticsearchIntegrationTest; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.format.DateTimeFormat; +import org.junit.After; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.search.aggregations.AggregationBuilders.dateHistogram; +import static org.elasticsearch.search.aggregations.AggregationBuilders.sum; +import static org.elasticsearch.search.aggregations.reducers.ReducerBuilders.derivative; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.core.IsNull.notNullValue; +import static org.hamcrest.core.IsNull.nullValue; + +@ElasticsearchIntegrationTest.SuiteScopeTest +//@AwaitsFix(bugUrl = "Fix factory selection for serialisation of Internal derivative") +public class DateDerivativeTests extends ElasticsearchIntegrationTest { + + private DateTime date(int month, int day) { + return new DateTime(2012, month, day, 0, 0, DateTimeZone.UTC); + } + + private DateTime date(String date) { + return DateFieldMapper.Defaults.DATE_TIME_FORMATTER.parser().parseDateTime(date); + } + + private static String format(DateTime date, String pattern) { + return DateTimeFormat.forPattern(pattern).print(date); + } + + private IndexRequestBuilder indexDoc(String idx, DateTime date, int value) throws Exception { + return client().prepareIndex(idx, "type").setSource( + jsonBuilder().startObject().field("date", date).field("value", value).startArray("dates").value(date) + .value(date.plusMonths(1).plusDays(1)).endArray().endObject()); + } + + private IndexRequestBuilder indexDoc(int month, int day, int value) throws Exception { + return client().prepareIndex("idx", "type").setSource( + jsonBuilder().startObject().field("value", value).field("date", date(month, day)).startArray("dates") + .value(date(month, day)).value(date(month + 1, day + 1)).endArray().endObject()); + } + + @Override + public void setupSuiteScopeCluster() throws Exception { + createIndex("idx"); + createIndex("idx_unmapped"); + // TODO: would be nice to have more random data here + prepareCreate("empty_bucket_idx").addMapping("type", "value", "type=integer").execute().actionGet(); + List builders = new ArrayList<>(); + for (int i = 0; i < 2; i++) { + builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + i).setSource( + jsonBuilder().startObject().field("value", i * 2).endObject())); + } + builders.addAll(Arrays.asList(indexDoc(1, 2, 1), // date: Jan 2, dates: Jan 2, Feb 3 + indexDoc(2, 2, 2), // date: Feb 2, dates: Feb 2, Mar 3 + indexDoc(2, 15, 3), // date: Feb 15, dates: Feb 15, Mar 16 + indexDoc(3, 2, 4), // date: Mar 2, dates: Mar 2, Apr 3 + indexDoc(3, 15, 5), // date: Mar 15, dates: Mar 15, Apr 16 + indexDoc(3, 23, 6))); // date: Mar 23, dates: Mar 23, Apr 24 + indexRandom(true, builders); + ensureSearchable(); + } + + @After + public void afterEachTest() throws IOException { + internalCluster().wipeIndices("idx2"); + } + + @AwaitsFix(bugUrl = "waiting for derivative to support _count") + // NOCOMMIT + @Test + public void singleValuedField() throws Exception { + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + dateHistogram("histo").field("date").interval(DateHistogramInterval.MONTH) + .subAggregation(derivative("deriv").setBucketsPaths("_count"))).execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram deriv = response.getAggregations().get("histo"); + assertThat(deriv, notNullValue()); + assertThat(deriv.getName(), equalTo("histo")); + List buckets = deriv.getBuckets(); + assertThat(buckets.size(), equalTo(2)); + + DateTime key = new DateTime(2012, 1, 1, 0, 0, DateTimeZone.UTC); + Histogram.Bucket bucket = buckets.get(0); + assertThat(bucket, notNullValue()); + assertThat((DateTime) bucket.getKey(), equalTo(key)); + assertThat(bucket.getDocCount(), equalTo(0l)); + SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(1d)); + + key = new DateTime(2012, 2, 1, 0, 0, DateTimeZone.UTC); + bucket = buckets.get(1); + assertThat(bucket, notNullValue()); + assertThat((DateTime) bucket.getKey(), equalTo(key)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(1d)); + } + + @Test + public void singleValuedField_WithSubAggregation() throws Exception { + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + dateHistogram("histo").field("date").interval(DateHistogramInterval.MONTH) + .subAggregation(derivative("deriv").setBucketsPaths("sum")).subAggregation(sum("sum").field("value"))) + .execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram histo = response.getAggregations().get("histo"); + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + assertThat(buckets.size(), equalTo(3)); + Object[] propertiesKeys = (Object[]) histo.getProperty("_key"); + Object[] propertiesDocCounts = (Object[]) histo.getProperty("_count"); + Object[] propertiesCounts = (Object[]) histo.getProperty("sum.value"); + + DateTime key = new DateTime(2012, 1, 1, 0, 0, DateTimeZone.UTC); + Histogram.Bucket bucket = buckets.get(0); + assertThat(bucket, notNullValue()); + assertThat((DateTime) bucket.getKey(), equalTo(key)); + assertThat(bucket.getDocCount(), equalTo(1l)); + assertThat(bucket.getAggregations().asList().isEmpty(), is(false)); + Sum sum = bucket.getAggregations().get("sum"); + assertThat(sum, notNullValue()); + assertThat(sum.getValue(), equalTo(1.0)); + SimpleValue deriv = bucket.getAggregations().get("deriv"); + assertThat(deriv, nullValue()); + assertThat((DateTime) propertiesKeys[0], equalTo(key)); + assertThat((long) propertiesDocCounts[0], equalTo(1l)); + assertThat((double) propertiesCounts[0], equalTo(1.0)); + + key = new DateTime(2012, 2, 1, 0, 0, DateTimeZone.UTC); + bucket = buckets.get(1); + assertThat(bucket, notNullValue()); + assertThat((DateTime) bucket.getKey(), equalTo(key)); + assertThat(bucket.getDocCount(), equalTo(2l)); + assertThat(bucket.getAggregations().asList().isEmpty(), is(false)); + sum = bucket.getAggregations().get("sum"); + assertThat(sum, notNullValue()); + assertThat(sum.getValue(), equalTo(5.0)); + deriv = bucket.getAggregations().get("deriv"); + assertThat(deriv, notNullValue()); + assertThat(deriv.value(), equalTo(4.0)); + assertThat((double) bucket.getProperty("histo", AggregationPath.parse("deriv.value").getPathElementsAsStringList()), equalTo(4.0)); + assertThat((DateTime) propertiesKeys[1], equalTo(key)); + assertThat((long) propertiesDocCounts[1], equalTo(2l)); + assertThat((double) propertiesCounts[1], equalTo(5.0)); + + key = new DateTime(2012, 3, 1, 0, 0, DateTimeZone.UTC); + bucket = buckets.get(2); + assertThat(bucket, notNullValue()); + assertThat((DateTime) bucket.getKey(), equalTo(key)); + assertThat(bucket.getDocCount(), equalTo(3l)); + assertThat(bucket.getAggregations().asList().isEmpty(), is(false)); + sum = bucket.getAggregations().get("sum"); + assertThat(sum, notNullValue()); + assertThat(sum.getValue(), equalTo(15.0)); + deriv = bucket.getAggregations().get("deriv"); + assertThat(deriv, notNullValue()); + assertThat(deriv.value(), equalTo(10.0)); + assertThat((double) bucket.getProperty("histo", AggregationPath.parse("deriv.value").getPathElementsAsStringList()), equalTo(10.0)); + assertThat((DateTime) propertiesKeys[2], equalTo(key)); + assertThat((long) propertiesDocCounts[2], equalTo(3l)); + assertThat((double) propertiesCounts[2], equalTo(15.0)); + } + + @AwaitsFix(bugUrl = "waiting for derivative to support _count") + // NOCOMMIT + @Test + public void multiValuedField() throws Exception { + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + dateHistogram("histo").field("dates").interval(DateHistogramInterval.MONTH) + .subAggregation(derivative("deriv").setBucketsPaths("_count"))).execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram deriv = response.getAggregations().get("histo"); + assertThat(deriv, notNullValue()); + assertThat(deriv.getName(), equalTo("histo")); + List buckets = deriv.getBuckets(); + assertThat(buckets.size(), equalTo(3)); + + DateTime key = new DateTime(2012, 1, 1, 0, 0, DateTimeZone.UTC); + Histogram.Bucket bucket = buckets.get(0); + assertThat(bucket, notNullValue()); + assertThat((DateTime) bucket.getKey(), equalTo(key)); + assertThat(bucket.getDocCount(), equalTo(0l)); + assertThat(bucket.getAggregations().asList().isEmpty(), is(false)); + SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(2.0)); + + key = new DateTime(2012, 2, 1, 0, 0, DateTimeZone.UTC); + bucket = buckets.get(1); + assertThat(bucket, notNullValue()); + assertThat((DateTime) bucket.getKey(), equalTo(key)); + assertThat(bucket.getDocCount(), equalTo(0l)); + assertThat(bucket.getAggregations().asList().isEmpty(), is(false)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(2.0)); + + key = new DateTime(2012, 3, 1, 0, 0, DateTimeZone.UTC); + bucket = buckets.get(2); + assertThat(bucket, notNullValue()); + assertThat((DateTime) bucket.getKey(), equalTo(key)); + assertThat(bucket.getDocCount(), equalTo(0l)); + assertThat(bucket.getAggregations().asList().isEmpty(), is(false)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(-2.0)); + } + + @AwaitsFix(bugUrl = "waiting for derivative to support _count") + // NOCOMMIT + @Test + public void unmapped() throws Exception { + SearchResponse response = client() + .prepareSearch("idx_unmapped") + .addAggregation( + dateHistogram("histo").field("date").interval(DateHistogramInterval.MONTH) + .subAggregation(derivative("deriv").setBucketsPaths("_count"))).execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram deriv = response.getAggregations().get("histo"); + assertThat(deriv, notNullValue()); + assertThat(deriv.getName(), equalTo("histo")); + assertThat(deriv.getBuckets().size(), equalTo(0)); + } + + @AwaitsFix(bugUrl = "waiting for derivative to support _count") + // NOCOMMIT + @Test + public void partiallyUnmapped() throws Exception { + SearchResponse response = client() + .prepareSearch("idx", "idx_unmapped") + .addAggregation( + dateHistogram("histo").field("date").interval(DateHistogramInterval.MONTH) + .subAggregation(derivative("deriv").setBucketsPaths("_count"))).execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram deriv = response.getAggregations().get("histo"); + assertThat(deriv, notNullValue()); + assertThat(deriv.getName(), equalTo("histo")); + List buckets = deriv.getBuckets(); + assertThat(buckets.size(), equalTo(2)); + + DateTime key = new DateTime(2012, 1, 1, 0, 0, DateTimeZone.UTC); + Histogram.Bucket bucket = buckets.get(0); + assertThat(bucket, notNullValue()); + assertThat((DateTime) bucket.getKey(), equalTo(key)); + assertThat(bucket.getDocCount(), equalTo(0l)); + assertThat(bucket.getAggregations().asList().isEmpty(), is(false)); + SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(1.0)); + + key = new DateTime(2012, 2, 1, 0, 0, DateTimeZone.UTC); + bucket = buckets.get(1); + assertThat(bucket, notNullValue()); + assertThat((DateTime) bucket.getKey(), equalTo(key)); + assertThat(bucket.getDocCount(), equalTo(0l)); + assertThat(bucket.getAggregations().asList().isEmpty(), is(false)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(1.0)); + } + +} diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java new file mode 100644 index 0000000000000..3b51bbbf6b2c7 --- /dev/null +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java @@ -0,0 +1,568 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers; + +import org.apache.lucene.util.LuceneTestCase.AwaitsFix; +import org.elasticsearch.action.index.IndexRequestBuilder; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; +import org.elasticsearch.search.aggregations.bucket.histogram.Histogram.Bucket; +import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; +import org.elasticsearch.search.aggregations.metrics.sum.Sum; +import org.elasticsearch.search.aggregations.support.AggregationPath; +import org.elasticsearch.test.ElasticsearchIntegrationTest; +import org.hamcrest.Matchers; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.elasticsearch.search.aggregations.AggregationBuilders.histogram; +import static org.elasticsearch.search.aggregations.AggregationBuilders.sum; +import static org.elasticsearch.search.aggregations.reducers.ReducerBuilders.derivative; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.core.IsNull.notNullValue; + +@ElasticsearchIntegrationTest.SuiteScopeTest +public class DerivativeTests extends ElasticsearchIntegrationTest { + + private static final String SINGLE_VALUED_FIELD_NAME = "l_value"; + private static final String MULTI_VALUED_FIELD_NAME = "l_values"; + + static int numDocs; + static int interval; + static int numValueBuckets, numValuesBuckets; + static int numFirstDerivValueBuckets, numFirstDerivValuesBuckets; + static long[] valueCounts, valuesCounts; + static long[] firstDerivValueCounts, firstDerivValuesCounts; + + @Override + public void setupSuiteScopeCluster() throws Exception { + createIndex("idx"); + createIndex("idx_unmapped"); + + numDocs = randomIntBetween(6, 20); + interval = randomIntBetween(2, 5); + + numValueBuckets = numDocs / interval + 1; + valueCounts = new long[numValueBuckets]; + for (int i = 0; i < numDocs; i++) { + final int bucket = (i + 1) / interval; + valueCounts[bucket]++; + } + + numValuesBuckets = (numDocs + 1) / interval + 1; + valuesCounts = new long[numValuesBuckets]; + for (int i = 0; i < numDocs; i++) { + final int bucket1 = (i + 1) / interval; + final int bucket2 = (i + 2) / interval; + valuesCounts[bucket1]++; + if (bucket1 != bucket2) { + valuesCounts[bucket2]++; + } + } + + numFirstDerivValueBuckets = numValueBuckets - 1; + firstDerivValueCounts = new long[numFirstDerivValueBuckets]; + long lastValueCount = -1; + for (int i = 0; i < numValueBuckets; i++) { + long thisValue = valueCounts[i]; + if (lastValueCount != -1) { + long diff = thisValue - lastValueCount; + firstDerivValueCounts[i - 1] = diff; + } + lastValueCount = thisValue; + } + + numFirstDerivValuesBuckets = numValuesBuckets - 1; + firstDerivValuesCounts = new long[numFirstDerivValuesBuckets]; + long lastValuesCount = -1; + for (int i = 0; i < numValuesBuckets; i++) { + long thisValue = valuesCounts[i]; + if (lastValuesCount != -1) { + long diff = thisValue - lastValuesCount; + firstDerivValuesCounts[i - 1] = diff; + } + lastValuesCount = thisValue; + } + + List builders = new ArrayList<>(); + + for (int i = 0; i < numDocs; i++) { + builders.add(client().prepareIndex("idx", "type").setSource( + jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, i + 1).startArray(MULTI_VALUED_FIELD_NAME).value(i + 1) + .value(i + 2).endArray().field("tag", "tag" + i).endObject())); + } + + assertAcked(prepareCreate("empty_bucket_idx").addMapping("type", SINGLE_VALUED_FIELD_NAME, "type=integer")); + builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 0).setSource( + jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 0).endObject())); + + builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 1).setSource( + jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 1).endObject())); + + builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 2).setSource( + jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 2).endObject())); + builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 3).setSource( + jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 2).endObject())); + + builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 4).setSource( + jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 4).endObject())); + builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 5).setSource( + jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 4).endObject())); + + builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 6).setSource( + jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 5).endObject())); + builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 7).setSource( + jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 5).endObject())); + + builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 8).setSource( + jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 9).endObject())); + builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 9).setSource( + jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 9).endObject())); + builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 10).setSource( + jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 9).endObject())); + + builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 11).setSource( + jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 10).endObject())); + builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 12).setSource( + jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 10).endObject())); + + builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 13).setSource( + jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 11).endObject())); + + indexRandom(true, builders); + ensureSearchable(); + } + + @AwaitsFix(bugUrl="waiting for derivative to support _count") // NOCOMMIT + @Test + public void singleValuedField() { + + SearchResponse response = client().prepareSearch("idx") + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) + .subAggregation(derivative("deriv").setBucketsPaths("_count"))) + .execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram deriv = response.getAggregations().get("histo"); + assertThat(deriv, notNullValue()); + assertThat(deriv.getName(), equalTo("histo")); + List buckets = deriv.getBuckets(); + assertThat(buckets.size(), equalTo(numFirstDerivValueBuckets)); + + for (int i = 0; i < numFirstDerivValueBuckets; ++i) { + Histogram.Bucket bucket = buckets.get(i); + assertThat(bucket, notNullValue()); + assertThat(bucket.getKeyAsString(), equalTo(String.valueOf(i * interval))); + assertThat(((Number) bucket.getKey()).longValue(), equalTo((long) i * interval)); + assertThat(bucket.getDocCount(), equalTo(valueCounts[i])); + SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo((double) firstDerivValueCounts[i])); + } + } + + @Test + public void singleValuedField_WithSubAggregation() throws Exception { + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) + .subAggregation(sum("sum").field(SINGLE_VALUED_FIELD_NAME)) + .subAggregation(derivative("deriv").setBucketsPaths("sum"))).execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram deriv = response.getAggregations().get("histo"); + assertThat(deriv, notNullValue()); + assertThat(deriv.getName(), equalTo("histo")); + assertThat(deriv.getBuckets().size(), equalTo(numValueBuckets)); + Object[] propertiesKeys = (Object[]) deriv.getProperty("_key"); + Object[] propertiesDocCounts = (Object[]) deriv.getProperty("_count"); + Object[] propertiesSumCounts = (Object[]) deriv.getProperty("sum.value"); + + List buckets = new ArrayList<>(deriv.getBuckets()); + for (int i = 0; i < numValueBuckets; ++i) { + Histogram.Bucket bucket = buckets.get(i); + assertThat(bucket, notNullValue()); + assertThat(bucket.getKeyAsString(), equalTo(String.valueOf(i * interval))); + assertThat(((Number) bucket.getKey()).longValue(), equalTo((long) i * interval)); + assertThat(bucket.getDocCount(), equalTo(valueCounts[i])); + assertThat(bucket.getAggregations().asList().isEmpty(), is(false)); + Sum sum = bucket.getAggregations().get("sum"); + assertThat(sum, notNullValue()); + long s = 0; + for (int j = 0; j < numDocs; ++j) { + if ((j + 1) / interval == i) { + s += j + 1; + } + } + assertThat(sum.getValue(), equalTo((double) s)); + if (i > 0) { + SimpleValue sumDeriv = bucket.getAggregations().get("deriv"); + assertThat(sumDeriv, notNullValue()); + long s1 = 0; + long s2 = 0; + for (int j = 0; j < numDocs; ++j) { + if ((j + 1) / interval == i - 1) { + s1 += j + 1; + } + if ((j + 1) / interval == i) { + s2 += j + 1; + } + } + long sumDerivValue = s2 - s1; + assertThat(sumDeriv.value(), equalTo((double) sumDerivValue)); + assertThat((double) bucket.getProperty("histo", AggregationPath.parse("deriv.value").getPathElementsAsStringList()), + equalTo((double) sumDerivValue)); + } + assertThat((long) propertiesKeys[i], equalTo((long) i * interval)); + assertThat((long) propertiesDocCounts[i], equalTo(valueCounts[i])); + assertThat((double) propertiesSumCounts[i], equalTo((double) s)); + } + } + + @AwaitsFix(bugUrl="waiting for derivative to support _count") // NOCOMMIT + @Test + public void multiValuedField() throws Exception { + SearchResponse response = client().prepareSearch("idx") + .addAggregation( + histogram("histo").field(MULTI_VALUED_FIELD_NAME).interval(interval) + .subAggregation(derivative("deriv").setBucketsPaths("_count"))) + .execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram deriv = response.getAggregations().get("histo"); + assertThat(deriv, notNullValue()); + assertThat(deriv.getName(), equalTo("histo")); + List buckets = deriv.getBuckets(); + assertThat(deriv.getBuckets().size(), equalTo(numFirstDerivValuesBuckets)); + + for (int i = 0; i < numFirstDerivValuesBuckets; ++i) { + Histogram.Bucket bucket = buckets.get(i); + assertThat(bucket, notNullValue()); + assertThat(bucket.getKeyAsString(), equalTo(String.valueOf(i * interval))); + assertThat(((Number) bucket.getKey()).longValue(), equalTo((long) i * interval)); + assertThat(bucket.getDocCount(), equalTo(valueCounts[i])); + SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo((double) firstDerivValuesCounts[i])); + } + } + + @AwaitsFix(bugUrl="waiting for derivative to support _count") // NOCOMMIT + @Test + public void unmapped() throws Exception { + SearchResponse response = client().prepareSearch("idx_unmapped") + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) + .subAggregation(derivative("deriv").setBucketsPaths("_count"))) + .execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram deriv = response.getAggregations().get("histo"); + assertThat(deriv, notNullValue()); + assertThat(deriv.getName(), equalTo("histo")); + assertThat(deriv.getBuckets().size(), equalTo(0)); + } + + @AwaitsFix(bugUrl="waiting for derivative to support _count") // NOCOMMIT + @Test + public void partiallyUnmapped() throws Exception { + SearchResponse response = client().prepareSearch("idx", "idx_unmapped") + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) + .subAggregation(derivative("deriv").setBucketsPaths("_count"))) + .execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram deriv = response.getAggregations().get("histo"); + assertThat(deriv, notNullValue()); + assertThat(deriv.getName(), equalTo("histo")); + List buckets = deriv.getBuckets(); + assertThat(deriv.getBuckets().size(), equalTo(numFirstDerivValueBuckets)); + + for (int i = 0; i < numFirstDerivValueBuckets; ++i) { + Histogram.Bucket bucket = buckets.get(i); + assertThat(bucket, notNullValue()); + assertThat(bucket.getKeyAsString(), equalTo(String.valueOf(i * interval))); + assertThat(((Number) bucket.getKey()).longValue(), equalTo((long) i * interval)); + assertThat(bucket.getDocCount(), equalTo(valueCounts[i])); + SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo((double) firstDerivValueCounts[i])); + } + } + + @AwaitsFix(bugUrl="waiting for derivative to support _count and gaps") // NOCOMMIT + @Test + public void singleValuedFieldWithGaps() throws Exception { + SearchResponse searchResponse = client() + .prepareSearch("empty_bucket_idx") + .setQuery(matchAllQuery()) + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) + .subAggregation(derivative("deriv").setBucketsPaths("_count"))) + .execute().actionGet(); + + assertThat(searchResponse.getHits().getTotalHits(), equalTo(14l)); + + InternalHistogram deriv = searchResponse.getAggregations().get("histo"); + assertThat(deriv, Matchers.notNullValue()); + assertThat(deriv.getName(), equalTo("histo")); + List buckets = (List) deriv.getBuckets(); + assertThat(buckets.size(), equalTo(5)); + + Histogram.Bucket bucket = buckets.get(0); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(0l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(0d)); + + bucket = buckets.get(1); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(1l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(1d)); + + bucket = buckets.get(2); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(4l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(0d)); + + bucket = buckets.get(3); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(9l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(-1d)); + + bucket = buckets.get(4); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(10l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(-1d)); + } + + @AwaitsFix(bugUrl="waiting for derivative to support _count and insert_zeros gap policy") // NOCOMMIT + @Test + public void singleValuedFieldWithGaps_insertZeros() throws Exception { + SearchResponse searchResponse = client() + .prepareSearch("empty_bucket_idx") + .setQuery(matchAllQuery()) + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) + .subAggregation(derivative("deriv").setBucketsPaths("_count"))) // NOCOMMIT add insert_zeros gapPolicy + .execute().actionGet(); + + assertThat(searchResponse.getHits().getTotalHits(), equalTo(14l)); + + InternalHistogram deriv = searchResponse.getAggregations().get("histo"); + assertThat(deriv, Matchers.notNullValue()); + assertThat(deriv.getName(), equalTo("histo")); + List buckets = (List) deriv.getBuckets(); + assertThat(buckets.size(), equalTo(11)); + + Histogram.Bucket bucket = buckets.get(0); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(0l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(0d)); + + bucket = buckets.get(1); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(1l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(1d)); + + bucket = buckets.get(2); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(2l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(-2d)); + + bucket = buckets.get(3); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(3l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(2d)); + + bucket = buckets.get(4); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(4l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(0d)); + + bucket = buckets.get(5); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(5l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(-2d)); + + bucket = buckets.get(6); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(6l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(0d)); + + bucket = buckets.get(7); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(7l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(0d)); + + bucket = buckets.get(8); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(8l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(3d)); + + bucket = buckets.get(9); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(9l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(-1d)); + + bucket = buckets.get(10); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(10l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(-1d)); + } + + @AwaitsFix(bugUrl="waiting for derivative to support _count and interpolate gapPolicy") // NOCOMMIT + @Test + public void singleValuedFieldWithGaps_interpolate() throws Exception { + SearchResponse searchResponse = client() + .prepareSearch("empty_bucket_idx") + .setQuery(matchAllQuery()) + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) + .subAggregation(derivative("deriv").setBucketsPaths("_count"))).execute().actionGet(); // NOCOMMIT add interpolate gapPolicy + + assertThat(searchResponse.getHits().getTotalHits(), equalTo(14l)); + + InternalHistogram deriv = searchResponse.getAggregations().get("deriv"); + assertThat(deriv, Matchers.notNullValue()); + assertThat(deriv.getName(), equalTo("histo")); + List buckets = (List) deriv.getBuckets(); + assertThat(buckets.size(), equalTo(7)); + + Histogram.Bucket bucket = buckets.get(0); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(0l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(0d)); + + bucket = buckets.get(1); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(1l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(1d)); + + bucket = buckets.get(2); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(2l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(0d)); + + bucket = buckets.get(3); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(4l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(0d)); + + bucket = buckets.get(4); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(5l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(0.25d)); + + bucket = buckets.get(5); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(9l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(-1d)); + + bucket = buckets.get(6); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(10l)); + assertThat(bucket.getDocCount(), equalTo(0l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(-1d)); + } + +} From 269d4bc30ed78ad0a07f3ac420d629c2f0ca595c Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Tue, 17 Feb 2015 10:07:56 +0000 Subject: [PATCH 23/68] InternalHistogram.Factory.create() can now work from prototype Another InternalHistogram instance can be passed into the method with the buckets and the name and will be used to set all the options such as minDocCount, formatter, Order etc. --- .../bucket/histogram/InternalDateHistogram.java | 13 ------------- .../bucket/histogram/InternalHistogram.java | 9 +++++---- .../bucket/histogram/InternalOrder.java | 2 +- 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java index 0457ad9e92cde..503d3626b2f17 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java @@ -22,15 +22,10 @@ import org.elasticsearch.search.aggregations.AggregationExecutionException; import org.elasticsearch.search.aggregations.InternalAggregation.Type; import org.elasticsearch.search.aggregations.InternalAggregations; -import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram.EmptyBucketInfo; -import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; -import java.util.List; -import java.util.Map; - /** * */ @@ -75,14 +70,6 @@ public String type() { return TYPE.name(); } - @Override - public InternalHistogram create(String name, List buckets, InternalOrder order, - long minDocCount, - EmptyBucketInfo emptyBucketInfo, @Nullable ValueFormatter formatter, boolean keyed, List reducers, - Map metaData) { - return new InternalHistogram(name, buckets, order, minDocCount, emptyBucketInfo, formatter, keyed, this, reducers, metaData); - } - @Override public InternalDateHistogram.Bucket createBucket(Object key, long docCount, InternalAggregations aggregations, boolean keyed, @Nullable ValueFormatter formatter) { diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java index 4171cc3f51449..ad17e3796fea8 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java @@ -248,6 +248,11 @@ public InternalHistogram create(String name, List buckets, InternalOrder o return new InternalHistogram<>(name, buckets, order, minDocCount, emptyBucketInfo, formatter, keyed, this, reducers, metaData); } + public InternalHistogram create(String name, List buckets, InternalHistogram prototype) { + return new InternalHistogram<>(name, buckets, prototype.order, prototype.minDocCount, prototype.emptyBucketInfo, + prototype.formatter, prototype.keyed, this, prototype.reducers(), prototype.metaData); + } + public B createBucket(Object key, long docCount, InternalAggregations aggregations, boolean keyed, @Nullable ValueFormatter formatter) { if (key instanceof Number) { @@ -301,10 +306,6 @@ public Factory getFactory() { return factory; } - public InternalOrder getOrder() { - return order; - } - private static class IteratorAndCurrent { private final Iterator iterator; diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalOrder.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalOrder.java index 1090206478620..9d503a8e90b87 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalOrder.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalOrder.java @@ -29,7 +29,7 @@ /** * An internal {@link Histogram.Order} strategy which is identified by a unique id. */ -public class InternalOrder extends Histogram.Order { +class InternalOrder extends Histogram.Order { final byte id; final String key; From 19cdfe256ecae064bb2ed6e09dc4818f24898edd Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Tue, 17 Feb 2015 10:08:25 +0000 Subject: [PATCH 24/68] DerivativeReducer now copies histogram options from old histogram instance --- .../aggregations/reducers/derivative/DerivativeReducer.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java index 730a85a2d419b..40397d8f46ef6 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java @@ -97,7 +97,6 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext List newBuckets = new ArrayList<>(); Double lastBucketValue = null; - // NOCOMMIT this needs to be improved so that the aggs are cloned correctly to ensure aggs are fully immutable. for (InternalHistogram.Bucket bucket : buckets) { Double thisBucketValue = resolveBucketValue(histo, bucket); if (lastBucketValue != null) { @@ -116,8 +115,7 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext } lastBucketValue = thisBucketValue; } - return factory.create(histo.getName(), newBuckets, histo.getOrder(), 1, null, null, false, new ArrayList(), - histo.getMetaData()); // NOCOMMIT get order, minDocCount, emptyBucketInfo etc. from histo + return factory.create(histo.getName(), newBuckets, histo); } private Double resolveBucketValue(InternalHistogram histo, InternalHistogram.Bucket bucket) { From 3375c02b42f834a7aa4656a993ca2fac5307c383 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Tue, 17 Feb 2015 11:14:47 +0000 Subject: [PATCH 25/68] Added support for _count and _key as bucketsPaths --- .../search/aggregations/AggregatorFactories.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java index 258b90c265396..628fe3144a939 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java @@ -157,7 +157,7 @@ public Builder addReducer(ReducerFactory reducerFactory) { } public AggregatorFactories build() { - if (factories.isEmpty()) { + if (factories.isEmpty() && reducerFactories.isEmpty()) { return EMPTY; } List orderedReducers = resolveReducerOrder(this.reducerFactories, this.factories); @@ -212,7 +212,7 @@ private void resolveReducerOrder(Set aggFactoryNames, Map Date: Tue, 17 Feb 2015 11:15:04 +0000 Subject: [PATCH 26/68] updated derivative tests to test _count --- .../reducers/DateDerivativeTests.java | 71 ++++++++++++------- .../reducers/DerivativeTests.java | 55 ++++++++------ 2 files changed, 78 insertions(+), 48 deletions(-) diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java index ec131b3a60949..ad1c131c885a1 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java @@ -19,7 +19,6 @@ package org.elasticsearch.search.aggregations.reducers; -import org.apache.lucene.util.LuceneTestCase.AwaitsFix; import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.index.mapper.core.DateFieldMapper; @@ -105,8 +104,6 @@ public void afterEachTest() throws IOException { internalCluster().wipeIndices("idx2"); } - @AwaitsFix(bugUrl = "waiting for derivative to support _count") - // NOCOMMIT @Test public void singleValuedField() throws Exception { SearchResponse response = client() @@ -121,22 +118,30 @@ public void singleValuedField() throws Exception { assertThat(deriv, notNullValue()); assertThat(deriv.getName(), equalTo("histo")); List buckets = deriv.getBuckets(); - assertThat(buckets.size(), equalTo(2)); + assertThat(buckets.size(), equalTo(3)); DateTime key = new DateTime(2012, 1, 1, 0, 0, DateTimeZone.UTC); Histogram.Bucket bucket = buckets.get(0); assertThat(bucket, notNullValue()); assertThat((DateTime) bucket.getKey(), equalTo(key)); - assertThat(bucket.getDocCount(), equalTo(0l)); + assertThat(bucket.getDocCount(), equalTo(1l)); SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(1d)); + assertThat(docCountDeriv, nullValue()); key = new DateTime(2012, 2, 1, 0, 0, DateTimeZone.UTC); bucket = buckets.get(1); assertThat(bucket, notNullValue()); assertThat((DateTime) bucket.getKey(), equalTo(key)); - assertThat(bucket.getDocCount(), equalTo(0l)); + assertThat(bucket.getDocCount(), equalTo(2l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(1d)); + + key = new DateTime(2012, 3, 1, 0, 0, DateTimeZone.UTC); + bucket = buckets.get(2); + assertThat(bucket, notNullValue()); + assertThat((DateTime) bucket.getKey(), equalTo(key)); + assertThat(bucket.getDocCount(), equalTo(3l)); docCountDeriv = bucket.getAggregations().get("deriv"); assertThat(docCountDeriv, notNullValue()); assertThat(docCountDeriv.value(), equalTo(1d)); @@ -212,8 +217,6 @@ public void singleValuedField_WithSubAggregation() throws Exception { assertThat((double) propertiesCounts[2], equalTo(15.0)); } - @AwaitsFix(bugUrl = "waiting for derivative to support _count") - // NOCOMMIT @Test public void multiValuedField() throws Exception { SearchResponse response = client() @@ -228,23 +231,22 @@ public void multiValuedField() throws Exception { assertThat(deriv, notNullValue()); assertThat(deriv.getName(), equalTo("histo")); List buckets = deriv.getBuckets(); - assertThat(buckets.size(), equalTo(3)); + assertThat(buckets.size(), equalTo(4)); DateTime key = new DateTime(2012, 1, 1, 0, 0, DateTimeZone.UTC); Histogram.Bucket bucket = buckets.get(0); assertThat(bucket, notNullValue()); assertThat((DateTime) bucket.getKey(), equalTo(key)); - assertThat(bucket.getDocCount(), equalTo(0l)); - assertThat(bucket.getAggregations().asList().isEmpty(), is(false)); + assertThat(bucket.getDocCount(), equalTo(1l)); + assertThat(bucket.getAggregations().asList().isEmpty(), is(true)); SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(2.0)); + assertThat(docCountDeriv, nullValue()); key = new DateTime(2012, 2, 1, 0, 0, DateTimeZone.UTC); bucket = buckets.get(1); assertThat(bucket, notNullValue()); assertThat((DateTime) bucket.getKey(), equalTo(key)); - assertThat(bucket.getDocCount(), equalTo(0l)); + assertThat(bucket.getDocCount(), equalTo(3l)); assertThat(bucket.getAggregations().asList().isEmpty(), is(false)); docCountDeriv = bucket.getAggregations().get("deriv"); assertThat(docCountDeriv, notNullValue()); @@ -254,15 +256,23 @@ public void multiValuedField() throws Exception { bucket = buckets.get(2); assertThat(bucket, notNullValue()); assertThat((DateTime) bucket.getKey(), equalTo(key)); - assertThat(bucket.getDocCount(), equalTo(0l)); + assertThat(bucket.getDocCount(), equalTo(5l)); + assertThat(bucket.getAggregations().asList().isEmpty(), is(false)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(2.0)); + + key = new DateTime(2012, 4, 1, 0, 0, DateTimeZone.UTC); + bucket = buckets.get(3); + assertThat(bucket, notNullValue()); + assertThat((DateTime) bucket.getKey(), equalTo(key)); + assertThat(bucket.getDocCount(), equalTo(3l)); assertThat(bucket.getAggregations().asList().isEmpty(), is(false)); docCountDeriv = bucket.getAggregations().get("deriv"); assertThat(docCountDeriv, notNullValue()); assertThat(docCountDeriv.value(), equalTo(-2.0)); } - @AwaitsFix(bugUrl = "waiting for derivative to support _count") - // NOCOMMIT @Test public void unmapped() throws Exception { SearchResponse response = client() @@ -279,8 +289,6 @@ public void unmapped() throws Exception { assertThat(deriv.getBuckets().size(), equalTo(0)); } - @AwaitsFix(bugUrl = "waiting for derivative to support _count") - // NOCOMMIT @Test public void partiallyUnmapped() throws Exception { SearchResponse response = client() @@ -295,23 +303,32 @@ public void partiallyUnmapped() throws Exception { assertThat(deriv, notNullValue()); assertThat(deriv.getName(), equalTo("histo")); List buckets = deriv.getBuckets(); - assertThat(buckets.size(), equalTo(2)); + assertThat(buckets.size(), equalTo(3)); DateTime key = new DateTime(2012, 1, 1, 0, 0, DateTimeZone.UTC); Histogram.Bucket bucket = buckets.get(0); assertThat(bucket, notNullValue()); assertThat((DateTime) bucket.getKey(), equalTo(key)); - assertThat(bucket.getDocCount(), equalTo(0l)); - assertThat(bucket.getAggregations().asList().isEmpty(), is(false)); + assertThat(bucket.getDocCount(), equalTo(1l)); + assertThat(bucket.getAggregations().asList().isEmpty(), is(true)); SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(1.0)); + assertThat(docCountDeriv, nullValue()); key = new DateTime(2012, 2, 1, 0, 0, DateTimeZone.UTC); bucket = buckets.get(1); assertThat(bucket, notNullValue()); assertThat((DateTime) bucket.getKey(), equalTo(key)); - assertThat(bucket.getDocCount(), equalTo(0l)); + assertThat(bucket.getDocCount(), equalTo(2l)); + assertThat(bucket.getAggregations().asList().isEmpty(), is(false)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(1.0)); + + key = new DateTime(2012, 3, 1, 0, 0, DateTimeZone.UTC); + bucket = buckets.get(2); + assertThat(bucket, notNullValue()); + assertThat((DateTime) bucket.getKey(), equalTo(key)); + assertThat(bucket.getDocCount(), equalTo(3l)); assertThat(bucket.getAggregations().asList().isEmpty(), is(false)); docCountDeriv = bucket.getAggregations().get("deriv"); assertThat(docCountDeriv, notNullValue()); diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java index 3b51bbbf6b2c7..aadc05cd00376 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java @@ -44,6 +44,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.core.IsNull.notNullValue; +import static org.hamcrest.core.IsNull.nullValue; @ElasticsearchIntegrationTest.SuiteScopeTest public class DerivativeTests extends ElasticsearchIntegrationTest { @@ -157,7 +158,6 @@ public void setupSuiteScopeCluster() throws Exception { ensureSearchable(); } - @AwaitsFix(bugUrl="waiting for derivative to support _count") // NOCOMMIT @Test public void singleValuedField() { @@ -173,17 +173,21 @@ public void singleValuedField() { assertThat(deriv, notNullValue()); assertThat(deriv.getName(), equalTo("histo")); List buckets = deriv.getBuckets(); - assertThat(buckets.size(), equalTo(numFirstDerivValueBuckets)); + assertThat(buckets.size(), equalTo(numValueBuckets)); - for (int i = 0; i < numFirstDerivValueBuckets; ++i) { + for (int i = 0; i < numValueBuckets; ++i) { Histogram.Bucket bucket = buckets.get(i); assertThat(bucket, notNullValue()); assertThat(bucket.getKeyAsString(), equalTo(String.valueOf(i * interval))); assertThat(((Number) bucket.getKey()).longValue(), equalTo((long) i * interval)); assertThat(bucket.getDocCount(), equalTo(valueCounts[i])); SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo((double) firstDerivValueCounts[i])); + if (i > 0) { + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo((double) firstDerivValueCounts[i - 1])); + } else { + assertThat(docCountDeriv, nullValue()); + } } } @@ -222,9 +226,9 @@ public void singleValuedField_WithSubAggregation() throws Exception { s += j + 1; } } + SimpleValue sumDeriv = bucket.getAggregations().get("deriv"); assertThat(sum.getValue(), equalTo((double) s)); if (i > 0) { - SimpleValue sumDeriv = bucket.getAggregations().get("deriv"); assertThat(sumDeriv, notNullValue()); long s1 = 0; long s2 = 0; @@ -240,6 +244,8 @@ public void singleValuedField_WithSubAggregation() throws Exception { assertThat(sumDeriv.value(), equalTo((double) sumDerivValue)); assertThat((double) bucket.getProperty("histo", AggregationPath.parse("deriv.value").getPathElementsAsStringList()), equalTo((double) sumDerivValue)); + } else { + assertThat(sumDeriv, nullValue()); } assertThat((long) propertiesKeys[i], equalTo((long) i * interval)); assertThat((long) propertiesDocCounts[i], equalTo(valueCounts[i])); @@ -247,7 +253,6 @@ public void singleValuedField_WithSubAggregation() throws Exception { } } - @AwaitsFix(bugUrl="waiting for derivative to support _count") // NOCOMMIT @Test public void multiValuedField() throws Exception { SearchResponse response = client().prepareSearch("idx") @@ -262,21 +267,24 @@ public void multiValuedField() throws Exception { assertThat(deriv, notNullValue()); assertThat(deriv.getName(), equalTo("histo")); List buckets = deriv.getBuckets(); - assertThat(deriv.getBuckets().size(), equalTo(numFirstDerivValuesBuckets)); + assertThat(deriv.getBuckets().size(), equalTo(numValuesBuckets)); - for (int i = 0; i < numFirstDerivValuesBuckets; ++i) { + for (int i = 0; i < numValuesBuckets; ++i) { Histogram.Bucket bucket = buckets.get(i); assertThat(bucket, notNullValue()); assertThat(bucket.getKeyAsString(), equalTo(String.valueOf(i * interval))); assertThat(((Number) bucket.getKey()).longValue(), equalTo((long) i * interval)); - assertThat(bucket.getDocCount(), equalTo(valueCounts[i])); + assertThat(bucket.getDocCount(), equalTo(valuesCounts[i])); SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo((double) firstDerivValuesCounts[i])); + if (i > 0) { + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo((double) firstDerivValuesCounts[i - 1])); + } else { + assertThat(docCountDeriv, nullValue()); + } } } - @AwaitsFix(bugUrl="waiting for derivative to support _count") // NOCOMMIT @Test public void unmapped() throws Exception { SearchResponse response = client().prepareSearch("idx_unmapped") @@ -293,7 +301,6 @@ public void unmapped() throws Exception { assertThat(deriv.getBuckets().size(), equalTo(0)); } - @AwaitsFix(bugUrl="waiting for derivative to support _count") // NOCOMMIT @Test public void partiallyUnmapped() throws Exception { SearchResponse response = client().prepareSearch("idx", "idx_unmapped") @@ -308,21 +315,25 @@ public void partiallyUnmapped() throws Exception { assertThat(deriv, notNullValue()); assertThat(deriv.getName(), equalTo("histo")); List buckets = deriv.getBuckets(); - assertThat(deriv.getBuckets().size(), equalTo(numFirstDerivValueBuckets)); + assertThat(deriv.getBuckets().size(), equalTo(numValueBuckets)); - for (int i = 0; i < numFirstDerivValueBuckets; ++i) { + for (int i = 0; i < numValueBuckets; ++i) { Histogram.Bucket bucket = buckets.get(i); assertThat(bucket, notNullValue()); assertThat(bucket.getKeyAsString(), equalTo(String.valueOf(i * interval))); assertThat(((Number) bucket.getKey()).longValue(), equalTo((long) i * interval)); assertThat(bucket.getDocCount(), equalTo(valueCounts[i])); SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo((double) firstDerivValueCounts[i])); + if (i > 0) { + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo((double) firstDerivValueCounts[i - 1])); + } else { + assertThat(docCountDeriv, nullValue()); + } } } - @AwaitsFix(bugUrl="waiting for derivative to support _count and gaps") // NOCOMMIT + @AwaitsFix(bugUrl="waiting for derivative to gaps") // NOCOMMIT @Test public void singleValuedFieldWithGaps() throws Exception { SearchResponse searchResponse = client() @@ -382,7 +393,8 @@ public void singleValuedFieldWithGaps() throws Exception { assertThat(docCountDeriv.value(), equalTo(-1d)); } - @AwaitsFix(bugUrl="waiting for derivative to support _count and insert_zeros gap policy") // NOCOMMIT + @AwaitsFix(bugUrl = "waiting for derivative to support insert_zeros gap policy") + // NOCOMMIT @Test public void singleValuedFieldWithGaps_insertZeros() throws Exception { SearchResponse searchResponse = client() @@ -490,7 +502,8 @@ public void singleValuedFieldWithGaps_insertZeros() throws Exception { assertThat(docCountDeriv.value(), equalTo(-1d)); } - @AwaitsFix(bugUrl="waiting for derivative to support _count and interpolate gapPolicy") // NOCOMMIT + @AwaitsFix(bugUrl = "waiting for derivative to support interpolate gapPolicy") + // NOCOMMIT @Test public void singleValuedFieldWithGaps_interpolate() throws Exception { SearchResponse searchResponse = client() From f03fe5b8b6aa36603e0539fb668c54d9d8b0d250 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Tue, 17 Feb 2015 11:31:02 +0000 Subject: [PATCH 27/68] Cleaning up NOCOMMITs which are resolved --- .../search/aggregations/AggregatorFactories.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java index 628fe3144a939..552ff49fe1dc0 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java @@ -206,7 +206,7 @@ private void resolveReducerOrder(Set aggFactoryNames, Map orderedReducers, List unmarkedFactories, Set temporarilyMarked, ReducerFactory factory) { if (temporarilyMarked.contains(factory)) { - throw new ElasticsearchIllegalStateException("Cyclical dependancy found with reducer [" + factory.getName() + "]"); // NOCOMMIT is this the right Exception to throw? + throw new ElasticsearchIllegalStateException("Cyclical dependancy found with reducer [" + factory.getName() + "]"); } else if (unmarkedFactories.contains(factory)) { temporarilyMarked.add(factory); String[] bucketsPaths = factory.getBucketsPaths(); @@ -218,7 +218,7 @@ private void resolveReducerOrder(Set aggFactoryNames, Map Date: Tue, 17 Feb 2015 11:31:24 +0000 Subject: [PATCH 28/68] Cleaning up NOCOMMITs --- .../search/aggregations/reducers/DerivativeTests.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java index aadc05cd00376..5cdf2a8cee8e6 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java @@ -333,7 +333,7 @@ public void partiallyUnmapped() throws Exception { } } - @AwaitsFix(bugUrl="waiting for derivative to gaps") // NOCOMMIT + @AwaitsFix(bugUrl = "waiting for derivative to gaps") @Test public void singleValuedFieldWithGaps() throws Exception { SearchResponse searchResponse = client() @@ -341,7 +341,7 @@ public void singleValuedFieldWithGaps() throws Exception { .setQuery(matchAllQuery()) .addAggregation( histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) - .subAggregation(derivative("deriv").setBucketsPaths("_count"))) + .subAggregation(derivative("deriv").setBucketsPaths("_count"))) // NOCOMMITadd ignore gapPolicy .execute().actionGet(); assertThat(searchResponse.getHits().getTotalHits(), equalTo(14l)); @@ -394,7 +394,6 @@ public void singleValuedFieldWithGaps() throws Exception { } @AwaitsFix(bugUrl = "waiting for derivative to support insert_zeros gap policy") - // NOCOMMIT @Test public void singleValuedFieldWithGaps_insertZeros() throws Exception { SearchResponse searchResponse = client() @@ -503,7 +502,6 @@ public void singleValuedFieldWithGaps_insertZeros() throws Exception { } @AwaitsFix(bugUrl = "waiting for derivative to support interpolate gapPolicy") - // NOCOMMIT @Test public void singleValuedFieldWithGaps_interpolate() throws Exception { SearchResponse searchResponse = client() From 5a2c4ab5ae9ab3275867bd4bb3509100fde6a43c Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Tue, 17 Feb 2015 11:37:28 +0000 Subject: [PATCH 29/68] Added test for second_derivative --- .../reducers/DerivativeTests.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java index 5cdf2a8cee8e6..11bac92908153 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java @@ -56,8 +56,10 @@ public class DerivativeTests extends ElasticsearchIntegrationTest { static int interval; static int numValueBuckets, numValuesBuckets; static int numFirstDerivValueBuckets, numFirstDerivValuesBuckets; + static int numSecondDerivValueBuckets; static long[] valueCounts, valuesCounts; static long[] firstDerivValueCounts, firstDerivValuesCounts; + static long[] secondDerivValueCounts; @Override public void setupSuiteScopeCluster() throws Exception { @@ -97,6 +99,18 @@ public void setupSuiteScopeCluster() throws Exception { lastValueCount = thisValue; } + numSecondDerivValueBuckets = numFirstDerivValueBuckets - 1; + secondDerivValueCounts = new long[numSecondDerivValueBuckets]; + long lastFirstDerivativeValueCount = -1; + for (int i = 0; i < numFirstDerivValueBuckets; i++) { + long thisFirstDerivativeValue = firstDerivValueCounts[i]; + if (lastFirstDerivativeValueCount != -1) { + long diff = thisFirstDerivativeValue - lastFirstDerivativeValueCount; + secondDerivValueCounts[i - 1] = diff; + } + lastFirstDerivativeValueCount = thisFirstDerivativeValue; + } + numFirstDerivValuesBuckets = numValuesBuckets - 1; firstDerivValuesCounts = new long[numFirstDerivValuesBuckets]; long lastValuesCount = -1; @@ -191,6 +205,47 @@ public void singleValuedField() { } } + @Test + public void singleValuedField_secondDerivative() { + + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) + .subAggregation(derivative("deriv").setBucketsPaths("_count")) + .subAggregation(derivative("2nd_deriv").setBucketsPaths("deriv"))).execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram deriv = response.getAggregations().get("histo"); + assertThat(deriv, notNullValue()); + assertThat(deriv.getName(), equalTo("histo")); + List buckets = deriv.getBuckets(); + assertThat(buckets.size(), equalTo(numValueBuckets)); + + for (int i = 0; i < numValueBuckets; ++i) { + Histogram.Bucket bucket = buckets.get(i); + assertThat(bucket, notNullValue()); + assertThat(bucket.getKeyAsString(), equalTo(String.valueOf(i * interval))); + assertThat(((Number) bucket.getKey()).longValue(), equalTo((long) i * interval)); + assertThat(bucket.getDocCount(), equalTo(valueCounts[i])); + SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); + if (i > 0) { + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo((double) firstDerivValueCounts[i - 1])); + } else { + assertThat(docCountDeriv, nullValue()); + } + SimpleValue docCount2ndDeriv = bucket.getAggregations().get("2nd_deriv"); + if (i > 1) { + assertThat(docCount2ndDeriv, notNullValue()); + assertThat(docCount2ndDeriv.value(), equalTo((double) secondDerivValueCounts[i - 2])); + } else { + assertThat(docCount2ndDeriv, nullValue()); + } + } + } + @Test public void singleValuedField_WithSubAggregation() throws Exception { SearchResponse response = client() From 7c046d28bf4f077f9bbe0b5e9069c74b2319d212 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Mon, 2 Mar 2015 14:53:05 +0000 Subject: [PATCH 30/68] Implementation of GapPolicy for derivative --- .../reducers/InternalSimpleValue.java | 2 +- .../derivative/DerivativeBuilder.java | 10 + .../reducers/derivative/DerivativeParser.java | 8 +- .../derivative/DerivativeReducer.java | 99 ++++++++- .../reducers/DerivativeTests.java | 189 ++++++++---------- 5 files changed, 193 insertions(+), 115 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/InternalSimpleValue.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/InternalSimpleValue.java index 7d204c007c6b3..9641f187c6c39 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/InternalSimpleValue.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/InternalSimpleValue.java @@ -93,7 +93,7 @@ protected void doWriteTo(StreamOutput out) throws IOException { @Override public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { - boolean hasValue = !Double.isInfinite(value); + boolean hasValue = !(Double.isInfinite(value) || Double.isNaN(value)); builder.field(CommonFields.VALUE, hasValue ? value : null); if (hasValue && valueFormatter != null) { builder.field(CommonFields.VALUE_AS_STRING, valueFormatter.format(value)); diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeBuilder.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeBuilder.java index 87165c32ac094..f868e673b1d5f 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeBuilder.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeBuilder.java @@ -21,12 +21,14 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.search.aggregations.reducers.ReducerBuilder; +import org.elasticsearch.search.aggregations.reducers.derivative.DerivativeReducer.GapPolicy; import java.io.IOException; public class DerivativeBuilder extends ReducerBuilder { private String format; + private GapPolicy gapPolicy; public DerivativeBuilder(String name) { super(name, DerivativeReducer.TYPE.name()); @@ -37,11 +39,19 @@ public DerivativeBuilder format(String format) { return this; } + public DerivativeBuilder gapPolicy(GapPolicy gapPolicy) { + this.gapPolicy = gapPolicy; + return this; + } + @Override protected XContentBuilder internalXContent(XContentBuilder builder, Params params) throws IOException { if (format != null) { builder.field(DerivativeParser.FORMAT.getPreferredName(), format); } + if (gapPolicy != null) { + builder.field(DerivativeParser.GAP_POLICY.getPreferredName(), gapPolicy.getName()); + } return builder; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java index 8a562050dcbc9..6b6b826ec6fcc 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java @@ -24,6 +24,7 @@ import org.elasticsearch.search.SearchParseException; import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.reducers.ReducerFactory; +import org.elasticsearch.search.aggregations.reducers.derivative.DerivativeReducer.GapPolicy; import org.elasticsearch.search.aggregations.support.format.ValueFormat; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.internal.SearchContext; @@ -33,7 +34,9 @@ import java.util.List; public class DerivativeParser implements Reducer.Parser { + public static final ParseField FORMAT = new ParseField("format"); + public static final ParseField GAP_POLICY = new ParseField("gap_policy"); @Override public String type() { @@ -46,6 +49,7 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon String currentFieldName = null; String[] bucketsPaths = null; String format = null; + GapPolicy gapPolicy = GapPolicy.IGNORE; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { @@ -55,6 +59,8 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon format = parser.text(); } else if (BUCKETS_PATH.match(currentFieldName)) { bucketsPaths = new String[] { parser.text() }; + } else if (GAP_POLICY.match(currentFieldName)) { + gapPolicy = GapPolicy.parse(context, parser.text()); } else { throw new SearchParseException(context, "Unknown key for a " + token + " in [" + reducerName + "]: [" + currentFieldName + "]."); @@ -86,7 +92,7 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon formatter = ValueFormat.Patternable.Number.format(format).formatter(); } - return new DerivativeReducer.Factory(reducerName, bucketsPaths, formatter); + return new DerivativeReducer.Factory(reducerName, bucketsPaths, formatter, gapPolicy); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java index 40397d8f46ef6..c0d96f4056b10 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java @@ -24,8 +24,10 @@ import org.elasticsearch.ElasticsearchIllegalStateException; import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.search.SearchParseException; import org.elasticsearch.search.aggregations.Aggregation; import org.elasticsearch.search.aggregations.AggregationExecutionException; import org.elasticsearch.search.aggregations.Aggregator; @@ -44,9 +46,11 @@ import org.elasticsearch.search.aggregations.support.AggregationPath; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; +import org.elasticsearch.search.internal.SearchContext; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -75,13 +79,16 @@ public InternalAggregation apply(Aggregation input) { }; private ValueFormatter formatter; + private GapPolicy gapPolicy; public DerivativeReducer() { } - public DerivativeReducer(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter, Map metadata) { + public DerivativeReducer(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter, GapPolicy gapPolicy, + Map metadata) { super(name, bucketsPaths, metadata); this.formatter = formatter; + this.gapPolicy = gapPolicy; } @Override @@ -100,9 +107,6 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext for (InternalHistogram.Bucket bucket : buckets) { Double thisBucketValue = resolveBucketValue(histo, bucket); if (lastBucketValue != null) { - if (thisBucketValue == null) { - throw new ElasticsearchIllegalStateException("FOUND GAP IN DATA"); // NOCOMMIT deal with gaps in data - } double diff = thisBucketValue - lastBucketValue; List aggs = new ArrayList<>(Lists.transform(bucket.getAggregations().asList(), FUNCTION)); @@ -122,13 +126,30 @@ private Double resolveBucketValue(InternalHistogram metaData) throws IOException { - return new DerivativeReducer(name, bucketsPaths, formatter, metaData); + return new DerivativeReducer(name, bucketsPaths, formatter, gapPolicy, metaData); + } + + } + + public static enum GapPolicy { + INSERT_ZEROS((byte) 0, "insert_zeros"), IGNORE((byte) 1, "ignore"); + + public static GapPolicy parse(SearchContext context, String text) { + GapPolicy result = null; + for (GapPolicy policy : values()) { + if (policy.parseField.match(text)) { + if (result == null) { + result = policy; + } else { + throw new ElasticsearchIllegalStateException("Text can be parsed to 2 different gap policies: text=[" + text + + "], " + "policies=" + Arrays.asList(result, policy)); + } + } + } + if (result == null) { + final List validNames = new ArrayList<>(); + for (GapPolicy policy : values()) { + validNames.add(policy.getName()); + } + throw new SearchParseException(context, "Invalid gap policy: [" + text + "], accepted values: " + validNames); + } + return result; + } + + private final byte id; + private final ParseField parseField; + + private GapPolicy(byte id, String name) { + this.id = id; + this.parseField = new ParseField(name); + } + + public void writeTo(StreamOutput out) throws IOException { + out.writeByte(id); + } + + public static GapPolicy readFrom(StreamInput in) throws IOException { + byte id = in.readByte(); + for (GapPolicy gapPolicy : values()) { + if (id == gapPolicy.id) { + return gapPolicy; + } + } + throw new IllegalStateException("Unknown GapPolicy with id [" + id + "]"); } + public String getName() { + return parseField.getPreferredName(); + } } } diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java index 11bac92908153..7d2d5500cd173 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java @@ -19,13 +19,13 @@ package org.elasticsearch.search.aggregations.reducers; -import org.apache.lucene.util.LuceneTestCase.AwaitsFix; import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; import org.elasticsearch.search.aggregations.bucket.histogram.Histogram.Bucket; import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; import org.elasticsearch.search.aggregations.metrics.sum.Sum; +import org.elasticsearch.search.aggregations.reducers.derivative.DerivativeReducer.GapPolicy; import org.elasticsearch.search.aggregations.support.AggregationPath; import org.elasticsearch.test.ElasticsearchIntegrationTest; import org.hamcrest.Matchers; @@ -388,15 +388,14 @@ public void partiallyUnmapped() throws Exception { } } - @AwaitsFix(bugUrl = "waiting for derivative to gaps") @Test public void singleValuedFieldWithGaps() throws Exception { SearchResponse searchResponse = client() .prepareSearch("empty_bucket_idx") .setQuery(matchAllQuery()) .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) - .subAggregation(derivative("deriv").setBucketsPaths("_count"))) // NOCOMMITadd ignore gapPolicy + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(1).minDocCount(0) + .subAggregation(derivative("deriv").setBucketsPaths("_count"))) .execute().actionGet(); assertThat(searchResponse.getHits().getTotalHits(), equalTo(14l)); @@ -405,91 +404,30 @@ public void singleValuedFieldWithGaps() throws Exception { assertThat(deriv, Matchers.notNullValue()); assertThat(deriv.getName(), equalTo("histo")); List buckets = (List) deriv.getBuckets(); - assertThat(buckets.size(), equalTo(5)); + assertThat(buckets.size(), equalTo(12)); Histogram.Bucket bucket = buckets.get(0); assertThat(bucket, notNullValue()); assertThat(((Number) bucket.getKey()).longValue(), equalTo(0l)); - assertThat(bucket.getDocCount(), equalTo(0l)); + assertThat(bucket.getDocCount(), equalTo(1l)); SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(0d)); + assertThat(docCountDeriv, nullValue()); bucket = buckets.get(1); assertThat(bucket, notNullValue()); assertThat(((Number) bucket.getKey()).longValue(), equalTo(1l)); - assertThat(bucket.getDocCount(), equalTo(0l)); + assertThat(bucket.getDocCount(), equalTo(1l)); docCountDeriv = bucket.getAggregations().get("deriv"); assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(1d)); - - bucket = buckets.get(2); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(4l)); - assertThat(bucket.getDocCount(), equalTo(0l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(0d)); - - bucket = buckets.get(3); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(9l)); - assertThat(bucket.getDocCount(), equalTo(0l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(-1d)); - - bucket = buckets.get(4); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(10l)); - assertThat(bucket.getDocCount(), equalTo(0l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(-1d)); - } - - @AwaitsFix(bugUrl = "waiting for derivative to support insert_zeros gap policy") - @Test - public void singleValuedFieldWithGaps_insertZeros() throws Exception { - SearchResponse searchResponse = client() - .prepareSearch("empty_bucket_idx") - .setQuery(matchAllQuery()) - .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) - .subAggregation(derivative("deriv").setBucketsPaths("_count"))) // NOCOMMIT add insert_zeros gapPolicy - .execute().actionGet(); - - assertThat(searchResponse.getHits().getTotalHits(), equalTo(14l)); - - InternalHistogram deriv = searchResponse.getAggregations().get("histo"); - assertThat(deriv, Matchers.notNullValue()); - assertThat(deriv.getName(), equalTo("histo")); - List buckets = (List) deriv.getBuckets(); - assertThat(buckets.size(), equalTo(11)); - - Histogram.Bucket bucket = buckets.get(0); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(0l)); - assertThat(bucket.getDocCount(), equalTo(0l)); - SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); assertThat(docCountDeriv.value(), equalTo(0d)); - bucket = buckets.get(1); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(1l)); - assertThat(bucket.getDocCount(), equalTo(0l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(1d)); - bucket = buckets.get(2); assertThat(bucket, notNullValue()); assertThat(((Number) bucket.getKey()).longValue(), equalTo(2l)); - assertThat(bucket.getDocCount(), equalTo(0l)); + assertThat(bucket.getDocCount(), equalTo(2l)); docCountDeriv = bucket.getAggregations().get("deriv"); assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(-2d)); + assertThat(docCountDeriv.value(), equalTo(1d)); bucket = buckets.get(3); assertThat(bucket, notNullValue()); @@ -497,23 +435,23 @@ public void singleValuedFieldWithGaps_insertZeros() throws Exception { assertThat(bucket.getDocCount(), equalTo(0l)); docCountDeriv = bucket.getAggregations().get("deriv"); assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(2d)); + assertThat(docCountDeriv.value(), equalTo(-2d)); bucket = buckets.get(4); assertThat(bucket, notNullValue()); assertThat(((Number) bucket.getKey()).longValue(), equalTo(4l)); - assertThat(bucket.getDocCount(), equalTo(0l)); + assertThat(bucket.getDocCount(), equalTo(2l)); docCountDeriv = bucket.getAggregations().get("deriv"); assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(0d)); + assertThat(docCountDeriv.value(), equalTo(2d)); bucket = buckets.get(5); assertThat(bucket, notNullValue()); assertThat(((Number) bucket.getKey()).longValue(), equalTo(5l)); - assertThat(bucket.getDocCount(), equalTo(0l)); + assertThat(bucket.getDocCount(), equalTo(2l)); docCountDeriv = bucket.getAggregations().get("deriv"); assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(-2d)); + assertThat(docCountDeriv.value(), equalTo(0d)); bucket = buckets.get(6); assertThat(bucket, notNullValue()); @@ -521,7 +459,7 @@ public void singleValuedFieldWithGaps_insertZeros() throws Exception { assertThat(bucket.getDocCount(), equalTo(0l)); docCountDeriv = bucket.getAggregations().get("deriv"); assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(0d)); + assertThat(docCountDeriv.value(), equalTo(-2d)); bucket = buckets.get(7); assertThat(bucket, notNullValue()); @@ -537,97 +475,144 @@ public void singleValuedFieldWithGaps_insertZeros() throws Exception { assertThat(bucket.getDocCount(), equalTo(0l)); docCountDeriv = bucket.getAggregations().get("deriv"); assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(3d)); + assertThat(docCountDeriv.value(), equalTo(0d)); bucket = buckets.get(9); assertThat(bucket, notNullValue()); assertThat(((Number) bucket.getKey()).longValue(), equalTo(9l)); - assertThat(bucket.getDocCount(), equalTo(0l)); + assertThat(bucket.getDocCount(), equalTo(3l)); docCountDeriv = bucket.getAggregations().get("deriv"); assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(-1d)); + assertThat(docCountDeriv.value(), equalTo(3d)); bucket = buckets.get(10); assertThat(bucket, notNullValue()); assertThat(((Number) bucket.getKey()).longValue(), equalTo(10l)); - assertThat(bucket.getDocCount(), equalTo(0l)); + assertThat(bucket.getDocCount(), equalTo(2l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(-1d)); + + bucket = buckets.get(11); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(11l)); + assertThat(bucket.getDocCount(), equalTo(1l)); docCountDeriv = bucket.getAggregations().get("deriv"); assertThat(docCountDeriv, notNullValue()); assertThat(docCountDeriv.value(), equalTo(-1d)); } - @AwaitsFix(bugUrl = "waiting for derivative to support interpolate gapPolicy") @Test - public void singleValuedFieldWithGaps_interpolate() throws Exception { + public void singleValuedFieldWithGaps_insertZeros() throws Exception { SearchResponse searchResponse = client() .prepareSearch("empty_bucket_idx") .setQuery(matchAllQuery()) .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) - .subAggregation(derivative("deriv").setBucketsPaths("_count"))).execute().actionGet(); // NOCOMMIT add interpolate gapPolicy + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(1).minDocCount(0) + .subAggregation(derivative("deriv").setBucketsPaths("_count").gapPolicy(GapPolicy.INSERT_ZEROS))) + .execute().actionGet(); assertThat(searchResponse.getHits().getTotalHits(), equalTo(14l)); - InternalHistogram deriv = searchResponse.getAggregations().get("deriv"); + InternalHistogram deriv = searchResponse.getAggregations().get("histo"); assertThat(deriv, Matchers.notNullValue()); assertThat(deriv.getName(), equalTo("histo")); List buckets = (List) deriv.getBuckets(); - assertThat(buckets.size(), equalTo(7)); + assertThat(buckets.size(), equalTo(12)); Histogram.Bucket bucket = buckets.get(0); assertThat(bucket, notNullValue()); assertThat(((Number) bucket.getKey()).longValue(), equalTo(0l)); - assertThat(bucket.getDocCount(), equalTo(0l)); + assertThat(bucket.getDocCount(), equalTo(1l)); SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(0d)); + assertThat(docCountDeriv, nullValue()); bucket = buckets.get(1); assertThat(bucket, notNullValue()); assertThat(((Number) bucket.getKey()).longValue(), equalTo(1l)); - assertThat(bucket.getDocCount(), equalTo(0l)); + assertThat(bucket.getDocCount(), equalTo(1l)); docCountDeriv = bucket.getAggregations().get("deriv"); assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(1d)); + assertThat(docCountDeriv.value(), equalTo(0d)); bucket = buckets.get(2); assertThat(bucket, notNullValue()); assertThat(((Number) bucket.getKey()).longValue(), equalTo(2l)); - assertThat(bucket.getDocCount(), equalTo(0l)); + assertThat(bucket.getDocCount(), equalTo(2l)); docCountDeriv = bucket.getAggregations().get("deriv"); assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(0d)); + assertThat(docCountDeriv.value(), equalTo(1d)); bucket = buckets.get(3); assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(4l)); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(3l)); assertThat(bucket.getDocCount(), equalTo(0l)); docCountDeriv = bucket.getAggregations().get("deriv"); assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(0d)); + assertThat(docCountDeriv.value(), equalTo(-2d)); bucket = buckets.get(4); assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(4l)); + assertThat(bucket.getDocCount(), equalTo(2l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(2d)); + + bucket = buckets.get(5); + assertThat(bucket, notNullValue()); assertThat(((Number) bucket.getKey()).longValue(), equalTo(5l)); + assertThat(bucket.getDocCount(), equalTo(2l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(0d)); + + bucket = buckets.get(6); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(6l)); assertThat(bucket.getDocCount(), equalTo(0l)); docCountDeriv = bucket.getAggregations().get("deriv"); assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(0.25d)); + assertThat(docCountDeriv.value(), equalTo(-2d)); - bucket = buckets.get(5); + bucket = buckets.get(7); assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(9l)); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(7l)); assertThat(bucket.getDocCount(), equalTo(0l)); docCountDeriv = bucket.getAggregations().get("deriv"); assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(-1d)); + assertThat(docCountDeriv.value(), equalTo(0d)); - bucket = buckets.get(6); + bucket = buckets.get(8); assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(10l)); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(8l)); assertThat(bucket.getDocCount(), equalTo(0l)); docCountDeriv = bucket.getAggregations().get("deriv"); assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(0d)); + + bucket = buckets.get(9); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(9l)); + assertThat(bucket.getDocCount(), equalTo(3l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(3d)); + + bucket = buckets.get(10); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(10l)); + assertThat(bucket.getDocCount(), equalTo(2l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), equalTo(-1d)); + + bucket = buckets.get(11); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(11l)); + assertThat(bucket.getDocCount(), equalTo(1l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); assertThat(docCountDeriv.value(), equalTo(-1d)); } From 3131e01c9d0264a5168e12395db513398e2eb7fb Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Mon, 2 Mar 2015 17:27:02 -0500 Subject: [PATCH 31/68] Move GapPolicy and resolveBucketValues() to static helper methods Will allow many reducers to share the same helper functionality without repeating code. Chose to put these in static helpers instead of adding to Reducer base class. I can imagine other reducers that aren't time-based (or don't care about contiguous buckets), which would make things like gap policy useless. Since these seemed more like helpers than inherent traits of a Reducer, they went into their own static class. Closes #9954 --- .../aggregations/reducers/BucketHelpers.java | 160 ++++++++++++++++++ .../derivative/DerivativeBuilder.java | 3 +- .../reducers/derivative/DerivativeParser.java | 3 +- .../derivative/DerivativeReducer.java | 106 +----------- .../reducers/DerivativeTests.java | 3 +- 5 files changed, 171 insertions(+), 104 deletions(-) create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java new file mode 100644 index 0000000000000..145ff1dea1ffa --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java @@ -0,0 +1,160 @@ +package org.elasticsearch.search.aggregations.reducers; + +import org.elasticsearch.ElasticsearchIllegalStateException; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.search.SearchParseException; +import org.elasticsearch.search.aggregations.AggregationExecutionException; +import org.elasticsearch.search.aggregations.InvalidAggregationPathException; +import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; +import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; +import org.elasticsearch.search.aggregations.reducers.derivative.DerivativeParser; +import org.elasticsearch.search.aggregations.support.AggregationPath; +import org.elasticsearch.search.internal.SearchContext; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A set of static helpers to simplify working with aggregation buckets, in particular + * providing utilities that help reducers. + */ +public class BucketHelpers { + + /** + * A gap policy determines how "holes" in a set of buckets should be handled. For example, + * a date_histogram might have empty buckets due to no data existing for that time interval. + * This can cause problems for operations like a derivative, which relies on a continuous + * function. + * + * "insert_zeros": empty buckets will be filled with zeros for all metrics + * "ignore": empty buckets will simply be ignored + */ + public static enum GapPolicy { + INSERT_ZEROS((byte) 0, "insert_zeros"), IGNORE((byte) 1, "ignore"); + + /** + * Parse a string GapPolicy into the byte enum + * + * @param context SearchContext this is taking place in + * @param text GapPolicy in string format (e.g. "ignore") + * @return GapPolicy enum + */ + public static GapPolicy parse(SearchContext context, String text) { + GapPolicy result = null; + for (GapPolicy policy : values()) { + if (policy.parseField.match(text)) { + if (result == null) { + result = policy; + } else { + throw new ElasticsearchIllegalStateException("Text can be parsed to 2 different gap policies: text=[" + text + + "], " + "policies=" + Arrays.asList(result, policy)); + } + } + } + if (result == null) { + final List validNames = new ArrayList<>(); + for (GapPolicy policy : values()) { + validNames.add(policy.getName()); + } + throw new SearchParseException(context, "Invalid gap policy: [" + text + "], accepted values: " + validNames); + } + return result; + } + + private final byte id; + private final ParseField parseField; + + private GapPolicy(byte id, String name) { + this.id = id; + this.parseField = new ParseField(name); + } + + /** + * Serialize the GapPolicy to the output stream + * + * @param out + * @throws IOException + */ + public void writeTo(StreamOutput out) throws IOException { + out.writeByte(id); + } + + /** + * Deserialize the GapPolicy from the input stream + * + * @param in + * @return GapPolicy Enum + * @throws IOException + */ + public static GapPolicy readFrom(StreamInput in) throws IOException { + byte id = in.readByte(); + for (GapPolicy gapPolicy : values()) { + if (id == gapPolicy.id) { + return gapPolicy; + } + } + throw new IllegalStateException("Unknown GapPolicy with id [" + id + "]"); + } + + /** + * Return the english-formatted name of the GapPolicy + * + * @return English representation of GapPolicy + */ + public String getName() { + return parseField.getPreferredName(); + } + } + + /** + * Given a path and a set of buckets, this method will return the value inside the agg at + * that path. This is used to extract values for use by reducers (e.g. a derivative might need + * the price for each bucket). If the bucket is empty, the configured GapPolicy is invoked to + * resolve the missing bucket + * + * @param histo A series of agg buckets in the form of a histogram + * @param bucket A specific bucket that a value needs to be extracted from. This bucket should be present + * in the histo parameter + * @param aggPath The path to a particular value that needs to be extracted. This path should point to a metric + * inside the bucket + * @param gapPolicy The gap policy to apply if empty buckets are found + * @return The value extracted from bucket found at aggPath + */ + public static Double resolveBucketValue(InternalHistogram histo, InternalHistogram.Bucket bucket, + String aggPath, GapPolicy gapPolicy) { + try { + Object propertyValue = bucket.getProperty(histo.getName(), AggregationPath.parse(aggPath).getPathElementsAsStringList()); + if (propertyValue == null) { + throw new AggregationExecutionException(DerivativeParser.BUCKETS_PATH.getPreferredName() + + " must reference either a number value or a single value numeric metric aggregation"); + } else { + double value; + if (propertyValue instanceof Number) { + value = ((Number) propertyValue).doubleValue(); + } else if (propertyValue instanceof InternalNumericMetricsAggregation.SingleValue) { + value = ((InternalNumericMetricsAggregation.SingleValue) propertyValue).value(); + } else { + throw new AggregationExecutionException(DerivativeParser.BUCKETS_PATH.getPreferredName() + + " must reference either a number value or a single value numeric metric aggregation"); + } + if (Double.isInfinite(value) || Double.isNaN(value)) { + switch (gapPolicy) { + case INSERT_ZEROS: + return 0.0; + case IGNORE: + default: + return Double.NaN; + } + } else { + return value; + } + } + } catch (InvalidAggregationPathException e) { + return null; + } + } +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeBuilder.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeBuilder.java index f868e673b1d5f..210d56d4a6fea 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeBuilder.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeBuilder.java @@ -21,10 +21,11 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.search.aggregations.reducers.ReducerBuilder; -import org.elasticsearch.search.aggregations.reducers.derivative.DerivativeReducer.GapPolicy; import java.io.IOException; +import static org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; + public class DerivativeBuilder extends ReducerBuilder { private String format; diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java index 6b6b826ec6fcc..c4d3aa2a22978 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java @@ -24,7 +24,6 @@ import org.elasticsearch.search.SearchParseException; import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.reducers.ReducerFactory; -import org.elasticsearch.search.aggregations.reducers.derivative.DerivativeReducer.GapPolicy; import org.elasticsearch.search.aggregations.support.format.ValueFormat; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.internal.SearchContext; @@ -33,6 +32,8 @@ import java.util.ArrayList; import java.util.List; +import static org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; + public class DerivativeParser implements Reducer.Parser { public static final ParseField FORMAT = new ParseField("format"); diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java index c0d96f4056b10..1130639a1a2c6 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java @@ -22,38 +22,30 @@ import com.google.common.base.Function; import com.google.common.collect.Lists; -import org.elasticsearch.ElasticsearchIllegalStateException; + import org.elasticsearch.common.Nullable; -import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.search.SearchParseException; import org.elasticsearch.search.aggregations.Aggregation; -import org.elasticsearch.search.aggregations.AggregationExecutionException; import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; import org.elasticsearch.search.aggregations.InternalAggregation.Type; import org.elasticsearch.search.aggregations.InternalAggregations; -import org.elasticsearch.search.aggregations.InvalidAggregationPathException; import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; -import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; -import org.elasticsearch.search.aggregations.reducers.InternalSimpleValue; -import org.elasticsearch.search.aggregations.reducers.Reducer; -import org.elasticsearch.search.aggregations.reducers.ReducerFactory; -import org.elasticsearch.search.aggregations.reducers.ReducerStreams; +import org.elasticsearch.search.aggregations.reducers.*; import org.elasticsearch.search.aggregations.support.AggregationContext; -import org.elasticsearch.search.aggregations.support.AggregationPath; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; -import org.elasticsearch.search.internal.SearchContext; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; +import static org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; +import static org.elasticsearch.search.aggregations.reducers.BucketHelpers.resolveBucketValue; + public class DerivativeReducer extends Reducer { public final static Type TYPE = new Type("derivative"); @@ -105,7 +97,7 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext List newBuckets = new ArrayList<>(); Double lastBucketValue = null; for (InternalHistogram.Bucket bucket : buckets) { - Double thisBucketValue = resolveBucketValue(histo, bucket); + Double thisBucketValue = resolveBucketValue(histo, bucket, bucketsPaths()[0], gapPolicy); if (lastBucketValue != null) { double diff = thisBucketValue - lastBucketValue; @@ -122,40 +114,6 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext return factory.create(histo.getName(), newBuckets, histo); } - private Double resolveBucketValue(InternalHistogram histo, InternalHistogram.Bucket bucket) { - try { - Object propertyValue = bucket.getProperty(histo.getName(), AggregationPath.parse(bucketsPaths()[0]) - .getPathElementsAsStringList()); - if (propertyValue == null) { - throw new AggregationExecutionException(DerivativeParser.BUCKETS_PATH.getPreferredName() - + " must reference either a number value or a single value numeric metric aggregation"); - } else { - double value; - if (propertyValue instanceof Number) { - value = ((Number) propertyValue).doubleValue(); - } else if (propertyValue instanceof InternalNumericMetricsAggregation.SingleValue) { - value = ((InternalNumericMetricsAggregation.SingleValue) propertyValue).value(); - } else { - throw new AggregationExecutionException(DerivativeParser.BUCKETS_PATH.getPreferredName() - + " must reference either a number value or a single value numeric metric aggregation"); - } - if (Double.isInfinite(value) || Double.isNaN(value)) { - switch (gapPolicy) { - case INSERT_ZEROS: - return 0.0; - case IGNORE: - default: - return Double.NaN; - } - } else { - return value; - } - } - } catch (InvalidAggregationPathException e) { - return null; - } - } - @Override public void doReadFrom(StreamInput in) throws IOException { formatter = ValueFormatterStreams.readOptional(in); @@ -186,56 +144,4 @@ protected Reducer createInternal(AggregationContext context, Aggregator parent, } } - - public static enum GapPolicy { - INSERT_ZEROS((byte) 0, "insert_zeros"), IGNORE((byte) 1, "ignore"); - - public static GapPolicy parse(SearchContext context, String text) { - GapPolicy result = null; - for (GapPolicy policy : values()) { - if (policy.parseField.match(text)) { - if (result == null) { - result = policy; - } else { - throw new ElasticsearchIllegalStateException("Text can be parsed to 2 different gap policies: text=[" + text - + "], " + "policies=" + Arrays.asList(result, policy)); - } - } - } - if (result == null) { - final List validNames = new ArrayList<>(); - for (GapPolicy policy : values()) { - validNames.add(policy.getName()); - } - throw new SearchParseException(context, "Invalid gap policy: [" + text + "], accepted values: " + validNames); - } - return result; - } - - private final byte id; - private final ParseField parseField; - - private GapPolicy(byte id, String name) { - this.id = id; - this.parseField = new ParseField(name); - } - - public void writeTo(StreamOutput out) throws IOException { - out.writeByte(id); - } - - public static GapPolicy readFrom(StreamInput in) throws IOException { - byte id = in.readByte(); - for (GapPolicy gapPolicy : values()) { - if (id == gapPolicy.id) { - return gapPolicy; - } - } - throw new IllegalStateException("Unknown GapPolicy with id [" + id + "]"); - } - - public String getName() { - return parseField.getPreferredName(); - } - } } diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java index 7d2d5500cd173..24a4c8cff5a9a 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java @@ -25,7 +25,6 @@ import org.elasticsearch.search.aggregations.bucket.histogram.Histogram.Bucket; import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; import org.elasticsearch.search.aggregations.metrics.sum.Sum; -import org.elasticsearch.search.aggregations.reducers.derivative.DerivativeReducer.GapPolicy; import org.elasticsearch.search.aggregations.support.AggregationPath; import org.elasticsearch.test.ElasticsearchIntegrationTest; import org.hamcrest.Matchers; @@ -509,7 +508,7 @@ public void singleValuedFieldWithGaps_insertZeros() throws Exception { .setQuery(matchAllQuery()) .addAggregation( histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(1).minDocCount(0) - .subAggregation(derivative("deriv").setBucketsPaths("_count").gapPolicy(GapPolicy.INSERT_ZEROS))) + .subAggregation(derivative("deriv").setBucketsPaths("_count").gapPolicy(BucketHelpers.GapPolicy.INSERT_ZEROS))) .execute().actionGet(); assertThat(searchResponse.getHits().getTotalHits(), equalTo(14l)); From 8e02a8565de162ee2a6df7df86e0d7efa16799a1 Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Thu, 5 Mar 2015 09:58:22 -0500 Subject: [PATCH 32/68] Add header to BucketHelpers class --- .../aggregations/reducers/BucketHelpers.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java index 145ff1dea1ffa..f92a2b70d3b20 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java @@ -1,3 +1,22 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package org.elasticsearch.search.aggregations.reducers; import org.elasticsearch.ElasticsearchIllegalStateException; From 3063f06fc7506a9be7331553bf77614d9ca2dd35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Tue, 3 Mar 2015 12:54:12 +0100 Subject: [PATCH 33/68] Add randomiziation to test for derivative aggregation --- .../reducers/DerivativeTests.java | 551 +++++------------- 1 file changed, 160 insertions(+), 391 deletions(-) diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java index 24a4c8cff5a9a..a5c9506aeac68 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java @@ -21,16 +21,20 @@ import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; -import org.elasticsearch.search.aggregations.bucket.histogram.Histogram.Bucket; import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; +import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram.Bucket; import org.elasticsearch.search.aggregations.metrics.sum.Sum; +import org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; import org.elasticsearch.search.aggregations.support.AggregationPath; import org.elasticsearch.test.ElasticsearchIntegrationTest; import org.hamcrest.Matchers; import org.junit.Test; +import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; @@ -41,7 +45,6 @@ import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; import static org.hamcrest.core.IsNull.notNullValue; import static org.hamcrest.core.IsNull.nullValue; @@ -49,49 +52,44 @@ public class DerivativeTests extends ElasticsearchIntegrationTest { private static final String SINGLE_VALUED_FIELD_NAME = "l_value"; - private static final String MULTI_VALUED_FIELD_NAME = "l_values"; - static int numDocs; - static int interval; - static int numValueBuckets, numValuesBuckets; - static int numFirstDerivValueBuckets, numFirstDerivValuesBuckets; - static int numSecondDerivValueBuckets; - static long[] valueCounts, valuesCounts; - static long[] firstDerivValueCounts, firstDerivValuesCounts; - static long[] secondDerivValueCounts; + private static int interval; + private static int numValueBuckets; + private static int numFirstDerivValueBuckets; + private static int numSecondDerivValueBuckets; + private static long[] valueCounts; + private static long[] firstDerivValueCounts; + private static long[] secondDerivValueCounts; + + private static Long[] valueCounts_empty; + private static long numDocsEmptyIdx; + private static Double[] firstDerivValueCounts_empty; + + // expected bucket values for random setup with gaps + private static int numBuckets_empty_rnd; + private static Long[] valueCounts_empty_rnd; + private static Double[] firstDerivValueCounts_empty_rnd; + private static long numDocsEmptyIdx_rnd; @Override public void setupSuiteScopeCluster() throws Exception { createIndex("idx"); createIndex("idx_unmapped"); - numDocs = randomIntBetween(6, 20); - interval = randomIntBetween(2, 5); + interval = 5; + numValueBuckets = randomIntBetween(6, 80); - numValueBuckets = numDocs / interval + 1; valueCounts = new long[numValueBuckets]; - for (int i = 0; i < numDocs; i++) { - final int bucket = (i + 1) / interval; - valueCounts[bucket]++; - } - - numValuesBuckets = (numDocs + 1) / interval + 1; - valuesCounts = new long[numValuesBuckets]; - for (int i = 0; i < numDocs; i++) { - final int bucket1 = (i + 1) / interval; - final int bucket2 = (i + 2) / interval; - valuesCounts[bucket1]++; - if (bucket1 != bucket2) { - valuesCounts[bucket2]++; - } + for (int i = 0; i < numValueBuckets; i++) { + valueCounts[i] = randomIntBetween(1, 20); } numFirstDerivValueBuckets = numValueBuckets - 1; firstDerivValueCounts = new long[numFirstDerivValueBuckets]; - long lastValueCount = -1; + Long lastValueCount = null; for (int i = 0; i < numValueBuckets; i++) { long thisValue = valueCounts[i]; - if (lastValueCount != -1) { + if (lastValueCount != null) { long diff = thisValue - lastValueCount; firstDerivValueCounts[i - 1] = diff; } @@ -100,112 +98,69 @@ public void setupSuiteScopeCluster() throws Exception { numSecondDerivValueBuckets = numFirstDerivValueBuckets - 1; secondDerivValueCounts = new long[numSecondDerivValueBuckets]; - long lastFirstDerivativeValueCount = -1; + Long lastFirstDerivativeValueCount = null; for (int i = 0; i < numFirstDerivValueBuckets; i++) { long thisFirstDerivativeValue = firstDerivValueCounts[i]; - if (lastFirstDerivativeValueCount != -1) { + if (lastFirstDerivativeValueCount != null) { long diff = thisFirstDerivativeValue - lastFirstDerivativeValueCount; secondDerivValueCounts[i - 1] = diff; } lastFirstDerivativeValueCount = thisFirstDerivativeValue; } - numFirstDerivValuesBuckets = numValuesBuckets - 1; - firstDerivValuesCounts = new long[numFirstDerivValuesBuckets]; - long lastValuesCount = -1; - for (int i = 0; i < numValuesBuckets; i++) { - long thisValue = valuesCounts[i]; - if (lastValuesCount != -1) { - long diff = thisValue - lastValuesCount; - firstDerivValuesCounts[i - 1] = diff; + List builders = new ArrayList<>(); + for (int i = 0; i < numValueBuckets; i++) { + for (int docs = 0; docs < valueCounts[i]; docs++) { + builders.add(client().prepareIndex("idx", "type").setSource(newDocBuilder(i * interval))); } - lastValuesCount = thisValue; } - List builders = new ArrayList<>(); + // setup for index with empty buckets + valueCounts_empty = new Long[] { 1l, 1l, 2l, 0l, 2l, 2l, 0l, 0l, 0l, 3l, 2l, 1l }; + firstDerivValueCounts_empty = new Double[] { null, 0d, 1d, -2d, 2d, 0d, -2d, 0d, 0d, 3d, -1d, -1d }; - for (int i = 0; i < numDocs; i++) { - builders.add(client().prepareIndex("idx", "type").setSource( - jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, i + 1).startArray(MULTI_VALUED_FIELD_NAME).value(i + 1) - .value(i + 2).endArray().field("tag", "tag" + i).endObject())); + assertAcked(prepareCreate("empty_bucket_idx").addMapping("type", SINGLE_VALUED_FIELD_NAME, "type=integer")); + for (int i = 0; i < valueCounts_empty.length; i++) { + for (int docs = 0; docs < valueCounts_empty[i]; docs++) { + builders.add(client().prepareIndex("empty_bucket_idx", "type").setSource(newDocBuilder(i))); + numDocsEmptyIdx++; + } } - assertAcked(prepareCreate("empty_bucket_idx").addMapping("type", SINGLE_VALUED_FIELD_NAME, "type=integer")); - builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 0).setSource( - jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 0).endObject())); - - builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 1).setSource( - jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 1).endObject())); - - builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 2).setSource( - jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 2).endObject())); - builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 3).setSource( - jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 2).endObject())); - - builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 4).setSource( - jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 4).endObject())); - builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 5).setSource( - jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 4).endObject())); - - builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 6).setSource( - jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 5).endObject())); - builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 7).setSource( - jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 5).endObject())); - - builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 8).setSource( - jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 9).endObject())); - builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 9).setSource( - jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 9).endObject())); - builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 10).setSource( - jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 9).endObject())); - - builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 11).setSource( - jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 10).endObject())); - builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 12).setSource( - jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 10).endObject())); - - builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 13).setSource( - jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 11).endObject())); + // randomized setup for index with empty buckets + numBuckets_empty_rnd = randomIntBetween(20, 100); + valueCounts_empty_rnd = new Long[numBuckets_empty_rnd]; + firstDerivValueCounts_empty_rnd = new Double[numBuckets_empty_rnd]; + firstDerivValueCounts_empty_rnd[0] = null; + + assertAcked(prepareCreate("empty_bucket_idx_rnd").addMapping("type", SINGLE_VALUED_FIELD_NAME, "type=integer")); + for (int i = 0; i < numBuckets_empty_rnd; i++) { + valueCounts_empty_rnd[i] = (long) randomIntBetween(1, 10); + // make approximately half of the buckets empty + if (randomBoolean()) + valueCounts_empty_rnd[i] = 0l; + for (int docs = 0; docs < valueCounts_empty_rnd[i]; docs++) { + builders.add(client().prepareIndex("empty_bucket_idx_rnd", "type").setSource(newDocBuilder(i))); + numDocsEmptyIdx_rnd++; + } + if (i > 0) { + firstDerivValueCounts_empty_rnd[i] = (double) valueCounts_empty_rnd[i] - valueCounts_empty_rnd[i - 1]; + } + } indexRandom(true, builders); ensureSearchable(); } - @Test - public void singleValuedField() { - - SearchResponse response = client().prepareSearch("idx") - .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) - .subAggregation(derivative("deriv").setBucketsPaths("_count"))) - .execute().actionGet(); - - assertSearchResponse(response); - - InternalHistogram deriv = response.getAggregations().get("histo"); - assertThat(deriv, notNullValue()); - assertThat(deriv.getName(), equalTo("histo")); - List buckets = deriv.getBuckets(); - assertThat(buckets.size(), equalTo(numValueBuckets)); - - for (int i = 0; i < numValueBuckets; ++i) { - Histogram.Bucket bucket = buckets.get(i); - assertThat(bucket, notNullValue()); - assertThat(bucket.getKeyAsString(), equalTo(String.valueOf(i * interval))); - assertThat(((Number) bucket.getKey()).longValue(), equalTo((long) i * interval)); - assertThat(bucket.getDocCount(), equalTo(valueCounts[i])); - SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); - if (i > 0) { - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo((double) firstDerivValueCounts[i - 1])); - } else { - assertThat(docCountDeriv, nullValue()); - } - } + private XContentBuilder newDocBuilder(int singleValueFieldValue) throws IOException { + return jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, singleValueFieldValue).endObject(); } + /** + * test first and second derivative on the sing + */ @Test - public void singleValuedField_secondDerivative() { + public void singleValuedField() { SearchResponse response = client() .prepareSearch("idx") @@ -216,7 +171,7 @@ public void singleValuedField_secondDerivative() { assertSearchResponse(response); - InternalHistogram deriv = response.getAggregations().get("histo"); + InternalHistogram deriv = response.getAggregations().get("histo"); assertThat(deriv, notNullValue()); assertThat(deriv.getName(), equalTo("histo")); List buckets = deriv.getBuckets(); @@ -224,10 +179,7 @@ public void singleValuedField_secondDerivative() { for (int i = 0; i < numValueBuckets; ++i) { Histogram.Bucket bucket = buckets.get(i); - assertThat(bucket, notNullValue()); - assertThat(bucket.getKeyAsString(), equalTo(String.valueOf(i * interval))); - assertThat(((Number) bucket.getKey()).longValue(), equalTo((long) i * interval)); - assertThat(bucket.getDocCount(), equalTo(valueCounts[i])); + checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, valueCounts[i]); SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); if (i > 0) { assertThat(docCountDeriv, notNullValue()); @@ -256,7 +208,7 @@ public void singleValuedField_WithSubAggregation() throws Exception { assertSearchResponse(response); - InternalHistogram deriv = response.getAggregations().get("histo"); + InternalHistogram deriv = response.getAggregations().get("histo"); assertThat(deriv, notNullValue()); assertThat(deriv.getName(), equalTo("histo")); assertThat(deriv.getBuckets().size(), equalTo(numValueBuckets)); @@ -264,92 +216,44 @@ public void singleValuedField_WithSubAggregation() throws Exception { Object[] propertiesDocCounts = (Object[]) deriv.getProperty("_count"); Object[] propertiesSumCounts = (Object[]) deriv.getProperty("sum.value"); - List buckets = new ArrayList<>(deriv.getBuckets()); + List buckets = new ArrayList(deriv.getBuckets()); + Long expectedSumPreviousBucket = Long.MIN_VALUE; // start value, gets + // overwritten for (int i = 0; i < numValueBuckets; ++i) { Histogram.Bucket bucket = buckets.get(i); - assertThat(bucket, notNullValue()); - assertThat(bucket.getKeyAsString(), equalTo(String.valueOf(i * interval))); - assertThat(((Number) bucket.getKey()).longValue(), equalTo((long) i * interval)); - assertThat(bucket.getDocCount(), equalTo(valueCounts[i])); - assertThat(bucket.getAggregations().asList().isEmpty(), is(false)); + checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, valueCounts[i]); Sum sum = bucket.getAggregations().get("sum"); assertThat(sum, notNullValue()); - long s = 0; - for (int j = 0; j < numDocs; ++j) { - if ((j + 1) / interval == i) { - s += j + 1; - } - } + long expectedSum = valueCounts[i] * (i * interval); + assertThat(sum.getValue(), equalTo((double) expectedSum)); SimpleValue sumDeriv = bucket.getAggregations().get("deriv"); - assertThat(sum.getValue(), equalTo((double) s)); if (i > 0) { assertThat(sumDeriv, notNullValue()); - long s1 = 0; - long s2 = 0; - for (int j = 0; j < numDocs; ++j) { - if ((j + 1) / interval == i - 1) { - s1 += j + 1; - } - if ((j + 1) / interval == i) { - s2 += j + 1; - } - } - long sumDerivValue = s2 - s1; + long sumDerivValue = expectedSum - expectedSumPreviousBucket; assertThat(sumDeriv.value(), equalTo((double) sumDerivValue)); assertThat((double) bucket.getProperty("histo", AggregationPath.parse("deriv.value").getPathElementsAsStringList()), equalTo((double) sumDerivValue)); } else { assertThat(sumDeriv, nullValue()); } + expectedSumPreviousBucket = expectedSum; assertThat((long) propertiesKeys[i], equalTo((long) i * interval)); assertThat((long) propertiesDocCounts[i], equalTo(valueCounts[i])); - assertThat((double) propertiesSumCounts[i], equalTo((double) s)); - } - } - - @Test - public void multiValuedField() throws Exception { - SearchResponse response = client().prepareSearch("idx") - .addAggregation( - histogram("histo").field(MULTI_VALUED_FIELD_NAME).interval(interval) - .subAggregation(derivative("deriv").setBucketsPaths("_count"))) - .execute().actionGet(); - - assertSearchResponse(response); - - InternalHistogram deriv = response.getAggregations().get("histo"); - assertThat(deriv, notNullValue()); - assertThat(deriv.getName(), equalTo("histo")); - List buckets = deriv.getBuckets(); - assertThat(deriv.getBuckets().size(), equalTo(numValuesBuckets)); - - for (int i = 0; i < numValuesBuckets; ++i) { - Histogram.Bucket bucket = buckets.get(i); - assertThat(bucket, notNullValue()); - assertThat(bucket.getKeyAsString(), equalTo(String.valueOf(i * interval))); - assertThat(((Number) bucket.getKey()).longValue(), equalTo((long) i * interval)); - assertThat(bucket.getDocCount(), equalTo(valuesCounts[i])); - SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); - if (i > 0) { - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo((double) firstDerivValuesCounts[i - 1])); - } else { - assertThat(docCountDeriv, nullValue()); - } + assertThat((double) propertiesSumCounts[i], equalTo((double) expectedSum)); } } @Test public void unmapped() throws Exception { - SearchResponse response = client().prepareSearch("idx_unmapped") + SearchResponse response = client() + .prepareSearch("idx_unmapped") .addAggregation( histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) - .subAggregation(derivative("deriv").setBucketsPaths("_count"))) - .execute().actionGet(); + .subAggregation(derivative("deriv").setBucketsPaths("_count"))).execute().actionGet(); assertSearchResponse(response); - InternalHistogram deriv = response.getAggregations().get("histo"); + InternalHistogram deriv = response.getAggregations().get("histo"); assertThat(deriv, notNullValue()); assertThat(deriv.getName(), equalTo("histo")); assertThat(deriv.getBuckets().size(), equalTo(0)); @@ -357,15 +261,15 @@ public void unmapped() throws Exception { @Test public void partiallyUnmapped() throws Exception { - SearchResponse response = client().prepareSearch("idx", "idx_unmapped") + SearchResponse response = client() + .prepareSearch("idx", "idx_unmapped") .addAggregation( histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) - .subAggregation(derivative("deriv").setBucketsPaths("_count"))) - .execute().actionGet(); + .subAggregation(derivative("deriv").setBucketsPaths("_count"))).execute().actionGet(); assertSearchResponse(response); - InternalHistogram deriv = response.getAggregations().get("histo"); + InternalHistogram deriv = response.getAggregations().get("histo"); assertThat(deriv, notNullValue()); assertThat(deriv.getName(), equalTo("histo")); List buckets = deriv.getBuckets(); @@ -373,10 +277,7 @@ public void partiallyUnmapped() throws Exception { for (int i = 0; i < numValueBuckets; ++i) { Histogram.Bucket bucket = buckets.get(i); - assertThat(bucket, notNullValue()); - assertThat(bucket.getKeyAsString(), equalTo(String.valueOf(i * interval))); - assertThat(((Number) bucket.getKey()).longValue(), equalTo((long) i * interval)); - assertThat(bucket.getDocCount(), equalTo(valueCounts[i])); + checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, valueCounts[i]); SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); if (i > 0) { assertThat(docCountDeriv, notNullValue()); @@ -394,111 +295,57 @@ public void singleValuedFieldWithGaps() throws Exception { .setQuery(matchAllQuery()) .addAggregation( histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(1).minDocCount(0) - .subAggregation(derivative("deriv").setBucketsPaths("_count"))) - .execute().actionGet(); + .subAggregation(derivative("deriv").setBucketsPaths("_count"))).execute().actionGet(); - assertThat(searchResponse.getHits().getTotalHits(), equalTo(14l)); + assertThat(searchResponse.getHits().getTotalHits(), equalTo(numDocsEmptyIdx)); - InternalHistogram deriv = searchResponse.getAggregations().get("histo"); + InternalHistogram deriv = searchResponse.getAggregations().get("histo"); assertThat(deriv, Matchers.notNullValue()); assertThat(deriv.getName(), equalTo("histo")); - List buckets = (List) deriv.getBuckets(); - assertThat(buckets.size(), equalTo(12)); - - Histogram.Bucket bucket = buckets.get(0); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(0l)); - assertThat(bucket.getDocCount(), equalTo(1l)); - SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, nullValue()); - - bucket = buckets.get(1); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(1l)); - assertThat(bucket.getDocCount(), equalTo(1l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(0d)); - - bucket = buckets.get(2); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(2l)); - assertThat(bucket.getDocCount(), equalTo(2l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(1d)); - - bucket = buckets.get(3); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(3l)); - assertThat(bucket.getDocCount(), equalTo(0l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(-2d)); - - bucket = buckets.get(4); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(4l)); - assertThat(bucket.getDocCount(), equalTo(2l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(2d)); - - bucket = buckets.get(5); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(5l)); - assertThat(bucket.getDocCount(), equalTo(2l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(0d)); - - bucket = buckets.get(6); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(6l)); - assertThat(bucket.getDocCount(), equalTo(0l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(-2d)); - - bucket = buckets.get(7); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(7l)); - assertThat(bucket.getDocCount(), equalTo(0l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(0d)); - - bucket = buckets.get(8); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(8l)); - assertThat(bucket.getDocCount(), equalTo(0l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(0d)); - - bucket = buckets.get(9); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(9l)); - assertThat(bucket.getDocCount(), equalTo(3l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(3d)); - - bucket = buckets.get(10); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(10l)); - assertThat(bucket.getDocCount(), equalTo(2l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(-1d)); - - bucket = buckets.get(11); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(11l)); - assertThat(bucket.getDocCount(), equalTo(1l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(-1d)); + List buckets = deriv.getBuckets(); + assertThat(buckets.size(), equalTo(valueCounts_empty.length)); + + for (int i = 0; i < valueCounts_empty.length; i++) { + Histogram.Bucket bucket = buckets.get(i); + checkBucketKeyAndDocCount("Bucket " + i, bucket, i, valueCounts_empty[i]); + SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); + if (firstDerivValueCounts_empty[i] == null) { + assertThat(docCountDeriv, nullValue()); + } else { + assertThat(docCountDeriv.value(), equalTo(firstDerivValueCounts_empty[i])); + } + } + } + + @Test + public void singleValuedFieldWithGaps_random() throws Exception { + SearchResponse searchResponse = client() + .prepareSearch("empty_bucket_idx_rnd") + .setQuery(matchAllQuery()) + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(1).minDocCount(0) + .extendedBounds(0l, (long) numBuckets_empty_rnd - 1) + .subAggregation(derivative("deriv").setBucketsPaths("_count"))).execute().actionGet(); + + assertThat(searchResponse.getHits().getTotalHits(), equalTo(numDocsEmptyIdx_rnd)); + + InternalHistogram deriv = searchResponse.getAggregations().get("histo"); + assertThat(deriv, Matchers.notNullValue()); + assertThat(deriv.getName(), equalTo("histo")); + List buckets = deriv.getBuckets(); + assertThat(buckets.size(), equalTo(numBuckets_empty_rnd)); + + for (int i = 0; i < valueCounts_empty_rnd.length; i++) { + Histogram.Bucket bucket = buckets.get(i); + System.out.println(bucket.getDocCount()); + checkBucketKeyAndDocCount("Bucket " + i, bucket, i, valueCounts_empty_rnd[i]); + SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); + if (firstDerivValueCounts_empty_rnd[i] == null) { + assertThat(docCountDeriv, nullValue()); + } else { + assertThat(docCountDeriv.value(), equalTo(firstDerivValueCounts_empty_rnd[i])); + } + } } @Test @@ -508,111 +355,33 @@ public void singleValuedFieldWithGaps_insertZeros() throws Exception { .setQuery(matchAllQuery()) .addAggregation( histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(1).minDocCount(0) - .subAggregation(derivative("deriv").setBucketsPaths("_count").gapPolicy(BucketHelpers.GapPolicy.INSERT_ZEROS))) - .execute().actionGet(); + .subAggregation(derivative("deriv").setBucketsPaths("_count").gapPolicy(GapPolicy.INSERT_ZEROS))).execute() + .actionGet(); - assertThat(searchResponse.getHits().getTotalHits(), equalTo(14l)); + assertThat(searchResponse.getHits().getTotalHits(), equalTo(numDocsEmptyIdx)); - InternalHistogram deriv = searchResponse.getAggregations().get("histo"); + InternalHistogram deriv = searchResponse.getAggregations().get("histo"); assertThat(deriv, Matchers.notNullValue()); assertThat(deriv.getName(), equalTo("histo")); - List buckets = (List) deriv.getBuckets(); - assertThat(buckets.size(), equalTo(12)); - - Histogram.Bucket bucket = buckets.get(0); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(0l)); - assertThat(bucket.getDocCount(), equalTo(1l)); - SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, nullValue()); - - bucket = buckets.get(1); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(1l)); - assertThat(bucket.getDocCount(), equalTo(1l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(0d)); - - bucket = buckets.get(2); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(2l)); - assertThat(bucket.getDocCount(), equalTo(2l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(1d)); - - bucket = buckets.get(3); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(3l)); - assertThat(bucket.getDocCount(), equalTo(0l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(-2d)); - - bucket = buckets.get(4); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(4l)); - assertThat(bucket.getDocCount(), equalTo(2l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(2d)); - - bucket = buckets.get(5); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(5l)); - assertThat(bucket.getDocCount(), equalTo(2l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(0d)); - - bucket = buckets.get(6); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(6l)); - assertThat(bucket.getDocCount(), equalTo(0l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(-2d)); - - bucket = buckets.get(7); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(7l)); - assertThat(bucket.getDocCount(), equalTo(0l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(0d)); - - bucket = buckets.get(8); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(8l)); - assertThat(bucket.getDocCount(), equalTo(0l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(0d)); - - bucket = buckets.get(9); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(9l)); - assertThat(bucket.getDocCount(), equalTo(3l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(3d)); - - bucket = buckets.get(10); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(10l)); - assertThat(bucket.getDocCount(), equalTo(2l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(-1d)); - - bucket = buckets.get(11); - assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(11l)); - assertThat(bucket.getDocCount(), equalTo(1l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(-1d)); + List buckets = deriv.getBuckets(); + assertThat(buckets.size(), equalTo(valueCounts_empty.length)); + + for (int i = 0; i < valueCounts_empty.length; i++) { + Histogram.Bucket bucket = buckets.get(i); + checkBucketKeyAndDocCount("Bucket " + i + ": ", bucket, i, valueCounts_empty[i]); + SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); + if (firstDerivValueCounts_empty[i] == null) { + assertThat(docCountDeriv, nullValue()); + } else { + assertThat(docCountDeriv.value(), equalTo(firstDerivValueCounts_empty[i])); + } + } } + private void checkBucketKeyAndDocCount(final String msg, final Histogram.Bucket bucket, final long expectedKey, + final long expectedDocCount) { + assertThat(msg, bucket, notNullValue()); + assertThat(msg + " key", ((Number) bucket.getKey()).longValue(), equalTo(expectedKey)); + assertThat(msg + " docCount", bucket.getDocCount(), equalTo(expectedDocCount)); + } } From 02679e7c4364fe51b15823fc8017657b5117e15d Mon Sep 17 00:00:00 2001 From: Simon Willnauer Date: Mon, 16 Mar 2015 22:59:26 -0700 Subject: [PATCH 34/68] [BUILD] fix snapshot URL --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 954f4d897e311..30fd9468946cf 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ lucene-snapshots Lucene Snapshots - https://download.elasticsearch.org/lucenesnapshots/1662607 + https://download.elastic.co/lucenesnapshots/1662607 From cb4ab060214aa8f6beff8e107e65d1a691176530 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Wed, 18 Mar 2015 14:14:00 -0700 Subject: [PATCH 35/68] missed file in merge --- .../bucket/significant/SignificanceHeuristicTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificanceHeuristicTests.java b/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificanceHeuristicTests.java index c3fe8b9407119..5b669cb917551 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificanceHeuristicTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificanceHeuristicTests.java @@ -35,6 +35,7 @@ import org.elasticsearch.search.aggregations.bucket.significant.heuristics.JLHScore; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.MutualInformation; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.PercentageScore; +import org.elasticsearch.search.aggregations.bucket.significant.heuristics.ScriptHeuristic; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristic; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristicBuilder; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.SignificanceHeuristicParser; From b751f0e11bacedd4d684c5cf826bbc64dc314722 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Mon, 23 Mar 2015 08:58:44 +0000 Subject: [PATCH 36/68] added validation of reducers --- .../aggregations/AggregatorFactories.java | 4 ++- .../aggregations/AggregatorFactory.java | 4 +++ .../bucket/histogram/HistogramAggregator.java | 4 +++ .../aggregations/reducers/ReducerFactory.java | 15 +++++++--- .../derivative/DerivativeReducer.java | 29 +++++++++++++++++-- .../reducers/DateDerivativeTests.java | 10 +++---- .../reducers/DerivativeTests.java | 9 +++--- 7 files changed, 57 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java index 552ff49fe1dc0..1a4c157da8e7b 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java @@ -40,6 +40,7 @@ public class AggregatorFactories { public static final AggregatorFactories EMPTY = new Empty(); + private AggregatorFactory parent; private AggregatorFactory[] factories; private List reducerFactories; @@ -101,6 +102,7 @@ public int count() { } void setParent(AggregatorFactory parent) { + this.parent = parent; for (AggregatorFactory factory : factories) { factory.parent = parent; } @@ -111,7 +113,7 @@ public void validate() { factory.validate(); } for (ReducerFactory factory : reducerFactories) { - factory.validate(); + factory.validate(parent, factories, reducerFactories); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactory.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactory.java index 41aee8f931f3e..f69e54ee710f5 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactory.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactory.java @@ -66,6 +66,10 @@ public AggregatorFactory subFactories(AggregatorFactories subFactories) { return this; } + public String name() { + return name; + } + /** * Validates the state of this factory (makes sure the factory is properly configured) */ diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/HistogramAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/HistogramAggregator.java index 0a6a8bce73296..63325c12aad8a 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/HistogramAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/HistogramAggregator.java @@ -169,6 +169,10 @@ public Factory(String name, ValuesSourceConfig config, this.histogramFactory = histogramFactory; } + public long minDocCount() { + return minDocCount; + } + @Override protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, List reducers, Map metaData) throws IOException { diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java index 05cb6fbed48bf..ccdd2ac0328af 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java @@ -19,9 +19,11 @@ package org.elasticsearch.search.aggregations.reducers; import org.elasticsearch.search.aggregations.Aggregator; +import org.elasticsearch.search.aggregations.AggregatorFactory; import org.elasticsearch.search.aggregations.support.AggregationContext; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -49,10 +51,15 @@ public ReducerFactory(String name, String type, String[] bucketsPaths) { } /** - * Validates the state of this factory (makes sure the factory is properly configured) + * Validates the state of this factory (makes sure the factory is properly + * configured) + * + * @param reducerFactories + * @param factories + * @param parent */ - public final void validate() { - doValidate(); + public final void validate(AggregatorFactory parent, AggregatorFactory[] factories, List reducerFactories) { + doValidate(parent, factories, reducerFactories); } protected abstract Reducer createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, @@ -79,7 +86,7 @@ public final Reducer create(AggregationContext context, Aggregator parent, boole return aggregator; } - public void doValidate() { + public void doValidate(AggregatorFactory parent, AggregatorFactory[] factories, List reducerFactories) { } public void setMetaData(Map metaData) { diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java index 1130639a1a2c6..5f40ab2906ee0 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java @@ -22,18 +22,24 @@ import com.google.common.base.Function; import com.google.common.collect.Lists; - +import org.elasticsearch.ElasticsearchIllegalStateException; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.search.aggregations.Aggregation; import org.elasticsearch.search.aggregations.Aggregator; +import org.elasticsearch.search.aggregations.AggregatorFactory; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; import org.elasticsearch.search.aggregations.InternalAggregation.Type; import org.elasticsearch.search.aggregations.InternalAggregations; +import org.elasticsearch.search.aggregations.bucket.histogram.HistogramAggregator; import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; -import org.elasticsearch.search.aggregations.reducers.*; +import org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; +import org.elasticsearch.search.aggregations.reducers.InternalSimpleValue; +import org.elasticsearch.search.aggregations.reducers.Reducer; +import org.elasticsearch.search.aggregations.reducers.ReducerFactory; +import org.elasticsearch.search.aggregations.reducers.ReducerStreams; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; @@ -43,7 +49,6 @@ import java.util.List; import java.util.Map; -import static org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; import static org.elasticsearch.search.aggregations.reducers.BucketHelpers.resolveBucketValue; public class DerivativeReducer extends Reducer { @@ -143,5 +148,23 @@ protected Reducer createInternal(AggregationContext context, Aggregator parent, return new DerivativeReducer(name, bucketsPaths, formatter, gapPolicy, metaData); } + @Override + public void doValidate(AggregatorFactory parent, AggregatorFactory[] aggFactories, List reducerFactories) { + if (bucketsPaths.length != 1) { + throw new ElasticsearchIllegalStateException(Reducer.Parser.BUCKETS_PATH.getPreferredName() + + " must contain a single entry for reducer [" + name + "]"); + } + if (!(parent instanceof HistogramAggregator.Factory)) { + throw new ElasticsearchIllegalStateException("derivative reducer [" + name + + "] must have a histogram or date_histogram as parent"); + } else { + HistogramAggregator.Factory histoParent = (HistogramAggregator.Factory) parent; + if (histoParent.minDocCount() != 0) { + throw new ElasticsearchIllegalStateException("parent histogram of derivative reducer [" + name + + "] must have min_doc_count of 0"); + } + } + } + } } diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java index ad1c131c885a1..ede94abd97336 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java @@ -109,7 +109,7 @@ public void singleValuedField() throws Exception { SearchResponse response = client() .prepareSearch("idx") .addAggregation( - dateHistogram("histo").field("date").interval(DateHistogramInterval.MONTH) + dateHistogram("histo").field("date").interval(DateHistogramInterval.MONTH).minDocCount(0) .subAggregation(derivative("deriv").setBucketsPaths("_count"))).execute().actionGet(); assertSearchResponse(response); @@ -152,7 +152,7 @@ public void singleValuedField_WithSubAggregation() throws Exception { SearchResponse response = client() .prepareSearch("idx") .addAggregation( - dateHistogram("histo").field("date").interval(DateHistogramInterval.MONTH) + dateHistogram("histo").field("date").interval(DateHistogramInterval.MONTH).minDocCount(0) .subAggregation(derivative("deriv").setBucketsPaths("sum")).subAggregation(sum("sum").field("value"))) .execute().actionGet(); @@ -222,7 +222,7 @@ public void multiValuedField() throws Exception { SearchResponse response = client() .prepareSearch("idx") .addAggregation( - dateHistogram("histo").field("dates").interval(DateHistogramInterval.MONTH) + dateHistogram("histo").field("dates").interval(DateHistogramInterval.MONTH).minDocCount(0) .subAggregation(derivative("deriv").setBucketsPaths("_count"))).execute().actionGet(); assertSearchResponse(response); @@ -278,7 +278,7 @@ public void unmapped() throws Exception { SearchResponse response = client() .prepareSearch("idx_unmapped") .addAggregation( - dateHistogram("histo").field("date").interval(DateHistogramInterval.MONTH) + dateHistogram("histo").field("date").interval(DateHistogramInterval.MONTH).minDocCount(0) .subAggregation(derivative("deriv").setBucketsPaths("_count"))).execute().actionGet(); assertSearchResponse(response); @@ -294,7 +294,7 @@ public void partiallyUnmapped() throws Exception { SearchResponse response = client() .prepareSearch("idx", "idx_unmapped") .addAggregation( - dateHistogram("histo").field("date").interval(DateHistogramInterval.MONTH) + dateHistogram("histo").field("date").interval(DateHistogramInterval.MONTH).minDocCount(0) .subAggregation(derivative("deriv").setBucketsPaths("_count"))).execute().actionGet(); assertSearchResponse(response); diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java index a5c9506aeac68..6f5641fcffab5 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java @@ -34,7 +34,6 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; @@ -165,7 +164,7 @@ public void singleValuedField() { SearchResponse response = client() .prepareSearch("idx") .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) .subAggregation(derivative("deriv").setBucketsPaths("_count")) .subAggregation(derivative("2nd_deriv").setBucketsPaths("deriv"))).execute().actionGet(); @@ -202,7 +201,7 @@ public void singleValuedField_WithSubAggregation() throws Exception { SearchResponse response = client() .prepareSearch("idx") .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) .subAggregation(sum("sum").field(SINGLE_VALUED_FIELD_NAME)) .subAggregation(derivative("deriv").setBucketsPaths("sum"))).execute().actionGet(); @@ -248,7 +247,7 @@ public void unmapped() throws Exception { SearchResponse response = client() .prepareSearch("idx_unmapped") .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) .subAggregation(derivative("deriv").setBucketsPaths("_count"))).execute().actionGet(); assertSearchResponse(response); @@ -264,7 +263,7 @@ public void partiallyUnmapped() throws Exception { SearchResponse response = client() .prepareSearch("idx", "idx_unmapped") .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) .subAggregation(derivative("deriv").setBucketsPaths("_count"))).execute().actionGet(); assertSearchResponse(response); From 53de93a89be2143465b2bf7e3304a8c05caf755e Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Thu, 5 Mar 2015 14:21:16 +0000 Subject: [PATCH 37/68] Aggregations: Added Factory for all MultiBucketAggregations to implement This allows things like reducers to add aggregations to buckets without needing to know how to construct the aggregation or bucket itself. --- .../InternalMultiBucketAggregation.java | 34 +++++++++++++- .../bucket/filters/InternalFilters.java | 13 +++++- .../bucket/geogrid/InternalGeoHashGrid.java | 14 +++++- .../histogram/InternalDateHistogram.java | 5 +++ .../bucket/histogram/InternalHistogram.java | 25 +++++++++-- .../bucket/range/InternalRange.java | 44 ++++++++++++++++--- .../bucket/range/date/InternalDateRange.java | 18 ++++++-- .../geodistance/InternalGeoDistance.java | 18 ++++++-- .../bucket/range/ipv4/InternalIPv4Range.java | 17 +++++-- .../significant/InternalSignificantTerms.java | 17 ++++--- .../significant/SignificantLongTerms.java | 30 +++++++++---- .../significant/SignificantStringTerms.java | 27 ++++++++---- .../significant/UnmappedSignificantTerms.java | 23 +++++++--- .../bucket/terms/DoubleTerms.java | 36 ++++++++++----- .../bucket/terms/InternalTerms.java | 18 ++++---- .../aggregations/bucket/terms/LongTerms.java | 25 ++++++++--- .../bucket/terms/StringTerms.java | 27 ++++++++---- .../bucket/terms/UnmappedTerms.java | 24 +++++++--- .../derivative/DerivativeReducer.java | 4 +- 19 files changed, 325 insertions(+), 94 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java b/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java index e7377414eda5b..856b96979f23f 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java +++ b/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java @@ -25,7 +25,8 @@ import java.util.List; import java.util.Map; -public abstract class InternalMultiBucketAggregation extends InternalAggregation implements MultiBucketsAggregation { +public abstract class InternalMultiBucketAggregation + extends InternalAggregation implements MultiBucketsAggregation { public InternalMultiBucketAggregation() { } @@ -34,6 +35,28 @@ public InternalMultiBucketAggregation(String name, List reducers, Map buckets); + + /** + * Create a new {@link InternalBucket} using the provided prototype bucket + * and aggregations. + * + * @param aggregations + * the aggregations for the new bucket + * @param prototype + * the bucket to use as a prototype + * @return the new bucket + */ + public abstract B createBucket(InternalAggregations aggregations, B prototype); + @Override public Object getProperty(List path) { if (path.isEmpty()) { @@ -75,4 +98,13 @@ public Object getProperty(String containingAggName, List path) { return aggregation.getProperty(path.subList(1, path.size())); } } + + public static abstract class Factory { + + public abstract String type(); + + public abstract A create(List buckets, A prototype); + + public abstract B createBucket(InternalAggregations aggregations, B prototype); + } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/InternalFilters.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/InternalFilters.java index 1e4c882ef5f1b..0383164ba863a 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/InternalFilters.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/InternalFilters.java @@ -29,6 +29,7 @@ import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.InternalMultiBucketAggregation; +import org.elasticsearch.search.aggregations.InternalMultiBucketAggregation.InternalBucket; import org.elasticsearch.search.aggregations.bucket.BucketStreamContext; import org.elasticsearch.search.aggregations.bucket.BucketStreams; import org.elasticsearch.search.aggregations.reducers.Reducer; @@ -42,7 +43,7 @@ /** * */ -public class InternalFilters extends InternalMultiBucketAggregation implements Filters { +public class InternalFilters extends InternalMultiBucketAggregation implements Filters { public final static Type TYPE = new Type("filters"); @@ -175,6 +176,16 @@ public Type type() { return TYPE; } + @Override + public InternalFilters create(List buckets) { + return new InternalFilters(this.name, buckets, this.keyed, this.reducers(), this.metaData); + } + + @Override + public Bucket createBucket(InternalAggregations aggregations, Bucket prototype) { + return new Bucket(prototype.key, prototype.docCount, aggregations, prototype.keyed); + } + @Override public List getBuckets() { return buckets; diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoHashGrid.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoHashGrid.java index 83428f8c2092e..6bbf1e2dc7ffc 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoHashGrid.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoHashGrid.java @@ -46,7 +46,8 @@ * All geohashes in a grid are of the same precision and held internally as a single long * for efficiency's sake. */ -public class InternalGeoHashGrid extends InternalMultiBucketAggregation implements GeoHashGrid { +public class InternalGeoHashGrid extends InternalMultiBucketAggregation implements + GeoHashGrid { public static final Type TYPE = new Type("geohash_grid", "ghcells"); @@ -163,7 +164,6 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } } - private int requiredSize; private Collection buckets; protected Map bucketMap; @@ -183,6 +183,16 @@ public Type type() { return TYPE; } + @Override + public InternalGeoHashGrid create(List buckets) { + return new InternalGeoHashGrid(this.name, this.requiredSize, buckets, this.reducers(), this.metaData); + } + + @Override + public Bucket createBucket(InternalAggregations aggregations, Bucket prototype) { + return new Bucket(prototype.geohashAsLong, prototype.docCount, aggregations); + } + @Override public List getBuckets() { Object o = buckets; diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java index 503d3626b2f17..a82a089066bdb 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java @@ -70,6 +70,11 @@ public String type() { return TYPE.name(); } + @Override + public InternalDateHistogram.Bucket createBucket(InternalAggregations aggregations, InternalDateHistogram.Bucket prototype) { + return new Bucket(prototype.key, prototype.docCount, aggregations, prototype.getKeyed(), prototype.formatter, this); + } + @Override public InternalDateHistogram.Bucket createBucket(Object key, long docCount, InternalAggregations aggregations, boolean keyed, @Nullable ValueFormatter formatter) { diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java index ad17e3796fea8..8c5b219379c5b 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java @@ -52,7 +52,8 @@ /** * TODO should be renamed to InternalNumericHistogram (see comment on {@link Histogram})? */ -public class InternalHistogram extends InternalMultiBucketAggregation implements Histogram { +public class InternalHistogram extends InternalMultiBucketAggregation implements + Histogram { final static Type TYPE = new Type("histogram", "histo"); @@ -233,7 +234,7 @@ public static void writeTo(EmptyBucketInfo info, StreamOutput out) throws IOExce } - public static class Factory { + public static class Factory extends InternalMultiBucketAggregation.Factory, B> { protected Factory() { } @@ -248,11 +249,17 @@ public InternalHistogram create(String name, List buckets, InternalOrder o return new InternalHistogram<>(name, buckets, order, minDocCount, emptyBucketInfo, formatter, keyed, this, reducers, metaData); } - public InternalHistogram create(String name, List buckets, InternalHistogram prototype) { - return new InternalHistogram<>(name, buckets, prototype.order, prototype.minDocCount, prototype.emptyBucketInfo, + @Override + public InternalHistogram create(List buckets, InternalHistogram prototype) { + return new InternalHistogram<>(prototype.name, buckets, prototype.order, prototype.minDocCount, prototype.emptyBucketInfo, prototype.formatter, prototype.keyed, this, prototype.reducers(), prototype.metaData); } + @Override + public B createBucket(InternalAggregations aggregations, B prototype) { + return (B) new Bucket(prototype.key, prototype.docCount, prototype.getKeyed(), prototype.formatter, this, aggregations); + } + public B createBucket(Object key, long docCount, InternalAggregations aggregations, boolean keyed, @Nullable ValueFormatter formatter) { if (key instanceof Number) { @@ -306,6 +313,16 @@ public Factory getFactory() { return factory; } + @Override + public InternalHistogram create(List buckets) { + return getFactory().create(buckets, this); + } + + @Override + public B createBucket(InternalAggregations aggregations, B prototype) { + return getFactory().createBucket(aggregations, prototype); + } + private static class IteratorAndCurrent { private final Iterator iterator; diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java index 00ff8b0803070..a3602060fd2e2 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java @@ -43,7 +43,8 @@ /** * */ -public class InternalRange extends InternalMultiBucketAggregation implements Range { +public class InternalRange> extends InternalMultiBucketAggregation + implements Range { static final Factory FACTORY = new Factory(); @@ -124,6 +125,14 @@ public Object getTo() { return to; } + public boolean getKeyed() { + return keyed; + } + + public ValueFormatter getFormatter() { + return formatter; + } + @Override public String getFromAsString() { if (Double.isInfinite(from)) { @@ -216,7 +225,7 @@ public void writeTo(StreamOutput out) throws IOException { } } - public static class Factory> { + public static class Factory> extends InternalMultiBucketAggregation.Factory { public String type() { return TYPE.name(); @@ -231,12 +240,25 @@ public R create(String name, List ranges, @Nullable ValueFormatter formatter, public B createBucket(String key, double from, double to, long docCount, InternalAggregations aggregations, boolean keyed, @Nullable ValueFormatter formatter) { return (B) new Bucket(key, from, to, docCount, aggregations, keyed, formatter); } + + @Override + public R create(List ranges, R prototype) { + return (R) new InternalRange<>(prototype.name, ranges, prototype.formatter, prototype.keyed, prototype.reducers(), + prototype.metaData); + } + + @Override + public B createBucket(InternalAggregations aggregations, B prototype) { + return (B) new Bucket(prototype.getKey(), prototype.from, prototype.to, prototype.getDocCount(), aggregations, prototype.keyed, + prototype.formatter); + } } private List ranges; private Map rangeMap; - private @Nullable ValueFormatter formatter; - private boolean keyed; + @Nullable + protected ValueFormatter formatter; + protected boolean keyed; public InternalRange() {} // for serialization @@ -258,10 +280,20 @@ public List getBuckets() { return ranges; } - protected Factory getFactory() { + public Factory getFactory() { return FACTORY; } + @Override + public R create(List buckets) { + return getFactory().create(buckets, (R) this); + } + + @Override + public B createBucket(InternalAggregations aggregations, B prototype) { + return getFactory().createBucket(aggregations, prototype); + } + @Override public InternalAggregation doReduce(ReduceContext reduceContext) { List aggregations = reduceContext.aggregations(); @@ -271,7 +303,7 @@ public InternalAggregation doReduce(ReduceContext reduceContext) { rangeList[i] = new ArrayList(); } for (InternalAggregation aggregation : aggregations) { - InternalRange ranges = (InternalRange) aggregation; + InternalRange ranges = (InternalRange) aggregation; int i = 0; for (Bucket range : ranges.ranges) { rangeList[i++].add(range); diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/date/InternalDateRange.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/date/InternalDateRange.java index b679a6bc3d5a5..6444f53e527b2 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/date/InternalDateRange.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/date/InternalDateRange.java @@ -38,7 +38,7 @@ /** * */ -public class InternalDateRange extends InternalRange { +public class InternalDateRange extends InternalRange { public final static Type TYPE = new Type("date_range", "drange"); @@ -113,7 +113,7 @@ ValueFormatter formatter() { } } - private static class Factory extends InternalRange.Factory { + public static class Factory extends InternalRange.Factory { @Override public String type() { @@ -126,10 +126,22 @@ public InternalDateRange create(String name, List rang return new InternalDateRange(name, ranges, formatter, keyed, reducers, metaData); } + @Override + public InternalDateRange create(List ranges, InternalDateRange prototype) { + return new InternalDateRange(prototype.name, ranges, prototype.formatter, prototype.keyed, prototype.reducers(), + prototype.metaData); + } + @Override public Bucket createBucket(String key, double from, double to, long docCount, InternalAggregations aggregations, boolean keyed, ValueFormatter formatter) { return new Bucket(key, from, to, docCount, aggregations, keyed, formatter); } + + @Override + public Bucket createBucket(InternalAggregations aggregations, Bucket prototype) { + return new Bucket(prototype.getKey(), ((Number) prototype.getFrom()).doubleValue(), ((Number) prototype.getTo()).doubleValue(), + prototype.getDocCount(), aggregations, prototype.getKeyed(), prototype.getFormatter()); + } } InternalDateRange() {} // for serialization @@ -145,7 +157,7 @@ public Type type() { } @Override - protected InternalRange.Factory getFactory() { + public InternalRange.Factory getFactory() { return FACTORY; } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/InternalGeoDistance.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/InternalGeoDistance.java index 0fef2e2ba0016..b271c3336e0f4 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/InternalGeoDistance.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/InternalGeoDistance.java @@ -36,7 +36,7 @@ /** * */ -public class InternalGeoDistance extends InternalRange { +public class InternalGeoDistance extends InternalRange { public static final Type TYPE = new Type("geo_distance", "gdist"); @@ -101,7 +101,7 @@ ValueFormatter formatter() { } } - private static class Factory extends InternalRange.Factory { + public static class Factory extends InternalRange.Factory { @Override public String type() { @@ -114,10 +114,22 @@ public InternalGeoDistance create(String name, List ranges, @Nullable Va return new InternalGeoDistance(name, ranges, formatter, keyed, reducers, metaData); } + @Override + public InternalGeoDistance create(List ranges, InternalGeoDistance prototype) { + return new InternalGeoDistance(prototype.name, ranges, prototype.formatter, prototype.keyed, prototype.reducers(), + prototype.metaData); + } + @Override public Bucket createBucket(String key, double from, double to, long docCount, InternalAggregations aggregations, boolean keyed, @Nullable ValueFormatter formatter) { return new Bucket(key, from, to, docCount, aggregations, keyed, formatter); } + + @Override + public Bucket createBucket(InternalAggregations aggregations, Bucket prototype) { + return new Bucket(prototype.getKey(), ((Number) prototype.getFrom()).doubleValue(), ((Number) prototype.getTo()).doubleValue(), + prototype.getDocCount(), aggregations, prototype.getKeyed(), prototype.getFormatter()); + } } InternalGeoDistance() {} // for serialization @@ -133,7 +145,7 @@ public Type type() { } @Override - protected InternalRange.Factory getFactory() { + public InternalRange.Factory getFactory() { return FACTORY; } } \ No newline at end of file diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ipv4/InternalIPv4Range.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ipv4/InternalIPv4Range.java index be2f8e52f8f4c..96668e67c69ca 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ipv4/InternalIPv4Range.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ipv4/InternalIPv4Range.java @@ -36,7 +36,7 @@ /** * */ -public class InternalIPv4Range extends InternalRange { +public class InternalIPv4Range extends InternalRange { public static final long MAX_IP = 4294967296l; @@ -110,7 +110,7 @@ boolean keyed() { } } - private static class Factory extends InternalRange.Factory { + public static class Factory extends InternalRange.Factory { @Override public String type() { @@ -123,10 +123,21 @@ public InternalIPv4Range create(String name, List ranges, @Nullable Valu return new InternalIPv4Range(name, ranges, keyed, reducers, metaData); } + @Override + public InternalIPv4Range create(List ranges, InternalIPv4Range prototype) { + return new InternalIPv4Range(prototype.name, ranges, prototype.keyed, prototype.reducers(), prototype.metaData); + } + @Override public Bucket createBucket(String key, double from, double to, long docCount, InternalAggregations aggregations, boolean keyed, @Nullable ValueFormatter formatter) { return new Bucket(key, from, to, docCount, aggregations, keyed); } + + @Override + public Bucket createBucket(InternalAggregations aggregations, Bucket prototype) { + return new Bucket(prototype.getKey(), ((Number) prototype.getFrom()).doubleValue(), ((Number) prototype.getTo()).doubleValue(), + prototype.getDocCount(), aggregations, prototype.getKeyed()); + } } public InternalIPv4Range() {} // for serialization @@ -142,7 +153,7 @@ public Type type() { } @Override - protected InternalRange.Factory getFactory() { + public InternalRange.Factory getFactory() { return FACTORY; } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/InternalSignificantTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/InternalSignificantTerms.java index a48fc850b90e6..a949c916c7d09 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/InternalSignificantTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/InternalSignificantTerms.java @@ -39,12 +39,13 @@ /** * */ -public abstract class InternalSignificantTerms extends InternalMultiBucketAggregation implements SignificantTerms, ToXContent, Streamable { +public abstract class InternalSignificantTerms extends + InternalMultiBucketAggregation implements SignificantTerms, ToXContent, Streamable { protected SignificanceHeuristic significanceHeuristic; protected int requiredSize; protected long minDocCount; - protected List buckets; + protected List buckets; protected Map bucketMap; protected long subsetSize; protected long supersetSize; @@ -124,7 +125,8 @@ public double getSignificanceScore() { } protected InternalSignificantTerms(long subsetSize, long supersetSize, String name, int requiredSize, long minDocCount, - SignificanceHeuristic significanceHeuristic, List buckets, List reducers, Map metaData) { + SignificanceHeuristic significanceHeuristic, List buckets, List reducers, + Map metaData) { super(name, reducers, metaData); this.requiredSize = requiredSize; this.minDocCount = minDocCount; @@ -166,13 +168,13 @@ public InternalAggregation doReduce(ReduceContext reduceContext) { // Compute the overall result set size and the corpus size using the // top-level Aggregations from each shard for (InternalAggregation aggregation : aggregations) { - InternalSignificantTerms terms = (InternalSignificantTerms) aggregation; + InternalSignificantTerms terms = (InternalSignificantTerms) aggregation; globalSubsetSize += terms.subsetSize; globalSupersetSize += terms.supersetSize; } Map> buckets = new HashMap<>(); for (InternalAggregation aggregation : aggregations) { - InternalSignificantTerms terms = (InternalSignificantTerms) aggregation; + InternalSignificantTerms terms = (InternalSignificantTerms) aggregation; for (Bucket bucket : terms.buckets) { List existingBuckets = buckets.get(bucket.getKey()); if (existingBuckets == null) { @@ -200,9 +202,10 @@ public InternalAggregation doReduce(ReduceContext reduceContext) { for (int i = ordered.size() - 1; i >= 0; i--) { list[i] = (Bucket) ordered.pop(); } - return newAggregation(globalSubsetSize, globalSupersetSize, Arrays.asList(list)); + return create(globalSubsetSize, globalSupersetSize, Arrays.asList(list), this); } - abstract InternalSignificantTerms newAggregation(long subsetSize, long supersetSize, List buckets); + protected abstract A create(long subsetSize, long supersetSize, List buckets, + InternalSignificantTerms prototype); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTerms.java index 85ae983ef18b4..a450f9d093320 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTerms.java @@ -42,7 +42,7 @@ /** * */ -public class SignificantLongTerms extends InternalSignificantTerms { +public class SignificantLongTerms extends InternalSignificantTerms { public static final Type TYPE = new Type("significant_terms", "siglterms"); @@ -162,15 +162,13 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } } - private ValueFormatter formatter; SignificantLongTerms() { } // for serialization - public SignificantLongTerms(long subsetSize, long supersetSize, String name, @Nullable ValueFormatter formatter, - int requiredSize, - long minDocCount, SignificanceHeuristic significanceHeuristic, List buckets, + public SignificantLongTerms(long subsetSize, long supersetSize, String name, @Nullable ValueFormatter formatter, int requiredSize, + long minDocCount, SignificanceHeuristic significanceHeuristic, List buckets, List reducers, Map metaData) { super(subsetSize, supersetSize, name, requiredSize, minDocCount, significanceHeuristic, buckets, reducers, metaData); @@ -183,10 +181,24 @@ public Type type() { } @Override - InternalSignificantTerms newAggregation(long subsetSize, long supersetSize, - List buckets) { - return new SignificantLongTerms(subsetSize, supersetSize, getName(), formatter, requiredSize, minDocCount, significanceHeuristic, - buckets, reducers(), getMetaData()); + public SignificantLongTerms create(List buckets) { + return new SignificantLongTerms(this.subsetSize, this.supersetSize, this.name, this.formatter, this.requiredSize, this.minDocCount, + this.significanceHeuristic, buckets, this.reducers(), this.metaData); + } + + @Override + public Bucket createBucket(InternalAggregations aggregations, SignificantLongTerms.Bucket prototype) { + return new Bucket(prototype.subsetDf, prototype.subsetSize, prototype.supersetDf, prototype.supersetSize, prototype.term, + aggregations, prototype.formatter); + } + + @Override + protected SignificantLongTerms create(long subsetSize, long supersetSize, + List buckets, + InternalSignificantTerms prototype) { + return new SignificantLongTerms(subsetSize, supersetSize, prototype.getName(), ((SignificantLongTerms) prototype).formatter, + prototype.requiredSize, prototype.minDocCount, prototype.significanceHeuristic, buckets, prototype.reducers(), + prototype.getMetaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTerms.java index d8fc74c9bc528..9fbaa6cc375a1 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTerms.java @@ -41,7 +41,7 @@ /** * */ -public class SignificantStringTerms extends InternalSignificantTerms { +public class SignificantStringTerms extends InternalSignificantTerms { public static final InternalAggregation.Type TYPE = new Type("significant_terms", "sigsterms"); @@ -160,9 +160,8 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws SignificantStringTerms() {} // for serialization - public SignificantStringTerms(long subsetSize, long supersetSize, String name, int requiredSize, - long minDocCount, - SignificanceHeuristic significanceHeuristic, List buckets, List reducers, + public SignificantStringTerms(long subsetSize, long supersetSize, String name, int requiredSize, long minDocCount, + SignificanceHeuristic significanceHeuristic, List buckets, List reducers, Map metaData) { super(subsetSize, supersetSize, name, requiredSize, minDocCount, significanceHeuristic, buckets, reducers, metaData); } @@ -173,10 +172,22 @@ public Type type() { } @Override - InternalSignificantTerms newAggregation(long subsetSize, long supersetSize, - List buckets) { - return new SignificantStringTerms(subsetSize, supersetSize, getName(), requiredSize, minDocCount, significanceHeuristic, buckets, - reducers(), getMetaData()); + public SignificantStringTerms create(List buckets) { + return new SignificantStringTerms(this.subsetSize, this.supersetSize, this.name, this.requiredSize, this.minDocCount, + this.significanceHeuristic, buckets, this.reducers(), this.metaData); + } + + @Override + public Bucket createBucket(InternalAggregations aggregations, SignificantStringTerms.Bucket prototype) { + return new Bucket(prototype.termBytes, prototype.subsetDf, prototype.subsetSize, prototype.supersetDf, prototype.supersetSize, + aggregations); + } + + @Override + protected SignificantStringTerms create(long subsetSize, long supersetSize, List buckets, + InternalSignificantTerms prototype) { + return new SignificantStringTerms(subsetSize, supersetSize, prototype.getName(), prototype.requiredSize, prototype.minDocCount, + prototype.significanceHeuristic, buckets, prototype.reducers(), prototype.getMetaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/UnmappedSignificantTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/UnmappedSignificantTerms.java index 0409900927280..2d0309c9da1c1 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/UnmappedSignificantTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/UnmappedSignificantTerms.java @@ -23,6 +23,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.JLHScore; import org.elasticsearch.search.aggregations.reducers.Reducer; @@ -34,7 +35,7 @@ /** * */ -public class UnmappedSignificantTerms extends InternalSignificantTerms { +public class UnmappedSignificantTerms extends InternalSignificantTerms { public static final Type TYPE = new Type("significant_terms", "umsigterms"); @@ -67,6 +68,21 @@ public Type type() { return TYPE; } + @Override + public UnmappedSignificantTerms create(List buckets) { + return new UnmappedSignificantTerms(this.name, this.requiredSize, this.minDocCount, this.reducers(), this.metaData); + } + + @Override + public InternalSignificantTerms.Bucket createBucket(InternalAggregations aggregations, InternalSignificantTerms.Bucket prototype) { + throw new UnsupportedOperationException("not supported for UnmappedSignificantTerms"); + } + + @Override + protected UnmappedSignificantTerms create(long subsetSize, long supersetSize, List buckets, InternalSignificantTerms prototype) { + throw new UnsupportedOperationException("not supported for UnmappedSignificantTerms"); + } + @Override public InternalAggregation doReduce(ReduceContext reduceContext) { for (InternalAggregation aggregation : reduceContext.aggregations()) { @@ -77,11 +93,6 @@ public InternalAggregation doReduce(ReduceContext reduceContext) { return this; } - @Override - InternalSignificantTerms newAggregation(long subsetSize, long supersetSize, List buckets) { - throw new UnsupportedOperationException("How did you get there?"); - } - @Override protected void doReadFrom(StreamInput in) throws IOException { this.requiredSize = readSize(in); diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTerms.java index 0e6ca403407ef..dbb8061db0917 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTerms.java @@ -40,7 +40,7 @@ /** * */ -public class DoubleTerms extends InternalTerms { +public class DoubleTerms extends InternalTerms { public static final Type TYPE = new Type("terms", "dterms"); @@ -85,7 +85,8 @@ public Bucket(@Nullable ValueFormatter formatter, boolean showDocCountError) { super(formatter, showDocCountError); } - public Bucket(double term, long docCount, InternalAggregations aggregations, boolean showDocCountError, long docCountError, @Nullable ValueFormatter formatter) { + public Bucket(double term, long docCount, InternalAggregations aggregations, boolean showDocCountError, long docCountError, + @Nullable ValueFormatter formatter) { super(docCount, aggregations, showDocCountError, docCountError, formatter); this.term = term; } @@ -153,13 +154,15 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } } - private @Nullable ValueFormatter formatter; + private @Nullable + ValueFormatter formatter; - DoubleTerms() {} // for serialization + DoubleTerms() { + } // for serialization public DoubleTerms(String name, Terms.Order order, @Nullable ValueFormatter formatter, int requiredSize, int shardSize, - long minDocCount, List buckets, boolean showTermDocCountError, long docCountError, long otherDocCount, - List reducers, Map metaData) { + long minDocCount, List buckets, boolean showTermDocCountError, long docCountError, + long otherDocCount, List reducers, Map metaData) { super(name, order, requiredSize, shardSize, minDocCount, buckets, showTermDocCountError, docCountError, otherDocCount, reducers, metaData); this.formatter = formatter; @@ -171,10 +174,23 @@ public Type type() { } @Override - protected InternalTerms newAggregation(String name, List buckets, boolean showTermDocCountError, - long docCountError, long otherDocCount, List reducers, Map metaData) { - return new DoubleTerms(name, order, formatter, requiredSize, shardSize, minDocCount, buckets, showTermDocCountError, docCountError, - otherDocCount, reducers, metaData); + public DoubleTerms create(List buckets) { + return new DoubleTerms(this.name, this.order, this.formatter, this.requiredSize, this.shardSize, this.minDocCount, buckets, + this.showTermDocCountError, this.docCountError, this.otherDocCount, this.reducers(), this.metaData); + } + + @Override + public Bucket createBucket(InternalAggregations aggregations, Bucket prototype) { + return new Bucket(prototype.term, prototype.docCount, aggregations, prototype.showDocCountError, prototype.docCountError, + prototype.formatter); + } + + @Override + protected DoubleTerms create(String name, List buckets, + long docCountError, long otherDocCount, InternalTerms prototype) { + return new DoubleTerms(name, prototype.order, ((DoubleTerms) prototype).formatter, prototype.requiredSize, prototype.shardSize, + prototype.minDocCount, buckets, prototype.showTermDocCountError, docCountError, otherDocCount, prototype.reducers(), + prototype.getMetaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalTerms.java index 129698daffa3b..b753f32279672 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/InternalTerms.java @@ -43,7 +43,8 @@ /** * */ -public abstract class InternalTerms extends InternalMultiBucketAggregation implements Terms, ToXContent, Streamable { +public abstract class InternalTerms extends InternalMultiBucketAggregation + implements Terms, ToXContent, Streamable { protected static final String DOC_COUNT_ERROR_UPPER_BOUND_FIELD_NAME = "doc_count_error_upper_bound"; protected static final String SUM_OF_OTHER_DOC_COUNTS = "sum_other_doc_count"; @@ -115,7 +116,7 @@ public Bucket reduce(List buckets, ReduceContext context) { protected int requiredSize; protected int shardSize; protected long minDocCount; - protected List buckets; + protected List buckets; protected Map bucketMap; protected long docCountError; protected boolean showTermDocCountError; @@ -123,8 +124,9 @@ public Bucket reduce(List buckets, ReduceContext context) { protected InternalTerms() {} // for serialization - protected InternalTerms(String name, Terms.Order order, int requiredSize, int shardSize, long minDocCount, List buckets, - boolean showTermDocCountError, long docCountError, long otherDocCount, List reducers, Map metaData) { + protected InternalTerms(String name, Terms.Order order, int requiredSize, int shardSize, long minDocCount, + List buckets, boolean showTermDocCountError, long docCountError, long otherDocCount, List reducers, + Map metaData) { super(name, reducers, metaData); this.order = order; this.requiredSize = requiredSize; @@ -171,7 +173,7 @@ public InternalAggregation doReduce(ReduceContext reduceContext) { long sumDocCountError = 0; long otherDocCount = 0; for (InternalAggregation aggregation : aggregations) { - InternalTerms terms = (InternalTerms) aggregation; + InternalTerms terms = (InternalTerms) aggregation; otherDocCount += terms.getSumOfOtherDocCounts(); final long thisAggDocCountError; if (terms.buckets.size() < this.shardSize || this.order == InternalOrder.TERM_ASC || this.order == InternalOrder.TERM_DESC) { @@ -224,10 +226,10 @@ public InternalAggregation doReduce(ReduceContext reduceContext) { } else { docCountError = aggregations.size() == 1 ? 0 : sumDocCountError; } - return newAggregation(name, Arrays.asList(list), showTermDocCountError, docCountError, otherDocCount, reducers(), getMetaData()); + return create(name, Arrays.asList(list), docCountError, otherDocCount, this); } - protected abstract InternalTerms newAggregation(String name, List buckets, boolean showTermDocCountError, long docCountError, - long otherDocCount, List reducers, Map metaData); + protected abstract A create(String name, List buckets, long docCountError, long otherDocCount, + InternalTerms prototype); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTerms.java index b8edad21dd98e..eee9e6bfc4bc0 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTerms.java @@ -39,7 +39,7 @@ /** * */ -public class LongTerms extends InternalTerms { +public class LongTerms extends InternalTerms { public static final Type TYPE = new Type("terms", "lterms"); @@ -157,7 +157,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws LongTerms() {} // for serialization public LongTerms(String name, Terms.Order order, @Nullable ValueFormatter formatter, int requiredSize, int shardSize, long minDocCount, - List buckets, boolean showTermDocCountError, long docCountError, long otherDocCount, + List buckets, boolean showTermDocCountError, long docCountError, long otherDocCount, List reducers, Map metaData) { super(name, order, requiredSize, shardSize, minDocCount, buckets, showTermDocCountError, docCountError, otherDocCount, reducers, metaData); @@ -170,10 +170,23 @@ public Type type() { } @Override - protected InternalTerms newAggregation(String name, List buckets, boolean showTermDocCountError, - long docCountError, long otherDocCount, List reducers, Map metaData) { - return new LongTerms(name, order, formatter, requiredSize, shardSize, minDocCount, buckets, showTermDocCountError, docCountError, - otherDocCount, reducers, metaData); + public LongTerms create(List buckets) { + return new LongTerms(this.name, this.order, this.formatter, this.requiredSize, this.shardSize, this.minDocCount, buckets, + this.showTermDocCountError, this.docCountError, this.otherDocCount, this.reducers(), this.metaData); + } + + @Override + public Bucket createBucket(InternalAggregations aggregations, Bucket prototype) { + return new Bucket(prototype.term, prototype.docCount, aggregations, prototype.showDocCountError, prototype.docCountError, + prototype.formatter); + } + + @Override + protected LongTerms create(String name, List buckets, + long docCountError, long otherDocCount, InternalTerms prototype) { + return new LongTerms(name, prototype.order, ((LongTerms) prototype).formatter, prototype.requiredSize, prototype.shardSize, + prototype.minDocCount, buckets, prototype.showTermDocCountError, docCountError, otherDocCount, prototype.reducers(), + prototype.getMetaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTerms.java index ef9ec91e80c5f..ee458acdf1323 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTerms.java @@ -38,7 +38,7 @@ /** * */ -public class StringTerms extends InternalTerms { +public class StringTerms extends InternalTerms { public static final InternalAggregation.Type TYPE = new Type("terms", "sterms"); @@ -74,7 +74,6 @@ public static void registerStreams() { BucketStreams.registerStream(BUCKET_STREAM, TYPE.stream()); } - public static class Bucket extends InternalTerms.Bucket { BytesRef termBytes; @@ -149,10 +148,11 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } } - StringTerms() {} // for serialization + StringTerms() { + } // for serialization public StringTerms(String name, Terms.Order order, int requiredSize, int shardSize, long minDocCount, - List buckets, boolean showTermDocCountError, long docCountError, long otherDocCount, + List buckets, boolean showTermDocCountError, long docCountError, long otherDocCount, List reducers, Map metaData) { super(name, order, requiredSize, shardSize, minDocCount, buckets, showTermDocCountError, docCountError, otherDocCount, reducers, metaData); @@ -164,10 +164,21 @@ public Type type() { } @Override - protected InternalTerms newAggregation(String name, List buckets, boolean showTermDocCountError, - long docCountError, long otherDocCount, List reducers, Map metaData) { - return new StringTerms(name, order, requiredSize, shardSize, minDocCount, buckets, showTermDocCountError, docCountError, - otherDocCount, reducers, metaData); + public StringTerms create(List buckets) { + return new StringTerms(this.name, this.order, this.requiredSize, this.shardSize, this.minDocCount, buckets, + this.showTermDocCountError, this.docCountError, this.otherDocCount, this.reducers(), this.metaData); + } + + @Override + public Bucket createBucket(InternalAggregations aggregations, Bucket prototype) { + return new Bucket(prototype.termBytes, prototype.docCount, aggregations, prototype.showDocCountError, prototype.docCountError); + } + + @Override + protected StringTerms create(String name, List buckets, + long docCountError, long otherDocCount, InternalTerms prototype) { + return new StringTerms(name, prototype.order, prototype.requiredSize, prototype.shardSize, prototype.minDocCount, buckets, + prototype.showTermDocCountError, docCountError, otherDocCount, prototype.reducers(), prototype.getMetaData()); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedTerms.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedTerms.java index 89134a394ec67..14f07c57e8342 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedTerms.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/UnmappedTerms.java @@ -23,6 +23,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.search.aggregations.AggregationStreams; import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.reducers.Reducer; import java.io.IOException; @@ -33,7 +34,7 @@ /** * */ -public class UnmappedTerms extends InternalTerms { +public class UnmappedTerms extends InternalTerms { public static final Type TYPE = new Type("terms", "umterms"); @@ -65,6 +66,21 @@ public Type type() { return TYPE; } + @Override + public UnmappedTerms create(List buckets) { + return new UnmappedTerms(this.name, this.order, this.requiredSize, this.shardSize, this.minDocCount, this.reducers(), this.metaData); + } + + @Override + public InternalTerms.Bucket createBucket(InternalAggregations aggregations, InternalTerms.Bucket prototype) { + throw new UnsupportedOperationException("not supported for UnmappedTerms"); + } + + @Override + protected UnmappedTerms create(String name, List buckets, long docCountError, long otherDocCount, InternalTerms prototype) { + throw new UnsupportedOperationException("not supported for UnmappedTerms"); + } + @Override protected void doReadFrom(StreamInput in) throws IOException { this.docCountError = 0; @@ -92,12 +108,6 @@ public InternalAggregation doReduce(ReduceContext reduceContext) { return this; } - @Override - protected InternalTerms newAggregation(String name, List buckets, boolean showTermDocCountError, long docCountError, - long otherDocCount, List reducers, Map metaData) { - throw new UnsupportedOperationException("How did you get there?"); - } - @Override public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { builder.field(InternalTerms.DOC_COUNT_ERROR_UPPER_BOUND_FIELD_NAME, docCountError); diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java index 5f40ab2906ee0..40a5b005560d4 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java @@ -95,7 +95,7 @@ public Type type() { @Override public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext reduceContext) { - InternalHistogram histo = (InternalHistogram) aggregation; + InternalHistogram histo = (InternalHistogram) aggregation; List buckets = histo.getBuckets(); InternalHistogram.Factory factory = histo.getFactory(); @@ -116,7 +116,7 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext } lastBucketValue = thisBucketValue; } - return factory.create(histo.getName(), newBuckets, histo); + return factory.create(newBuckets, histo); } @Override From a824184bf2fddecb0ed4daa2c2deacbb66d33c30 Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Wed, 8 Apr 2015 10:15:46 -0400 Subject: [PATCH 38/68] Aggregations: Add MovAvg Reducer Allows the user to calculate a Moving Average over a histogram of buckets. Provides four different moving averages: - Simple - Linear weighted - Single Exponentially weighted (aka EWMA) - Double Exponentially weighted (aka Holt-winters) Closes #10024 --- .../aggregations/AggregationModule.java | 5 +- .../TransportAggregationModule.java | 5 +- .../reducers/ReducerBuilders.java | 5 + .../reducers/movavg/MovAvgBuilder.java | 102 ++++ .../reducers/movavg/MovAvgParser.java | 142 +++++ .../reducers/movavg/MovAvgReducer.java | 182 +++++++ .../movavg/models/DoubleExpModel.java | 194 +++++++ .../reducers/movavg/models/LinearModel.java | 93 ++++ .../reducers/movavg/models/MovAvgModel.java | 49 ++ .../movavg/models/MovAvgModelBuilder.java | 33 ++ .../movavg/models/MovAvgModelModule.java | 55 ++ .../movavg/models/MovAvgModelParser.java | 34 ++ .../models/MovAvgModelParserMapper.java | 54 ++ .../movavg/models/MovAvgModelStreams.java | 74 +++ .../reducers/movavg/models/SimpleModel.java | 86 +++ .../movavg/models/SingleExpModel.java | 133 +++++ .../models/TransportMovAvgModelModule.java | 51 ++ .../aggregations/reducers/MovAvgTests.java | 500 ++++++++++++++++++ 18 files changed, 1795 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgBuilder.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgParser.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgReducer.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/DoubleExpModel.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/LinearModel.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModel.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelBuilder.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelModule.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelParser.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelParserMapper.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelStreams.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/SimpleModel.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/SingleExpModel.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/TransportMovAvgModelModule.java create mode 100644 src/test/java/org/elasticsearch/search/aggregations/reducers/MovAvgTests.java diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregationModule.java b/src/main/java/org/elasticsearch/search/aggregations/AggregationModule.java index d1cb6d968005b..a8d3895ec780e 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregationModule.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregationModule.java @@ -57,6 +57,8 @@ import org.elasticsearch.search.aggregations.metrics.valuecount.ValueCountParser; import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.reducers.derivative.DerivativeParser; +import org.elasticsearch.search.aggregations.reducers.movavg.MovAvgParser; +import org.elasticsearch.search.aggregations.reducers.movavg.models.MovAvgModelModule; import java.util.List; @@ -101,6 +103,7 @@ public AggregationModule() { aggParsers.add(ChildrenParser.class); reducerParsers.add(DerivativeParser.class); + reducerParsers.add(MovAvgParser.class); } /** @@ -129,7 +132,7 @@ protected void configure() { @Override public Iterable spawnModules() { - return ImmutableList.of(new SignificantTermsHeuristicModule()); + return ImmutableList.of(new SignificantTermsHeuristicModule(), new MovAvgModelModule()); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/TransportAggregationModule.java b/src/main/java/org/elasticsearch/search/aggregations/TransportAggregationModule.java index fe4542830cc5b..c3d89cf4f8fd1 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/TransportAggregationModule.java +++ b/src/main/java/org/elasticsearch/search/aggregations/TransportAggregationModule.java @@ -59,6 +59,8 @@ import org.elasticsearch.search.aggregations.metrics.valuecount.InternalValueCount; import org.elasticsearch.search.aggregations.reducers.InternalSimpleValue; import org.elasticsearch.search.aggregations.reducers.derivative.DerivativeReducer; +import org.elasticsearch.search.aggregations.reducers.movavg.MovAvgReducer; +import org.elasticsearch.search.aggregations.reducers.movavg.models.TransportMovAvgModelModule; /** * A module that registers all the transport streams for the addAggregation @@ -108,10 +110,11 @@ protected void configure() { // Reducers DerivativeReducer.registerStreams(); + MovAvgReducer.registerStreams(); } @Override public Iterable spawnModules() { - return ImmutableList.of(new TransportSignificantTermsHeuristicModule()); + return ImmutableList.of(new TransportSignificantTermsHeuristicModule(), new TransportMovAvgModelModule()); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilders.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilders.java index 21c901af80d4d..0aa8be4e99213 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilders.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilders.java @@ -20,6 +20,7 @@ package org.elasticsearch.search.aggregations.reducers; import org.elasticsearch.search.aggregations.reducers.derivative.DerivativeBuilder; +import org.elasticsearch.search.aggregations.reducers.movavg.MovAvgBuilder; public final class ReducerBuilders { @@ -29,4 +30,8 @@ private ReducerBuilders() { public static final DerivativeBuilder derivative(String name) { return new DerivativeBuilder(name); } + + public static final MovAvgBuilder smooth(String name) { + return new MovAvgBuilder(name); + } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgBuilder.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgBuilder.java new file mode 100644 index 0000000000000..9790604197ded --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgBuilder.java @@ -0,0 +1,102 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.movavg; + +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.search.aggregations.reducers.ReducerBuilder; +import org.elasticsearch.search.aggregations.reducers.movavg.models.MovAvgModelBuilder; + +import java.io.IOException; + +import static org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; + +/** + * A builder to create MovingAvg reducer aggregations + */ +public class MovAvgBuilder extends ReducerBuilder { + + private String format; + private GapPolicy gapPolicy; + private MovAvgModelBuilder modelBuilder; + private Integer window; + + public MovAvgBuilder(String name) { + super(name, MovAvgReducer.TYPE.name()); + } + + public MovAvgBuilder format(String format) { + this.format = format; + return this; + } + + /** + * Defines what should be done when a gap in the series is discovered + * + * @param gapPolicy A GapPolicy enum defining the selected policy + * @return Returns the builder to continue chaining + */ + public MovAvgBuilder gapPolicy(GapPolicy gapPolicy) { + this.gapPolicy = gapPolicy; + return this; + } + + /** + * Sets a MovAvgModelBuilder for the Moving Average. The model builder is used to + * define what type of moving average you want to use on the series + * + * @param modelBuilder A MovAvgModelBuilder which has been prepopulated with settings + * @return Returns the builder to continue chaining + */ + public MovAvgBuilder modelBuilder(MovAvgModelBuilder modelBuilder) { + this.modelBuilder = modelBuilder; + return this; + } + + /** + * Sets the window size for the moving average. This window will "slide" across the + * series, and the values inside that window will be used to calculate the moving avg value + * + * @param window Size of window + * @return Returns the builder to continue chaining + */ + public MovAvgBuilder window(int window) { + this.window = window; + return this; + } + + + @Override + protected XContentBuilder internalXContent(XContentBuilder builder, Params params) throws IOException { + if (format != null) { + builder.field(MovAvgParser.FORMAT.getPreferredName(), format); + } + if (gapPolicy != null) { + builder.field(MovAvgParser.GAP_POLICY.getPreferredName(), gapPolicy.getName()); + } + if (modelBuilder != null) { + modelBuilder.toXContent(builder, params); + } + if (window != null) { + builder.field(MovAvgParser.WINDOW.getPreferredName(), window); + } + return builder; + } + +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgParser.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgParser.java new file mode 100644 index 0000000000000..3f241a67b3ac4 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgParser.java @@ -0,0 +1,142 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.movavg; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.search.SearchParseException; +import org.elasticsearch.search.aggregations.reducers.Reducer; +import org.elasticsearch.search.aggregations.reducers.ReducerFactory; +import org.elasticsearch.search.aggregations.reducers.movavg.models.MovAvgModel; +import org.elasticsearch.search.aggregations.reducers.movavg.models.MovAvgModelParser; +import org.elasticsearch.search.aggregations.reducers.movavg.models.MovAvgModelParserMapper; +import org.elasticsearch.search.aggregations.support.format.ValueFormat; +import org.elasticsearch.search.aggregations.support.format.ValueFormatter; +import org.elasticsearch.search.internal.SearchContext; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; + +public class MovAvgParser implements Reducer.Parser { + + public static final ParseField FORMAT = new ParseField("format"); + public static final ParseField GAP_POLICY = new ParseField("gap_policy"); + public static final ParseField MODEL = new ParseField("model"); + public static final ParseField WINDOW = new ParseField("window"); + public static final ParseField SETTINGS = new ParseField("settings"); + + private final MovAvgModelParserMapper movAvgModelParserMapper; + + @Inject + public MovAvgParser(MovAvgModelParserMapper movAvgModelParserMapper) { + this.movAvgModelParserMapper = movAvgModelParserMapper; + } + + @Override + public String type() { + return MovAvgReducer.TYPE.name(); + } + + @Override + public ReducerFactory parse(String reducerName, XContentParser parser, SearchContext context) throws IOException { + XContentParser.Token token; + String currentFieldName = null; + String[] bucketsPaths = null; + String format = null; + GapPolicy gapPolicy = GapPolicy.IGNORE; + int window = 5; + Map settings = null; + String model = "simple"; + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.VALUE_NUMBER) { + if (WINDOW.match(currentFieldName)) { + window = parser.intValue(); + } else { + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + reducerName + "]: [" + + currentFieldName + "]."); + } + } else if (token == XContentParser.Token.VALUE_STRING) { + if (FORMAT.match(currentFieldName)) { + format = parser.text(); + } else if (BUCKETS_PATH.match(currentFieldName)) { + bucketsPaths = new String[] { parser.text() }; + } else if (GAP_POLICY.match(currentFieldName)) { + gapPolicy = GapPolicy.parse(context, parser.text()); + } else if (MODEL.match(currentFieldName)) { + model = parser.text(); + } else { + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + reducerName + "]: [" + + currentFieldName + "]."); + } + } else if (token == XContentParser.Token.START_ARRAY) { + if (BUCKETS_PATH.match(currentFieldName)) { + List paths = new ArrayList<>(); + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + String path = parser.text(); + paths.add(path); + } + bucketsPaths = paths.toArray(new String[paths.size()]); + } else { + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + reducerName + "]: [" + + currentFieldName + "]."); + } + } else if (token == XContentParser.Token.START_OBJECT) { + if (SETTINGS.match(currentFieldName)) { + settings = parser.map(); + } else { + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + reducerName + "]: [" + + currentFieldName + "]."); + } + } else { + throw new SearchParseException(context, "Unexpected token " + token + " in [" + reducerName + "]."); + } + } + + if (bucketsPaths == null) { + throw new SearchParseException(context, "Missing required field [" + BUCKETS_PATH.getPreferredName() + + "] for smooth aggregation [" + reducerName + "]"); + } + + ValueFormatter formatter = null; + if (format != null) { + formatter = ValueFormat.Patternable.Number.format(format).formatter(); + } + + MovAvgModelParser modelParser = movAvgModelParserMapper.get(model); + if (modelParser == null) { + throw new SearchParseException(context, "Unknown model [" + model + + "] specified. Valid options are:" + movAvgModelParserMapper.getAllNames().toString()); + } + MovAvgModel movAvgModel = modelParser.parse(settings); + + + return new MovAvgReducer.Factory(reducerName, bucketsPaths, formatter, gapPolicy, window, movAvgModel); + } + + +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgReducer.java new file mode 100644 index 0000000000000..b339cdf487def --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgReducer.java @@ -0,0 +1,182 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.movavg; + +import com.google.common.base.Function; +import com.google.common.collect.EvictingQueue; +import com.google.common.collect.Lists; +import org.elasticsearch.ElasticsearchIllegalStateException; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.search.aggregations.*; +import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; +import org.elasticsearch.search.aggregations.InternalAggregation.Type; +import org.elasticsearch.search.aggregations.bucket.histogram.HistogramAggregator; +import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; +import org.elasticsearch.search.aggregations.reducers.*; +import org.elasticsearch.search.aggregations.reducers.movavg.models.MovAvgModel; +import org.elasticsearch.search.aggregations.reducers.movavg.models.MovAvgModelStreams; +import org.elasticsearch.search.aggregations.support.AggregationContext; +import org.elasticsearch.search.aggregations.support.format.ValueFormatter; +import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; +import static org.elasticsearch.search.aggregations.reducers.BucketHelpers.resolveBucketValue; + +public class MovAvgReducer extends Reducer { + + public final static Type TYPE = new Type("moving_avg"); + + public final static ReducerStreams.Stream STREAM = new ReducerStreams.Stream() { + @Override + public MovAvgReducer readResult(StreamInput in) throws IOException { + MovAvgReducer result = new MovAvgReducer(); + result.readFrom(in); + return result; + } + }; + + public static void registerStreams() { + ReducerStreams.registerStream(STREAM, TYPE.stream()); + } + + private static final Function FUNCTION = new Function() { + @Override + public InternalAggregation apply(Aggregation input) { + return (InternalAggregation) input; + } + }; + + private ValueFormatter formatter; + private GapPolicy gapPolicy; + private int window; + private MovAvgModel model; + + public MovAvgReducer() { + } + + public MovAvgReducer(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter, GapPolicy gapPolicy, + int window, MovAvgModel model, Map metadata) { + super(name, bucketsPaths, metadata); + this.formatter = formatter; + this.gapPolicy = gapPolicy; + this.window = window; + this.model = model; + } + + @Override + public Type type() { + return TYPE; + } + + @Override + public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext reduceContext) { + InternalHistogram histo = (InternalHistogram) aggregation; + List buckets = histo.getBuckets(); + InternalHistogram.Factory factory = histo.getFactory(); + + List newBuckets = new ArrayList<>(); + EvictingQueue values = EvictingQueue.create(this.window); + + for (InternalHistogram.Bucket bucket : buckets) { + Double thisBucketValue = resolveBucketValue(histo, bucket, bucketsPaths()[0], gapPolicy); + if (thisBucketValue != null) { + values.offer(thisBucketValue); + + // TODO handle "edge policy" + double movavg = model.next(values); + + List aggs = new ArrayList<>(Lists.transform(bucket.getAggregations().asList(), FUNCTION)); + aggs.add(new InternalSimpleValue(name(), movavg, formatter, new ArrayList(), metaData())); + InternalHistogram.Bucket newBucket = factory.createBucket(bucket.getKey(), bucket.getDocCount(), new InternalAggregations( + aggs), bucket.getKeyed(), bucket.getFormatter()); + newBuckets.add(newBucket); + } else { + newBuckets.add(bucket); + } + } + //return factory.create(histo.getName(), newBuckets, histo); + return factory.create(newBuckets, histo); + } + + @Override + public void doReadFrom(StreamInput in) throws IOException { + formatter = ValueFormatterStreams.readOptional(in); + gapPolicy = GapPolicy.readFrom(in); + window = in.readVInt(); + model = MovAvgModelStreams.read(in); + } + + @Override + public void doWriteTo(StreamOutput out) throws IOException { + ValueFormatterStreams.writeOptional(formatter, out); + gapPolicy.writeTo(out); + out.writeVInt(window); + model.writeTo(out); + } + + public static class Factory extends ReducerFactory { + + private final ValueFormatter formatter; + private GapPolicy gapPolicy; + private int window; + private MovAvgModel model; + + public Factory(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter, GapPolicy gapPolicy, + int window, MovAvgModel model) { + super(name, TYPE.name(), bucketsPaths); + this.formatter = formatter; + this.gapPolicy = gapPolicy; + this.window = window; + this.model = model; + } + + @Override + protected Reducer createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, + Map metaData) throws IOException { + return new MovAvgReducer(name, bucketsPaths, formatter, gapPolicy, window, model, metaData); + } + + @Override + public void doValidate(AggregatorFactory parent, AggregatorFactory[] aggFactories, List reducerFactories) { + if (bucketsPaths.length != 1) { + throw new ElasticsearchIllegalStateException(Reducer.Parser.BUCKETS_PATH.getPreferredName() + + " must contain a single entry for reducer [" + name + "]"); + } + if (!(parent instanceof HistogramAggregator.Factory)) { + throw new ElasticsearchIllegalStateException("derivative reducer [" + name + + "] must have a histogram or date_histogram as parent"); + } else { + HistogramAggregator.Factory histoParent = (HistogramAggregator.Factory) parent; + if (histoParent.minDocCount() != 0) { + throw new ElasticsearchIllegalStateException("parent histogram of derivative reducer [" + name + + "] must have min_doc_count of 0"); + } + } + } + + } +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/DoubleExpModel.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/DoubleExpModel.java new file mode 100644 index 0000000000000..907c23fd213f8 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/DoubleExpModel.java @@ -0,0 +1,194 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.movavg.models; + +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.search.aggregations.reducers.movavg.MovAvgParser; + +import java.io.IOException; +import java.util.*; + +/** + * Calculate a doubly exponential weighted moving average + */ +public class DoubleExpModel extends MovAvgModel { + + protected static final ParseField NAME_FIELD = new ParseField("double_exp"); + + /** + * Controls smoothing of data. Alpha = 1 retains no memory of past values + * (e.g. random walk), while alpha = 0 retains infinite memory of past values (e.g. + * mean of the series). Useful values are somewhere in between + */ + private double alpha; + + /** + * Equivalent to alpha, but controls the smoothing of the trend instead of the data + */ + private double beta; + + public DoubleExpModel(double alpha, double beta) { + this.alpha = alpha; + this.beta = beta; + } + + + @Override + public double next(Collection values) { + return next(values, 1).get(0); + } + + /** + * Calculate a doubly exponential weighted moving average + * + * @param values Collection of values to calculate avg for + * @param numForecasts number of forecasts into the future to return + * + * @param Type T extending Number + * @return Returns a Double containing the moving avg for the window + */ + public List next(Collection values, int numForecasts) { + // Smoothed value + double s = 0; + double last_s = 0; + + // Trend value + double b = 0; + double last_b = 0; + + int counter = 0; + + //TODO bail if too few values + + T last; + for (T v : values) { + last = v; + if (counter == 1) { + s = v.doubleValue(); + b = v.doubleValue() - last.doubleValue(); + } else { + s = alpha * v.doubleValue() + (1.0d - alpha) * (last_s + last_b); + b = beta * (s - last_s) + (1 - beta) * last_b; + } + + counter += 1; + last_s = s; + last_b = b; + } + + List forecastValues = new ArrayList<>(numForecasts); + for (int i = 0; i < numForecasts; i++) { + forecastValues.add(s + (i * b)); + } + + return forecastValues; + } + + public static final MovAvgModelStreams.Stream STREAM = new MovAvgModelStreams.Stream() { + @Override + public MovAvgModel readResult(StreamInput in) throws IOException { + return new DoubleExpModel(in.readDouble(), in.readDouble()); + } + + @Override + public String getName() { + return NAME_FIELD.getPreferredName(); + } + }; + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(STREAM.getName()); + out.writeDouble(alpha); + out.writeDouble(beta); + } + + public static class DoubleExpModelParser implements MovAvgModelParser { + + @Override + public String getName() { + return NAME_FIELD.getPreferredName(); + } + + @Override + public MovAvgModel parse(@Nullable Map settings) { + + Double alpha; + Double beta; + + if (settings == null || (alpha = (Double)settings.get("alpha")) == null) { + alpha = 0.5; + } + + if (settings == null || (beta = (Double)settings.get("beta")) == null) { + beta = 0.5; + } + + return new DoubleExpModel(alpha, beta); + } + } + + public static class DoubleExpModelBuilder implements MovAvgModelBuilder { + + private double alpha = 0.5; + private double beta = 0.5; + + /** + * Alpha controls the smoothing of the data. Alpha = 1 retains no memory of past values + * (e.g. a random walk), while alpha = 0 retains infinite memory of past values (e.g. + * the series mean). Useful values are somewhere in between. Defaults to 0.5. + * + * @param alpha A double between 0-1 inclusive, controls data smoothing + * + * @return The builder to continue chaining + */ + public DoubleExpModelBuilder alpha(double alpha) { + this.alpha = alpha; + return this; + } + + /** + * Equivalent to alpha, but controls the smoothing of the trend instead of the data + * + * @param beta a double between 0-1 inclusive, controls trend smoothing + * + * @return The builder to continue chaining + */ + public DoubleExpModelBuilder beta(double beta) { + this.beta = beta; + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.field(MovAvgParser.MODEL.getPreferredName(), NAME_FIELD.getPreferredName()); + builder.startObject(MovAvgParser.SETTINGS.getPreferredName()); + builder.field("alpha", alpha); + builder.field("beta", beta); + builder.endObject(); + return builder; + } + } +} + diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/LinearModel.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/LinearModel.java new file mode 100644 index 0000000000000..6c269590d33d7 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/LinearModel.java @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.movavg.models; + + +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.search.aggregations.reducers.movavg.MovAvgParser; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; + +/** + * Calculate a linearly weighted moving average, such that older values are + * linearly less important. "Time" is determined by position in collection + */ +public class LinearModel extends MovAvgModel { + + protected static final ParseField NAME_FIELD = new ParseField("linear"); + + @Override + public double next(Collection values) { + double avg = 0; + long totalWeight = 1; + long current = 1; + + for (T v : values) { + avg += v.doubleValue() * current; + totalWeight += current; + current += 1; + } + return avg / totalWeight; + } + + public static final MovAvgModelStreams.Stream STREAM = new MovAvgModelStreams.Stream() { + @Override + public MovAvgModel readResult(StreamInput in) throws IOException { + return new LinearModel(); + } + + @Override + public String getName() { + return NAME_FIELD.getPreferredName(); + } + }; + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(STREAM.getName()); + } + + public static class LinearModelParser implements MovAvgModelParser { + + @Override + public String getName() { + return NAME_FIELD.getPreferredName(); + } + + @Override + public MovAvgModel parse(@Nullable Map settings) { + return new LinearModel(); + } + } + + public static class LinearModelBuilder implements MovAvgModelBuilder { + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.field(MovAvgParser.MODEL.getPreferredName(), NAME_FIELD.getPreferredName()); + return builder; + } + } +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModel.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModel.java new file mode 100644 index 0000000000000..84f7832f893ef --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModel.java @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.movavg.models; + +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.*; + +public abstract class MovAvgModel { + + /** + * Returns the next value in the series, according to the underlying smoothing model + * + * @param values Collection of numerics to smooth, usually windowed + * @param Type of numeric + * @return Returns a double, since most smoothing methods operate on floating points + */ + public abstract double next(Collection values); + + /** + * Write the model to the output stream + * + * @param out Output stream + * @throws IOException + */ + public abstract void writeTo(StreamOutput out) throws IOException; +} + + + + diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelBuilder.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelBuilder.java new file mode 100644 index 0000000000000..96bc9427de3d9 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelBuilder.java @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.movavg.models; + +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; + +/** + * Represents the common interface that all moving average models share. Moving + * average models are used by the MovAvg reducer + */ +public interface MovAvgModelBuilder extends ToXContent { + public abstract XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException; +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelModule.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelModule.java new file mode 100644 index 0000000000000..71ccbcb31b092 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelModule.java @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + + +package org.elasticsearch.search.aggregations.reducers.movavg.models; + +import com.google.common.collect.Lists; +import org.elasticsearch.common.inject.AbstractModule; +import org.elasticsearch.common.inject.multibindings.Multibinder; + +import java.util.List; + +/** + * Register the various model parsers + */ +public class MovAvgModelModule extends AbstractModule { + + private List> parsers = Lists.newArrayList(); + + public MovAvgModelModule() { + registerParser(SimpleModel.SimpleModelParser.class); + registerParser(LinearModel.LinearModelParser.class); + registerParser(SingleExpModel.SingleExpModelParser.class); + registerParser(DoubleExpModel.DoubleExpModelParser.class); + } + + public void registerParser(Class parser) { + parsers.add(parser); + } + + @Override + protected void configure() { + Multibinder parserMapBinder = Multibinder.newSetBinder(binder(), MovAvgModelParser.class); + for (Class clazz : parsers) { + parserMapBinder.addBinding().to(clazz); + } + bind(MovAvgModelParserMapper.class); + } +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelParser.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelParser.java new file mode 100644 index 0000000000000..d27e447baa479 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelParser.java @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.movavg.models; + + +import org.elasticsearch.common.Nullable; + +import java.util.Map; + +/** + * Common interface for parsers used by the various Moving Average models + */ +public interface MovAvgModelParser { + public MovAvgModel parse(@Nullable Map settings); + + public String getName(); +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelParserMapper.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelParserMapper.java new file mode 100644 index 0000000000000..459729d8960c5 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelParserMapper.java @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.movavg.models; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.collect.MapBuilder; +import org.elasticsearch.common.inject.Inject; + +import java.util.Set; + +/** + * Contains a map of all concrete model parsers which can be used to build Models + */ +public class MovAvgModelParserMapper { + + protected ImmutableMap movAvgParsers; + + @Inject + public MovAvgModelParserMapper(Set parsers) { + MapBuilder builder = MapBuilder.newMapBuilder(); + for (MovAvgModelParser parser : parsers) { + builder.put(parser.getName(), parser); + } + movAvgParsers = builder.immutableMap(); + } + + public @Nullable + MovAvgModelParser get(String parserName) { + return movAvgParsers.get(parserName); + } + + public ImmutableSet getAllNames() { + return movAvgParsers.keySet(); + } +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelStreams.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelStreams.java new file mode 100644 index 0000000000000..b11a36870215c --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelStreams.java @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.movavg.models; + +import com.google.common.collect.ImmutableMap; +import org.elasticsearch.common.collect.MapBuilder; +import org.elasticsearch.common.io.stream.StreamInput; + +import java.io.IOException; + +/** + * A registry for all moving average models. This is needed for reading them from a stream without knowing which + * one it is. + */ +public class MovAvgModelStreams { + + private static ImmutableMap STREAMS = ImmutableMap.of(); + + public static MovAvgModel read(StreamInput in) throws IOException { + return stream(in.readString()).readResult(in); + } + + /** + * A stream that knows how to read an heuristic from the input. + */ + public static interface Stream { + + MovAvgModel readResult(StreamInput in) throws IOException; + + String getName(); + } + + /** + * Registers the given stream and associate it with the given types. + * + * @param stream The stream to register + * @param names The names associated with the streams + */ + public static synchronized void registerStream(Stream stream, String... names) { + MapBuilder uStreams = MapBuilder.newMapBuilder(STREAMS); + for (String name : names) { + uStreams.put(name, stream); + } + STREAMS = uStreams.immutableMap(); + } + + /** + * Returns the stream that is registered for the given name + * + * @param name The given name + * @return The associated stream + */ + public static Stream stream(String name) { + return STREAMS.get(name); + } + +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/SimpleModel.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/SimpleModel.java new file mode 100644 index 0000000000000..243b022af2c7e --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/SimpleModel.java @@ -0,0 +1,86 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.movavg.models; + +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.search.aggregations.reducers.movavg.MovAvgParser; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; + +/** + * Calculate a simple unweighted (arithmetic) moving average + */ +public class SimpleModel extends MovAvgModel { + + protected static final ParseField NAME_FIELD = new ParseField("simple"); + + @Override + public double next(Collection values) { + double avg = 0; + for (T v : values) { + avg += v.doubleValue(); + } + return avg / values.size(); + } + + public static final MovAvgModelStreams.Stream STREAM = new MovAvgModelStreams.Stream() { + @Override + public MovAvgModel readResult(StreamInput in) throws IOException { + return new SimpleModel(); + } + + @Override + public String getName() { + return NAME_FIELD.getPreferredName(); + } + }; + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(STREAM.getName()); + } + + public static class SimpleModelParser implements MovAvgModelParser { + + @Override + public String getName() { + return NAME_FIELD.getPreferredName(); + } + + @Override + public MovAvgModel parse(@Nullable Map settings) { + return new SimpleModel(); + } + } + + public static class SimpleModelBuilder implements MovAvgModelBuilder { + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.field(MovAvgParser.MODEL.getPreferredName(), NAME_FIELD.getPreferredName()); + return builder; + } + } +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/SingleExpModel.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/SingleExpModel.java new file mode 100644 index 0000000000000..f17ba68f4984f --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/SingleExpModel.java @@ -0,0 +1,133 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.movavg.models; + +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.search.aggregations.reducers.movavg.MovAvgParser; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; + +/** + * Calculate a exponentially weighted moving average + */ +public class SingleExpModel extends MovAvgModel { + + protected static final ParseField NAME_FIELD = new ParseField("single_exp"); + + /** + * Controls smoothing of data. Alpha = 1 retains no memory of past values + * (e.g. random walk), while alpha = 0 retains infinite memory of past values (e.g. + * mean of the series). Useful values are somewhere in between + */ + private double alpha; + + public SingleExpModel(double alpha) { + this.alpha = alpha; + } + + + @Override + public double next(Collection values) { + double avg = 0; + boolean first = true; + + for (T v : values) { + if (first) { + avg = v.doubleValue(); + first = false; + } else { + avg = (v.doubleValue() * alpha) + (avg * (1 - alpha)); + } + } + return avg; + } + + public static final MovAvgModelStreams.Stream STREAM = new MovAvgModelStreams.Stream() { + @Override + public MovAvgModel readResult(StreamInput in) throws IOException { + return new SingleExpModel(in.readDouble()); + } + + @Override + public String getName() { + return NAME_FIELD.getPreferredName(); + } + }; + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(STREAM.getName()); + out.writeDouble(alpha); + } + + public static class SingleExpModelParser implements MovAvgModelParser { + + @Override + public String getName() { + return NAME_FIELD.getPreferredName(); + } + + @Override + public MovAvgModel parse(@Nullable Map settings) { + + Double alpha; + if (settings == null || (alpha = (Double)settings.get("alpha")) == null) { + alpha = 0.5; + } + + return new SingleExpModel(alpha); + } + } + + public static class SingleExpModelBuilder implements MovAvgModelBuilder { + + private double alpha = 0.5; + + /** + * Alpha controls the smoothing of the data. Alpha = 1 retains no memory of past values + * (e.g. a random walk), while alpha = 0 retains infinite memory of past values (e.g. + * the series mean). Useful values are somewhere in between. Defaults to 0.5. + * + * @param alpha A double between 0-1 inclusive, controls data smoothing + * + * @return The builder to continue chaining + */ + public SingleExpModelBuilder alpha(double alpha) { + this.alpha = alpha; + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.field(MovAvgParser.MODEL.getPreferredName(), NAME_FIELD.getPreferredName()); + builder.startObject(MovAvgParser.SETTINGS.getPreferredName()); + builder.field("alpha", alpha); + builder.endObject(); + return builder; + } + } +} + diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/TransportMovAvgModelModule.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/TransportMovAvgModelModule.java new file mode 100644 index 0000000000000..bc085f6241a6d --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/TransportMovAvgModelModule.java @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.movavg.models; + +import com.google.common.collect.Lists; +import org.elasticsearch.common.inject.AbstractModule; + +import java.util.List; + +/** + * Register the transport streams so that models can be serialized/deserialized from the stream + */ +public class TransportMovAvgModelModule extends AbstractModule { + + private List streams = Lists.newArrayList(); + + public TransportMovAvgModelModule() { + registerStream(SimpleModel.STREAM); + registerStream(LinearModel.STREAM); + registerStream(SingleExpModel.STREAM); + registerStream(DoubleExpModel.STREAM); + } + + public void registerStream(MovAvgModelStreams.Stream stream) { + streams.add(stream); + } + + @Override + protected void configure() { + for (MovAvgModelStreams.Stream stream : streams) { + MovAvgModelStreams.registerStream(stream, stream.getName()); + } + } +} diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/MovAvgTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/MovAvgTests.java new file mode 100644 index 0000000000000..d22656b0ad502 --- /dev/null +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/MovAvgTests.java @@ -0,0 +1,500 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers; + + +import com.google.common.collect.EvictingQueue; +import org.elasticsearch.action.index.IndexRequestBuilder; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; +import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; +import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram.Bucket; +import org.elasticsearch.search.aggregations.reducers.movavg.models.DoubleExpModel; +import org.elasticsearch.search.aggregations.reducers.movavg.models.LinearModel; +import org.elasticsearch.search.aggregations.reducers.movavg.models.SimpleModel; +import org.elasticsearch.search.aggregations.reducers.movavg.models.SingleExpModel; +import org.elasticsearch.test.ElasticsearchIntegrationTest; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.search.aggregations.AggregationBuilders.*; +import static org.elasticsearch.search.aggregations.reducers.ReducerBuilders.smooth; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.core.IsNull.notNullValue; + +@ElasticsearchIntegrationTest.SuiteScopeTest +public class MovAvgTests extends ElasticsearchIntegrationTest { + + private static final String SINGLE_VALUED_FIELD_NAME = "l_value"; + private static final String SINGLE_VALUED_VALUE_FIELD_NAME = "v_value"; + + static int interval; + static int numValueBuckets; + static int numFilledValueBuckets; + static int windowSize; + static BucketHelpers.GapPolicy gapPolicy; + + static long[] docCounts; + static long[] valueCounts; + static Double[] simpleMovAvgCounts; + static Double[] linearMovAvgCounts; + static Double[] singleExpMovAvgCounts; + static Double[] doubleExpMovAvgCounts; + + static Double[] simpleMovAvgValueCounts; + static Double[] linearMovAvgValueCounts; + static Double[] singleExpMovAvgValueCounts; + static Double[] doubleExpMovAvgValueCounts; + + @Override + public void setupSuiteScopeCluster() throws Exception { + createIndex("idx"); + createIndex("idx_unmapped"); + + interval = 5; + numValueBuckets = randomIntBetween(6, 80); + numFilledValueBuckets = numValueBuckets; + windowSize = randomIntBetween(3,10); + gapPolicy = randomBoolean() ? BucketHelpers.GapPolicy.IGNORE : BucketHelpers.GapPolicy.INSERT_ZEROS; + + docCounts = new long[numValueBuckets]; + valueCounts = new long[numValueBuckets]; + for (int i = 0; i < numValueBuckets; i++) { + docCounts[i] = randomIntBetween(0, 20); + valueCounts[i] = randomIntBetween(1,20); //this will be used as a constant for all values within a bucket + } + + this.setupSimple(); + this.setupLinear(); + this.setupSingle(); + this.setupDouble(); + + + List builders = new ArrayList<>(); + for (int i = 0; i < numValueBuckets; i++) { + for (int docs = 0; docs < docCounts[i]; docs++) { + builders.add(client().prepareIndex("idx", "type").setSource(jsonBuilder().startObject() + .field(SINGLE_VALUED_FIELD_NAME, i * interval) + .field(SINGLE_VALUED_VALUE_FIELD_NAME, 1).endObject())); + } + } + + indexRandom(true, builders); + ensureSearchable(); + } + + private void setupSimple() { + simpleMovAvgCounts = new Double[numValueBuckets]; + EvictingQueue window = EvictingQueue.create(windowSize); + for (int i = 0; i < numValueBuckets; i++) { + double thisValue = docCounts[i]; + window.offer(thisValue); + + double movAvg = 0; + for (double value : window) { + movAvg += value; + } + movAvg /= window.size(); + + simpleMovAvgCounts[i] = movAvg; + } + + window.clear(); + simpleMovAvgValueCounts = new Double[numValueBuckets]; + for (int i = 0; i < numValueBuckets; i++) { + window.offer((double)docCounts[i]); + + double movAvg = 0; + for (double value : window) { + movAvg += value; + } + movAvg /= window.size(); + + simpleMovAvgValueCounts[i] = movAvg; + + } + + } + + private void setupLinear() { + EvictingQueue window = EvictingQueue.create(windowSize); + linearMovAvgCounts = new Double[numValueBuckets]; + window.clear(); + for (int i = 0; i < numValueBuckets; i++) { + double thisValue = docCounts[i]; + if (thisValue == -1) { + thisValue = 0; + } + window.offer(thisValue); + + double avg = 0; + long totalWeight = 1; + long current = 1; + + for (double value : window) { + avg += value * current; + totalWeight += current; + current += 1; + } + linearMovAvgCounts[i] = avg / totalWeight; + } + + window.clear(); + linearMovAvgValueCounts = new Double[numValueBuckets]; + + for (int i = 0; i < numValueBuckets; i++) { + double thisValue = docCounts[i]; + window.offer(thisValue); + + double avg = 0; + long totalWeight = 1; + long current = 1; + + for (double value : window) { + avg += value * current; + totalWeight += current; + current += 1; + } + linearMovAvgValueCounts[i] = avg / totalWeight; + } + } + + private void setupSingle() { + EvictingQueue window = EvictingQueue.create(windowSize); + singleExpMovAvgCounts = new Double[numValueBuckets]; + for (int i = 0; i < numValueBuckets; i++) { + double thisValue = docCounts[i]; + if (thisValue == -1) { + thisValue = 0; + } + window.offer(thisValue); + + double avg = 0; + double alpha = 0.5; + boolean first = true; + + for (double value : window) { + if (first) { + avg = value; + first = false; + } else { + avg = (value * alpha) + (avg * (1 - alpha)); + } + } + singleExpMovAvgCounts[i] = avg ; + } + + singleExpMovAvgValueCounts = new Double[numValueBuckets]; + window.clear(); + + for (int i = 0; i < numValueBuckets; i++) { + window.offer((double)docCounts[i]); + + double avg = 0; + double alpha = 0.5; + boolean first = true; + + for (double value : window) { + if (first) { + avg = value; + first = false; + } else { + avg = (value * alpha) + (avg * (1 - alpha)); + } + } + singleExpMovAvgCounts[i] = avg ; + } + + } + + private void setupDouble() { + EvictingQueue window = EvictingQueue.create(windowSize); + doubleExpMovAvgCounts = new Double[numValueBuckets]; + + for (int i = 0; i < numValueBuckets; i++) { + double thisValue = docCounts[i]; + if (thisValue == -1) { + thisValue = 0; + } + window.offer(thisValue); + + double s = 0; + double last_s = 0; + + // Trend value + double b = 0; + double last_b = 0; + + double alpha = 0.5; + double beta = 0.5; + int counter = 0; + + double last; + for (double value : window) { + last = value; + if (counter == 1) { + s = value; + b = value - last; + } else { + s = alpha * value + (1.0d - alpha) * (last_s + last_b); + b = beta * (s - last_s) + (1 - beta) * last_b; + } + + counter += 1; + last_s = s; + last_b = b; + } + + doubleExpMovAvgCounts[i] = s + (0 * b) ; + } + + doubleExpMovAvgValueCounts = new Double[numValueBuckets]; + window.clear(); + + for (int i = 0; i < numValueBuckets; i++) { + window.offer((double)docCounts[i]); + + double s = 0; + double last_s = 0; + + // Trend value + double b = 0; + double last_b = 0; + + double alpha = 0.5; + double beta = 0.5; + int counter = 0; + + double last; + for (double value : window) { + last = value; + if (counter == 1) { + s = value; + b = value - last; + } else { + s = alpha * value + (1.0d - alpha) * (last_s + last_b); + b = beta * (s - last_s) + (1 - beta) * last_b; + } + + counter += 1; + last_s = s; + last_b = b; + } + + doubleExpMovAvgValueCounts[i] = s + (0 * b) ; + } + } + + /** + * test simple moving average on single value field + */ + @Test + public void simpleSingleValuedField() { + + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(smooth("smooth") + .window(windowSize) + .modelBuilder(new SimpleModel.SimpleModelBuilder()) + .gapPolicy(gapPolicy) + .setBucketsPaths("_count")) + .subAggregation(smooth("movavg_values") + .window(windowSize) + .modelBuilder(new SimpleModel.SimpleModelBuilder()) + .gapPolicy(gapPolicy) + .setBucketsPaths("the_sum")) + ).execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram histo = response.getAggregations().get("histo"); + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + assertThat(buckets.size(), equalTo(numValueBuckets)); + + for (int i = 0; i < numValueBuckets; ++i) { + Histogram.Bucket bucket = buckets.get(i); + checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, docCounts[i]); + SimpleValue docCountMovAvg = bucket.getAggregations().get("smooth"); + assertThat(docCountMovAvg, notNullValue()); + assertThat(docCountMovAvg.value(), equalTo(simpleMovAvgCounts[i])); + + SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); + assertThat(valuesMovAvg, notNullValue()); + assertThat(valuesMovAvg.value(), equalTo(simpleMovAvgCounts[i])); + } + } + + /** + * test linear moving average on single value field + */ + @Test + public void linearSingleValuedField() { + + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(smooth("smooth") + .window(windowSize) + .modelBuilder(new LinearModel.LinearModelBuilder()) + .gapPolicy(gapPolicy) + .setBucketsPaths("_count")) + .subAggregation(smooth("movavg_values") + .window(windowSize) + .modelBuilder(new LinearModel.LinearModelBuilder()) + .gapPolicy(gapPolicy) + .setBucketsPaths("the_sum")) + ).execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram histo = response.getAggregations().get("histo"); + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + assertThat(buckets.size(), equalTo(numValueBuckets)); + + for (int i = 0; i < numValueBuckets; ++i) { + Histogram.Bucket bucket = buckets.get(i); + checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, docCounts[i]); + SimpleValue docCountMovAvg = bucket.getAggregations().get("smooth"); + assertThat(docCountMovAvg, notNullValue()); + assertThat(docCountMovAvg.value(), equalTo(linearMovAvgCounts[i])); + + SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); + assertThat(valuesMovAvg, notNullValue()); + assertThat(valuesMovAvg.value(), equalTo(linearMovAvgCounts[i])); + } + } + + /** + * test single exponential moving average on single value field + */ + @Test + public void singleExpSingleValuedField() { + + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(smooth("smooth") + .window(windowSize) + .modelBuilder(new SingleExpModel.SingleExpModelBuilder().alpha(0.5)) + .gapPolicy(gapPolicy) + .setBucketsPaths("_count")) + .subAggregation(smooth("movavg_values") + .window(windowSize) + .modelBuilder(new SingleExpModel.SingleExpModelBuilder().alpha(0.5)) + .gapPolicy(gapPolicy) + .setBucketsPaths("the_sum")) + ).execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram histo = response.getAggregations().get("histo"); + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + assertThat(buckets.size(), equalTo(numValueBuckets)); + + for (int i = 0; i < numValueBuckets; ++i) { + Histogram.Bucket bucket = buckets.get(i); + checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, docCounts[i]); + SimpleValue docCountMovAvg = bucket.getAggregations().get("smooth"); + assertThat(docCountMovAvg, notNullValue()); + assertThat(docCountMovAvg.value(), equalTo(singleExpMovAvgCounts[i])); + + SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); + assertThat(valuesMovAvg, notNullValue()); + assertThat(valuesMovAvg.value(), equalTo(singleExpMovAvgCounts[i])); + } + } + + /** + * test double exponential moving average on single value field + */ + @Test + public void doubleExpSingleValuedField() { + + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(smooth("smooth") + .window(windowSize) + .modelBuilder(new DoubleExpModel.DoubleExpModelBuilder().alpha(0.5).beta(0.5)) + .gapPolicy(gapPolicy) + .setBucketsPaths("_count")) + .subAggregation(smooth("movavg_values") + .window(windowSize) + .modelBuilder(new DoubleExpModel.DoubleExpModelBuilder().alpha(0.5).beta(0.5)) + .gapPolicy(gapPolicy) + .setBucketsPaths("the_sum")) + ).execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram histo = response.getAggregations().get("histo"); + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + assertThat(buckets.size(), equalTo(numValueBuckets)); + + for (int i = 0; i < numValueBuckets; ++i) { + Histogram.Bucket bucket = buckets.get(i); + checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, docCounts[i]); + SimpleValue docCountMovAvg = bucket.getAggregations().get("smooth"); + assertThat(docCountMovAvg, notNullValue()); + assertThat(docCountMovAvg.value(), equalTo(doubleExpMovAvgCounts[i])); + + SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); + assertThat(valuesMovAvg, notNullValue()); + assertThat(valuesMovAvg.value(), equalTo(doubleExpMovAvgCounts[i])); + } + } + + + private void checkBucketKeyAndDocCount(final String msg, final Histogram.Bucket bucket, final long expectedKey, + long expectedDocCount) { + if (expectedDocCount == -1) { + expectedDocCount = 0; + } + assertThat(msg, bucket, notNullValue()); + assertThat(msg + " key", ((Number) bucket.getKey()).longValue(), equalTo(expectedKey)); + assertThat(msg + " docCount", bucket.getDocCount(), equalTo(expectedDocCount)); + } + +} From e19d20b407ce3a652c6a63d0bf335431b1fe0fde Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Tue, 17 Mar 2015 16:36:37 -0700 Subject: [PATCH 39/68] max bucket reducer and sibling reducer framework --- .../percolate/PercolateRequestBuilder.java | 6 +- .../percolate/PercolateShardResponse.java | 31 ++++ .../percolate/PercolateSourceBuilder.java | 5 +- .../action/search/SearchRequestBuilder.java | 1 + .../percolator/PercolatorService.java | 57 +++++-- .../aggregations/AggregationModule.java | 2 + .../search/aggregations/AggregationPhase.java | 22 ++- .../aggregations/AggregatorFactories.java | 18 +- .../TransportAggregationModule.java | 6 +- .../aggregations/reducers/BucketHelpers.java | 14 +- .../search/aggregations/reducers/Reducer.java | 10 ++ .../aggregations/reducers/ReducerBuilder.java | 15 +- .../reducers/ReducerBuilders.java | 5 + .../aggregations/reducers/ReducerFactory.java | 8 +- .../aggregations/reducers/SiblingReducer.java | 65 ++++++++ .../InternalBucketMetricValue.java | 132 +++++++++++++++ .../bucketmetrics/MaxBucketBuilder.java | 48 ++++++ .../bucketmetrics/MaxBucketParser.java | 92 +++++++++++ .../bucketmetrics/MaxBucketReducer.java | 144 ++++++++++++++++ .../derivative/DerivativeReducer.java | 16 +- .../reducers/movavg/MovAvgReducer.java | 17 +- .../search/builder/SearchSourceBuilder.java | 155 +++++++++++------- .../controller/SearchPhaseController.java | 33 +++- .../search/query/QuerySearchResult.java | 36 +++- .../PercolatorFacetsAndAggregationsTests.java | 79 +++++++++ .../aggregations/reducers/MaxBucketTests.java | 123 ++++++++++++++ 26 files changed, 1011 insertions(+), 129 deletions(-) create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/SiblingReducer.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/InternalBucketMetricValue.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketBuilder.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketParser.java create mode 100644 src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketReducer.java create mode 100644 src/test/java/org/elasticsearch/search/aggregations/reducers/MaxBucketTests.java diff --git a/src/main/java/org/elasticsearch/action/percolate/PercolateRequestBuilder.java b/src/main/java/org/elasticsearch/action/percolate/PercolateRequestBuilder.java index e1309a5c09579..732e08ac36bdd 100644 --- a/src/main/java/org/elasticsearch/action/percolate/PercolateRequestBuilder.java +++ b/src/main/java/org/elasticsearch/action/percolate/PercolateRequestBuilder.java @@ -28,7 +28,9 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.query.FilterBuilder; import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.search.aggregations.AbstractAggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilder; +import org.elasticsearch.search.aggregations.reducers.ReducerBuilder; import org.elasticsearch.search.highlight.HighlightBuilder; import org.elasticsearch.search.sort.SortBuilder; @@ -162,9 +164,9 @@ public PercolateRequestBuilder setHighlightBuilder(HighlightBuilder highlightBui } /** - * Delegates to {@link PercolateSourceBuilder#addAggregation(AggregationBuilder)} + * Delegates to {@link PercolateSourceBuilder#addAggregation(AbstractAggregationBuilder)} */ - public PercolateRequestBuilder addAggregation(AggregationBuilder aggregationBuilder) { + public PercolateRequestBuilder addAggregation(AbstractAggregationBuilder aggregationBuilder) { sourceBuilder().addAggregation(aggregationBuilder); return this; } diff --git a/src/main/java/org/elasticsearch/action/percolate/PercolateShardResponse.java b/src/main/java/org/elasticsearch/action/percolate/PercolateShardResponse.java index a0763fec33c16..6507dac66036d 100644 --- a/src/main/java/org/elasticsearch/action/percolate/PercolateShardResponse.java +++ b/src/main/java/org/elasticsearch/action/percolate/PercolateShardResponse.java @@ -19,13 +19,18 @@ package org.elasticsearch.action.percolate; import com.google.common.collect.ImmutableList; + import org.apache.lucene.util.BytesRef; import org.elasticsearch.action.support.broadcast.BroadcastShardOperationResponse; +import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.percolator.PercolateContext; import org.elasticsearch.search.aggregations.InternalAggregations; +import org.elasticsearch.search.aggregations.reducers.Reducer; +import org.elasticsearch.search.aggregations.reducers.ReducerStreams; +import org.elasticsearch.search.aggregations.reducers.SiblingReducer; import org.elasticsearch.search.highlight.HighlightField; import org.elasticsearch.search.query.QuerySearchResult; @@ -51,6 +56,7 @@ public class PercolateShardResponse extends BroadcastShardOperationResponse { private int requestedSize; private InternalAggregations aggregations; + private List reducers; PercolateShardResponse() { hls = new ArrayList<>(); @@ -69,6 +75,7 @@ public PercolateShardResponse(BytesRef[] matches, List reducers() { + return reducers; + } + public byte percolatorTypeId() { return percolatorTypeId; } @@ -144,6 +155,16 @@ public void readFrom(StreamInput in) throws IOException { hls.add(fields); } aggregations = InternalAggregations.readOptionalAggregations(in); + if (in.readBoolean()) { + int reducersSize = in.readVInt(); + List reducers = new ArrayList<>(reducersSize); + for (int i = 0; i < reducersSize; i++) { + BytesReference type = in.readBytesReference(); + Reducer reducer = ReducerStreams.stream(type).readResult(in); + reducers.add((SiblingReducer) reducer); + } + this.reducers = reducers; + } } @Override @@ -169,5 +190,15 @@ public void writeTo(StreamOutput out) throws IOException { } } out.writeOptionalStreamable(aggregations); + if (reducers == null) { + out.writeBoolean(false); + } else { + out.writeBoolean(true); + out.writeVInt(reducers.size()); + for (Reducer reducer : reducers) { + out.writeBytesReference(reducer.type().stream()); + reducer.writeTo(out); + } + } } } diff --git a/src/main/java/org/elasticsearch/action/percolate/PercolateSourceBuilder.java b/src/main/java/org/elasticsearch/action/percolate/PercolateSourceBuilder.java index f09e630f45986..68fc57b2a17f6 100644 --- a/src/main/java/org/elasticsearch/action/percolate/PercolateSourceBuilder.java +++ b/src/main/java/org/elasticsearch/action/percolate/PercolateSourceBuilder.java @@ -29,6 +29,7 @@ import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.search.aggregations.AbstractAggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilder; +import org.elasticsearch.search.aggregations.reducers.ReducerBuilder; import org.elasticsearch.search.highlight.HighlightBuilder; import org.elasticsearch.search.sort.ScoreSortBuilder; import org.elasticsearch.search.sort.SortBuilder; @@ -50,7 +51,7 @@ public class PercolateSourceBuilder implements ToXContent { private List sorts; private Boolean trackScores; private HighlightBuilder highlightBuilder; - private List aggregations; + private List aggregations; /** * Sets the document to run the percolate queries against. @@ -130,7 +131,7 @@ public PercolateSourceBuilder setHighlightBuilder(HighlightBuilder highlightBuil /** * Add an aggregation definition. */ - public PercolateSourceBuilder addAggregation(AggregationBuilder aggregationBuilder) { + public PercolateSourceBuilder addAggregation(AbstractAggregationBuilder aggregationBuilder) { if (aggregations == null) { aggregations = Lists.newArrayList(); } diff --git a/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java b/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java index 59d6db804b075..fcead5866b7c8 100644 --- a/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java +++ b/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java @@ -33,6 +33,7 @@ import org.elasticsearch.script.ScriptService; import org.elasticsearch.search.Scroll; import org.elasticsearch.search.aggregations.AbstractAggregationBuilder; +import org.elasticsearch.search.aggregations.reducers.ReducerBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.fetch.innerhits.InnerHitsBuilder; import org.elasticsearch.search.highlight.HighlightBuilder; diff --git a/src/main/java/org/elasticsearch/percolator/PercolatorService.java b/src/main/java/org/elasticsearch/percolator/PercolatorService.java index f19b3b076e750..cd5db78226dae 100644 --- a/src/main/java/org/elasticsearch/percolator/PercolatorService.java +++ b/src/main/java/org/elasticsearch/percolator/PercolatorService.java @@ -19,11 +19,20 @@ package org.elasticsearch.percolator; import com.carrotsearch.hppc.ByteObjectOpenHashMap; +import com.google.common.collect.Lists; + import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.ReaderUtil; import org.apache.lucene.index.memory.ExtendedMemoryIndex; import org.apache.lucene.index.memory.MemoryIndex; -import org.apache.lucene.search.*; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.ConstantScoreQuery; +import org.apache.lucene.search.Filter; +import org.apache.lucene.search.FilteredQuery; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.TopDocs; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.CloseableThreadLocal; import org.elasticsearch.ElasticsearchException; @@ -58,20 +67,30 @@ import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.fielddata.IndexFieldData; import org.elasticsearch.index.fielddata.SortedBinaryDocValues; -import org.elasticsearch.index.mapper.*; +import org.elasticsearch.index.mapper.DocumentMapper; +import org.elasticsearch.index.mapper.FieldMapper; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.mapper.ParsedDocument; +import org.elasticsearch.index.mapper.Uid; import org.elasticsearch.index.mapper.internal.UidFieldMapper; import org.elasticsearch.index.percolator.stats.ShardPercolateService; import org.elasticsearch.index.query.ParsedQuery; import org.elasticsearch.index.search.nested.NonNestedDocsFilter; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.indices.IndicesService; -import org.elasticsearch.percolator.QueryCollector.*; +import org.elasticsearch.percolator.QueryCollector.Count; +import org.elasticsearch.percolator.QueryCollector.Match; +import org.elasticsearch.percolator.QueryCollector.MatchAndScore; +import org.elasticsearch.percolator.QueryCollector.MatchAndSort; import org.elasticsearch.script.ScriptService; import org.elasticsearch.search.SearchParseElement; import org.elasticsearch.search.SearchShardTarget; import org.elasticsearch.search.aggregations.AggregationPhase; +import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; import org.elasticsearch.search.aggregations.InternalAggregations; +import org.elasticsearch.search.aggregations.reducers.Reducer; +import org.elasticsearch.search.aggregations.reducers.SiblingReducer; import org.elasticsearch.search.highlight.HighlightField; import org.elasticsearch.search.highlight.HighlightPhase; import org.elasticsearch.search.internal.SearchContext; @@ -83,7 +102,9 @@ import java.util.Map; import static org.elasticsearch.index.mapper.SourceToParse.source; -import static org.elasticsearch.percolator.QueryCollector.*; +import static org.elasticsearch.percolator.QueryCollector.count; +import static org.elasticsearch.percolator.QueryCollector.match; +import static org.elasticsearch.percolator.QueryCollector.matchAndScore; public class PercolatorService extends AbstractComponent { @@ -826,15 +847,29 @@ private InternalAggregations reduceAggregations(List sha return null; } + InternalAggregations aggregations; if (shardResults.size() == 1) { - return shardResults.get(0).aggregations(); - } - - List aggregationsList = new ArrayList<>(shardResults.size()); - for (PercolateShardResponse shardResult : shardResults) { - aggregationsList.add(shardResult.aggregations()); + aggregations = shardResults.get(0).aggregations(); + } else { + List aggregationsList = new ArrayList<>(shardResults.size()); + for (PercolateShardResponse shardResult : shardResults) { + aggregationsList.add(shardResult.aggregations()); + } + aggregations = InternalAggregations.reduce(aggregationsList, new ReduceContext(null, bigArrays, scriptService)); + } + if (aggregations != null) { + List reducers = shardResults.get(0).reducers(); + if (reducers != null) { + List newAggs = new ArrayList<>(Lists.transform(aggregations.asList(), Reducer.AGGREGATION_TRANFORM_FUNCTION)); + for (SiblingReducer reducer : reducers) { + InternalAggregation newAgg = reducer.doReduce(new InternalAggregations(newAggs), new ReduceContext(null, bigArrays, + scriptService)); + newAggs.add(newAgg); + } + aggregations = new InternalAggregations(newAggs); + } } - return InternalAggregations.reduce(aggregationsList, new ReduceContext(null, bigArrays, scriptService)); + return aggregations; } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregationModule.java b/src/main/java/org/elasticsearch/search/aggregations/AggregationModule.java index a8d3895ec780e..e9da6e719b989 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregationModule.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregationModule.java @@ -56,6 +56,7 @@ import org.elasticsearch.search.aggregations.metrics.tophits.TopHitsParser; import org.elasticsearch.search.aggregations.metrics.valuecount.ValueCountParser; import org.elasticsearch.search.aggregations.reducers.Reducer; +import org.elasticsearch.search.aggregations.reducers.bucketmetrics.MaxBucketParser; import org.elasticsearch.search.aggregations.reducers.derivative.DerivativeParser; import org.elasticsearch.search.aggregations.reducers.movavg.MovAvgParser; import org.elasticsearch.search.aggregations.reducers.movavg.models.MovAvgModelModule; @@ -103,6 +104,7 @@ public AggregationModule() { aggParsers.add(ChildrenParser.class); reducerParsers.add(DerivativeParser.class); + reducerParsers.add(MaxBucketParser.class); reducerParsers.add(MovAvgParser.class); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregationPhase.java b/src/main/java/org/elasticsearch/search/aggregations/AggregationPhase.java index 9d627310142c0..dd915b80f8758 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregationPhase.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregationPhase.java @@ -30,6 +30,8 @@ import org.elasticsearch.search.SearchParseElement; import org.elasticsearch.search.SearchPhase; import org.elasticsearch.search.aggregations.bucket.global.GlobalAggregator; +import org.elasticsearch.search.aggregations.reducers.Reducer; +import org.elasticsearch.search.aggregations.reducers.SiblingReducer; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.search.query.QueryPhaseExecutionException; @@ -74,8 +76,11 @@ public void preProcess(SearchContext context) { List collectors = new ArrayList<>(); Aggregator[] aggregators; + List reducers; try { - aggregators = context.aggregations().factories().createTopLevelAggregators(aggregationContext); + AggregatorFactories factories = context.aggregations().factories(); + aggregators = factories.createTopLevelAggregators(aggregationContext); + reducers = factories.createReducers(); } catch (IOException e) { throw new AggregationInitializationException("Could not initialize aggregators", e); } @@ -136,6 +141,21 @@ public void execute(SearchContext context) throws ElasticsearchException { } } context.queryResult().aggregations(new InternalAggregations(aggregations)); + try { + List reducers = context.aggregations().factories().createReducers(); + List siblingReducers = new ArrayList<>(reducers.size()); + for (Reducer reducer : reducers) { + if (reducer instanceof SiblingReducer) { + siblingReducers.add((SiblingReducer) reducer); + } else { + throw new AggregationExecutionException("Invalid reducer named [" + reducer.name() + "] of type [" + + reducer.type().name() + "]. Only sibling reducers are allowed at the top level"); + } + } + context.queryResult().reducers(siblingReducers); + } catch (IOException e) { + throw new AggregationExecutionException("Failed to build top level reducers", e); + } // disable aggregations so that they don't run on next pages in case of scrolling context.aggregations(null); diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java index 1a4c157da8e7b..1a4dcd4f177b6 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java @@ -56,7 +56,7 @@ private AggregatorFactories(AggregatorFactory[] factories, List public List createReducers() throws IOException { List reducers = new ArrayList<>(); for (ReducerFactory factory : this.reducerFactories) { - reducers.add(factory.create(null, null, false)); // NOCOMIT add context, parent etc. + reducers.add(factory.create()); } return reducers; } @@ -213,14 +213,18 @@ private void resolveReducerOrder(Set aggFactoryNames, Map'); + String firstAggName = aggSepIndex == -1 ? bucketsPath : bucketsPath.substring(0, aggSepIndex); + if (bucketsPath.equals("_count") || bucketsPath.equals("_key") || aggFactoryNames.contains(firstAggName)) { continue; - } else if (matchingFactory != null) { - resolveReducerOrder(aggFactoryNames, reducerFactoriesMap, orderedReducers, unmarkedFactories, temporarilyMarked, - matchingFactory); } else { - throw new ElasticsearchIllegalStateException("No reducer found for path [" + bucketsPath + "]"); + ReducerFactory matchingFactory = reducerFactoriesMap.get(firstAggName); + if (matchingFactory != null) { + resolveReducerOrder(aggFactoryNames, reducerFactoriesMap, orderedReducers, unmarkedFactories, + temporarilyMarked, matchingFactory); + } else { + throw new ElasticsearchIllegalStateException("No aggregation found for path [" + bucketsPath + "]"); + } } } unmarkedFactories.remove(factory); diff --git a/src/main/java/org/elasticsearch/search/aggregations/TransportAggregationModule.java b/src/main/java/org/elasticsearch/search/aggregations/TransportAggregationModule.java index c3d89cf4f8fd1..d405db6c741fa 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/TransportAggregationModule.java +++ b/src/main/java/org/elasticsearch/search/aggregations/TransportAggregationModule.java @@ -58,6 +58,8 @@ import org.elasticsearch.search.aggregations.metrics.tophits.InternalTopHits; import org.elasticsearch.search.aggregations.metrics.valuecount.InternalValueCount; import org.elasticsearch.search.aggregations.reducers.InternalSimpleValue; +import org.elasticsearch.search.aggregations.reducers.bucketmetrics.InternalBucketMetricValue; +import org.elasticsearch.search.aggregations.reducers.bucketmetrics.MaxBucketReducer; import org.elasticsearch.search.aggregations.reducers.derivative.DerivativeReducer; import org.elasticsearch.search.aggregations.reducers.movavg.MovAvgReducer; import org.elasticsearch.search.aggregations.reducers.movavg.models.TransportMovAvgModelModule; @@ -106,10 +108,12 @@ protected void configure() { InternalTopHits.registerStreams(); InternalGeoBounds.registerStream(); InternalChildren.registerStream(); - InternalSimpleValue.registerStreams(); // Reducers DerivativeReducer.registerStreams(); + InternalSimpleValue.registerStreams(); + InternalBucketMetricValue.registerStreams(); + MaxBucketReducer.registerStreams(); MovAvgReducer.registerStreams(); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java index f92a2b70d3b20..30d6fc0107eb9 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java @@ -25,8 +25,8 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.search.SearchParseException; import org.elasticsearch.search.aggregations.AggregationExecutionException; +import org.elasticsearch.search.aggregations.InternalMultiBucketAggregation; import org.elasticsearch.search.aggregations.InvalidAggregationPathException; -import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; import org.elasticsearch.search.aggregations.reducers.derivative.DerivativeParser; import org.elasticsearch.search.aggregations.support.AggregationPath; @@ -143,10 +143,16 @@ public String getName() { * @param gapPolicy The gap policy to apply if empty buckets are found * @return The value extracted from bucket found at aggPath */ - public static Double resolveBucketValue(InternalHistogram histo, InternalHistogram.Bucket bucket, - String aggPath, GapPolicy gapPolicy) { + public static Double resolveBucketValue(InternalMultiBucketAggregation agg, + InternalMultiBucketAggregation.Bucket bucket, String aggPath, GapPolicy gapPolicy) { + List aggPathsList = AggregationPath.parse(aggPath).getPathElementsAsStringList(); + return resolveBucketValue(agg, bucket, aggPathsList, gapPolicy); + } + + public static Double resolveBucketValue(InternalMultiBucketAggregation agg, + InternalMultiBucketAggregation.Bucket bucket, List aggPathsList, GapPolicy gapPolicy) { try { - Object propertyValue = bucket.getProperty(histo.getName(), AggregationPath.parse(aggPath).getPathElementsAsStringList()); + Object propertyValue = bucket.getProperty(agg.getName(), aggPathsList); if (propertyValue == null) { throw new AggregationExecutionException(DerivativeParser.BUCKETS_PATH.getPreferredName() + " must reference either a number value or a single value numeric metric aggregation"); diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java index ed602b3175109..3c0b4fdbe221e 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java @@ -19,11 +19,14 @@ package org.elasticsearch.search.aggregations.reducers; +import com.google.common.base.Function; + import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Streamable; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.search.aggregations.Aggregation; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; import org.elasticsearch.search.aggregations.InternalAggregation.Type; @@ -66,6 +69,13 @@ public static interface Parser { } + public static final Function AGGREGATION_TRANFORM_FUNCTION = new Function() { + @Override + public InternalAggregation apply(Aggregation input) { + return (InternalAggregation) input; + } + }; + private String name; private String[] bucketsPaths; private Map metaData; diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilder.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilder.java index 0f0f9225635f7..4dee8ea96a2b7 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilder.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilder.java @@ -21,6 +21,7 @@ import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.search.aggregations.AbstractAggregationBuilder; import java.io.IOException; import java.util.Map; @@ -28,10 +29,8 @@ /** * A base class for all reducer builders. */ -public abstract class ReducerBuilder> implements ToXContent { +public abstract class ReducerBuilder> extends AbstractAggregationBuilder { - private final String name; - protected final String type; private String[] bucketsPaths; private Map metaData; @@ -39,15 +38,7 @@ public abstract class ReducerBuilder> implements ToX * Sole constructor, typically used by sub-classes. */ protected ReducerBuilder(String name, String type) { - this.name = name; - this.type = type; - } - - /** - * Return the name of the reducer that is being built. - */ - public String getName() { - return name; + super(name, type); } /** diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilders.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilders.java index 0aa8be4e99213..3f45964153baf 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilders.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilders.java @@ -19,6 +19,7 @@ package org.elasticsearch.search.aggregations.reducers; +import org.elasticsearch.search.aggregations.reducers.bucketmetrics.MaxBucketBuilder; import org.elasticsearch.search.aggregations.reducers.derivative.DerivativeBuilder; import org.elasticsearch.search.aggregations.reducers.movavg.MovAvgBuilder; @@ -31,6 +32,10 @@ public static final DerivativeBuilder derivative(String name) { return new DerivativeBuilder(name); } + public static final MaxBucketBuilder maxBucket(String name) { + return new MaxBucketBuilder(name); + } + public static final MovAvgBuilder smooth(String name) { return new MovAvgBuilder(name); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java index ccdd2ac0328af..46ac844808c45 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerFactory.java @@ -20,7 +20,6 @@ import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorFactory; -import org.elasticsearch.search.aggregations.support.AggregationContext; import java.io.IOException; import java.util.List; @@ -62,8 +61,7 @@ public final void validate(AggregatorFactory parent, AggregatorFactory[] factori doValidate(parent, factories, reducerFactories); } - protected abstract Reducer createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, - Map metaData) throws IOException; + protected abstract Reducer createInternal(Map metaData) throws IOException; /** * Creates the reducer @@ -81,8 +79,8 @@ protected abstract Reducer createInternal(AggregationContext context, Aggregator * * @return The created aggregator */ - public final Reducer create(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket) throws IOException { - Reducer aggregator = createInternal(context, parent, collectsFromSingleBucket, this.metaData); + public final Reducer create() throws IOException { + Reducer aggregator = createInternal(this.metaData); return aggregator; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/SiblingReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/SiblingReducer.java new file mode 100644 index 0000000000000..b0be9634ddcba --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/SiblingReducer.java @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers; + +import com.google.common.collect.Lists; + +import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; +import org.elasticsearch.search.aggregations.InternalAggregations; +import org.elasticsearch.search.aggregations.InternalMultiBucketAggregation; +import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation.Bucket; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public abstract class SiblingReducer extends Reducer { + + protected SiblingReducer() { // for Serialisation + super(); + } + + protected SiblingReducer(String name, String[] bucketsPaths, Map metaData) { + super(name, bucketsPaths, metaData); + } + + @SuppressWarnings("unchecked") + @Override + public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext reduceContext) { + @SuppressWarnings("rawtypes") + InternalMultiBucketAggregation multiBucketsAgg = (InternalMultiBucketAggregation) aggregation; + List buckets = multiBucketsAgg.getBuckets(); + List newBuckets = new ArrayList<>(); + for (int i = 0; i < buckets.size(); i++) { + InternalMultiBucketAggregation.InternalBucket bucket = (InternalMultiBucketAggregation.InternalBucket) buckets.get(i); + InternalAggregation aggToAdd = doReduce(bucket.getAggregations(), reduceContext); + List aggs = new ArrayList<>(Lists.transform(bucket.getAggregations().asList(), AGGREGATION_TRANFORM_FUNCTION)); + aggs.add(aggToAdd); + InternalMultiBucketAggregation.InternalBucket newBucket = multiBucketsAgg.createBucket(new InternalAggregations(aggs), bucket); + newBuckets.add(newBucket); + } + + return multiBucketsAgg.create(newBuckets); + } + + public abstract InternalAggregation doReduce(Aggregations aggregations, ReduceContext context); +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/InternalBucketMetricValue.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/InternalBucketMetricValue.java new file mode 100644 index 0000000000000..69b23ae91ef14 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/InternalBucketMetricValue.java @@ -0,0 +1,132 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.bucketmetrics; + +import org.elasticsearch.ElasticsearchIllegalArgumentException; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.search.aggregations.AggregationStreams; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; +import org.elasticsearch.search.aggregations.reducers.Reducer; +import org.elasticsearch.search.aggregations.support.format.ValueFormatter; +import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public class InternalBucketMetricValue extends InternalNumericMetricsAggregation.SingleValue { + + public final static Type TYPE = new Type("bucket_metric_value"); + + public final static AggregationStreams.Stream STREAM = new AggregationStreams.Stream() { + @Override + public InternalBucketMetricValue readResult(StreamInput in) throws IOException { + InternalBucketMetricValue result = new InternalBucketMetricValue(); + result.readFrom(in); + return result; + } + }; + + public static void registerStreams() { + AggregationStreams.registerStream(STREAM, TYPE.stream()); + } + + private double value; + + private String[] keys; + + protected InternalBucketMetricValue() { + super(); + } + + public InternalBucketMetricValue(String name, String[] keys, double value, @Nullable ValueFormatter formatter, + List reducers, Map metaData) { + super(name, reducers, metaData); + this.keys = keys; + this.value = value; + this.valueFormatter = formatter; + } + + @Override + public Type type() { + return TYPE; + } + + @Override + public double value() { + return value; + } + + public String[] keys() { + return keys; + } + + @Override + public InternalAggregation doReduce(ReduceContext reduceContext) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object getProperty(List path) { + if (path.isEmpty()) { + return this; + } else if (path.size() == 1 && "value".equals(path.get(0))) { + return value(); + } else if (path.size() == 1 && "keys".equals(path.get(0))) { + return keys(); + } else { + throw new ElasticsearchIllegalArgumentException("path not supported for [" + getName() + "]: " + path); + } + } + + @Override + protected void doReadFrom(StreamInput in) throws IOException { + valueFormatter = ValueFormatterStreams.readOptional(in); + value = in.readDouble(); + keys = in.readStringArray(); + } + + @Override + protected void doWriteTo(StreamOutput out) throws IOException { + ValueFormatterStreams.writeOptional(valueFormatter, out); + out.writeDouble(value); + out.writeStringArray(keys); + } + + @Override + public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { + boolean hasValue = !Double.isInfinite(value); + builder.field(CommonFields.VALUE, hasValue ? value : null); + if (hasValue && valueFormatter != null) { + builder.field(CommonFields.VALUE_AS_STRING, valueFormatter.format(value)); + } + builder.startArray("keys"); + for (String key : keys) { + builder.value(key); + } + builder.endArray(); + return builder; + } + +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketBuilder.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketBuilder.java new file mode 100644 index 0000000000000..eb04617e548ff --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketBuilder.java @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.bucketmetrics; + +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.search.aggregations.reducers.ReducerBuilder; + +import java.io.IOException; + +public class MaxBucketBuilder extends ReducerBuilder { + + private String format; + + public MaxBucketBuilder(String name) { + super(name, MaxBucketReducer.TYPE.name()); + } + + public MaxBucketBuilder format(String format) { + this.format = format; + return this; + } + + @Override + protected XContentBuilder internalXContent(XContentBuilder builder, Params params) throws IOException { + if (format != null) { + builder.field(MaxBucketParser.FORMAT.getPreferredName(), format); + } + return builder; + } + +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketParser.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketParser.java new file mode 100644 index 0000000000000..2a9dab3b6bdb1 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketParser.java @@ -0,0 +1,92 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.bucketmetrics; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.search.SearchParseException; +import org.elasticsearch.search.aggregations.reducers.Reducer; +import org.elasticsearch.search.aggregations.reducers.ReducerFactory; +import org.elasticsearch.search.aggregations.support.format.ValueFormat; +import org.elasticsearch.search.aggregations.support.format.ValueFormatter; +import org.elasticsearch.search.internal.SearchContext; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class MaxBucketParser implements Reducer.Parser { + public static final ParseField FORMAT = new ParseField("format"); + + @Override + public String type() { + return MaxBucketReducer.TYPE.name(); + } + + @Override + public ReducerFactory parse(String reducerName, XContentParser parser, SearchContext context) throws IOException { + XContentParser.Token token; + String currentFieldName = null; + String[] bucketsPaths = null; + String format = null; + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.VALUE_STRING) { + if (FORMAT.match(currentFieldName)) { + format = parser.text(); + } else if (BUCKETS_PATH.match(currentFieldName)) { + bucketsPaths = new String[] { parser.text() }; + } else { + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + reducerName + "]: [" + + currentFieldName + "]."); + } + } else if (token == XContentParser.Token.START_ARRAY) { + if (BUCKETS_PATH.match(currentFieldName)) { + List paths = new ArrayList<>(); + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + String path = parser.text(); + paths.add(path); + } + bucketsPaths = paths.toArray(new String[paths.size()]); + } else { + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + reducerName + "]: [" + + currentFieldName + "]."); + } + } else { + throw new SearchParseException(context, "Unexpected token " + token + " in [" + reducerName + "]."); + } + } + + if (bucketsPaths == null) { + throw new SearchParseException(context, "Missing required field [" + BUCKETS_PATH.getPreferredName() + + "] for derivative aggregation [" + reducerName + "]"); + } + + ValueFormatter formatter = null; + if (format != null) { + formatter = ValueFormat.Patternable.Number.format(format).formatter(); + } + + return new MaxBucketReducer.Factory(reducerName, bucketsPaths, formatter); + } + +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketReducer.java new file mode 100644 index 0000000000000..e209684797c61 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketReducer.java @@ -0,0 +1,144 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.bucketmetrics; + +import org.elasticsearch.ElasticsearchIllegalStateException; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.search.aggregations.Aggregation; +import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.AggregatorFactory; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; +import org.elasticsearch.search.aggregations.InternalAggregation.Type; +import org.elasticsearch.search.aggregations.InternalMultiBucketAggregation; +import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation.Bucket; +import org.elasticsearch.search.aggregations.reducers.BucketHelpers; +import org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; +import org.elasticsearch.search.aggregations.reducers.Reducer; +import org.elasticsearch.search.aggregations.reducers.ReducerFactory; +import org.elasticsearch.search.aggregations.reducers.ReducerStreams; +import org.elasticsearch.search.aggregations.reducers.SiblingReducer; +import org.elasticsearch.search.aggregations.support.AggregationPath; +import org.elasticsearch.search.aggregations.support.format.ValueFormatter; +import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class MaxBucketReducer extends SiblingReducer { + + public final static Type TYPE = new Type("max_bucket"); + + public final static ReducerStreams.Stream STREAM = new ReducerStreams.Stream() { + @Override + public MaxBucketReducer readResult(StreamInput in) throws IOException { + MaxBucketReducer result = new MaxBucketReducer(); + result.readFrom(in); + return result; + } + }; + + private ValueFormatter formatter; + + public static void registerStreams() { + ReducerStreams.registerStream(STREAM, TYPE.stream()); + } + + private MaxBucketReducer() { + } + + protected MaxBucketReducer(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter, Map metaData) { + super(name, bucketsPaths, metaData); + this.formatter = formatter; + } + + @Override + public Type type() { + return TYPE; + } + + public InternalAggregation doReduce(Aggregations aggregations, ReduceContext context) { + List maxBucketKeys = new ArrayList<>(); + double maxValue = Double.NEGATIVE_INFINITY; + List bucketsPath = AggregationPath.parse(bucketsPaths()[0]).getPathElementsAsStringList(); + for (Aggregation aggregation : aggregations) { + if (aggregation.getName().equals(bucketsPath.get(0))) { + bucketsPath = bucketsPath.subList(1, bucketsPath.size()); + InternalMultiBucketAggregation multiBucketsAgg = (InternalMultiBucketAggregation) aggregation; + List buckets = multiBucketsAgg.getBuckets(); + for (int i = 0; i < buckets.size(); i++) { + Bucket bucket = buckets.get(i); + Double bucketValue = BucketHelpers.resolveBucketValue(multiBucketsAgg, bucket, bucketsPath, GapPolicy.IGNORE); + if (bucketValue != null) { + if (bucketValue > maxValue) { + maxBucketKeys.clear(); + maxBucketKeys.add(bucket.getKeyAsString()); + maxValue = bucketValue; + } else if (bucketValue.equals(maxValue)) { + maxBucketKeys.add(bucket.getKeyAsString()); + } + } + } + } + } + String[] keys = maxBucketKeys.toArray(new String[maxBucketKeys.size()]); + return new InternalBucketMetricValue(name(), keys, maxValue, formatter, Collections.EMPTY_LIST, metaData()); + } + + @Override + public void doReadFrom(StreamInput in) throws IOException { + formatter = ValueFormatterStreams.readOptional(in); + } + + @Override + public void doWriteTo(StreamOutput out) throws IOException { + ValueFormatterStreams.writeOptional(formatter, out); + } + + public static class Factory extends ReducerFactory { + + private final ValueFormatter formatter; + + public Factory(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter) { + super(name, TYPE.name(), bucketsPaths); + this.formatter = formatter; + } + + @Override + protected Reducer createInternal(Map metaData) throws IOException { + return new MaxBucketReducer(name, bucketsPaths, formatter, metaData); + } + + @Override + public void doValidate(AggregatorFactory parent, AggregatorFactory[] aggFactories, List reducerFactories) { + if (bucketsPaths.length != 1) { + throw new ElasticsearchIllegalStateException(Reducer.Parser.BUCKETS_PATH.getPreferredName() + + " must contain a single entry for reducer [" + name + "]"); + } + } + + } + +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java index 40a5b005560d4..a58d0f0e74e2b 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java @@ -19,15 +19,12 @@ package org.elasticsearch.search.aggregations.reducers.derivative; -import com.google.common.base.Function; import com.google.common.collect.Lists; import org.elasticsearch.ElasticsearchIllegalStateException; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.search.aggregations.Aggregation; -import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorFactory; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; @@ -40,7 +37,6 @@ import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.reducers.ReducerFactory; import org.elasticsearch.search.aggregations.reducers.ReducerStreams; -import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; @@ -68,13 +64,6 @@ public static void registerStreams() { ReducerStreams.registerStream(STREAM, TYPE.stream()); } - private static final Function FUNCTION = new Function() { - @Override - public InternalAggregation apply(Aggregation input) { - return (InternalAggregation) input; - } - }; - private ValueFormatter formatter; private GapPolicy gapPolicy; @@ -106,7 +95,7 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext if (lastBucketValue != null) { double diff = thisBucketValue - lastBucketValue; - List aggs = new ArrayList<>(Lists.transform(bucket.getAggregations().asList(), FUNCTION)); + List aggs = new ArrayList<>(Lists.transform(bucket.getAggregations().asList(), AGGREGATION_TRANFORM_FUNCTION)); aggs.add(new InternalSimpleValue(name(), diff, formatter, new ArrayList(), metaData())); InternalHistogram.Bucket newBucket = factory.createBucket(bucket.getKey(), bucket.getDocCount(), new InternalAggregations( aggs), bucket.getKeyed(), bucket.getFormatter()); @@ -143,8 +132,7 @@ public Factory(String name, String[] bucketsPaths, @Nullable ValueFormatter form } @Override - protected Reducer createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, - Map metaData) throws IOException { + protected Reducer createInternal(Map metaData) throws IOException { return new DerivativeReducer(name, bucketsPaths, formatter, gapPolicy, metaData); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgReducer.java index b339cdf487def..20baa1706f173 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgReducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgReducer.java @@ -22,19 +22,26 @@ import com.google.common.base.Function; import com.google.common.collect.EvictingQueue; import com.google.common.collect.Lists; + import org.elasticsearch.ElasticsearchIllegalStateException; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.search.aggregations.*; +import org.elasticsearch.search.aggregations.Aggregation; +import org.elasticsearch.search.aggregations.AggregatorFactory; +import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; import org.elasticsearch.search.aggregations.InternalAggregation.Type; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.histogram.HistogramAggregator; import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; -import org.elasticsearch.search.aggregations.reducers.*; +import org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; +import org.elasticsearch.search.aggregations.reducers.InternalSimpleValue; +import org.elasticsearch.search.aggregations.reducers.Reducer; +import org.elasticsearch.search.aggregations.reducers.ReducerFactory; +import org.elasticsearch.search.aggregations.reducers.ReducerStreams; import org.elasticsearch.search.aggregations.reducers.movavg.models.MovAvgModel; import org.elasticsearch.search.aggregations.reducers.movavg.models.MovAvgModelStreams; -import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; @@ -43,7 +50,6 @@ import java.util.List; import java.util.Map; -import static org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; import static org.elasticsearch.search.aggregations.reducers.BucketHelpers.resolveBucketValue; public class MovAvgReducer extends Reducer { @@ -155,8 +161,7 @@ public Factory(String name, String[] bucketsPaths, @Nullable ValueFormatter form } @Override - protected Reducer createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, - Map metaData) throws IOException { + protected Reducer createInternal(Map metaData) throws IOException { return new MovAvgReducer(name, bucketsPaths, formatter, gapPolicy, window, model, metaData); } diff --git a/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java b/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java index 6adfb53fd41fa..05ebaf44e05e0 100644 --- a/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java +++ b/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java @@ -23,6 +23,7 @@ import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; + import org.elasticsearch.ElasticsearchGenerationException; import org.elasticsearch.ElasticsearchIllegalArgumentException; import org.elasticsearch.client.Requests; @@ -38,6 +39,7 @@ import org.elasticsearch.index.query.FilterBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.search.aggregations.AbstractAggregationBuilder; +import org.elasticsearch.search.aggregations.reducers.ReducerBuilder; import org.elasticsearch.search.fetch.innerhits.InnerHitsBuilder; import org.elasticsearch.search.fetch.source.FetchSourceContext; import org.elasticsearch.search.highlight.HighlightBuilder; @@ -55,9 +57,10 @@ import java.util.Map; /** - * A search source builder allowing to easily build search source. Simple construction - * using {@link org.elasticsearch.search.builder.SearchSourceBuilder#searchSource()}. - * + * A search source builder allowing to easily build search source. Simple + * construction using + * {@link org.elasticsearch.search.builder.SearchSourceBuilder#searchSource()}. + * * @see org.elasticsearch.action.search.SearchRequest#source(SearchSourceBuilder) */ public class SearchSourceBuilder implements ToXContent { @@ -109,7 +112,6 @@ public static HighlightBuilder highlight() { private List aggregations; private BytesReference aggregationsBinary; - private HighlightBuilder highlightBuilder; private SuggestBuilder suggestBuilder; @@ -123,7 +125,6 @@ public static HighlightBuilder highlight() { private String[] stats; - /** * Constructs a new search source builder. */ @@ -132,7 +133,7 @@ public SearchSourceBuilder() { /** * Constructs a new search source builder with a search query. - * + * * @see org.elasticsearch.index.query.QueryBuilders */ public SearchSourceBuilder query(QueryBuilder query) { @@ -190,8 +191,9 @@ public SearchSourceBuilder query(Map query) { } /** - * Sets a filter that will be executed after the query has been executed and only has affect on the search hits - * (not aggregations). This filter is always executed as last filtering mechanism. + * Sets a filter that will be executed after the query has been executed and + * only has affect on the search hits (not aggregations). This filter is + * always executed as last filtering mechanism. */ public SearchSourceBuilder postFilter(FilterBuilder postFilter) { this.postFilterBuilder = postFilter; @@ -276,8 +278,8 @@ public SearchSourceBuilder minScore(float minScore) { } /** - * Should each {@link org.elasticsearch.search.SearchHit} be returned with an - * explanation of the hit (ranking). + * Should each {@link org.elasticsearch.search.SearchHit} be returned with + * an explanation of the hit (ranking). */ public SearchSourceBuilder explain(Boolean explain) { this.explain = explain; @@ -285,8 +287,8 @@ public SearchSourceBuilder explain(Boolean explain) { } /** - * Should each {@link org.elasticsearch.search.SearchHit} be returned with a version - * associated with it. + * Should each {@link org.elasticsearch.search.SearchHit} be returned with a + * version associated with it. */ public SearchSourceBuilder version(Boolean version) { this.version = version; @@ -310,21 +312,24 @@ public SearchSourceBuilder timeout(String timeout) { } /** - * An optional terminate_after to terminate the search after - * collecting terminateAfter documents + * An optional terminate_after to terminate the search after collecting + * terminateAfter documents */ - public SearchSourceBuilder terminateAfter(int terminateAfter) { + public SearchSourceBuilder terminateAfter(int terminateAfter) { if (terminateAfter <= 0) { throw new ElasticsearchIllegalArgumentException("terminateAfter must be > 0"); } this.terminateAfter = terminateAfter; return this; } + /** * Adds a sort against the given field name and the sort ordering. - * - * @param name The name of the field - * @param order The sort ordering + * + * @param name + * The name of the field + * @param order + * The sort ordering */ public SearchSourceBuilder sort(String name, SortOrder order) { return sort(SortBuilders.fieldSort(name).order(order)); @@ -332,8 +337,9 @@ public SearchSourceBuilder sort(String name, SortOrder order) { /** * Add a sort against the given field name. - * - * @param name The name of the field to sort by + * + * @param name + * The name of the field to sort by */ public SearchSourceBuilder sort(String name) { return sort(SortBuilders.fieldSort(name)); @@ -351,8 +357,8 @@ public SearchSourceBuilder sort(SortBuilder sort) { } /** - * Applies when sorting, and controls if scores will be tracked as well. Defaults to - * false. + * Applies when sorting, and controls if scores will be tracked as well. + * Defaults to false. */ public SearchSourceBuilder trackScores(boolean trackScores) { this.trackScores = trackScores; @@ -401,6 +407,7 @@ public SearchSourceBuilder aggregations(XContentBuilder aggs) { /** * Set the rescore window size for rescores that don't specify their window. + * * @param defaultRescoreWindowSize * @return */ @@ -465,8 +472,9 @@ public SearchSourceBuilder clearRescorers() { } /** - * Indicates whether the response should contain the stored _source for every hit - * + * Indicates whether the response should contain the stored _source for + * every hit + * * @param fetch * @return */ @@ -480,22 +488,33 @@ public SearchSourceBuilder fetchSource(boolean fetch) { } /** - * Indicate that _source should be returned with every hit, with an "include" and/or "exclude" set which can include simple wildcard + * Indicate that _source should be returned with every hit, with an + * "include" and/or "exclude" set which can include simple wildcard * elements. - * - * @param include An optional include (optionally wildcarded) pattern to filter the returned _source - * @param exclude An optional exclude (optionally wildcarded) pattern to filter the returned _source + * + * @param include + * An optional include (optionally wildcarded) pattern to filter + * the returned _source + * @param exclude + * An optional exclude (optionally wildcarded) pattern to filter + * the returned _source */ public SearchSourceBuilder fetchSource(@Nullable String include, @Nullable String exclude) { - return fetchSource(include == null ? Strings.EMPTY_ARRAY : new String[]{include}, exclude == null ? Strings.EMPTY_ARRAY : new String[]{exclude}); + return fetchSource(include == null ? Strings.EMPTY_ARRAY : new String[] { include }, exclude == null ? Strings.EMPTY_ARRAY + : new String[] { exclude }); } /** - * Indicate that _source should be returned with every hit, with an "include" and/or "exclude" set which can include simple wildcard + * Indicate that _source should be returned with every hit, with an + * "include" and/or "exclude" set which can include simple wildcard * elements. - * - * @param includes An optional list of include (optionally wildcarded) pattern to filter the returned _source - * @param excludes An optional list of exclude (optionally wildcarded) pattern to filter the returned _source + * + * @param includes + * An optional list of include (optionally wildcarded) pattern to + * filter the returned _source + * @param excludes + * An optional list of exclude (optionally wildcarded) pattern to + * filter the returned _source */ public SearchSourceBuilder fetchSource(@Nullable String[] includes, @Nullable String[] excludes) { fetchSourceContext = new FetchSourceContext(includes, excludes); @@ -511,7 +530,8 @@ public SearchSourceBuilder fetchSource(@Nullable FetchSourceContext fetchSourceC } /** - * Sets no fields to be loaded, resulting in only id and type to be returned per field. + * Sets no fields to be loaded, resulting in only id and type to be returned + * per field. */ public SearchSourceBuilder noFields() { this.fieldNames = ImmutableList.of(); @@ -519,8 +539,8 @@ public SearchSourceBuilder noFields() { } /** - * Sets the fields to load and return as part of the search request. If none are specified, - * the source of the document will be returned. + * Sets the fields to load and return as part of the search request. If none + * are specified, the source of the document will be returned. */ public SearchSourceBuilder fields(List fields) { this.fieldNames = fields; @@ -528,8 +548,8 @@ public SearchSourceBuilder fields(List fields) { } /** - * Adds the fields to load and return as part of the search request. If none are specified, - * the source of the document will be returned. + * Adds the fields to load and return as part of the search request. If none + * are specified, the source of the document will be returned. */ public SearchSourceBuilder fields(String... fields) { if (fieldNames == null) { @@ -542,8 +562,9 @@ public SearchSourceBuilder fields(String... fields) { } /** - * Adds a field to load and return (note, it must be stored) as part of the search request. - * If none are specified, the source of the document will be return. + * Adds a field to load and return (note, it must be stored) as part of the + * search request. If none are specified, the source of the document will be + * return. */ public SearchSourceBuilder field(String name) { if (fieldNames == null) { @@ -554,7 +575,8 @@ public SearchSourceBuilder field(String name) { } /** - * Adds a field to load from the field data cache and return as part of the search request. + * Adds a field to load from the field data cache and return as part of the + * search request. */ public SearchSourceBuilder fieldDataField(String name) { if (fieldDataFields == null) { @@ -566,9 +588,11 @@ public SearchSourceBuilder fieldDataField(String name) { /** * Adds a script field under the given name with the provided script. - * - * @param name The name of the field - * @param script The script + * + * @param name + * The name of the field + * @param script + * The script */ public SearchSourceBuilder scriptField(String name, String script) { return scriptField(name, null, script, null); @@ -576,10 +600,13 @@ public SearchSourceBuilder scriptField(String name, String script) { /** * Adds a script field. - * - * @param name The name of the field - * @param script The script to execute - * @param params The script parameters + * + * @param name + * The name of the field + * @param script + * The script to execute + * @param params + * The script parameters */ public SearchSourceBuilder scriptField(String name, String script, Map params) { return scriptField(name, null, script, params); @@ -587,11 +614,15 @@ public SearchSourceBuilder scriptField(String name, String script, Mapnull) + * + * @param name + * The name of the field + * @param lang + * The language of the script + * @param script + * The script to execute + * @param params + * The script parameters (can be null) */ public SearchSourceBuilder scriptField(String name, String lang, String script, Map params) { if (scriptFields == null) { @@ -602,10 +633,13 @@ public SearchSourceBuilder scriptField(String name, String lang, String script, } /** - * Sets the boost a specific index will receive when the query is executeed against it. - * - * @param index The index to apply the boost against - * @param indexBoost The boost to apply to the index + * Sets the boost a specific index will receive when the query is executeed + * against it. + * + * @param index + * The index to apply the boost against + * @param indexBoost + * The boost to apply to the index */ public SearchSourceBuilder indexBoost(String index, float indexBoost) { if (this.indexBoost == null) { @@ -648,7 +682,6 @@ public BytesReference buildAsBytes(XContentType contentType) throws SearchSource } } - @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); @@ -657,7 +690,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } - public void innerToXContent(XContentBuilder builder, Params params) throws IOException{ + public void innerToXContent(XContentBuilder builder, Params params) throws IOException { if (from != -1) { builder.field("from", from); } @@ -899,8 +932,8 @@ private PartialField(String name, String[] includes, String[] excludes) { private PartialField(String name, String include, String exclude) { this.name = name; - this.includes = include == null ? null : new String[]{include}; - this.excludes = exclude == null ? null : new String[]{exclude}; + this.includes = include == null ? null : new String[] { include }; + this.excludes = exclude == null ? null : new String[] { exclude }; } public String name() { diff --git a/src/main/java/org/elasticsearch/search/controller/SearchPhaseController.java b/src/main/java/org/elasticsearch/search/controller/SearchPhaseController.java index 91d8948878bfd..cdfacbaa0624d 100644 --- a/src/main/java/org/elasticsearch/search/controller/SearchPhaseController.java +++ b/src/main/java/org/elasticsearch/search/controller/SearchPhaseController.java @@ -21,9 +21,17 @@ import com.carrotsearch.hppc.IntArrayList; import com.carrotsearch.hppc.ObjectObjectOpenHashMap; +import com.google.common.collect.Lists; import org.apache.lucene.index.Term; -import org.apache.lucene.search.*; +import org.apache.lucene.search.CollectionStatistics; +import org.apache.lucene.search.FieldDoc; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.SortField; +import org.apache.lucene.search.TermStatistics; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.search.TopFieldDocs; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.common.collect.HppcMaps; import org.elasticsearch.common.component.AbstractComponent; @@ -33,8 +41,11 @@ import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.concurrent.AtomicArray; import org.elasticsearch.script.ScriptService; +import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; import org.elasticsearch.search.aggregations.InternalAggregations; +import org.elasticsearch.search.aggregations.reducers.Reducer; +import org.elasticsearch.search.aggregations.reducers.SiblingReducer; import org.elasticsearch.search.dfs.AggregatedDfs; import org.elasticsearch.search.dfs.DfsSearchResult; import org.elasticsearch.search.fetch.FetchSearchResult; @@ -47,7 +58,12 @@ import org.elasticsearch.search.suggest.Suggest; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * @@ -391,6 +407,19 @@ public InternalSearchResponse merge(ScoreDoc[] sortedDocs, AtomicArray reducers = firstResult.reducers(); + if (reducers != null) { + List newAggs = new ArrayList<>(Lists.transform(aggregations.asList(), Reducer.AGGREGATION_TRANFORM_FUNCTION)); + for (SiblingReducer reducer : reducers) { + InternalAggregation newAgg = reducer.doReduce(new InternalAggregations(newAggs), new ReduceContext(null, bigArrays, + scriptService)); + newAggs.add(newAgg); + } + aggregations = new InternalAggregations(newAggs); + } + } + InternalSearchHits searchHits = new InternalSearchHits(hits.toArray(new InternalSearchHit[hits.size()]), totalHits, maxScore); return new InternalSearchResponse(searchHits, aggregations, suggest, timedOut, terminatedEarly); diff --git a/src/main/java/org/elasticsearch/search/query/QuerySearchResult.java b/src/main/java/org/elasticsearch/search/query/QuerySearchResult.java index 50167676cc74b..e45006b2c3279 100644 --- a/src/main/java/org/elasticsearch/search/query/QuerySearchResult.java +++ b/src/main/java/org/elasticsearch/search/query/QuerySearchResult.java @@ -20,15 +20,20 @@ package org.elasticsearch.search.query; import org.apache.lucene.search.TopDocs; -import org.elasticsearch.Version; +import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.search.SearchShardTarget; import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.InternalAggregations; +import org.elasticsearch.search.aggregations.reducers.Reducer; +import org.elasticsearch.search.aggregations.reducers.ReducerStreams; +import org.elasticsearch.search.aggregations.reducers.SiblingReducer; import org.elasticsearch.search.suggest.Suggest; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import static org.elasticsearch.common.lucene.Lucene.readTopDocs; import static org.elasticsearch.common.lucene.Lucene.writeTopDocs; @@ -44,6 +49,7 @@ public class QuerySearchResult extends QuerySearchResultProvider { private int size; private TopDocs topDocs; private InternalAggregations aggregations; + private List reducers; private Suggest suggest; private boolean searchTimedOut; private Boolean terminatedEarly = null; @@ -114,6 +120,14 @@ public void aggregations(InternalAggregations aggregations) { this.aggregations = aggregations; } + public List reducers() { + return reducers; + } + + public void reducers(List reducers) { + this.reducers = reducers; + } + public Suggest suggest() { return suggest; } @@ -162,6 +176,16 @@ public void readFromWithId(long id, StreamInput in) throws IOException { if (in.readBoolean()) { aggregations = InternalAggregations.readAggregations(in); } + if (in.readBoolean()) { + int size = in.readVInt(); + List reducers = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + BytesReference type = in.readBytesReference(); + Reducer reducer = ReducerStreams.stream(type).readResult(in); + reducers.add((SiblingReducer) reducer); + } + this.reducers = reducers; + } if (in.readBoolean()) { suggest = Suggest.readSuggest(Suggest.Fields.SUGGEST, in); } @@ -187,6 +211,16 @@ public void writeToNoId(StreamOutput out) throws IOException { out.writeBoolean(true); aggregations.writeTo(out); } + if (reducers == null) { + out.writeBoolean(false); + } else { + out.writeBoolean(true); + out.writeVInt(reducers.size()); + for (Reducer reducer : reducers) { + out.writeBytesReference(reducer.type().stream()); + reducer.writeTo(out); + } + } if (suggest == null) { out.writeBoolean(false); } else { diff --git a/src/test/java/org/elasticsearch/percolator/PercolatorFacetsAndAggregationsTests.java b/src/test/java/org/elasticsearch/percolator/PercolatorFacetsAndAggregationsTests.java index 263af8548833f..9f04e4a37b0e9 100644 --- a/src/test/java/org/elasticsearch/percolator/PercolatorFacetsAndAggregationsTests.java +++ b/src/test/java/org/elasticsearch/percolator/PercolatorFacetsAndAggregationsTests.java @@ -23,8 +23,11 @@ import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.search.aggregations.Aggregation; import org.elasticsearch.search.aggregations.AggregationBuilders; +import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.Aggregator.SubAggCollectionMode; import org.elasticsearch.search.aggregations.bucket.terms.Terms; +import org.elasticsearch.search.aggregations.reducers.ReducerBuilders; +import org.elasticsearch.search.aggregations.reducers.bucketmetrics.InternalBucketMetricValue; import org.elasticsearch.test.ElasticsearchIntegrationTest; import org.junit.Test; @@ -40,6 +43,7 @@ import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures; import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; /** * @@ -111,6 +115,81 @@ public void testFacetsAndAggregations() throws Exception { } } + @Test + // Just test the integration with facets and aggregations, not the facet and aggregation functionality! + public void testAggregationsAndReducers() throws Exception { + assertAcked(prepareCreate("test").addMapping("type", "field1", "type=string", "field2", "type=string")); + ensureGreen(); + + int numQueries = scaledRandomIntBetween(250, 500); + int numUniqueQueries = between(1, numQueries / 2); + String[] values = new String[numUniqueQueries]; + for (int i = 0; i < values.length; i++) { + values[i] = "value" + i; + } + int[] expectedCount = new int[numUniqueQueries]; + + logger.info("--> registering {} queries", numQueries); + for (int i = 0; i < numQueries; i++) { + String value = values[i % numUniqueQueries]; + expectedCount[i % numUniqueQueries]++; + QueryBuilder queryBuilder = matchQuery("field1", value); + client().prepareIndex("test", PercolatorService.TYPE_NAME, Integer.toString(i)) + .setSource(jsonBuilder().startObject().field("query", queryBuilder).field("field2", "b").endObject()) + .execute().actionGet(); + } + client().admin().indices().prepareRefresh("test").execute().actionGet(); + + for (int i = 0; i < numQueries; i++) { + String value = values[i % numUniqueQueries]; + PercolateRequestBuilder percolateRequestBuilder = client().preparePercolate() + .setIndices("test").setDocumentType("type") + .setPercolateDoc(docBuilder().setDoc(jsonBuilder().startObject().field("field1", value).endObject())); + + SubAggCollectionMode aggCollectionMode = randomFrom(SubAggCollectionMode.values()); + percolateRequestBuilder.addAggregation(AggregationBuilders.terms("a").field("field2") + .collectMode(aggCollectionMode )); + + if (randomBoolean()) { + percolateRequestBuilder.setPercolateQuery(matchAllQuery()); + } + if (randomBoolean()) { + percolateRequestBuilder.setScore(true); + } else { + percolateRequestBuilder.setSortByScore(true).setSize(numQueries); + } + + boolean countOnly = randomBoolean(); + if (countOnly) { + percolateRequestBuilder.setOnlyCount(countOnly); + } + + percolateRequestBuilder.addAggregation(ReducerBuilders.maxBucket("max_a").setBucketsPaths("a>_count")); + + PercolateResponse response = percolateRequestBuilder.execute().actionGet(); + assertMatchCount(response, expectedCount[i % numUniqueQueries]); + if (!countOnly) { + assertThat(response.getMatches(), arrayWithSize(expectedCount[i % numUniqueQueries])); + } + + Aggregations aggregations = response.getAggregations(); + assertThat(aggregations.asList().size(), equalTo(2)); + Terms terms = aggregations.get("a"); + assertThat(terms, notNullValue()); + assertThat(terms.getName(), equalTo("a")); + List buckets = new ArrayList<>(terms.getBuckets()); + assertThat(buckets.size(), equalTo(1)); + assertThat(buckets.get(0).getKeyAsString(), equalTo("b")); + assertThat(buckets.get(0).getDocCount(), equalTo((long) expectedCount[i % values.length])); + + InternalBucketMetricValue maxA = aggregations.get("max_a"); + assertThat(maxA, notNullValue()); + assertThat(maxA.getName(), equalTo("max_a")); + assertThat(maxA.value(), equalTo((double) expectedCount[i % values.length])); + assertThat(maxA.keys(), equalTo(new String[] {"b"})); + } + } + @Test public void testSignificantAggs() throws Exception { client().admin().indices().prepareCreate("test").execute().actionGet(); diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/MaxBucketTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/MaxBucketTests.java new file mode 100644 index 0000000000000..f1932118601fa --- /dev/null +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/MaxBucketTests.java @@ -0,0 +1,123 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers; + +import org.elasticsearch.action.index.IndexRequestBuilder; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; +import org.elasticsearch.search.aggregations.bucket.histogram.Histogram.Bucket; +import org.elasticsearch.search.aggregations.reducers.bucketmetrics.InternalBucketMetricValue; +import org.elasticsearch.test.ElasticsearchIntegrationTest; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.search.aggregations.AggregationBuilders.histogram; +import static org.elasticsearch.search.aggregations.reducers.ReducerBuilders.maxBucket; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.core.IsNull.notNullValue; + +@ElasticsearchIntegrationTest.SuiteScopeTest +public class MaxBucketTests extends ElasticsearchIntegrationTest { + + private static final String SINGLE_VALUED_FIELD_NAME = "l_value"; + + static int numDocs; + static int interval; + static int minRandomValue; + static int maxRandomValue; + static int numValueBuckets; + static long[] valueCounts; + + @Override + public void setupSuiteScopeCluster() throws Exception { + createIndex("idx"); + createIndex("idx_unmapped"); + + numDocs = randomIntBetween(6, 20); + interval = randomIntBetween(2, 5); + + minRandomValue = 0; + maxRandomValue = 20; + + numValueBuckets = ((maxRandomValue - minRandomValue) / interval) + 1; + valueCounts = new long[numValueBuckets]; + + List builders = new ArrayList<>(); + + for (int i = 0; i < numDocs; i++) { + int fieldValue = randomIntBetween(minRandomValue, maxRandomValue); + builders.add(client().prepareIndex("idx", "type").setSource( + jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, fieldValue).field("tag", "tag" + i).endObject())); + final int bucket = (fieldValue / interval); // + (fieldValue < 0 ? -1 : 0) - (minRandomValue / interval - 1); + valueCounts[bucket]++; + } + + assertAcked(prepareCreate("empty_bucket_idx").addMapping("type", SINGLE_VALUED_FIELD_NAME, "type=integer")); + for (int i = 0; i < 2; i++) { + builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + i).setSource( + jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, i * 2).endObject())); + } + indexRandom(true, builders); + ensureSearchable(); + } + + @Test + public void singleValuedField() throws Exception { + SearchResponse response = client().prepareSearch("idx") + .addAggregation(histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) + .extendedBounds((long) minRandomValue, (long) maxRandomValue)) + .addAggregation(maxBucket("max_bucket").setBucketsPaths("histo>_count")).execute().actionGet(); + + assertSearchResponse(response); + + Histogram histo = response.getAggregations().get("histo"); + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + assertThat(buckets.size(), equalTo(numValueBuckets)); + + List maxKeys = new ArrayList<>(); + double maxValue = Double.NEGATIVE_INFINITY; + for (int i = 0; i < numValueBuckets; ++i) { + Histogram.Bucket bucket = buckets.get(i); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo((long) i * interval)); + assertThat(bucket.getDocCount(), equalTo(valueCounts[i])); + if (bucket.getDocCount() > maxValue) { + maxValue = bucket.getDocCount(); + maxKeys = new ArrayList<>(); + maxKeys.add(bucket.getKeyAsString()); + } else if (bucket.getDocCount() == maxValue) { + maxKeys.add(bucket.getKeyAsString()); + } + } + + InternalBucketMetricValue maxBucketValue = response.getAggregations().get("max_bucket"); + assertThat(maxBucketValue, notNullValue()); + assertThat(maxBucketValue.getName(), equalTo("max_bucket")); + assertThat(maxBucketValue.value(), equalTo(maxValue)); + assertThat(maxBucketValue.keys(), equalTo(maxKeys.toArray(new String[maxKeys.size()]))); + } +} From 48a94a41df7e9da359fef38901147fc73b25ed14 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Mon, 13 Apr 2015 11:44:29 +0100 Subject: [PATCH 40/68] Added normalisation to Derivative Reducer This changes adds the ability to specify the units for the x-axis for derivative values and calculate the derivative based on those units rather than the original histograms x-axis units --- .../derivative/DerivativeBuilder.java | 18 +++++++- .../reducers/derivative/DerivativeParser.java | 39 ++++++++++++++-- .../derivative/DerivativeReducer.java | 41 ++++++++++++++--- .../reducers/DateDerivativeTests.java | 45 +++++++++++++++++++ .../reducers/DerivativeTests.java | 42 +++++++++++++++++ 5 files changed, 173 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeBuilder.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeBuilder.java index 210d56d4a6fea..6504a26d72c04 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeBuilder.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeBuilder.java @@ -20,16 +20,17 @@ package org.elasticsearch.search.aggregations.reducers.derivative; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; +import org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; import org.elasticsearch.search.aggregations.reducers.ReducerBuilder; import java.io.IOException; -import static org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; - public class DerivativeBuilder extends ReducerBuilder { private String format; private GapPolicy gapPolicy; + private String units; public DerivativeBuilder(String name) { super(name, DerivativeReducer.TYPE.name()); @@ -45,6 +46,16 @@ public DerivativeBuilder gapPolicy(GapPolicy gapPolicy) { return this; } + public DerivativeBuilder units(String units) { + this.units = units; + return this; + } + + public DerivativeBuilder units(DateHistogramInterval units) { + this.units = units.toString(); + return this; + } + @Override protected XContentBuilder internalXContent(XContentBuilder builder, Params params) throws IOException { if (format != null) { @@ -53,6 +64,9 @@ protected XContentBuilder internalXContent(XContentBuilder builder, Params param if (gapPolicy != null) { builder.field(DerivativeParser.GAP_POLICY.getPreferredName(), gapPolicy.getName()); } + if (units != null) { + builder.field(DerivativeParser.UNITS.getPreferredName(), units); + } return builder; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java index c4d3aa2a22978..fab2bd3c0b62f 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java @@ -19,9 +19,15 @@ package org.elasticsearch.search.aggregations.reducers.derivative; +import com.google.common.collect.ImmutableMap; + import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.collect.MapBuilder; +import org.elasticsearch.common.rounding.DateTimeUnit; +import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.SearchParseException; +import org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.reducers.ReducerFactory; import org.elasticsearch.search.aggregations.support.format.ValueFormat; @@ -32,12 +38,23 @@ import java.util.ArrayList; import java.util.List; -import static org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; - public class DerivativeParser implements Reducer.Parser { public static final ParseField FORMAT = new ParseField("format"); public static final ParseField GAP_POLICY = new ParseField("gap_policy"); + public static final ParseField UNITS = new ParseField("units"); + + private final ImmutableMap dateFieldUnits; + + public DerivativeParser() { + dateFieldUnits = MapBuilder. newMapBuilder().put("year", DateTimeUnit.YEAR_OF_CENTURY) + .put("1y", DateTimeUnit.YEAR_OF_CENTURY).put("quarter", DateTimeUnit.QUARTER).put("1q", DateTimeUnit.QUARTER) + .put("month", DateTimeUnit.MONTH_OF_YEAR).put("1M", DateTimeUnit.MONTH_OF_YEAR).put("week", DateTimeUnit.WEEK_OF_WEEKYEAR) + .put("1w", DateTimeUnit.WEEK_OF_WEEKYEAR).put("day", DateTimeUnit.DAY_OF_MONTH).put("1d", DateTimeUnit.DAY_OF_MONTH) + .put("hour", DateTimeUnit.HOUR_OF_DAY).put("1h", DateTimeUnit.HOUR_OF_DAY).put("minute", DateTimeUnit.MINUTES_OF_HOUR) + .put("1m", DateTimeUnit.MINUTES_OF_HOUR).put("second", DateTimeUnit.SECOND_OF_MINUTE) + .put("1s", DateTimeUnit.SECOND_OF_MINUTE).immutableMap(); + } @Override public String type() { @@ -50,6 +67,7 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon String currentFieldName = null; String[] bucketsPaths = null; String format = null; + String units = null; GapPolicy gapPolicy = GapPolicy.IGNORE; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { @@ -62,6 +80,8 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon bucketsPaths = new String[] { parser.text() }; } else if (GAP_POLICY.match(currentFieldName)) { gapPolicy = GapPolicy.parse(context, parser.text()); + } else if (UNITS.match(currentFieldName)) { + units = parser.text(); } else { throw new SearchParseException(context, "Unknown key for a " + token + " in [" + reducerName + "]: [" + currentFieldName + "]."); @@ -93,7 +113,20 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon formatter = ValueFormat.Patternable.Number.format(format).formatter(); } - return new DerivativeReducer.Factory(reducerName, bucketsPaths, formatter, gapPolicy); + long xAxisUnits = -1; + if (units != null) { + DateTimeUnit dateTimeUnit = dateFieldUnits.get(units); + if (dateTimeUnit != null) { + xAxisUnits = dateTimeUnit.field().getDurationField().getUnitMillis(); + } else { + TimeValue timeValue = TimeValue.parseTimeValue(units, null); + if (timeValue != null) { + xAxisUnits = timeValue.getMillis(); + } + } + } + + return new DerivativeReducer.Factory(reducerName, bucketsPaths, formatter, gapPolicy, xAxisUnits); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java index a58d0f0e74e2b..7f02e66b73e32 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java @@ -25,6 +25,7 @@ import org.elasticsearch.common.Nullable; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.search.aggregations.AggregationExecutionException; import org.elasticsearch.search.aggregations.AggregatorFactory; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; @@ -39,6 +40,7 @@ import org.elasticsearch.search.aggregations.reducers.ReducerStreams; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; +import org.joda.time.DateTime; import java.io.IOException; import java.util.ArrayList; @@ -66,15 +68,17 @@ public static void registerStreams() { private ValueFormatter formatter; private GapPolicy gapPolicy; + private long xAxisUnits; public DerivativeReducer() { } - public DerivativeReducer(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter, GapPolicy gapPolicy, + public DerivativeReducer(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter, GapPolicy gapPolicy, long xAxisUnits, Map metadata) { super(name, bucketsPaths, metadata); this.formatter = formatter; this.gapPolicy = gapPolicy; + this.xAxisUnits = xAxisUnits; } @Override @@ -89,51 +93,74 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext InternalHistogram.Factory factory = histo.getFactory(); List newBuckets = new ArrayList<>(); + Long lastBucketKey = null; Double lastBucketValue = null; for (InternalHistogram.Bucket bucket : buckets) { + Long thisBucketKey = resolveBucketKeyAsLong(bucket); Double thisBucketValue = resolveBucketValue(histo, bucket, bucketsPaths()[0], gapPolicy); if (lastBucketValue != null) { - double diff = thisBucketValue - lastBucketValue; - - List aggs = new ArrayList<>(Lists.transform(bucket.getAggregations().asList(), AGGREGATION_TRANFORM_FUNCTION)); - aggs.add(new InternalSimpleValue(name(), diff, formatter, new ArrayList(), metaData())); + double gradient = thisBucketValue - lastBucketValue; + if (xAxisUnits != -1) { + double xDiff = (thisBucketKey - lastBucketKey) / xAxisUnits; + gradient = gradient / xDiff; + } + List aggs = new ArrayList<>(Lists.transform(bucket.getAggregations().asList(), + AGGREGATION_TRANFORM_FUNCTION)); + aggs.add(new InternalSimpleValue(name(), gradient, formatter, new ArrayList(), metaData())); InternalHistogram.Bucket newBucket = factory.createBucket(bucket.getKey(), bucket.getDocCount(), new InternalAggregations( aggs), bucket.getKeyed(), bucket.getFormatter()); newBuckets.add(newBucket); } else { newBuckets.add(bucket); } + lastBucketKey = thisBucketKey; lastBucketValue = thisBucketValue; } return factory.create(newBuckets, histo); } + private Long resolveBucketKeyAsLong(InternalHistogram.Bucket bucket) { + Object key = bucket.getKey(); + if (key instanceof DateTime) { + return ((DateTime) key).getMillis(); + } else if (key instanceof Number) { + return ((Number) key).longValue(); + } else { + throw new AggregationExecutionException("Bucket keys must be either a Number or a DateTime for aggregation " + name() + + ". Found bucket with key " + key); + } + } + @Override public void doReadFrom(StreamInput in) throws IOException { formatter = ValueFormatterStreams.readOptional(in); gapPolicy = GapPolicy.readFrom(in); + xAxisUnits = in.readLong(); } @Override public void doWriteTo(StreamOutput out) throws IOException { ValueFormatterStreams.writeOptional(formatter, out); gapPolicy.writeTo(out); + out.writeLong(xAxisUnits); } public static class Factory extends ReducerFactory { private final ValueFormatter formatter; private GapPolicy gapPolicy; + private long xAxisUnits; - public Factory(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter, GapPolicy gapPolicy) { + public Factory(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter, GapPolicy gapPolicy, long xAxisUnits) { super(name, TYPE.name(), bucketsPaths); this.formatter = formatter; this.gapPolicy = gapPolicy; + this.xAxisUnits = xAxisUnits; } @Override protected Reducer createInternal(Map metaData) throws IOException { - return new DerivativeReducer(name, bucketsPaths, formatter, gapPolicy, metaData); + return new DerivativeReducer(name, bucketsPaths, formatter, gapPolicy, xAxisUnits, metaData); } @Override diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java index ede94abd97336..eefbe41194050 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java @@ -45,6 +45,7 @@ import static org.elasticsearch.search.aggregations.AggregationBuilders.sum; import static org.elasticsearch.search.aggregations.reducers.ReducerBuilders.derivative; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; +import static org.hamcrest.Matchers.closeTo; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.core.IsNull.notNullValue; @@ -147,6 +148,50 @@ public void singleValuedField() throws Exception { assertThat(docCountDeriv.value(), equalTo(1d)); } + @Test + public void singleValuedField_normalised() throws Exception { + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + dateHistogram("histo").field("date").interval(DateHistogramInterval.MONTH).minDocCount(0) + .subAggregation(derivative("deriv").setBucketsPaths("_count").units(DateHistogramInterval.DAY))).execute() + .actionGet(); + + assertSearchResponse(response); + + InternalHistogram deriv = response.getAggregations().get("histo"); + assertThat(deriv, notNullValue()); + assertThat(deriv.getName(), equalTo("histo")); + List buckets = deriv.getBuckets(); + assertThat(buckets.size(), equalTo(3)); + + DateTime key = new DateTime(2012, 1, 1, 0, 0, DateTimeZone.UTC); + Histogram.Bucket bucket = buckets.get(0); + assertThat(bucket, notNullValue()); + assertThat((DateTime) bucket.getKey(), equalTo(key)); + assertThat(bucket.getDocCount(), equalTo(1l)); + SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, nullValue()); + + key = new DateTime(2012, 2, 1, 0, 0, DateTimeZone.UTC); + bucket = buckets.get(1); + assertThat(bucket, notNullValue()); + assertThat((DateTime) bucket.getKey(), equalTo(key)); + assertThat(bucket.getDocCount(), equalTo(2l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), closeTo(1d / 31d, 0.00001)); + + key = new DateTime(2012, 3, 1, 0, 0, DateTimeZone.UTC); + bucket = buckets.get(2); + assertThat(bucket, notNullValue()); + assertThat((DateTime) bucket.getKey(), equalTo(key)); + assertThat(bucket.getDocCount(), equalTo(3l)); + docCountDeriv = bucket.getAggregations().get("deriv"); + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), closeTo(1d / 29d, 0.00001)); + } + @Test public void singleValuedField_WithSubAggregation() throws Exception { SearchResponse response = client() diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java index 6f5641fcffab5..2e4c50fb8aab0 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java @@ -43,6 +43,7 @@ import static org.elasticsearch.search.aggregations.reducers.ReducerBuilders.derivative; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; +import static org.hamcrest.Matchers.closeTo; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.core.IsNull.notNullValue; import static org.hamcrest.core.IsNull.nullValue; @@ -196,6 +197,47 @@ public void singleValuedField() { } } + /** + * test first and second derivative on the sing + */ + @Test + public void singleValuedField_normalised() { + + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) + .subAggregation(derivative("deriv").setBucketsPaths("_count").units("1")) + .subAggregation(derivative("2nd_deriv").setBucketsPaths("deriv"))).execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram deriv = response.getAggregations().get("histo"); + assertThat(deriv, notNullValue()); + assertThat(deriv.getName(), equalTo("histo")); + List buckets = deriv.getBuckets(); + assertThat(buckets.size(), equalTo(numValueBuckets)); + + for (int i = 0; i < numValueBuckets; ++i) { + Histogram.Bucket bucket = buckets.get(i); + checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, valueCounts[i]); + SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); + if (i > 0) { + assertThat(docCountDeriv, notNullValue()); + assertThat(docCountDeriv.value(), closeTo((double) (firstDerivValueCounts[i - 1]) / 5, 0.00001)); + } else { + assertThat(docCountDeriv, nullValue()); + } + SimpleValue docCount2ndDeriv = bucket.getAggregations().get("2nd_deriv"); + if (i > 1) { + assertThat(docCount2ndDeriv, notNullValue()); + assertThat(docCount2ndDeriv.value(), closeTo((double) (secondDerivValueCounts[i - 2]) / 5, 0.00001)); + } else { + assertThat(docCount2ndDeriv, nullValue()); + } + } + } + @Test public void singleValuedField_WithSubAggregation() throws Exception { SearchResponse response = client() From 306d94adb97c4be2e18d0cd4266d97cd9dba1a55 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Mon, 13 Apr 2015 14:24:23 +0100 Subject: [PATCH 41/68] Revert "Added normalisation to Derivative Reducer" This reverts commit 48a94a41df7e9da359fef38901147fc73b25ed14. --- .../derivative/DerivativeBuilder.java | 18 +------- .../reducers/derivative/DerivativeParser.java | 39 ++-------------- .../derivative/DerivativeReducer.java | 41 +++-------------- .../reducers/DateDerivativeTests.java | 45 ------------------- .../reducers/DerivativeTests.java | 42 ----------------- 5 files changed, 12 insertions(+), 173 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeBuilder.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeBuilder.java index 6504a26d72c04..210d56d4a6fea 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeBuilder.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeBuilder.java @@ -20,17 +20,16 @@ package org.elasticsearch.search.aggregations.reducers.derivative; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; -import org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; import org.elasticsearch.search.aggregations.reducers.ReducerBuilder; import java.io.IOException; +import static org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; + public class DerivativeBuilder extends ReducerBuilder { private String format; private GapPolicy gapPolicy; - private String units; public DerivativeBuilder(String name) { super(name, DerivativeReducer.TYPE.name()); @@ -46,16 +45,6 @@ public DerivativeBuilder gapPolicy(GapPolicy gapPolicy) { return this; } - public DerivativeBuilder units(String units) { - this.units = units; - return this; - } - - public DerivativeBuilder units(DateHistogramInterval units) { - this.units = units.toString(); - return this; - } - @Override protected XContentBuilder internalXContent(XContentBuilder builder, Params params) throws IOException { if (format != null) { @@ -64,9 +53,6 @@ protected XContentBuilder internalXContent(XContentBuilder builder, Params param if (gapPolicy != null) { builder.field(DerivativeParser.GAP_POLICY.getPreferredName(), gapPolicy.getName()); } - if (units != null) { - builder.field(DerivativeParser.UNITS.getPreferredName(), units); - } return builder; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java index fab2bd3c0b62f..c4d3aa2a22978 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java @@ -19,15 +19,9 @@ package org.elasticsearch.search.aggregations.reducers.derivative; -import com.google.common.collect.ImmutableMap; - import org.elasticsearch.common.ParseField; -import org.elasticsearch.common.collect.MapBuilder; -import org.elasticsearch.common.rounding.DateTimeUnit; -import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.SearchParseException; -import org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.reducers.ReducerFactory; import org.elasticsearch.search.aggregations.support.format.ValueFormat; @@ -38,23 +32,12 @@ import java.util.ArrayList; import java.util.List; +import static org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; + public class DerivativeParser implements Reducer.Parser { public static final ParseField FORMAT = new ParseField("format"); public static final ParseField GAP_POLICY = new ParseField("gap_policy"); - public static final ParseField UNITS = new ParseField("units"); - - private final ImmutableMap dateFieldUnits; - - public DerivativeParser() { - dateFieldUnits = MapBuilder. newMapBuilder().put("year", DateTimeUnit.YEAR_OF_CENTURY) - .put("1y", DateTimeUnit.YEAR_OF_CENTURY).put("quarter", DateTimeUnit.QUARTER).put("1q", DateTimeUnit.QUARTER) - .put("month", DateTimeUnit.MONTH_OF_YEAR).put("1M", DateTimeUnit.MONTH_OF_YEAR).put("week", DateTimeUnit.WEEK_OF_WEEKYEAR) - .put("1w", DateTimeUnit.WEEK_OF_WEEKYEAR).put("day", DateTimeUnit.DAY_OF_MONTH).put("1d", DateTimeUnit.DAY_OF_MONTH) - .put("hour", DateTimeUnit.HOUR_OF_DAY).put("1h", DateTimeUnit.HOUR_OF_DAY).put("minute", DateTimeUnit.MINUTES_OF_HOUR) - .put("1m", DateTimeUnit.MINUTES_OF_HOUR).put("second", DateTimeUnit.SECOND_OF_MINUTE) - .put("1s", DateTimeUnit.SECOND_OF_MINUTE).immutableMap(); - } @Override public String type() { @@ -67,7 +50,6 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon String currentFieldName = null; String[] bucketsPaths = null; String format = null; - String units = null; GapPolicy gapPolicy = GapPolicy.IGNORE; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { @@ -80,8 +62,6 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon bucketsPaths = new String[] { parser.text() }; } else if (GAP_POLICY.match(currentFieldName)) { gapPolicy = GapPolicy.parse(context, parser.text()); - } else if (UNITS.match(currentFieldName)) { - units = parser.text(); } else { throw new SearchParseException(context, "Unknown key for a " + token + " in [" + reducerName + "]: [" + currentFieldName + "]."); @@ -113,20 +93,7 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon formatter = ValueFormat.Patternable.Number.format(format).formatter(); } - long xAxisUnits = -1; - if (units != null) { - DateTimeUnit dateTimeUnit = dateFieldUnits.get(units); - if (dateTimeUnit != null) { - xAxisUnits = dateTimeUnit.field().getDurationField().getUnitMillis(); - } else { - TimeValue timeValue = TimeValue.parseTimeValue(units, null); - if (timeValue != null) { - xAxisUnits = timeValue.getMillis(); - } - } - } - - return new DerivativeReducer.Factory(reducerName, bucketsPaths, formatter, gapPolicy, xAxisUnits); + return new DerivativeReducer.Factory(reducerName, bucketsPaths, formatter, gapPolicy); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java index 7f02e66b73e32..a58d0f0e74e2b 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeReducer.java @@ -25,7 +25,6 @@ import org.elasticsearch.common.Nullable; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.search.aggregations.AggregationExecutionException; import org.elasticsearch.search.aggregations.AggregatorFactory; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; @@ -40,7 +39,6 @@ import org.elasticsearch.search.aggregations.reducers.ReducerStreams; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; -import org.joda.time.DateTime; import java.io.IOException; import java.util.ArrayList; @@ -68,17 +66,15 @@ public static void registerStreams() { private ValueFormatter formatter; private GapPolicy gapPolicy; - private long xAxisUnits; public DerivativeReducer() { } - public DerivativeReducer(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter, GapPolicy gapPolicy, long xAxisUnits, + public DerivativeReducer(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter, GapPolicy gapPolicy, Map metadata) { super(name, bucketsPaths, metadata); this.formatter = formatter; this.gapPolicy = gapPolicy; - this.xAxisUnits = xAxisUnits; } @Override @@ -93,74 +89,51 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext InternalHistogram.Factory factory = histo.getFactory(); List newBuckets = new ArrayList<>(); - Long lastBucketKey = null; Double lastBucketValue = null; for (InternalHistogram.Bucket bucket : buckets) { - Long thisBucketKey = resolveBucketKeyAsLong(bucket); Double thisBucketValue = resolveBucketValue(histo, bucket, bucketsPaths()[0], gapPolicy); if (lastBucketValue != null) { - double gradient = thisBucketValue - lastBucketValue; - if (xAxisUnits != -1) { - double xDiff = (thisBucketKey - lastBucketKey) / xAxisUnits; - gradient = gradient / xDiff; - } - List aggs = new ArrayList<>(Lists.transform(bucket.getAggregations().asList(), - AGGREGATION_TRANFORM_FUNCTION)); - aggs.add(new InternalSimpleValue(name(), gradient, formatter, new ArrayList(), metaData())); + double diff = thisBucketValue - lastBucketValue; + + List aggs = new ArrayList<>(Lists.transform(bucket.getAggregations().asList(), AGGREGATION_TRANFORM_FUNCTION)); + aggs.add(new InternalSimpleValue(name(), diff, formatter, new ArrayList(), metaData())); InternalHistogram.Bucket newBucket = factory.createBucket(bucket.getKey(), bucket.getDocCount(), new InternalAggregations( aggs), bucket.getKeyed(), bucket.getFormatter()); newBuckets.add(newBucket); } else { newBuckets.add(bucket); } - lastBucketKey = thisBucketKey; lastBucketValue = thisBucketValue; } return factory.create(newBuckets, histo); } - private Long resolveBucketKeyAsLong(InternalHistogram.Bucket bucket) { - Object key = bucket.getKey(); - if (key instanceof DateTime) { - return ((DateTime) key).getMillis(); - } else if (key instanceof Number) { - return ((Number) key).longValue(); - } else { - throw new AggregationExecutionException("Bucket keys must be either a Number or a DateTime for aggregation " + name() - + ". Found bucket with key " + key); - } - } - @Override public void doReadFrom(StreamInput in) throws IOException { formatter = ValueFormatterStreams.readOptional(in); gapPolicy = GapPolicy.readFrom(in); - xAxisUnits = in.readLong(); } @Override public void doWriteTo(StreamOutput out) throws IOException { ValueFormatterStreams.writeOptional(formatter, out); gapPolicy.writeTo(out); - out.writeLong(xAxisUnits); } public static class Factory extends ReducerFactory { private final ValueFormatter formatter; private GapPolicy gapPolicy; - private long xAxisUnits; - public Factory(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter, GapPolicy gapPolicy, long xAxisUnits) { + public Factory(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter, GapPolicy gapPolicy) { super(name, TYPE.name(), bucketsPaths); this.formatter = formatter; this.gapPolicy = gapPolicy; - this.xAxisUnits = xAxisUnits; } @Override protected Reducer createInternal(Map metaData) throws IOException { - return new DerivativeReducer(name, bucketsPaths, formatter, gapPolicy, xAxisUnits, metaData); + return new DerivativeReducer(name, bucketsPaths, formatter, gapPolicy, metaData); } @Override diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java index eefbe41194050..ede94abd97336 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java @@ -45,7 +45,6 @@ import static org.elasticsearch.search.aggregations.AggregationBuilders.sum; import static org.elasticsearch.search.aggregations.reducers.ReducerBuilders.derivative; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; -import static org.hamcrest.Matchers.closeTo; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.core.IsNull.notNullValue; @@ -148,50 +147,6 @@ public void singleValuedField() throws Exception { assertThat(docCountDeriv.value(), equalTo(1d)); } - @Test - public void singleValuedField_normalised() throws Exception { - SearchResponse response = client() - .prepareSearch("idx") - .addAggregation( - dateHistogram("histo").field("date").interval(DateHistogramInterval.MONTH).minDocCount(0) - .subAggregation(derivative("deriv").setBucketsPaths("_count").units(DateHistogramInterval.DAY))).execute() - .actionGet(); - - assertSearchResponse(response); - - InternalHistogram deriv = response.getAggregations().get("histo"); - assertThat(deriv, notNullValue()); - assertThat(deriv.getName(), equalTo("histo")); - List buckets = deriv.getBuckets(); - assertThat(buckets.size(), equalTo(3)); - - DateTime key = new DateTime(2012, 1, 1, 0, 0, DateTimeZone.UTC); - Histogram.Bucket bucket = buckets.get(0); - assertThat(bucket, notNullValue()); - assertThat((DateTime) bucket.getKey(), equalTo(key)); - assertThat(bucket.getDocCount(), equalTo(1l)); - SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, nullValue()); - - key = new DateTime(2012, 2, 1, 0, 0, DateTimeZone.UTC); - bucket = buckets.get(1); - assertThat(bucket, notNullValue()); - assertThat((DateTime) bucket.getKey(), equalTo(key)); - assertThat(bucket.getDocCount(), equalTo(2l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), closeTo(1d / 31d, 0.00001)); - - key = new DateTime(2012, 3, 1, 0, 0, DateTimeZone.UTC); - bucket = buckets.get(2); - assertThat(bucket, notNullValue()); - assertThat((DateTime) bucket.getKey(), equalTo(key)); - assertThat(bucket.getDocCount(), equalTo(3l)); - docCountDeriv = bucket.getAggregations().get("deriv"); - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), closeTo(1d / 29d, 0.00001)); - } - @Test public void singleValuedField_WithSubAggregation() throws Exception { SearchResponse response = client() diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java index 2e4c50fb8aab0..6f5641fcffab5 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java @@ -43,7 +43,6 @@ import static org.elasticsearch.search.aggregations.reducers.ReducerBuilders.derivative; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; -import static org.hamcrest.Matchers.closeTo; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.core.IsNull.notNullValue; import static org.hamcrest.core.IsNull.nullValue; @@ -197,47 +196,6 @@ public void singleValuedField() { } } - /** - * test first and second derivative on the sing - */ - @Test - public void singleValuedField_normalised() { - - SearchResponse response = client() - .prepareSearch("idx") - .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) - .subAggregation(derivative("deriv").setBucketsPaths("_count").units("1")) - .subAggregation(derivative("2nd_deriv").setBucketsPaths("deriv"))).execute().actionGet(); - - assertSearchResponse(response); - - InternalHistogram deriv = response.getAggregations().get("histo"); - assertThat(deriv, notNullValue()); - assertThat(deriv.getName(), equalTo("histo")); - List buckets = deriv.getBuckets(); - assertThat(buckets.size(), equalTo(numValueBuckets)); - - for (int i = 0; i < numValueBuckets; ++i) { - Histogram.Bucket bucket = buckets.get(i); - checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, valueCounts[i]); - SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); - if (i > 0) { - assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), closeTo((double) (firstDerivValueCounts[i - 1]) / 5, 0.00001)); - } else { - assertThat(docCountDeriv, nullValue()); - } - SimpleValue docCount2ndDeriv = bucket.getAggregations().get("2nd_deriv"); - if (i > 1) { - assertThat(docCount2ndDeriv, notNullValue()); - assertThat(docCount2ndDeriv.value(), closeTo((double) (secondDerivValueCounts[i - 2]) / 5, 0.00001)); - } else { - assertThat(docCount2ndDeriv, nullValue()); - } - } - } - @Test public void singleValuedField_WithSubAggregation() throws Exception { SearchResponse response = client() From 392f9ce1f88ea0a609c05ea9d1bbd3738a25cd23 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Mon, 13 Apr 2015 14:34:53 +0100 Subject: [PATCH 42/68] clean up --- .../index/query/CommonTermsQueryBuilder.java | 5 ++++- .../aggregations/AggregationBuilder.java | 22 +------------------ .../search/aggregations/Aggregator.java | 2 +- .../aggregations/AggregatorParsers.java | 1 - .../aggregations/reducers/BucketHelpers.java | 4 ++-- 5 files changed, 8 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/elasticsearch/index/query/CommonTermsQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/CommonTermsQueryBuilder.java index 2dedbb44f8aaa..fef75c4e7fbaa 100644 --- a/src/main/java/org/elasticsearch/index/query/CommonTermsQueryBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/CommonTermsQueryBuilder.java @@ -19,6 +19,9 @@ package org.elasticsearch.index.query; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.similarities.Similarity; import org.elasticsearch.ElasticsearchIllegalArgumentException; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -27,7 +30,7 @@ /** * CommonTermsQuery query is a query that executes high-frequency terms in a * optional sub-query to prevent slow queries due to "common" terms like - * stopwords. This query basically builds 2 queries off the {@link #addAggregator(Term) + * stopwords. This query basically builds 2 queries off the {@link #add(Term) * added} terms where low-frequency terms are added to a required boolean clause * and high-frequency terms are added to an optional boolean clause. The * optional clause is only executed if the required "low-frequency' clause diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregationBuilder.java b/src/main/java/org/elasticsearch/search/aggregations/AggregationBuilder.java index cc3033e883fe5..d41daa7363ff6 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregationBuilder.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregationBuilder.java @@ -27,7 +27,6 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.search.aggregations.reducers.ReducerBuilder; import java.io.IOException; import java.util.List; @@ -39,7 +38,6 @@ public abstract class AggregationBuilder> extends AbstractAggregationBuilder { private List aggregations; - private List> reducers; private BytesReference aggregationsBinary; private Map metaData; @@ -62,18 +60,6 @@ public B subAggregation(AbstractAggregationBuilder aggregation) { return (B) this; } - /** - * Add a sub get to this bucket get. - */ - @SuppressWarnings("unchecked") - public B subAggregation(ReducerBuilder reducer) { - if (reducers == null) { - reducers = Lists.newArrayList(); - } - reducers.add(reducer); - return (B) this; - } - /** * Sets a raw (xcontent / json) sub addAggregation. */ @@ -135,7 +121,7 @@ public final XContentBuilder toXContent(XContentBuilder builder, Params params) builder.field(type); internalXContent(builder, params); - if (aggregations != null || aggregationsBinary != null || reducers != null) { + if (aggregations != null || aggregationsBinary != null) { builder.startObject("aggregations"); if (aggregations != null) { @@ -144,12 +130,6 @@ public final XContentBuilder toXContent(XContentBuilder builder, Params params) } } - if (reducers != null) { - for (ReducerBuilder subAgg : reducers) { - subAgg.toXContent(builder, params); - } - } - if (aggregationsBinary != null) { if (XContentFactory.xContentType(aggregationsBinary) == builder.contentType()) { builder.rawField("aggregations", aggregationsBinary); diff --git a/src/main/java/org/elasticsearch/search/aggregations/Aggregator.java b/src/main/java/org/elasticsearch/search/aggregations/Aggregator.java index bce1f9bc196ff..fd9519499a87c 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/Aggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/Aggregator.java @@ -105,7 +105,7 @@ public static boolean descendsFromBucketAggregator(Aggregator parent) { * Build an empty aggregation. */ public abstract InternalAggregation buildEmptyAggregation(); - + /** Aggregation mode for sub aggregations. */ public enum SubAggCollectionMode { diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorParsers.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorParsers.java index 62caa385585fd..1e1950a15c742 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorParsers.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorParsers.java @@ -197,7 +197,6 @@ private AggregatorFactories parseAggregators(XContentParser parser, SearchContex if (subFactories != null) { throw new SearchParseException(context, "Aggregation [" + aggregationName + "] cannot define sub-aggregations"); } - // TODO: should we validate here like aggs? factories.addReducer(reducerFactory); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java index 30d6fc0107eb9..b6955a086ab03 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java @@ -150,9 +150,9 @@ public static Double resolveBucketValue(InternalMultiBucketAggregation agg, - InternalMultiBucketAggregation.Bucket bucket, List aggPathsList, GapPolicy gapPolicy) { + InternalMultiBucketAggregation.Bucket bucket, List aggPathAsList, GapPolicy gapPolicy) { try { - Object propertyValue = bucket.getProperty(agg.getName(), aggPathsList); + Object propertyValue = bucket.getProperty(agg.getName(), aggPathAsList); if (propertyValue == null) { throw new AggregationExecutionException(DerivativeParser.BUCKETS_PATH.getPreferredName() + " must reference either a number value or a single value numeric metric aggregation"); From 7fdf32fb0dcb8a0b19a75f82ec3db2edd80fb2ff Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Mon, 13 Apr 2015 15:13:02 +0100 Subject: [PATCH 43/68] changed `bucketsPaths` to `buckets_paths` --- .../org/elasticsearch/search/aggregations/reducers/Reducer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java index 3c0b4fdbe221e..5ec45064c7f7f 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java @@ -45,7 +45,7 @@ public abstract class Reducer implements Streamable { */ public static interface Parser { - public static final ParseField BUCKETS_PATH = new ParseField("bucketsPath"); + public static final ParseField BUCKETS_PATH = new ParseField("buckets_path"); /** * @return The reducer type this parser is associated with. From ea1470a0807d47f49577403fc5cbf3370f7c067b Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Mon, 20 Apr 2015 13:58:08 +0100 Subject: [PATCH 44/68] More tests for max bucket reducer --- .../aggregations/reducers/MaxBucketTests.java | 253 +++++++++++++++++- 1 file changed, 251 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/MaxBucketTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/MaxBucketTests.java index f1932118601fa..48d93766bfca8 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/MaxBucketTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/MaxBucketTests.java @@ -23,6 +23,9 @@ import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; import org.elasticsearch.search.aggregations.bucket.histogram.Histogram.Bucket; +import org.elasticsearch.search.aggregations.bucket.terms.Terms; +import org.elasticsearch.search.aggregations.bucket.terms.Terms.Order; +import org.elasticsearch.search.aggregations.metrics.sum.Sum; import org.elasticsearch.search.aggregations.reducers.bucketmetrics.InternalBucketMetricValue; import org.elasticsearch.test.ElasticsearchIntegrationTest; import org.junit.Test; @@ -32,10 +35,13 @@ import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.search.aggregations.AggregationBuilders.histogram; +import static org.elasticsearch.search.aggregations.AggregationBuilders.sum; +import static org.elasticsearch.search.aggregations.AggregationBuilders.terms; import static org.elasticsearch.search.aggregations.reducers.ReducerBuilders.maxBucket; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.core.IsNull.notNullValue; @ElasticsearchIntegrationTest.SuiteScopeTest @@ -69,7 +75,8 @@ public void setupSuiteScopeCluster() throws Exception { for (int i = 0; i < numDocs; i++) { int fieldValue = randomIntBetween(minRandomValue, maxRandomValue); builders.add(client().prepareIndex("idx", "type").setSource( - jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, fieldValue).field("tag", "tag" + i).endObject())); + jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, fieldValue).field("tag", "tag" + (i % interval)) + .endObject())); final int bucket = (fieldValue / interval); // + (fieldValue < 0 ? -1 : 0) - (minRandomValue / interval - 1); valueCounts[bucket]++; } @@ -84,7 +91,7 @@ public void setupSuiteScopeCluster() throws Exception { } @Test - public void singleValuedField() throws Exception { + public void testDocCount_topLevel() throws Exception { SearchResponse response = client().prepareSearch("idx") .addAggregation(histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) .extendedBounds((long) minRandomValue, (long) maxRandomValue)) @@ -120,4 +127,246 @@ public void singleValuedField() throws Exception { assertThat(maxBucketValue.value(), equalTo(maxValue)); assertThat(maxBucketValue.keys(), equalTo(maxKeys.toArray(new String[maxKeys.size()]))); } + + @Test + public void testDocCount_asSubAgg() throws Exception { + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + terms("terms") + .field("tag") + .order(Order.term(true)) + .subAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) + .extendedBounds((long) minRandomValue, (long) maxRandomValue)) + .subAggregation(maxBucket("max_bucket").setBucketsPaths("histo>_count"))).execute().actionGet(); + + assertSearchResponse(response); + + Terms terms = response.getAggregations().get("terms"); + assertThat(terms, notNullValue()); + assertThat(terms.getName(), equalTo("terms")); + List termsBuckets = terms.getBuckets(); + assertThat(termsBuckets.size(), equalTo(interval)); + + for (int i = 0; i < interval; ++i) { + Terms.Bucket termsBucket = termsBuckets.get(i); + assertThat(termsBucket, notNullValue()); + assertThat((String) termsBucket.getKey(), equalTo("tag" + (i % interval))); + + Histogram histo = termsBucket.getAggregations().get("histo"); + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + + List maxKeys = new ArrayList<>(); + double maxValue = Double.NEGATIVE_INFINITY; + for (int j = 0; j < numValueBuckets; ++j) { + Histogram.Bucket bucket = buckets.get(j); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo((long) j * interval)); + if (bucket.getDocCount() > maxValue) { + maxValue = bucket.getDocCount(); + maxKeys = new ArrayList<>(); + maxKeys.add(bucket.getKeyAsString()); + } else if (bucket.getDocCount() == maxValue) { + maxKeys.add(bucket.getKeyAsString()); + } + } + + InternalBucketMetricValue maxBucketValue = termsBucket.getAggregations().get("max_bucket"); + assertThat(maxBucketValue, notNullValue()); + assertThat(maxBucketValue.getName(), equalTo("max_bucket")); + assertThat(maxBucketValue.value(), equalTo(maxValue)); + assertThat(maxBucketValue.keys(), equalTo(maxKeys.toArray(new String[maxKeys.size()]))); + } + } + + @Test + public void testMetric_topLevel() throws Exception { + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation(terms("terms").field("tag").subAggregation(sum("sum").field(SINGLE_VALUED_FIELD_NAME))) + .addAggregation(maxBucket("max_bucket").setBucketsPaths("terms>sum")).execute().actionGet(); + + assertSearchResponse(response); + + Terms terms = response.getAggregations().get("terms"); + assertThat(terms, notNullValue()); + assertThat(terms.getName(), equalTo("terms")); + List buckets = terms.getBuckets(); + assertThat(buckets.size(), equalTo(interval)); + + List maxKeys = new ArrayList<>(); + double maxValue = Double.NEGATIVE_INFINITY; + for (int i = 0; i < interval; ++i) { + Terms.Bucket bucket = buckets.get(i); + assertThat(bucket, notNullValue()); + assertThat((String) bucket.getKey(), equalTo("tag" + (i % interval))); + assertThat(bucket.getDocCount(), greaterThan(0l)); + Sum sum = bucket.getAggregations().get("sum"); + assertThat(sum, notNullValue()); + if (sum.value() > maxValue) { + maxValue = sum.value(); + maxKeys = new ArrayList<>(); + maxKeys.add(bucket.getKeyAsString()); + } else if (sum.value() == maxValue) { + maxKeys.add(bucket.getKeyAsString()); + } + } + + InternalBucketMetricValue maxBucketValue = response.getAggregations().get("max_bucket"); + assertThat(maxBucketValue, notNullValue()); + assertThat(maxBucketValue.getName(), equalTo("max_bucket")); + assertThat(maxBucketValue.value(), equalTo(maxValue)); + assertThat(maxBucketValue.keys(), equalTo(maxKeys.toArray(new String[maxKeys.size()]))); + } + + @Test + public void testMetric_asSubAgg() throws Exception { + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + terms("terms") + .field("tag") + .order(Order.term(true)) + .subAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) + .extendedBounds((long) minRandomValue, (long) maxRandomValue) + .subAggregation(sum("sum").field(SINGLE_VALUED_FIELD_NAME))) + .subAggregation(maxBucket("max_bucket").setBucketsPaths("histo>sum"))).execute().actionGet(); + + assertSearchResponse(response); + + Terms terms = response.getAggregations().get("terms"); + assertThat(terms, notNullValue()); + assertThat(terms.getName(), equalTo("terms")); + List termsBuckets = terms.getBuckets(); + assertThat(termsBuckets.size(), equalTo(interval)); + + for (int i = 0; i < interval; ++i) { + Terms.Bucket termsBucket = termsBuckets.get(i); + assertThat(termsBucket, notNullValue()); + assertThat((String) termsBucket.getKey(), equalTo("tag" + (i % interval))); + + Histogram histo = termsBucket.getAggregations().get("histo"); + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + + List maxKeys = new ArrayList<>(); + double maxValue = Double.NEGATIVE_INFINITY; + for (int j = 0; j < numValueBuckets; ++j) { + Histogram.Bucket bucket = buckets.get(j); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo((long) j * interval)); + Sum sum = bucket.getAggregations().get("sum"); + assertThat(sum, notNullValue()); + if (sum.value() > maxValue) { + maxValue = sum.value(); + maxKeys = new ArrayList<>(); + maxKeys.add(bucket.getKeyAsString()); + } else if (sum.value() == maxValue) { + maxKeys.add(bucket.getKeyAsString()); + } + } + + InternalBucketMetricValue maxBucketValue = termsBucket.getAggregations().get("max_bucket"); + assertThat(maxBucketValue, notNullValue()); + assertThat(maxBucketValue.getName(), equalTo("max_bucket")); + assertThat(maxBucketValue.value(), equalTo(maxValue)); + assertThat(maxBucketValue.keys(), equalTo(maxKeys.toArray(new String[maxKeys.size()]))); + } + } + + @Test + public void testNoBuckets() throws Exception { + SearchResponse response = client().prepareSearch("idx") + .addAggregation(terms("terms").field("tag").exclude("tag.*").subAggregation(sum("sum").field(SINGLE_VALUED_FIELD_NAME))) + .addAggregation(maxBucket("max_bucket").setBucketsPaths("terms>sum")).execute().actionGet(); + + assertSearchResponse(response); + + Terms terms = response.getAggregations().get("terms"); + assertThat(terms, notNullValue()); + assertThat(terms.getName(), equalTo("terms")); + List buckets = terms.getBuckets(); + assertThat(buckets.size(), equalTo(0)); + + InternalBucketMetricValue maxBucketValue = response.getAggregations().get("max_bucket"); + assertThat(maxBucketValue, notNullValue()); + assertThat(maxBucketValue.getName(), equalTo("max_bucket")); + assertThat(maxBucketValue.value(), equalTo(Double.NEGATIVE_INFINITY)); + assertThat(maxBucketValue.keys(), equalTo(new String[0])); + } + + @Test + public void testNested() throws Exception { + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + terms("terms") + .field("tag") + .order(Order.term(true)) + .subAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) + .extendedBounds((long) minRandomValue, (long) maxRandomValue)) + .subAggregation(maxBucket("max_histo_bucket").setBucketsPaths("histo>_count"))) + .addAggregation(maxBucket("max_terms_bucket").setBucketsPaths("terms>max_histo_bucket")).execute().actionGet(); + + assertSearchResponse(response); + + Terms terms = response.getAggregations().get("terms"); + assertThat(terms, notNullValue()); + assertThat(terms.getName(), equalTo("terms")); + List termsBuckets = terms.getBuckets(); + assertThat(termsBuckets.size(), equalTo(interval)); + + List maxTermsKeys = new ArrayList<>(); + double maxTermsValue = Double.NEGATIVE_INFINITY; + for (int i = 0; i < interval; ++i) { + Terms.Bucket termsBucket = termsBuckets.get(i); + assertThat(termsBucket, notNullValue()); + assertThat((String) termsBucket.getKey(), equalTo("tag" + (i % interval))); + + Histogram histo = termsBucket.getAggregations().get("histo"); + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + + List maxHistoKeys = new ArrayList<>(); + double maxHistoValue = Double.NEGATIVE_INFINITY; + for (int j = 0; j < numValueBuckets; ++j) { + Histogram.Bucket bucket = buckets.get(j); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo((long) j * interval)); + if (bucket.getDocCount() > maxHistoValue) { + maxHistoValue = bucket.getDocCount(); + maxHistoKeys = new ArrayList<>(); + maxHistoKeys.add(bucket.getKeyAsString()); + } else if (bucket.getDocCount() == maxHistoValue) { + maxHistoKeys.add(bucket.getKeyAsString()); + } + } + + InternalBucketMetricValue maxBucketValue = termsBucket.getAggregations().get("max_histo_bucket"); + assertThat(maxBucketValue, notNullValue()); + assertThat(maxBucketValue.getName(), equalTo("max_histo_bucket")); + assertThat(maxBucketValue.value(), equalTo(maxHistoValue)); + assertThat(maxBucketValue.keys(), equalTo(maxHistoKeys.toArray(new String[maxHistoKeys.size()]))); + if (maxHistoValue > maxTermsValue) { + maxTermsValue = maxHistoValue; + maxTermsKeys = new ArrayList<>(); + maxTermsKeys.add(termsBucket.getKeyAsString()); + } else if (maxHistoValue == maxTermsValue) { + maxTermsKeys.add(termsBucket.getKeyAsString()); + } + } + + InternalBucketMetricValue maxBucketValue = response.getAggregations().get("max_terms_bucket"); + assertThat(maxBucketValue, notNullValue()); + assertThat(maxBucketValue.getName(), equalTo("max_terms_bucket")); + assertThat(maxBucketValue.value(), equalTo(maxTermsValue)); + assertThat(maxBucketValue.keys(), equalTo(maxTermsKeys.toArray(new String[maxTermsKeys.size()]))); + } } From 0f4b7f3b5c1611e74e3de00411ec957004d4c5db Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Wed, 15 Apr 2015 14:23:29 +0100 Subject: [PATCH 45/68] Added section for reducer aggregations in the main aggregation docs page --- docs/reference/search/aggregations.asciidoc | 32 +++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/reference/search/aggregations.asciidoc b/docs/reference/search/aggregations.asciidoc index e7803a27e9c86..98e3ba4ccea81 100644 --- a/docs/reference/search/aggregations.asciidoc +++ b/docs/reference/search/aggregations.asciidoc @@ -116,6 +116,38 @@ aggregated for the buckets created by their "parent" bucket aggregation. There are different bucket aggregators, each with a different "bucketing" strategy. Some define a single bucket, some define fixed number of multiple buckets, and others dynamically create the buckets during the aggregation process. +[float] +=== Reducer Aggregations + +coming[2.0.0] + +experimental[] + +Reducer aggregations work on the outputs produced from other aggregations rather than from document sets, adding +information to the output tree. There are many different types of reducer, each computing different information from +other aggregations, but these type can broken down into two families: + +_Parent_:: + A family of reducer aggregations that is provided with the output of its parent aggregation and is able + to compute new buckets or new aggregations to add to existing buckets. + +_Sibling_:: + Reducer aggregations that are provided with the output of a sibling aggregation and are able to compute a + new aggregation which will be at the same level as the sibling aggregation. + +Reducer aggregations can reference the aggregations they need to perform their computation by using the `buckets_paths` +parameter to indicate the paths to the required metrics. The syntax for defining these paths can be found in the +<> section. + +?????? SHOULD THE SECTION ABOUT DEFINING AGGREGATION PATHS +BE IN THIS PAGE AND REFERENCED FROM THE TERMS AGGREGATION DOCUMENTATION ??????? + +Reducer aggregations cannot have sub-aggregations but depending on the type it can reference another reducer in the `buckets_path` +allowing reducers to be chained. + +NOTE: Because reducer aggregations only add to the output, when chaining reducer aggregations the output of each reducer will be +included in the final output. + [float] === Caching heavy aggregations From be647a89d3a9edb58f4e84f7256f8201d33efd8a Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Thu, 16 Apr 2015 14:07:40 +0100 Subject: [PATCH 46/68] Documentation for the derivative reducer --- docs/reference/search/aggregations.asciidoc | 3 + .../search/aggregations/reducer.asciidoc | 3 + .../reducer/derivative-aggregation.asciidoc | 192 ++++++++++++++++++ .../reducer/max-bucket-aggregation.asciidoc | 192 ++++++++++++++++++ 4 files changed, 390 insertions(+) create mode 100644 docs/reference/search/aggregations/reducer.asciidoc create mode 100644 docs/reference/search/aggregations/reducer/derivative-aggregation.asciidoc create mode 100644 docs/reference/search/aggregations/reducer/max-bucket-aggregation.asciidoc diff --git a/docs/reference/search/aggregations.asciidoc b/docs/reference/search/aggregations.asciidoc index 98e3ba4ccea81..74784c110a96b 100644 --- a/docs/reference/search/aggregations.asciidoc +++ b/docs/reference/search/aggregations.asciidoc @@ -227,3 +227,6 @@ Then that piece of metadata will be returned in place for our `titles` terms agg include::aggregations/metrics.asciidoc[] include::aggregations/bucket.asciidoc[] + +include::aggregations/reducer.asciidoc[] + diff --git a/docs/reference/search/aggregations/reducer.asciidoc b/docs/reference/search/aggregations/reducer.asciidoc new file mode 100644 index 0000000000000..5b3bff11c1898 --- /dev/null +++ b/docs/reference/search/aggregations/reducer.asciidoc @@ -0,0 +1,3 @@ +[[search-aggregations-reducer]] + +include::reducer/derivative.asciidoc[] diff --git a/docs/reference/search/aggregations/reducer/derivative-aggregation.asciidoc b/docs/reference/search/aggregations/reducer/derivative-aggregation.asciidoc new file mode 100644 index 0000000000000..f1fa8b44043b4 --- /dev/null +++ b/docs/reference/search/aggregations/reducer/derivative-aggregation.asciidoc @@ -0,0 +1,192 @@ +[[search-aggregations-reducer-derivative-aggregation]] +=== Derivative Aggregation + +A parent reducer aggregation which calculates the derivative of a specified metric in a parent histogram (or date_histogram) +aggregation. The specified metric must be numeric and the enclosing histogram must have `min_doc_count` set to `0`. + +The following snippet calculates the derivative of the total monthly `sales`: + +[source,js] +-------------------------------------------------- +{ + "aggs" : { + "sales_per_month" : { + "date_histogram" : { + "field" : "date", + "interval" : "month" + }, + "aggs": { + "sales": { + "sum": { + "field": "price" + } + }, + "sales_deriv": { + "derivative": { + "buckets_paths": "sales" <1> + } + } + } + } + } +} +-------------------------------------------------- + +<1> `bucket_paths` instructs this derivative aggregation to use the output of the `sales` aggregation for the derivative + +And the following may be the response: + +[source,js] +-------------------------------------------------- +{ + "aggregations": { + "sales_per_month": { + "buckets": [ + { + "key_as_string": "2015/01/01 00:00:00", + "key": 1420070400000, + "doc_count": 3, + "sales": { + "value": 550 + } <1> + }, + { + "key_as_string": "2015/02/01 00:00:00", + "key": 1422748800000, + "doc_count": 2, + "sales": { + "value": 60 + }, + "sales_deriv": { + "value": -490 <2> + } + }, + { + "key_as_string": "2015/03/01 00:00:00", + "key": 1425168000000, + "doc_count": 2, + "sales": { + "value": 375 + }, + "sales_deriv": { + "value": 315 + } + } + ] + } + } +} +-------------------------------------------------- + +<1> No derivative for the first bucket since we need at least 2 data points to calculate the derivative +<2> Derivative value units are implicitly defined by the `sales` aggregation and the parent histogram so in this case the units +would be $/month assuming the `price` field has units of $. + +==== Second Order Derivative + +A second order derivative can be calculated by chaining the derivative reducer aggregation onto the result of another derivative +reducer aggregation as in the following example which will calculate both the first and the second order derivative of the total +monthly sales: + +[source,js] +-------------------------------------------------- +{ + "aggs" : { + "sales_per_month" : { + "date_histogram" : { + "field" : "date", + "interval" : "month" + }, + "aggs": { + "sales": { + "sum": { + "field": "price" + } + }, + "sales_deriv": { + "derivative": { + "buckets_paths": "sales" + } + }, + "sales_2nd_deriv": { + "derivative": { + "buckets_paths": "sales_deriv" <1> + } + } + } + } + } +} +-------------------------------------------------- + +<1> `bucket_paths` for the second derivative points to the name of the first derivative + +And the following may be the response: + +[source,js] +-------------------------------------------------- +{ + "aggregations": { + "sales_per_month": { + "buckets": [ + { + "key_as_string": "2015/01/01 00:00:00", + "key": 1420070400000, + "doc_count": 3, + "sales": { + "value": 550 + } <1> + }, + { + "key_as_string": "2015/02/01 00:00:00", + "key": 1422748800000, + "doc_count": 2, + "sales": { + "value": 60 + }, + "sales_deriv": { + "value": -490 + } <1> + }, + { + "key_as_string": "2015/03/01 00:00:00", + "key": 1425168000000, + "doc_count": 2, + "sales": { + "value": 375 + }, + "sales_deriv": { + "value": 315 + }, + "sales_2nd_deriv": { + "value": 805 + } + } + ] + } + } +} +-------------------------------------------------- +<1> No second derivative for the first two buckets since we need at least 2 data points from the first derivative to calculate the +second derivative + +==== Dealing with gaps in the data + +There are a couple of reasons why the data output by the enclosing histogram may have gaps: + +* There are no documents matching the query for some buckets +* The data for a metric is missing in all of the documents falling into a bucket (this is most likely with either a small interval +on the enclosing histogram or with a query matching only a small number of documents) + +Where there is no data available in a bucket for a given metric it presents a problem for calculating the derivative value for both +the current bucket and the next bucket. In the derivative reducer aggregation has a `gap policy` parameter to define what the behavior +should be when a gap in the data is found. There are currently two options for controlling the gap policy: + +_ignore_:: + This option will not produce a derivative value for any buckets where the value in the current or previous bucket is + missing + +_insert_zeros_:: + This option will assume the missing value is `0` and calculate the derivative with the value `0`. + + diff --git a/docs/reference/search/aggregations/reducer/max-bucket-aggregation.asciidoc b/docs/reference/search/aggregations/reducer/max-bucket-aggregation.asciidoc new file mode 100644 index 0000000000000..659f3ff193010 --- /dev/null +++ b/docs/reference/search/aggregations/reducer/max-bucket-aggregation.asciidoc @@ -0,0 +1,192 @@ +[[search-aggregations-reducer-max-bucket-aggregation]] +=== Max Bucket Aggregation + +A parent reducer aggregation which calculates the derivative of a specified metric in a parent histogram (or date_histogram) +aggregation. The specified metric must be numeric and the enclosing histogram must have `min_doc_count` set to `0`. + +The following snippet calculates the derivative of the total monthly `sales`: + +[source,js] +-------------------------------------------------- +{ + "aggs" : { + "sales" : { + "date_histogram" : { + "field" : "date", + "interval" : "month" + }, + "aggs": { + "sales": { + "sum": { + "field": "price" + } + }, + "sales_deriv": { + "derivative": { + "buckets_paths": "sales" <1> + } + } + } + } + } +} +-------------------------------------------------- + +<1> `bucket_paths` instructs this derivative aggregation to use the output of the `sales` aggregation for the derivative + +And the following may be the response: + +[source,js] +-------------------------------------------------- +{ + "aggregations": { + "sales": { + "buckets": [ + { + "key_as_string": "2015/01/01 00:00:00", + "key": 1420070400000, + "doc_count": 3, + "sales": { + "value": 550 + } <1> + }, + { + "key_as_string": "2015/02/01 00:00:00", + "key": 1422748800000, + "doc_count": 2, + "sales": { + "value": 60 + }, + "sales_deriv": { + "value": -490 <2> + } + }, + { + "key_as_string": "2015/03/01 00:00:00", + "key": 1425168000000, + "doc_count": 2, + "sales": { + "value": 375 + }, + "sales_deriv": { + "value": 315 + } + } + ] + } + } +} +-------------------------------------------------- + +<1> No derivative for the first bucket since we need at least 2 data points to calculate the derivative +<2> Derivative value units are implicitly defined by the `sales` aggregation and the parent histogram so in this case the units +would be $/month assuming the `price` field has units of $. + +==== Second Order Derivative + +A second order derivative can be calculated by chaining the derivative reducer aggregation onto the result of another derivative +reducer aggregation as in the following example which will calculate both the first and the second order derivative of the total +monthly sales: + +[source,js] +-------------------------------------------------- +{ + "aggs" : { + "sales" : { + "date_histogram" : { + "field" : "date", + "interval" : "month" + }, + "aggs": { + "sales": { + "sum": { + "field": "price" + } + }, + "sales_deriv": { + "derivative": { + "buckets_paths": "sales" + } + }, + "sales_2nd_deriv": { + "derivative": { + "buckets_paths": "sales_deriv" <1> + } + } + } + } + } +} +-------------------------------------------------- + +<1> `bucket_paths` for the second derivative points to the name of the first derivative + +And the following may be the response: + +[source,js] +-------------------------------------------------- +{ + "aggregations": { + "sales": { + "buckets": [ + { + "key_as_string": "2015/01/01 00:00:00", + "key": 1420070400000, + "doc_count": 3, + "sales": { + "value": 550 + } <1> + }, + { + "key_as_string": "2015/02/01 00:00:00", + "key": 1422748800000, + "doc_count": 2, + "sales": { + "value": 60 + }, + "sales_deriv": { + "value": -490 + } <1> + }, + { + "key_as_string": "2015/03/01 00:00:00", + "key": 1425168000000, + "doc_count": 2, + "sales": { + "value": 375 + }, + "sales_deriv": { + "value": 315 + }, + "sales_2nd_deriv": { + "value": 805 + } + } + ] + } + } +} +-------------------------------------------------- +<1> No second derivative for the first two buckets since we need at least 2 data points from the first derivative to calculate the +second derivative + +==== Dealing with gaps in the data + +There are a couple of reasons why the data output by the enclosing histogram may have gaps: + +* There are no documents matching the query for some buckets +* The data for a metric is missing in all of the documents falling into a bucket (this is most likely with either a small interval +on the enclosing histogram or with a query matching only a small number of documents) + +Where there is no data available in a bucket for a given metric it presents a problem for calculating the derivative value for both +the current bucket and the next bucket. In the derivative reducer aggregation has a `gap policy` parameter to define what the behavior +should be when a gap in the data is found. There are currently two options for controlling the gap policy: + +_ignore_:: + This option will not produce a derivative value for any buckets where the value in the current or previous bucket is + missing + +_insert_zeros_:: + This option will assume the missing value is `0` and calculate the derivative with the value `0`. + + From bd28c9c44e779dc784bd59387682b35d441bd627 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Fri, 17 Apr 2015 11:34:01 +0100 Subject: [PATCH 47/68] Documentation for the max_bucket reducer --- .../reducer/max-bucket-aggregation.asciidoc | 148 +++--------------- 1 file changed, 19 insertions(+), 129 deletions(-) diff --git a/docs/reference/search/aggregations/reducer/max-bucket-aggregation.asciidoc b/docs/reference/search/aggregations/reducer/max-bucket-aggregation.asciidoc index 659f3ff193010..ca6f274d18974 100644 --- a/docs/reference/search/aggregations/reducer/max-bucket-aggregation.asciidoc +++ b/docs/reference/search/aggregations/reducer/max-bucket-aggregation.asciidoc @@ -1,16 +1,17 @@ [[search-aggregations-reducer-max-bucket-aggregation]] === Max Bucket Aggregation -A parent reducer aggregation which calculates the derivative of a specified metric in a parent histogram (or date_histogram) -aggregation. The specified metric must be numeric and the enclosing histogram must have `min_doc_count` set to `0`. +A sibling reducer aggregation which identifies the bucket(s) with the maximum value of a specified metric in a sibing aggregation +and outputs both the value and the key(s) of the bucket(s). The specified metric must be numeric and the sibling aggregation must +be a multi-bucket aggregation. -The following snippet calculates the derivative of the total monthly `sales`: +The following snippet calculates the maximum of the total monthly `sales`: [source,js] -------------------------------------------------- { "aggs" : { - "sales" : { + "sales_per_month" : { "date_histogram" : { "field" : "date", "interval" : "month" @@ -20,106 +21,20 @@ The following snippet calculates the derivative of the total monthly `sales`: "sum": { "field": "price" } - }, - "sales_deriv": { - "derivative": { - "buckets_paths": "sales" <1> - } } } - } - } -} --------------------------------------------------- - -<1> `bucket_paths` instructs this derivative aggregation to use the output of the `sales` aggregation for the derivative - -And the following may be the response: - -[source,js] --------------------------------------------------- -{ - "aggregations": { - "sales": { - "buckets": [ - { - "key_as_string": "2015/01/01 00:00:00", - "key": 1420070400000, - "doc_count": 3, - "sales": { - "value": 550 - } <1> - }, - { - "key_as_string": "2015/02/01 00:00:00", - "key": 1422748800000, - "doc_count": 2, - "sales": { - "value": 60 - }, - "sales_deriv": { - "value": -490 <2> - } - }, - { - "key_as_string": "2015/03/01 00:00:00", - "key": 1425168000000, - "doc_count": 2, - "sales": { - "value": 375 - }, - "sales_deriv": { - "value": 315 - } - } - ] - } - } -} --------------------------------------------------- - -<1> No derivative for the first bucket since we need at least 2 data points to calculate the derivative -<2> Derivative value units are implicitly defined by the `sales` aggregation and the parent histogram so in this case the units -would be $/month assuming the `price` field has units of $. - -==== Second Order Derivative - -A second order derivative can be calculated by chaining the derivative reducer aggregation onto the result of another derivative -reducer aggregation as in the following example which will calculate both the first and the second order derivative of the total -monthly sales: - -[source,js] --------------------------------------------------- -{ - "aggs" : { - "sales" : { - "date_histogram" : { - "field" : "date", - "interval" : "month" - }, - "aggs": { - "sales": { - "sum": { - "field": "price" - } - }, - "sales_deriv": { - "derivative": { - "buckets_paths": "sales" - } - }, - "sales_2nd_deriv": { - "derivative": { - "buckets_paths": "sales_deriv" <1> - } - } + }, + "max_monthly_sales": { + "max_bucket": { + "buckets_paths": "sales_per_month>sales" <1> } } } } -------------------------------------------------- -<1> `bucket_paths` for the second derivative points to the name of the first derivative +<1> `bucket_paths` instructs this max_bucket aggregation that we want the maximum value of the `sales` aggregation in the +"sales_per_month` date histogram. And the following may be the response: @@ -127,7 +42,7 @@ And the following may be the response: -------------------------------------------------- { "aggregations": { - "sales": { + "sales_per_month": { "buckets": [ { "key_as_string": "2015/01/01 00:00:00", @@ -135,7 +50,7 @@ And the following may be the response: "doc_count": 3, "sales": { "value": 550 - } <1> + } }, { "key_as_string": "2015/02/01 00:00:00", @@ -143,10 +58,7 @@ And the following may be the response: "doc_count": 2, "sales": { "value": 60 - }, - "sales_deriv": { - "value": -490 - } <1> + } }, { "key_as_string": "2015/03/01 00:00:00", @@ -154,39 +66,17 @@ And the following may be the response: "doc_count": 2, "sales": { "value": 375 - }, - "sales_deriv": { - "value": 315 - }, - "sales_2nd_deriv": { - "value": 805 } } ] + }, + "max_monthly_sales": { + "keys": ["2015/01/01 00:00:00"], <1> + "value": 550 } } } -------------------------------------------------- -<1> No second derivative for the first two buckets since we need at least 2 data points from the first derivative to calculate the -second derivative - -==== Dealing with gaps in the data - -There are a couple of reasons why the data output by the enclosing histogram may have gaps: - -* There are no documents matching the query for some buckets -* The data for a metric is missing in all of the documents falling into a bucket (this is most likely with either a small interval -on the enclosing histogram or with a query matching only a small number of documents) - -Where there is no data available in a bucket for a given metric it presents a problem for calculating the derivative value for both -the current bucket and the next bucket. In the derivative reducer aggregation has a `gap policy` parameter to define what the behavior -should be when a gap in the data is found. There are currently two options for controlling the gap policy: - -_ignore_:: - This option will not produce a derivative value for any buckets where the value in the current or previous bucket is - missing - -_insert_zeros_:: - This option will assume the missing value is `0` and calculate the derivative with the value `0`. +<1> `keys` is an array of strings since the maximum value may be present in multiple buckets From 89d424e074c0b42584ce7e2a594767613b95a7a6 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Tue, 21 Apr 2015 16:00:02 +0100 Subject: [PATCH 48/68] Derivative can now access multi-value metric aggregations --- .../aggregations/AggregatorFactories.java | 30 ++++------ .../reducers/DerivativeTests.java | 59 +++++++++++++++++-- 2 files changed, 66 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java index 286f0c55b92d7..843180960802a 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java @@ -23,6 +23,7 @@ import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.reducers.ReducerFactory; import org.elasticsearch.search.aggregations.support.AggregationContext; +import org.elasticsearch.search.aggregations.support.AggregationPath; import java.io.IOException; import java.util.ArrayList; @@ -62,7 +63,8 @@ public List createReducers() throws IOException { } /** - * Create all aggregators so that they can be consumed with multiple buckets. + * Create all aggregators so that they can be consumed with multiple + * buckets. */ public Aggregator[] createSubAggregators(Aggregator parent) throws IOException { Aggregator[] aggregators = new Aggregator[count()]; @@ -138,7 +140,8 @@ public static class Builder { public Builder addAggregator(AggregatorFactory factory) { if (!names.add(factory.name)) { - throw new ElasticsearchIllegalArgumentException("Two sibling aggregations cannot have the same name: [" + factory.name + "]"); + throw new ElasticsearchIllegalArgumentException("Two sibling aggregations cannot have the same name: [" + factory.name + + "]"); } factories.add(factory); return this; @@ -158,19 +161,12 @@ public AggregatorFactories build() { } /* - * L ← Empty list that will contain the sorted nodes - * while there are unmarked nodes do - * select an unmarked node n - * visit(n) - * function visit(node n) - * if n has a temporary mark then stop (not a DAG) - * if n is not marked (i.e. has not been visited yet) then - * mark n temporarily - * for each node m with an edge from n to m do - * visit(m) - * mark n permanently - * unmark n temporarily - * add n to head of L + * L ← Empty list that will contain the sorted nodes while there are + * unmarked nodes do select an unmarked node n visit(n) function + * visit(node n) if n has a temporary mark then stop (not a DAG) if n is + * not marked (i.e. has not been visited yet) then mark n temporarily + * for each node m with an edge from n to m do visit(m) mark n + * permanently unmark n temporarily add n to head of L */ private List resolveReducerOrder(List reducerFactories, List aggFactories) { Map reducerFactoriesMap = new HashMap<>(); @@ -204,8 +200,8 @@ private void resolveReducerOrder(Set aggFactoryNames, Map'); - String firstAggName = aggSepIndex == -1 ? bucketsPath : bucketsPath.substring(0, aggSepIndex); + List bucketsPathElements = AggregationPath.parse(bucketsPath).getPathElementsAsStringList(); + String firstAggName = bucketsPathElements.get(0); if (bucketsPath.equals("_count") || bucketsPath.equals("_key") || aggFactoryNames.contains(firstAggName)) { continue; } else { diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java index 6f5641fcffab5..95c13b6fd2f71 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java @@ -25,6 +25,7 @@ import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram.Bucket; +import org.elasticsearch.search.aggregations.metrics.stats.Stats; import org.elasticsearch.search.aggregations.metrics.sum.Sum; import org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; import org.elasticsearch.search.aggregations.support.AggregationPath; @@ -39,6 +40,7 @@ import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; import static org.elasticsearch.search.aggregations.AggregationBuilders.histogram; +import static org.elasticsearch.search.aggregations.AggregationBuilders.stats; import static org.elasticsearch.search.aggregations.AggregationBuilders.sum; import static org.elasticsearch.search.aggregations.reducers.ReducerBuilders.derivative; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; @@ -159,7 +161,7 @@ private XContentBuilder newDocBuilder(int singleValueFieldValue) throws IOExcept * test first and second derivative on the sing */ @Test - public void singleValuedField() { + public void docCountDerivative() { SearchResponse response = client() .prepareSearch("idx") @@ -197,7 +199,7 @@ public void singleValuedField() { } @Test - public void singleValuedField_WithSubAggregation() throws Exception { + public void singleValueAggDerivative() throws Exception { SearchResponse response = client() .prepareSearch("idx") .addAggregation( @@ -242,6 +244,52 @@ public void singleValuedField_WithSubAggregation() throws Exception { } } + @Test + public void multiValueAggDerivative() throws Exception { + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) + .subAggregation(stats("stats").field(SINGLE_VALUED_FIELD_NAME)) + .subAggregation(derivative("deriv").setBucketsPaths("stats.sum"))).execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram deriv = response.getAggregations().get("histo"); + assertThat(deriv, notNullValue()); + assertThat(deriv.getName(), equalTo("histo")); + assertThat(deriv.getBuckets().size(), equalTo(numValueBuckets)); + Object[] propertiesKeys = (Object[]) deriv.getProperty("_key"); + Object[] propertiesDocCounts = (Object[]) deriv.getProperty("_count"); + Object[] propertiesSumCounts = (Object[]) deriv.getProperty("stats.sum"); + + List buckets = new ArrayList(deriv.getBuckets()); + Long expectedSumPreviousBucket = Long.MIN_VALUE; // start value, gets + // overwritten + for (int i = 0; i < numValueBuckets; ++i) { + Histogram.Bucket bucket = buckets.get(i); + checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, valueCounts[i]); + Stats stats = bucket.getAggregations().get("stats"); + assertThat(stats, notNullValue()); + long expectedSum = valueCounts[i] * (i * interval); + assertThat(stats.getSum(), equalTo((double) expectedSum)); + SimpleValue sumDeriv = bucket.getAggregations().get("deriv"); + if (i > 0) { + assertThat(sumDeriv, notNullValue()); + long sumDerivValue = expectedSum - expectedSumPreviousBucket; + assertThat(sumDeriv.value(), equalTo((double) sumDerivValue)); + assertThat((double) bucket.getProperty("histo", AggregationPath.parse("deriv.value").getPathElementsAsStringList()), + equalTo((double) sumDerivValue)); + } else { + assertThat(sumDeriv, nullValue()); + } + expectedSumPreviousBucket = expectedSum; + assertThat((long) propertiesKeys[i], equalTo((long) i * interval)); + assertThat((long) propertiesDocCounts[i], equalTo(valueCounts[i])); + assertThat((double) propertiesSumCounts[i], equalTo((double) expectedSum)); + } + } + @Test public void unmapped() throws Exception { SearchResponse response = client() @@ -288,7 +336,7 @@ public void partiallyUnmapped() throws Exception { } @Test - public void singleValuedFieldWithGaps() throws Exception { + public void docCountDerivativeWithGaps() throws Exception { SearchResponse searchResponse = client() .prepareSearch("empty_bucket_idx") .setQuery(matchAllQuery()) @@ -317,7 +365,7 @@ public void singleValuedFieldWithGaps() throws Exception { } @Test - public void singleValuedFieldWithGaps_random() throws Exception { + public void docCountDerivativeWithGaps_random() throws Exception { SearchResponse searchResponse = client() .prepareSearch("empty_bucket_idx_rnd") .setQuery(matchAllQuery()) @@ -336,7 +384,6 @@ public void singleValuedFieldWithGaps_random() throws Exception { for (int i = 0; i < valueCounts_empty_rnd.length; i++) { Histogram.Bucket bucket = buckets.get(i); - System.out.println(bucket.getDocCount()); checkBucketKeyAndDocCount("Bucket " + i, bucket, i, valueCounts_empty_rnd[i]); SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); if (firstDerivValueCounts_empty_rnd[i] == null) { @@ -348,7 +395,7 @@ public void singleValuedFieldWithGaps_random() throws Exception { } @Test - public void singleValuedFieldWithGaps_insertZeros() throws Exception { + public void docCountDerivativeWithGaps_insertZeros() throws Exception { SearchResponse searchResponse = client() .prepareSearch("empty_bucket_idx") .setQuery(matchAllQuery()) From f6934e0410130a08a5bf53caab82e53832ed1d63 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Wed, 22 Apr 2015 10:06:22 +0100 Subject: [PATCH 49/68] unit test for derivative of metric agg with gaps --- .../reducers/DerivativeTests.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java index 95c13b6fd2f71..1c579c6cd5fae 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java @@ -45,6 +45,7 @@ import static org.elasticsearch.search.aggregations.reducers.ReducerBuilders.derivative; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; +import static org.hamcrest.Matchers.closeTo; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.core.IsNull.notNullValue; import static org.hamcrest.core.IsNull.nullValue; @@ -424,6 +425,40 @@ public void docCountDerivativeWithGaps_insertZeros() throws Exception { } } + @Test + public void singleValueAggDerivativeWithGaps() throws Exception { + SearchResponse searchResponse = client() + .prepareSearch("empty_bucket_idx") + .setQuery(matchAllQuery()) + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(1).minDocCount(0) + .subAggregation(sum("sum").field(SINGLE_VALUED_FIELD_NAME)) + .subAggregation(derivative("deriv").setBucketsPaths("sum"))).execute().actionGet(); + + assertThat(searchResponse.getHits().getTotalHits(), equalTo(numDocsEmptyIdx)); + + InternalHistogram deriv = searchResponse.getAggregations().get("histo"); + assertThat(deriv, Matchers.notNullValue()); + assertThat(deriv.getName(), equalTo("histo")); + List buckets = deriv.getBuckets(); + assertThat(buckets.size(), equalTo(valueCounts_empty.length)); + + double lastSumValue = Double.NaN; + for (int i = 0; i < valueCounts_empty.length; i++) { + Histogram.Bucket bucket = buckets.get(i); + checkBucketKeyAndDocCount("Bucket " + i, bucket, i, valueCounts_empty[i]); + Sum sum = bucket.getAggregations().get("sum"); + double thisSumValue = sum.value(); + SimpleValue docCountDeriv = bucket.getAggregations().get("deriv"); + if (i == 0) { + assertThat(docCountDeriv, nullValue()); + } else { + assertThat(docCountDeriv.value(), closeTo(thisSumValue - lastSumValue, 0.00001)); + } + lastSumValue = thisSumValue; + } + } + private void checkBucketKeyAndDocCount(final String msg, final Histogram.Bucket bucket, final long expectedKey, final long expectedDocCount) { assertThat(msg, bucket, notNullValue()); From 77e2f644e32a78b4350fa162c4d7c8ba4b0becf4 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Wed, 22 Apr 2015 14:50:49 +0100 Subject: [PATCH 50/68] Derivative tests for gaps in metrics --- .../aggregations/reducers/BucketHelpers.java | 14 +-- .../reducers/DerivativeTests.java | 100 +++++++++++++++++- 2 files changed, 104 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java index b6955a086ab03..f6cdd8ca1f960 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java @@ -166,13 +166,15 @@ public static Double resolveBucketValue(InternalMultiBucketAggregation deriv = searchResponse.getAggregations().get("histo"); + assertThat(deriv, Matchers.notNullValue()); + assertThat(deriv.getName(), equalTo("histo")); + List buckets = deriv.getBuckets(); + assertThat(buckets.size(), equalTo(valueCounts_empty.length)); + + double lastSumValue = Double.NaN; + for (int i = 0; i < valueCounts_empty.length; i++) { + Histogram.Bucket bucket = buckets.get(i); + checkBucketKeyAndDocCount("Bucket " + i, bucket, i, valueCounts_empty[i]); + Sum sum = bucket.getAggregations().get("sum"); + double thisSumValue = sum.value(); + if (bucket.getDocCount() == 0) { + thisSumValue = 0; + } + SimpleValue sumDeriv = bucket.getAggregations().get("deriv"); + if (i == 0) { + assertThat(sumDeriv, nullValue()); + } else { + double expectedDerivative = thisSumValue - lastSumValue; + assertThat(sumDeriv.value(), closeTo(expectedDerivative, 0.00001)); + } + lastSumValue = thisSumValue; + } + } + + @Test + public void singleValueAggDerivativeWithGaps_random() throws Exception { + GapPolicy gapPolicy = randomFrom(GapPolicy.values()); + SearchResponse searchResponse = client() + .prepareSearch("empty_bucket_idx_rnd") + .setQuery(matchAllQuery()) + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(1).minDocCount(0) + .extendedBounds(0l, (long) numBuckets_empty_rnd - 1) + .subAggregation(sum("sum").field(SINGLE_VALUED_FIELD_NAME)) + .subAggregation(derivative("deriv").setBucketsPaths("sum").gapPolicy(gapPolicy))).execute().actionGet(); + + assertThat(searchResponse.getHits().getTotalHits(), equalTo(numDocsEmptyIdx_rnd)); + + InternalHistogram deriv = searchResponse.getAggregations().get("histo"); + assertThat(deriv, Matchers.notNullValue()); + assertThat(deriv.getName(), equalTo("histo")); + List buckets = deriv.getBuckets(); + assertThat(buckets.size(), equalTo(numBuckets_empty_rnd)); + + double lastSumValue = Double.NaN; + for (int i = 0; i < valueCounts_empty_rnd.length; i++) { + Histogram.Bucket bucket = buckets.get(i); + checkBucketKeyAndDocCount("Bucket " + i, bucket, i, valueCounts_empty_rnd[i]); + Sum sum = bucket.getAggregations().get("sum"); + double thisSumValue = sum.value(); + if (bucket.getDocCount() == 0) { + thisSumValue = gapPolicy == GapPolicy.INSERT_ZEROS ? 0 : Double.NaN; + } + SimpleValue sumDeriv = bucket.getAggregations().get("deriv"); + if (i == 0) { + assertThat(sumDeriv, nullValue()); } else { - assertThat(docCountDeriv.value(), closeTo(thisSumValue - lastSumValue, 0.00001)); + double expectedDerivative = thisSumValue - lastSumValue; + if (Double.isNaN(expectedDerivative)) { + assertThat(sumDeriv.value(), equalTo(expectedDerivative)); + } else { + assertThat(sumDeriv.value(), closeTo(expectedDerivative, 0.00001)); + } } lastSumValue = thisSumValue; } From dcf91ff02f721beb49a9952f7166d6731e72f36d Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Wed, 22 Apr 2015 16:01:23 +0100 Subject: [PATCH 51/68] Temporarily disabled gap policy randomisation in MovAvgTests --- .../search/aggregations/reducers/MovAvgTests.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/MovAvgTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/MovAvgTests.java index d22656b0ad502..4f0e3c0d1cfc4 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/MovAvgTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/MovAvgTests.java @@ -21,6 +21,7 @@ import com.google.common.collect.EvictingQueue; + import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; @@ -37,7 +38,8 @@ import java.util.List; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; -import static org.elasticsearch.search.aggregations.AggregationBuilders.*; +import static org.elasticsearch.search.aggregations.AggregationBuilders.histogram; +import static org.elasticsearch.search.aggregations.AggregationBuilders.sum; import static org.elasticsearch.search.aggregations.reducers.ReducerBuilders.smooth; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; import static org.hamcrest.Matchers.equalTo; @@ -76,7 +78,7 @@ public void setupSuiteScopeCluster() throws Exception { numValueBuckets = randomIntBetween(6, 80); numFilledValueBuckets = numValueBuckets; windowSize = randomIntBetween(3,10); - gapPolicy = randomBoolean() ? BucketHelpers.GapPolicy.IGNORE : BucketHelpers.GapPolicy.INSERT_ZEROS; + gapPolicy = BucketHelpers.GapPolicy.INSERT_ZEROS; // TODO randomBoolean() ? BucketHelpers.GapPolicy.IGNORE : BucketHelpers.GapPolicy.INSERT_ZEROS; docCounts = new long[numValueBuckets]; valueCounts = new long[numValueBuckets]; From 30177887b155a4bcb44cf1ec257fd1ae81028661 Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Thu, 9 Apr 2015 15:02:01 -0400 Subject: [PATCH 52/68] Add prediction capability to MovAvgReducer This commit adds the ability for moving average models to output a "prediction" based on the current moving average model. For simple, linear and single, this prediction is simply converges on the moving average's mean at the last point, leading to a straight line. For double, this will predict in the direction of the linear trend (either globally or locally, depending on beta). Also adds some more tests. Closes #10545 --- .../reducers/ReducerBuilders.java | 2 +- .../reducers/movavg/MovAvgBuilder.java | 17 + .../reducers/movavg/MovAvgParser.java | 17 +- .../reducers/movavg/MovAvgReducer.java | 62 +- .../movavg/models/DoubleExpModel.java | 28 +- .../reducers/movavg/models/MovAvgModel.java | 53 +- .../aggregations/reducers/MovAvgTests.java | 502 -------- .../reducers/moving/avg/MovAvgTests.java | 1018 +++++++++++++++++ .../reducers/moving/avg/MovAvgUnitTests.java | 297 +++++ 9 files changed, 1477 insertions(+), 519 deletions(-) delete mode 100644 src/test/java/org/elasticsearch/search/aggregations/reducers/MovAvgTests.java create mode 100644 src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java create mode 100644 src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgUnitTests.java diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilders.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilders.java index 3f45964153baf..ba6d3ebe7c21a 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilders.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/ReducerBuilders.java @@ -36,7 +36,7 @@ public static final MaxBucketBuilder maxBucket(String name) { return new MaxBucketBuilder(name); } - public static final MovAvgBuilder smooth(String name) { + public static final MovAvgBuilder movingAvg(String name) { return new MovAvgBuilder(name); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgBuilder.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgBuilder.java index 9790604197ded..5fba23957e94e 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgBuilder.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgBuilder.java @@ -36,6 +36,7 @@ public class MovAvgBuilder extends ReducerBuilder { private GapPolicy gapPolicy; private MovAvgModelBuilder modelBuilder; private Integer window; + private Integer predict; public MovAvgBuilder(String name) { super(name, MovAvgReducer.TYPE.name()); @@ -81,6 +82,19 @@ public MovAvgBuilder window(int window) { return this; } + /** + * Sets the number of predictions that should be returned. Each prediction will be spaced at + * the intervals specified in the histogram. E.g "predict: 2" will return two new buckets at the + * end of the histogram with the predicted values. + * + * @param numPredictions Number of predictions to make + * @return Returns the builder to continue chaining + */ + public MovAvgBuilder predict(int numPredictions) { + this.predict = numPredictions; + return this; + } + @Override protected XContentBuilder internalXContent(XContentBuilder builder, Params params) throws IOException { @@ -96,6 +110,9 @@ protected XContentBuilder internalXContent(XContentBuilder builder, Params param if (window != null) { builder.field(MovAvgParser.WINDOW.getPreferredName(), window); } + if (predict != null) { + builder.field(MovAvgParser.PREDICT.getPreferredName(), predict); + } return builder; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgParser.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgParser.java index 3f241a67b3ac4..c1cdadf91ea44 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgParser.java @@ -46,6 +46,7 @@ public class MovAvgParser implements Reducer.Parser { public static final ParseField MODEL = new ParseField("model"); public static final ParseField WINDOW = new ParseField("window"); public static final ParseField SETTINGS = new ParseField("settings"); + public static final ParseField PREDICT = new ParseField("predict"); private final MovAvgModelParserMapper movAvgModelParserMapper; @@ -65,10 +66,12 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon String currentFieldName = null; String[] bucketsPaths = null; String format = null; + GapPolicy gapPolicy = GapPolicy.IGNORE; int window = 5; Map settings = null; String model = "simple"; + int predict = 0; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { @@ -76,6 +79,16 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon } else if (token == XContentParser.Token.VALUE_NUMBER) { if (WINDOW.match(currentFieldName)) { window = parser.intValue(); + if (window <= 0) { + throw new SearchParseException(context, "[" + currentFieldName + "] value must be a positive, " + + "non-zero integer. Value supplied was [" + predict + "] in [" + reducerName + "]."); + } + } else if (PREDICT.match(currentFieldName)) { + predict = parser.intValue(); + if (predict <= 0) { + throw new SearchParseException(context, "[" + currentFieldName + "] value must be a positive, " + + "non-zero integer. Value supplied was [" + predict + "] in [" + reducerName + "]."); + } } else { throw new SearchParseException(context, "Unknown key for a " + token + " in [" + reducerName + "]: [" + currentFieldName + "]."); @@ -119,7 +132,7 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon if (bucketsPaths == null) { throw new SearchParseException(context, "Missing required field [" + BUCKETS_PATH.getPreferredName() - + "] for smooth aggregation [" + reducerName + "]"); + + "] for movingAvg aggregation [" + reducerName + "]"); } ValueFormatter formatter = null; @@ -135,7 +148,7 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon MovAvgModel movAvgModel = modelParser.parse(settings); - return new MovAvgReducer.Factory(reducerName, bucketsPaths, formatter, gapPolicy, window, movAvgModel); + return new MovAvgReducer.Factory(reducerName, bucketsPaths, formatter, gapPolicy, window, predict, movAvgModel); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgReducer.java index 20baa1706f173..4bd2ff4c50a83 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgReducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgReducer.java @@ -27,12 +27,9 @@ import org.elasticsearch.common.Nullable; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.search.aggregations.Aggregation; -import org.elasticsearch.search.aggregations.AggregatorFactory; -import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.*; import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; import org.elasticsearch.search.aggregations.InternalAggregation.Type; -import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.histogram.HistogramAggregator; import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; import org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; @@ -44,6 +41,7 @@ import org.elasticsearch.search.aggregations.reducers.movavg.models.MovAvgModelStreams; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; +import org.joda.time.DateTime; import java.io.IOException; import java.util.ArrayList; @@ -80,17 +78,19 @@ public InternalAggregation apply(Aggregation input) { private GapPolicy gapPolicy; private int window; private MovAvgModel model; + private int predict; public MovAvgReducer() { } public MovAvgReducer(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter, GapPolicy gapPolicy, - int window, MovAvgModel model, Map metadata) { + int window, int predict, MovAvgModel model, Map metadata) { super(name, bucketsPaths, metadata); this.formatter = formatter; this.gapPolicy = gapPolicy; this.window = window; this.model = model; + this.predict = predict; } @Override @@ -107,8 +107,14 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext List newBuckets = new ArrayList<>(); EvictingQueue values = EvictingQueue.create(this.window); + long lastKey = 0; + long interval = Long.MAX_VALUE; + Object currentKey; + for (InternalHistogram.Bucket bucket : buckets) { Double thisBucketValue = resolveBucketValue(histo, bucket, bucketsPaths()[0], gapPolicy); + currentKey = bucket.getKey(); + if (thisBucketValue != null) { values.offer(thisBucketValue); @@ -117,14 +123,46 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext List aggs = new ArrayList<>(Lists.transform(bucket.getAggregations().asList(), FUNCTION)); aggs.add(new InternalSimpleValue(name(), movavg, formatter, new ArrayList(), metaData())); - InternalHistogram.Bucket newBucket = factory.createBucket(bucket.getKey(), bucket.getDocCount(), new InternalAggregations( + InternalHistogram.Bucket newBucket = factory.createBucket(currentKey, bucket.getDocCount(), new InternalAggregations( aggs), bucket.getKeyed(), bucket.getFormatter()); newBuckets.add(newBucket); + } else { newBuckets.add(bucket); } + + if (predict > 0) { + if (currentKey instanceof Number) { + interval = Math.min(interval, ((Number) bucket.getKey()).longValue() - lastKey); + lastKey = ((Number) bucket.getKey()).longValue(); + } else if (currentKey instanceof DateTime) { + interval = Math.min(interval, ((DateTime) bucket.getKey()).getMillis() - lastKey); + lastKey = ((DateTime) bucket.getKey()).getMillis(); + } else { + throw new AggregationExecutionException("Expected key of type Number or DateTime but got [" + currentKey + "]"); + } + } + } - //return factory.create(histo.getName(), newBuckets, histo); + + + if (buckets.size() > 0 && predict > 0) { + + boolean keyed; + ValueFormatter formatter; + keyed = buckets.get(0).getKeyed(); + formatter = buckets.get(0).getFormatter(); + + double[] predictions = model.predict(values, predict); + for (int i = 0; i < predictions.length; i++) { + List aggs = new ArrayList<>(); + aggs.add(new InternalSimpleValue(name(), predictions[i], formatter, new ArrayList(), metaData())); + InternalHistogram.Bucket newBucket = factory.createBucket(lastKey + (interval * (i + 1)), 0, new InternalAggregations( + aggs), keyed, formatter); + newBuckets.add(newBucket); + } + } + return factory.create(newBuckets, histo); } @@ -133,7 +171,9 @@ public void doReadFrom(StreamInput in) throws IOException { formatter = ValueFormatterStreams.readOptional(in); gapPolicy = GapPolicy.readFrom(in); window = in.readVInt(); + predict = in.readVInt(); model = MovAvgModelStreams.read(in); + } @Override @@ -141,7 +181,9 @@ public void doWriteTo(StreamOutput out) throws IOException { ValueFormatterStreams.writeOptional(formatter, out); gapPolicy.writeTo(out); out.writeVInt(window); + out.writeVInt(predict); model.writeTo(out); + } public static class Factory extends ReducerFactory { @@ -150,19 +192,21 @@ public static class Factory extends ReducerFactory { private GapPolicy gapPolicy; private int window; private MovAvgModel model; + private int predict; public Factory(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter, GapPolicy gapPolicy, - int window, MovAvgModel model) { + int window, int predict, MovAvgModel model) { super(name, TYPE.name(), bucketsPaths); this.formatter = formatter; this.gapPolicy = gapPolicy; this.window = window; this.model = model; + this.predict = predict; } @Override protected Reducer createInternal(Map metaData) throws IOException { - return new MovAvgReducer(name, bucketsPaths, formatter, gapPolicy, window, model, metaData); + return new MovAvgReducer(name, bucketsPaths, formatter, gapPolicy, window, predict, model, metaData); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/DoubleExpModel.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/DoubleExpModel.java index 907c23fd213f8..7d32989cda155 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/DoubleExpModel.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/DoubleExpModel.java @@ -53,10 +53,25 @@ public DoubleExpModel(double alpha, double beta) { this.beta = beta; } + /** + * Predicts the next `n` values in the series, using the smoothing model to generate new values. + * Unlike the other moving averages, double-exp has forecasting/prediction built into the algorithm. + * Prediction is more than simply adding the next prediction to the window and repeating. Double-exp + * will extrapolate into the future by applying the trend information to the smoothed data. + * + * @param values Collection of numerics to movingAvg, usually windowed + * @param numPredictions Number of newly generated predictions to return + * @param Type of numeric + * @return Returns an array of doubles, since most smoothing methods operate on floating points + */ + @Override + public double[] predict(Collection values, int numPredictions) { + return next(values, numPredictions); + } @Override public double next(Collection values) { - return next(values, 1).get(0); + return next(values, 1)[0]; } /** @@ -68,7 +83,12 @@ public double next(Collection values) { * @param Type T extending Number * @return Returns a Double containing the moving avg for the window */ - public List next(Collection values, int numForecasts) { + public double[] next(Collection values, int numForecasts) { + + if (values.size() == 0) { + return emptyPredictions(numForecasts); + } + // Smoothed value double s = 0; double last_s = 0; @@ -97,9 +117,9 @@ public List next(Collection values, int numForecas last_b = b; } - List forecastValues = new ArrayList<>(numForecasts); + double[] forecastValues = new double[numForecasts]; for (int i = 0; i < numForecasts; i++) { - forecastValues.add(s + (i * b)); + forecastValues[i] = s + (i * b); } return forecastValues; diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModel.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModel.java index 84f7832f893ef..d798887c836aa 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModel.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModel.java @@ -19,6 +19,8 @@ package org.elasticsearch.search.aggregations.reducers.movavg.models; +import com.google.common.collect.EvictingQueue; +import org.elasticsearch.ElasticsearchIllegalArgumentException; import org.elasticsearch.common.io.stream.StreamOutput; import java.io.IOException; @@ -29,12 +31,61 @@ public abstract class MovAvgModel { /** * Returns the next value in the series, according to the underlying smoothing model * - * @param values Collection of numerics to smooth, usually windowed + * @param values Collection of numerics to movingAvg, usually windowed * @param Type of numeric * @return Returns a double, since most smoothing methods operate on floating points */ public abstract double next(Collection values); + /** + * Predicts the next `n` values in the series, using the smoothing model to generate new values. + * Default prediction mode is to simply continuing calling next() and adding the + * predicted value back into the windowed buffer. + * + * @param values Collection of numerics to movingAvg, usually windowed + * @param numPredictions Number of newly generated predictions to return + * @param Type of numeric + * @return Returns an array of doubles, since most smoothing methods operate on floating points + */ + public double[] predict(Collection values, int numPredictions) { + double[] predictions = new double[numPredictions]; + + // If there are no values, we can't do anything. Return an array of NaNs. + if (values.size() == 0) { + return emptyPredictions(numPredictions); + } + + // special case for one prediction, avoids allocation + if (numPredictions < 1) { + throw new ElasticsearchIllegalArgumentException("numPredictions may not be less than 1."); + } else if (numPredictions == 1){ + predictions[0] = next(values); + return predictions; + } + + // nocommit + // I don't like that it creates a new queue here + // The alternative to this is to just use `values` directly, but that would "consume" values + // and potentially change state elsewhere. Maybe ok? + Collection predictionBuffer = EvictingQueue.create(values.size()); + predictionBuffer.addAll(values); + + for (int i = 0; i < numPredictions; i++) { + predictions[i] = next(predictionBuffer); + + // Add the last value to the buffer, so we can keep predicting + predictionBuffer.add(predictions[i]); + } + + return predictions; + } + + protected double[] emptyPredictions(int numPredictions) { + double[] predictions = new double[numPredictions]; + Arrays.fill(predictions, Double.NaN); + return predictions; + } + /** * Write the model to the output stream * diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/MovAvgTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/MovAvgTests.java deleted file mode 100644 index 4f0e3c0d1cfc4..0000000000000 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/MovAvgTests.java +++ /dev/null @@ -1,502 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.search.aggregations.reducers; - - -import com.google.common.collect.EvictingQueue; - -import org.elasticsearch.action.index.IndexRequestBuilder; -import org.elasticsearch.action.search.SearchResponse; -import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; -import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; -import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram.Bucket; -import org.elasticsearch.search.aggregations.reducers.movavg.models.DoubleExpModel; -import org.elasticsearch.search.aggregations.reducers.movavg.models.LinearModel; -import org.elasticsearch.search.aggregations.reducers.movavg.models.SimpleModel; -import org.elasticsearch.search.aggregations.reducers.movavg.models.SingleExpModel; -import org.elasticsearch.test.ElasticsearchIntegrationTest; -import org.junit.Test; - -import java.util.ArrayList; -import java.util.List; - -import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; -import static org.elasticsearch.search.aggregations.AggregationBuilders.histogram; -import static org.elasticsearch.search.aggregations.AggregationBuilders.sum; -import static org.elasticsearch.search.aggregations.reducers.ReducerBuilders.smooth; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.core.IsNull.notNullValue; - -@ElasticsearchIntegrationTest.SuiteScopeTest -public class MovAvgTests extends ElasticsearchIntegrationTest { - - private static final String SINGLE_VALUED_FIELD_NAME = "l_value"; - private static final String SINGLE_VALUED_VALUE_FIELD_NAME = "v_value"; - - static int interval; - static int numValueBuckets; - static int numFilledValueBuckets; - static int windowSize; - static BucketHelpers.GapPolicy gapPolicy; - - static long[] docCounts; - static long[] valueCounts; - static Double[] simpleMovAvgCounts; - static Double[] linearMovAvgCounts; - static Double[] singleExpMovAvgCounts; - static Double[] doubleExpMovAvgCounts; - - static Double[] simpleMovAvgValueCounts; - static Double[] linearMovAvgValueCounts; - static Double[] singleExpMovAvgValueCounts; - static Double[] doubleExpMovAvgValueCounts; - - @Override - public void setupSuiteScopeCluster() throws Exception { - createIndex("idx"); - createIndex("idx_unmapped"); - - interval = 5; - numValueBuckets = randomIntBetween(6, 80); - numFilledValueBuckets = numValueBuckets; - windowSize = randomIntBetween(3,10); - gapPolicy = BucketHelpers.GapPolicy.INSERT_ZEROS; // TODO randomBoolean() ? BucketHelpers.GapPolicy.IGNORE : BucketHelpers.GapPolicy.INSERT_ZEROS; - - docCounts = new long[numValueBuckets]; - valueCounts = new long[numValueBuckets]; - for (int i = 0; i < numValueBuckets; i++) { - docCounts[i] = randomIntBetween(0, 20); - valueCounts[i] = randomIntBetween(1,20); //this will be used as a constant for all values within a bucket - } - - this.setupSimple(); - this.setupLinear(); - this.setupSingle(); - this.setupDouble(); - - - List builders = new ArrayList<>(); - for (int i = 0; i < numValueBuckets; i++) { - for (int docs = 0; docs < docCounts[i]; docs++) { - builders.add(client().prepareIndex("idx", "type").setSource(jsonBuilder().startObject() - .field(SINGLE_VALUED_FIELD_NAME, i * interval) - .field(SINGLE_VALUED_VALUE_FIELD_NAME, 1).endObject())); - } - } - - indexRandom(true, builders); - ensureSearchable(); - } - - private void setupSimple() { - simpleMovAvgCounts = new Double[numValueBuckets]; - EvictingQueue window = EvictingQueue.create(windowSize); - for (int i = 0; i < numValueBuckets; i++) { - double thisValue = docCounts[i]; - window.offer(thisValue); - - double movAvg = 0; - for (double value : window) { - movAvg += value; - } - movAvg /= window.size(); - - simpleMovAvgCounts[i] = movAvg; - } - - window.clear(); - simpleMovAvgValueCounts = new Double[numValueBuckets]; - for (int i = 0; i < numValueBuckets; i++) { - window.offer((double)docCounts[i]); - - double movAvg = 0; - for (double value : window) { - movAvg += value; - } - movAvg /= window.size(); - - simpleMovAvgValueCounts[i] = movAvg; - - } - - } - - private void setupLinear() { - EvictingQueue window = EvictingQueue.create(windowSize); - linearMovAvgCounts = new Double[numValueBuckets]; - window.clear(); - for (int i = 0; i < numValueBuckets; i++) { - double thisValue = docCounts[i]; - if (thisValue == -1) { - thisValue = 0; - } - window.offer(thisValue); - - double avg = 0; - long totalWeight = 1; - long current = 1; - - for (double value : window) { - avg += value * current; - totalWeight += current; - current += 1; - } - linearMovAvgCounts[i] = avg / totalWeight; - } - - window.clear(); - linearMovAvgValueCounts = new Double[numValueBuckets]; - - for (int i = 0; i < numValueBuckets; i++) { - double thisValue = docCounts[i]; - window.offer(thisValue); - - double avg = 0; - long totalWeight = 1; - long current = 1; - - for (double value : window) { - avg += value * current; - totalWeight += current; - current += 1; - } - linearMovAvgValueCounts[i] = avg / totalWeight; - } - } - - private void setupSingle() { - EvictingQueue window = EvictingQueue.create(windowSize); - singleExpMovAvgCounts = new Double[numValueBuckets]; - for (int i = 0; i < numValueBuckets; i++) { - double thisValue = docCounts[i]; - if (thisValue == -1) { - thisValue = 0; - } - window.offer(thisValue); - - double avg = 0; - double alpha = 0.5; - boolean first = true; - - for (double value : window) { - if (first) { - avg = value; - first = false; - } else { - avg = (value * alpha) + (avg * (1 - alpha)); - } - } - singleExpMovAvgCounts[i] = avg ; - } - - singleExpMovAvgValueCounts = new Double[numValueBuckets]; - window.clear(); - - for (int i = 0; i < numValueBuckets; i++) { - window.offer((double)docCounts[i]); - - double avg = 0; - double alpha = 0.5; - boolean first = true; - - for (double value : window) { - if (first) { - avg = value; - first = false; - } else { - avg = (value * alpha) + (avg * (1 - alpha)); - } - } - singleExpMovAvgCounts[i] = avg ; - } - - } - - private void setupDouble() { - EvictingQueue window = EvictingQueue.create(windowSize); - doubleExpMovAvgCounts = new Double[numValueBuckets]; - - for (int i = 0; i < numValueBuckets; i++) { - double thisValue = docCounts[i]; - if (thisValue == -1) { - thisValue = 0; - } - window.offer(thisValue); - - double s = 0; - double last_s = 0; - - // Trend value - double b = 0; - double last_b = 0; - - double alpha = 0.5; - double beta = 0.5; - int counter = 0; - - double last; - for (double value : window) { - last = value; - if (counter == 1) { - s = value; - b = value - last; - } else { - s = alpha * value + (1.0d - alpha) * (last_s + last_b); - b = beta * (s - last_s) + (1 - beta) * last_b; - } - - counter += 1; - last_s = s; - last_b = b; - } - - doubleExpMovAvgCounts[i] = s + (0 * b) ; - } - - doubleExpMovAvgValueCounts = new Double[numValueBuckets]; - window.clear(); - - for (int i = 0; i < numValueBuckets; i++) { - window.offer((double)docCounts[i]); - - double s = 0; - double last_s = 0; - - // Trend value - double b = 0; - double last_b = 0; - - double alpha = 0.5; - double beta = 0.5; - int counter = 0; - - double last; - for (double value : window) { - last = value; - if (counter == 1) { - s = value; - b = value - last; - } else { - s = alpha * value + (1.0d - alpha) * (last_s + last_b); - b = beta * (s - last_s) + (1 - beta) * last_b; - } - - counter += 1; - last_s = s; - last_b = b; - } - - doubleExpMovAvgValueCounts[i] = s + (0 * b) ; - } - } - - /** - * test simple moving average on single value field - */ - @Test - public void simpleSingleValuedField() { - - SearchResponse response = client() - .prepareSearch("idx") - .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) - .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) - .subAggregation(smooth("smooth") - .window(windowSize) - .modelBuilder(new SimpleModel.SimpleModelBuilder()) - .gapPolicy(gapPolicy) - .setBucketsPaths("_count")) - .subAggregation(smooth("movavg_values") - .window(windowSize) - .modelBuilder(new SimpleModel.SimpleModelBuilder()) - .gapPolicy(gapPolicy) - .setBucketsPaths("the_sum")) - ).execute().actionGet(); - - assertSearchResponse(response); - - InternalHistogram histo = response.getAggregations().get("histo"); - assertThat(histo, notNullValue()); - assertThat(histo.getName(), equalTo("histo")); - List buckets = histo.getBuckets(); - assertThat(buckets.size(), equalTo(numValueBuckets)); - - for (int i = 0; i < numValueBuckets; ++i) { - Histogram.Bucket bucket = buckets.get(i); - checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, docCounts[i]); - SimpleValue docCountMovAvg = bucket.getAggregations().get("smooth"); - assertThat(docCountMovAvg, notNullValue()); - assertThat(docCountMovAvg.value(), equalTo(simpleMovAvgCounts[i])); - - SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); - assertThat(valuesMovAvg, notNullValue()); - assertThat(valuesMovAvg.value(), equalTo(simpleMovAvgCounts[i])); - } - } - - /** - * test linear moving average on single value field - */ - @Test - public void linearSingleValuedField() { - - SearchResponse response = client() - .prepareSearch("idx") - .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) - .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) - .subAggregation(smooth("smooth") - .window(windowSize) - .modelBuilder(new LinearModel.LinearModelBuilder()) - .gapPolicy(gapPolicy) - .setBucketsPaths("_count")) - .subAggregation(smooth("movavg_values") - .window(windowSize) - .modelBuilder(new LinearModel.LinearModelBuilder()) - .gapPolicy(gapPolicy) - .setBucketsPaths("the_sum")) - ).execute().actionGet(); - - assertSearchResponse(response); - - InternalHistogram histo = response.getAggregations().get("histo"); - assertThat(histo, notNullValue()); - assertThat(histo.getName(), equalTo("histo")); - List buckets = histo.getBuckets(); - assertThat(buckets.size(), equalTo(numValueBuckets)); - - for (int i = 0; i < numValueBuckets; ++i) { - Histogram.Bucket bucket = buckets.get(i); - checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, docCounts[i]); - SimpleValue docCountMovAvg = bucket.getAggregations().get("smooth"); - assertThat(docCountMovAvg, notNullValue()); - assertThat(docCountMovAvg.value(), equalTo(linearMovAvgCounts[i])); - - SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); - assertThat(valuesMovAvg, notNullValue()); - assertThat(valuesMovAvg.value(), equalTo(linearMovAvgCounts[i])); - } - } - - /** - * test single exponential moving average on single value field - */ - @Test - public void singleExpSingleValuedField() { - - SearchResponse response = client() - .prepareSearch("idx") - .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) - .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) - .subAggregation(smooth("smooth") - .window(windowSize) - .modelBuilder(new SingleExpModel.SingleExpModelBuilder().alpha(0.5)) - .gapPolicy(gapPolicy) - .setBucketsPaths("_count")) - .subAggregation(smooth("movavg_values") - .window(windowSize) - .modelBuilder(new SingleExpModel.SingleExpModelBuilder().alpha(0.5)) - .gapPolicy(gapPolicy) - .setBucketsPaths("the_sum")) - ).execute().actionGet(); - - assertSearchResponse(response); - - InternalHistogram histo = response.getAggregations().get("histo"); - assertThat(histo, notNullValue()); - assertThat(histo.getName(), equalTo("histo")); - List buckets = histo.getBuckets(); - assertThat(buckets.size(), equalTo(numValueBuckets)); - - for (int i = 0; i < numValueBuckets; ++i) { - Histogram.Bucket bucket = buckets.get(i); - checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, docCounts[i]); - SimpleValue docCountMovAvg = bucket.getAggregations().get("smooth"); - assertThat(docCountMovAvg, notNullValue()); - assertThat(docCountMovAvg.value(), equalTo(singleExpMovAvgCounts[i])); - - SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); - assertThat(valuesMovAvg, notNullValue()); - assertThat(valuesMovAvg.value(), equalTo(singleExpMovAvgCounts[i])); - } - } - - /** - * test double exponential moving average on single value field - */ - @Test - public void doubleExpSingleValuedField() { - - SearchResponse response = client() - .prepareSearch("idx") - .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) - .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) - .subAggregation(smooth("smooth") - .window(windowSize) - .modelBuilder(new DoubleExpModel.DoubleExpModelBuilder().alpha(0.5).beta(0.5)) - .gapPolicy(gapPolicy) - .setBucketsPaths("_count")) - .subAggregation(smooth("movavg_values") - .window(windowSize) - .modelBuilder(new DoubleExpModel.DoubleExpModelBuilder().alpha(0.5).beta(0.5)) - .gapPolicy(gapPolicy) - .setBucketsPaths("the_sum")) - ).execute().actionGet(); - - assertSearchResponse(response); - - InternalHistogram histo = response.getAggregations().get("histo"); - assertThat(histo, notNullValue()); - assertThat(histo.getName(), equalTo("histo")); - List buckets = histo.getBuckets(); - assertThat(buckets.size(), equalTo(numValueBuckets)); - - for (int i = 0; i < numValueBuckets; ++i) { - Histogram.Bucket bucket = buckets.get(i); - checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, docCounts[i]); - SimpleValue docCountMovAvg = bucket.getAggregations().get("smooth"); - assertThat(docCountMovAvg, notNullValue()); - assertThat(docCountMovAvg.value(), equalTo(doubleExpMovAvgCounts[i])); - - SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); - assertThat(valuesMovAvg, notNullValue()); - assertThat(valuesMovAvg.value(), equalTo(doubleExpMovAvgCounts[i])); - } - } - - - private void checkBucketKeyAndDocCount(final String msg, final Histogram.Bucket bucket, final long expectedKey, - long expectedDocCount) { - if (expectedDocCount == -1) { - expectedDocCount = 0; - } - assertThat(msg, bucket, notNullValue()); - assertThat(msg + " key", ((Number) bucket.getKey()).longValue(), equalTo(expectedKey)); - assertThat(msg + " docCount", bucket.getDocCount(), equalTo(expectedDocCount)); - } - -} diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java new file mode 100644 index 0000000000000..9c3a6f2341990 --- /dev/null +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java @@ -0,0 +1,1018 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.moving.avg; + + +import com.google.common.collect.EvictingQueue; + +import org.elasticsearch.action.index.IndexRequestBuilder; +import org.elasticsearch.action.search.SearchPhaseExecutionException; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.index.query.RangeFilterBuilder; +import org.elasticsearch.search.aggregations.bucket.filter.InternalFilter; +import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; +import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; +import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram.Bucket; +import org.elasticsearch.search.aggregations.reducers.BucketHelpers; +import org.elasticsearch.search.aggregations.reducers.SimpleValue; +import org.elasticsearch.search.aggregations.reducers.movavg.models.*; +import org.elasticsearch.test.ElasticsearchIntegrationTest; +import org.hamcrest.Matchers; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.search.aggregations.AggregationBuilders.histogram; +import static org.elasticsearch.search.aggregations.AggregationBuilders.sum; +import static org.elasticsearch.search.aggregations.reducers.ReducerBuilders.smooth; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.core.IsNull.notNullValue; + +@ElasticsearchIntegrationTest.SuiteScopeTest +public class MovAvgTests extends ElasticsearchIntegrationTest { + + private static final String SINGLE_VALUED_FIELD_NAME = "l_value"; + private static final String SINGLE_VALUED_VALUE_FIELD_NAME = "v_value"; + private static final String GAP_FIELD = "g_value"; + + static int interval; + static int numValueBuckets; + static int numFilledValueBuckets; + static int windowSize; + static BucketHelpers.GapPolicy gapPolicy; + + static long[] docCounts; + static long[] valueCounts; + static Double[] simpleMovAvgCounts; + static Double[] linearMovAvgCounts; + static Double[] singleExpMovAvgCounts; + static Double[] doubleExpMovAvgCounts; + + static Double[] simpleMovAvgValueCounts; + static Double[] linearMovAvgValueCounts; + static Double[] singleExpMovAvgValueCounts; + static Double[] doubleExpMovAvgValueCounts; + + @Override + public void setupSuiteScopeCluster() throws Exception { + createIndex("idx"); + createIndex("idx_unmapped"); + List builders = new ArrayList<>(); + + interval = 5; + numValueBuckets = randomIntBetween(6, 80); + numFilledValueBuckets = numValueBuckets; + windowSize = randomIntBetween(3,10); + gapPolicy = BucketHelpers.GapPolicy.INSERT_ZEROS; // TODO randomBoolean() ? BucketHelpers.GapPolicy.IGNORE : BucketHelpers.GapPolicy.INSERT_ZEROS; + + docCounts = new long[numValueBuckets]; + valueCounts = new long[numValueBuckets]; + for (int i = 0; i < numValueBuckets; i++) { + docCounts[i] = randomIntBetween(0, 20); + valueCounts[i] = randomIntBetween(1,20); //this will be used as a constant for all values within a bucket + } + + // Used for the gap tests + builders.add(client().prepareIndex("idx", "type").setSource(jsonBuilder().startObject() + .field("gap_test", 0) + .field(GAP_FIELD, 1).endObject())); + builders.add(client().prepareIndex("idx", "type").setSource(jsonBuilder().startObject() + .field("gap_test", (numValueBuckets - 1) * interval) + .field(GAP_FIELD, 1).endObject())); + + this.setupSimple(); + this.setupLinear(); + this.setupSingle(); + this.setupDouble(); + + + + for (int i = 0; i < numValueBuckets; i++) { + for (int docs = 0; docs < docCounts[i]; docs++) { + builders.add(client().prepareIndex("idx", "type").setSource(jsonBuilder().startObject() + .field(SINGLE_VALUED_FIELD_NAME, i * interval) + .field(SINGLE_VALUED_VALUE_FIELD_NAME, 1).endObject())); + } + } + + indexRandom(true, builders); + ensureSearchable(); + } + + private void setupSimple() { + simpleMovAvgCounts = new Double[numValueBuckets]; + EvictingQueue window = EvictingQueue.create(windowSize); + for (int i = 0; i < numValueBuckets; i++) { + double thisValue = docCounts[i]; + window.offer(thisValue); + + double movAvg = 0; + for (double value : window) { + movAvg += value; + } + movAvg /= window.size(); + + simpleMovAvgCounts[i] = movAvg; + } + + window.clear(); + simpleMovAvgValueCounts = new Double[numValueBuckets]; + for (int i = 0; i < numValueBuckets; i++) { + window.offer((double)docCounts[i]); + + double movAvg = 0; + for (double value : window) { + movAvg += value; + } + movAvg /= window.size(); + + simpleMovAvgValueCounts[i] = movAvg; + + } + + } + + private void setupLinear() { + EvictingQueue window = EvictingQueue.create(windowSize); + linearMovAvgCounts = new Double[numValueBuckets]; + window.clear(); + for (int i = 0; i < numValueBuckets; i++) { + double thisValue = docCounts[i]; + if (thisValue == -1) { + thisValue = 0; + } + window.offer(thisValue); + + double avg = 0; + long totalWeight = 1; + long current = 1; + + for (double value : window) { + avg += value * current; + totalWeight += current; + current += 1; + } + linearMovAvgCounts[i] = avg / totalWeight; + } + + window.clear(); + linearMovAvgValueCounts = new Double[numValueBuckets]; + + for (int i = 0; i < numValueBuckets; i++) { + double thisValue = docCounts[i]; + window.offer(thisValue); + + double avg = 0; + long totalWeight = 1; + long current = 1; + + for (double value : window) { + avg += value * current; + totalWeight += current; + current += 1; + } + linearMovAvgValueCounts[i] = avg / totalWeight; + } + } + + private void setupSingle() { + EvictingQueue window = EvictingQueue.create(windowSize); + singleExpMovAvgCounts = new Double[numValueBuckets]; + for (int i = 0; i < numValueBuckets; i++) { + double thisValue = docCounts[i]; + if (thisValue == -1) { + thisValue = 0; + } + window.offer(thisValue); + + double avg = 0; + double alpha = 0.5; + boolean first = true; + + for (double value : window) { + if (first) { + avg = value; + first = false; + } else { + avg = (value * alpha) + (avg * (1 - alpha)); + } + } + singleExpMovAvgCounts[i] = avg ; + } + + singleExpMovAvgValueCounts = new Double[numValueBuckets]; + window.clear(); + + for (int i = 0; i < numValueBuckets; i++) { + window.offer((double)docCounts[i]); + + double avg = 0; + double alpha = 0.5; + boolean first = true; + + for (double value : window) { + if (first) { + avg = value; + first = false; + } else { + avg = (value * alpha) + (avg * (1 - alpha)); + } + } + singleExpMovAvgCounts[i] = avg ; + } + + } + + private void setupDouble() { + EvictingQueue window = EvictingQueue.create(windowSize); + doubleExpMovAvgCounts = new Double[numValueBuckets]; + + for (int i = 0; i < numValueBuckets; i++) { + double thisValue = docCounts[i]; + if (thisValue == -1) { + thisValue = 0; + } + window.offer(thisValue); + + double s = 0; + double last_s = 0; + + // Trend value + double b = 0; + double last_b = 0; + + double alpha = 0.5; + double beta = 0.5; + int counter = 0; + + double last; + for (double value : window) { + last = value; + if (counter == 1) { + s = value; + b = value - last; + } else { + s = alpha * value + (1.0d - alpha) * (last_s + last_b); + b = beta * (s - last_s) + (1 - beta) * last_b; + } + + counter += 1; + last_s = s; + last_b = b; + } + + doubleExpMovAvgCounts[i] = s + (0 * b) ; + } + + doubleExpMovAvgValueCounts = new Double[numValueBuckets]; + window.clear(); + + for (int i = 0; i < numValueBuckets; i++) { + window.offer((double)docCounts[i]); + + double s = 0; + double last_s = 0; + + // Trend value + double b = 0; + double last_b = 0; + + double alpha = 0.5; + double beta = 0.5; + int counter = 0; + + double last; + for (double value : window) { + last = value; + if (counter == 1) { + s = value; + b = value - last; + } else { + s = alpha * value + (1.0d - alpha) * (last_s + last_b); + b = beta * (s - last_s) + (1 - beta) * last_b; + } + + counter += 1; + last_s = s; + last_b = b; + } + + doubleExpMovAvgValueCounts[i] = s + (0 * b) ; + } + } + + /** + * test simple moving average on single value field + */ + @Test + public void simpleSingleValuedField() { + + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(movingAvg("movingAvg") + .window(windowSize) + .modelBuilder(new SimpleModel.SimpleModelBuilder()) + .gapPolicy(gapPolicy) + .setBucketsPaths("_count")) + .subAggregation(movingAvg("movavg_values") + .window(windowSize) + .modelBuilder(new SimpleModel.SimpleModelBuilder()) + .gapPolicy(gapPolicy) + .setBucketsPaths("the_sum")) + ).execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram histo = response.getAggregations().get("histo"); + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + assertThat(buckets.size(), equalTo(numValueBuckets)); + + for (int i = 0; i < numValueBuckets; ++i) { + Histogram.Bucket bucket = buckets.get(i); + checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, docCounts[i]); + SimpleValue docCountMovAvg = bucket.getAggregations().get("movingAvg"); + assertThat(docCountMovAvg, notNullValue()); + assertThat(docCountMovAvg.value(), equalTo(simpleMovAvgCounts[i])); + + SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); + assertThat(valuesMovAvg, notNullValue()); + assertThat(valuesMovAvg.value(), equalTo(simpleMovAvgCounts[i])); + } + } + + /** + * test linear moving average on single value field + */ + @Test + public void linearSingleValuedField() { + + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(movingAvg("movingAvg") + .window(windowSize) + .modelBuilder(new LinearModel.LinearModelBuilder()) + .gapPolicy(gapPolicy) + .setBucketsPaths("_count")) + .subAggregation(movingAvg("movavg_values") + .window(windowSize) + .modelBuilder(new LinearModel.LinearModelBuilder()) + .gapPolicy(gapPolicy) + .setBucketsPaths("the_sum")) + ).execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram histo = response.getAggregations().get("histo"); + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + assertThat(buckets.size(), equalTo(numValueBuckets)); + + for (int i = 0; i < numValueBuckets; ++i) { + Histogram.Bucket bucket = buckets.get(i); + checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, docCounts[i]); + SimpleValue docCountMovAvg = bucket.getAggregations().get("movingAvg"); + assertThat(docCountMovAvg, notNullValue()); + assertThat(docCountMovAvg.value(), equalTo(linearMovAvgCounts[i])); + + SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); + assertThat(valuesMovAvg, notNullValue()); + assertThat(valuesMovAvg.value(), equalTo(linearMovAvgCounts[i])); + } + } + + /** + * test single exponential moving average on single value field + */ + @Test + public void singleExpSingleValuedField() { + + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(movingAvg("movingAvg") + .window(windowSize) + .modelBuilder(new SingleExpModel.SingleExpModelBuilder().alpha(0.5)) + .gapPolicy(gapPolicy) + .setBucketsPaths("_count")) + .subAggregation(movingAvg("movavg_values") + .window(windowSize) + .modelBuilder(new SingleExpModel.SingleExpModelBuilder().alpha(0.5)) + .gapPolicy(gapPolicy) + .setBucketsPaths("the_sum")) + ).execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram histo = response.getAggregations().get("histo"); + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + assertThat(buckets.size(), equalTo(numValueBuckets)); + + for (int i = 0; i < numValueBuckets; ++i) { + Histogram.Bucket bucket = buckets.get(i); + checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, docCounts[i]); + SimpleValue docCountMovAvg = bucket.getAggregations().get("movingAvg"); + assertThat(docCountMovAvg, notNullValue()); + assertThat(docCountMovAvg.value(), equalTo(singleExpMovAvgCounts[i])); + + SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); + assertThat(valuesMovAvg, notNullValue()); + assertThat(valuesMovAvg.value(), equalTo(singleExpMovAvgCounts[i])); + } + } + + /** + * test double exponential moving average on single value field + */ + @Test + public void doubleExpSingleValuedField() { + + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(movingAvg("movingAvg") + .window(windowSize) + .modelBuilder(new DoubleExpModel.DoubleExpModelBuilder().alpha(0.5).beta(0.5)) + .gapPolicy(gapPolicy) + .setBucketsPaths("_count")) + .subAggregation(movingAvg("movavg_values") + .window(windowSize) + .modelBuilder(new DoubleExpModel.DoubleExpModelBuilder().alpha(0.5).beta(0.5)) + .gapPolicy(gapPolicy) + .setBucketsPaths("the_sum")) + ).execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram histo = response.getAggregations().get("histo"); + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + assertThat(buckets.size(), equalTo(numValueBuckets)); + + for (int i = 0; i < numValueBuckets; ++i) { + Histogram.Bucket bucket = buckets.get(i); + checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, docCounts[i]); + SimpleValue docCountMovAvg = bucket.getAggregations().get("movingAvg"); + assertThat(docCountMovAvg, notNullValue()); + assertThat(docCountMovAvg.value(), equalTo(doubleExpMovAvgCounts[i])); + + SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); + assertThat(valuesMovAvg, notNullValue()); + assertThat(valuesMovAvg.value(), equalTo(doubleExpMovAvgCounts[i])); + } + } + + @Test + public void testSizeZeroWindow() { + try { + client() + .prepareSearch("idx") + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(movingAvg("movingAvg") + .window(0) + .modelBuilder(new SimpleModel.SimpleModelBuilder()) + .gapPolicy(gapPolicy) + .setBucketsPaths("the_sum")) + ).execute().actionGet(); + fail("MovingAvg should not accept a window that is zero"); + + } catch (SearchPhaseExecutionException exception) { + //Throwable rootCause = exception.unwrapCause(); + //assertThat(rootCause, instanceOf(SearchParseException.class)); + //assertThat("[window] value must be a positive, non-zero integer. Value supplied was [0] in [movingAvg].", equalTo(exception.getMessage())); + } + } + + @Test + public void testBadParent() { + try { + client() + .prepareSearch("idx") + .addAggregation( + range("histo").field(SINGLE_VALUED_FIELD_NAME).addRange(0,10) + .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(movingAvg("movingAvg") + .window(0) + .modelBuilder(new SimpleModel.SimpleModelBuilder()) + .gapPolicy(gapPolicy) + .setBucketsPaths("the_sum")) + ).execute().actionGet(); + fail("MovingAvg should not accept non-histogram as parent"); + + } catch (SearchPhaseExecutionException exception) { + // All good + } + } + + @Test + public void testNegativeWindow() { + try { + client() + .prepareSearch("idx") + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(movingAvg("movingAvg") + .window(-10) + .modelBuilder(new SimpleModel.SimpleModelBuilder()) + .gapPolicy(gapPolicy) + .setBucketsPaths("_count")) + ).execute().actionGet(); + fail("MovingAvg should not accept a window that is negative"); + + } catch (SearchPhaseExecutionException exception) { + //Throwable rootCause = exception.unwrapCause(); + //assertThat(rootCause, instanceOf(SearchParseException.class)); + //assertThat("[window] value must be a positive, non-zero integer. Value supplied was [0] in [movingAvg].", equalTo(exception.getMessage())); + } + } + + @Test + public void testNoBucketsInHistogram() { + + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + histogram("histo").field("test").interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(movingAvg("movingAvg") + .window(windowSize) + .modelBuilder(new SimpleModel.SimpleModelBuilder()) + .gapPolicy(gapPolicy) + .setBucketsPaths("the_sum")) + ).execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram histo = response.getAggregations().get("histo"); + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + assertThat(buckets.size(), equalTo(0)); + } + + @Test + public void testZeroPrediction() { + try { + client() + .prepareSearch("idx") + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(movingAvg("movingAvg") + .window(windowSize) + .modelBuilder(randomModelBuilder()) + .gapPolicy(gapPolicy) + .predict(0) + .setBucketsPaths("the_sum")) + ).execute().actionGet(); + fail("MovingAvg should not accept a prediction size that is zero"); + + } catch (SearchPhaseExecutionException exception) { + // All Good + } + } + + @Test + public void testNegativePrediction() { + try { + client() + .prepareSearch("idx") + .addAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(movingAvg("movingAvg") + .window(windowSize) + .modelBuilder(randomModelBuilder()) + .gapPolicy(gapPolicy) + .predict(-10) + .setBucketsPaths("the_sum")) + ).execute().actionGet(); + fail("MovingAvg should not accept a prediction size that is negative"); + + } catch (SearchPhaseExecutionException exception) { + // All Good + } + } + + /** + * This test uses the "gap" dataset, which is simply a doc at the beginning and end of + * the SINGLE_VALUED_FIELD_NAME range. These docs have a value of 1 in the `g_field`. + * This test verifies that large gaps don't break things, and that the mov avg roughly works + * in the correct manner (checks direction of change, but not actual values) + */ + @Test + public void testGiantGap() { + + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + histogram("histo").field("gap_test").interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + .subAggregation(sum("the_sum").field(GAP_FIELD)) + .subAggregation(movingAvg("movingAvg") + .window(windowSize) + .modelBuilder(randomModelBuilder()) + .gapPolicy(gapPolicy) + .setBucketsPaths("the_sum")) + ).execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram histo = response.getAggregations().get("histo"); + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + assertThat(buckets.size(), equalTo(numValueBuckets)); + + double lastValue = ((SimpleValue)(buckets.get(0).getAggregations().get("movingAvg"))).value(); + assertThat(Double.compare(lastValue, 0.0d), greaterThanOrEqualTo(0)); + + double currentValue; + for (int i = 1; i < numValueBuckets - 2; i++) { + currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movingAvg"))).value(); + + // Since there are only two values in this test, at the beginning and end, the moving average should + // decrease every step (until it reaches zero). Crude way to check that it's doing the right thing + // without actually verifying the computed values. Should work for all types of moving avgs and + // gap policies + assertThat(Double.compare(lastValue, currentValue), greaterThanOrEqualTo(0)); + lastValue = currentValue; + } + + // The last bucket has a real value, so this should always increase the moving avg + currentValue = ((SimpleValue)(buckets.get(numValueBuckets - 1).getAggregations().get("movingAvg"))).value(); + assertThat(Double.compare(lastValue, currentValue), equalTo(-1)); + } + + /** + * Big gap, but with prediction at the end. + */ + @Test + public void testGiantGapWithPredict() { + + MovAvgModelBuilder model = randomModelBuilder(); + int numPredictions = randomIntBetween(0, 10); + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + histogram("histo").field("gap_test").interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + .subAggregation(sum("the_sum").field(GAP_FIELD)) + .subAggregation(movingAvg("movingAvg") + .window(windowSize) + .modelBuilder(model) + .gapPolicy(gapPolicy) + .predict(numPredictions) + .setBucketsPaths("the_sum")) + ).execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram histo = response.getAggregations().get("histo"); + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + assertThat(buckets.size(), equalTo(numValueBuckets + numPredictions)); + + double lastValue = ((SimpleValue)(buckets.get(0).getAggregations().get("movingAvg"))).value(); + assertThat(Double.compare(lastValue, 0.0d), greaterThanOrEqualTo(0)); + + double currentValue; + for (int i = 1; i < numValueBuckets - 2; i++) { + currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movingAvg"))).value(); + + // Since there are only two values in this test, at the beginning and end, the moving average should + // decrease every step (until it reaches zero). Crude way to check that it's doing the right thing + // without actually verifying the computed values. Should work for all types of moving avgs and + // gap policies + assertThat(Double.compare(lastValue, currentValue), greaterThanOrEqualTo(0)); + lastValue = currentValue; + } + + // The last bucket has a real value, so this should always increase the moving avg + currentValue = ((SimpleValue)(buckets.get(numValueBuckets - 1).getAggregations().get("movingAvg"))).value(); + assertThat(Double.compare(lastValue, currentValue), equalTo(-1)); + + // Now check predictions + for (int i = numValueBuckets; i < numValueBuckets + numPredictions; i++) { + // Unclear at this point which direction the predictions will go, just verify they are + // not null, and that we don't have the_sum anymore + assertThat((buckets.get(i).getAggregations().get("movingAvg")), notNullValue()); + assertThat((buckets.get(i).getAggregations().get("the_sum")), nullValue()); + } + } + + /** + * This test filters the "gap" data so that the first doc is excluded. This leaves a long stretch of empty + * buckets until the final bucket. The moving avg should be zero up until the last bucket, and should work + * regardless of mov avg type or gap policy. + */ + @Test + public void testLeftGap() { + + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + filter("filtered").filter(new RangeFilterBuilder("gap_test").from(1)).subAggregation( + histogram("histo").field("gap_test").interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + .subAggregation(sum("the_sum").field(GAP_FIELD)) + .subAggregation(movingAvg("movingAvg") + .window(windowSize) + .modelBuilder(randomModelBuilder()) + .gapPolicy(gapPolicy) + .setBucketsPaths("the_sum")) + ) + + ).execute().actionGet(); + + assertSearchResponse(response); + + InternalFilter filtered = response.getAggregations().get("filtered"); + assertThat(filtered, notNullValue()); + assertThat(filtered.getName(), equalTo("filtered")); + + InternalHistogram histo = filtered.getAggregations().get("histo"); + + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + assertThat(buckets.size(), equalTo(numValueBuckets)); + + double currentValue; + double lastValue = 0.0; + for (int i = 0; i < numValueBuckets - 1; i++) { + currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movingAvg"))).value(); + + assertThat(Double.compare(lastValue, currentValue), lessThanOrEqualTo(0)); + lastValue = currentValue; + } + + } + + @Test + public void testLeftGapWithPrediction() { + + int numPredictions = randomIntBetween(0, 10); + + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + filter("filtered").filter(new RangeFilterBuilder("gap_test").from(1)).subAggregation( + histogram("histo").field("gap_test").interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + .subAggregation(sum("the_sum").field(GAP_FIELD)) + .subAggregation(movingAvg("movingAvg") + .window(windowSize) + .modelBuilder(randomModelBuilder()) + .gapPolicy(gapPolicy) + .predict(numPredictions) + .setBucketsPaths("the_sum")) + ) + + ).execute().actionGet(); + + assertSearchResponse(response); + + InternalFilter filtered = response.getAggregations().get("filtered"); + assertThat(filtered, notNullValue()); + assertThat(filtered.getName(), equalTo("filtered")); + + InternalHistogram histo = filtered.getAggregations().get("histo"); + + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + assertThat(buckets.size(), equalTo(numValueBuckets + numPredictions)); + + double currentValue; + double lastValue = 0.0; + for (int i = 0; i < numValueBuckets - 1; i++) { + currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movingAvg"))).value(); + + assertThat(Double.compare(lastValue, currentValue), lessThanOrEqualTo(0)); + lastValue = currentValue; + } + + // Now check predictions + for (int i = numValueBuckets; i < numValueBuckets + numPredictions; i++) { + // Unclear at this point which direction the predictions will go, just verify they are + // not null, and that we don't have the_sum anymore + assertThat((buckets.get(i).getAggregations().get("movingAvg")), notNullValue()); + assertThat((buckets.get(i).getAggregations().get("the_sum")), nullValue()); + } + } + + /** + * This test filters the "gap" data so that the last doc is excluded. This leaves a long stretch of empty + * buckets after the first bucket. The moving avg should be one at the beginning, then zero for the rest + * regardless of mov avg type or gap policy. + */ + @Test + public void testRightGap() { + + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + filter("filtered").filter(new RangeFilterBuilder("gap_test").to((interval * (numValueBuckets - 1) - interval))).subAggregation( + histogram("histo").field("gap_test").interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + .subAggregation(sum("the_sum").field(GAP_FIELD)) + .subAggregation(movingAvg("movingAvg") + .window(windowSize) + .modelBuilder(randomModelBuilder()) + .gapPolicy(gapPolicy) + .setBucketsPaths("the_sum")) + ) + + ).execute().actionGet(); + + assertSearchResponse(response); + + InternalFilter filtered = response.getAggregations().get("filtered"); + assertThat(filtered, notNullValue()); + assertThat(filtered.getName(), equalTo("filtered")); + + InternalHistogram histo = filtered.getAggregations().get("histo"); + + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + assertThat(buckets.size(), equalTo(numValueBuckets)); + + double currentValue; + double lastValue = ((SimpleValue)(buckets.get(0).getAggregations().get("movingAvg"))).value(); + for (int i = 1; i < numValueBuckets - 1; i++) { + currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movingAvg"))).value(); + + assertThat(Double.compare(lastValue, currentValue), greaterThanOrEqualTo(0)); + lastValue = currentValue; + } + + } + + @Test + public void testRightGapWithPredictions() { + + int numPredictions = randomIntBetween(0, 10); + + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + filter("filtered").filter(new RangeFilterBuilder("gap_test").to((interval * (numValueBuckets - 1) - interval))).subAggregation( + histogram("histo").field("gap_test").interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + .subAggregation(sum("the_sum").field(GAP_FIELD)) + .subAggregation(movingAvg("movingAvg") + .window(windowSize) + .modelBuilder(randomModelBuilder()) + .gapPolicy(gapPolicy) + .predict(numPredictions) + .setBucketsPaths("the_sum")) + ) + + ).execute().actionGet(); + + assertSearchResponse(response); + + InternalFilter filtered = response.getAggregations().get("filtered"); + assertThat(filtered, notNullValue()); + assertThat(filtered.getName(), equalTo("filtered")); + + InternalHistogram histo = filtered.getAggregations().get("histo"); + + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + assertThat(buckets.size(), equalTo(numValueBuckets + numPredictions)); + + double currentValue; + double lastValue = ((SimpleValue)(buckets.get(0).getAggregations().get("movingAvg"))).value(); + for (int i = 1; i < numValueBuckets - 1; i++) { + currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movingAvg"))).value(); + + assertThat(Double.compare(lastValue, currentValue), greaterThanOrEqualTo(0)); + lastValue = currentValue; + } + + // Now check predictions + for (int i = numValueBuckets; i < numValueBuckets + numPredictions; i++) { + // Unclear at this point which direction the predictions will go, just verify they are + // not null, and that we don't have the_sum anymore + assertThat((buckets.get(i).getAggregations().get("movingAvg")), notNullValue()); + assertThat((buckets.get(i).getAggregations().get("the_sum")), nullValue()); + } + } + + @Test + public void testPredictWithNoBuckets() { + + int numPredictions = randomIntBetween(0, 10); + + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + // Filter so we are above all values + filter("filtered").filter(new RangeFilterBuilder("gap_test").from((interval * (numValueBuckets - 1) + interval))).subAggregation( + histogram("histo").field("gap_test").interval(interval).minDocCount(0) + .subAggregation(sum("the_sum").field(GAP_FIELD)) + .subAggregation(movingAvg("movingAvg") + .window(windowSize) + .modelBuilder(randomModelBuilder()) + .gapPolicy(gapPolicy) + .predict(numPredictions) + .setBucketsPaths("the_sum")) + ) + + ).execute().actionGet(); + + assertSearchResponse(response); + + InternalFilter filtered = response.getAggregations().get("filtered"); + assertThat(filtered, notNullValue()); + assertThat(filtered.getName(), equalTo("filtered")); + + InternalHistogram histo = filtered.getAggregations().get("histo"); + + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + assertThat(buckets.size(), equalTo(0)); + } + + + private void checkBucketKeyAndDocCount(final String msg, final Histogram.Bucket bucket, final long expectedKey, + long expectedDocCount) { + if (expectedDocCount == -1) { + expectedDocCount = 0; + } + assertThat(msg, bucket, notNullValue()); + assertThat(msg + " key", ((Number) bucket.getKey()).longValue(), equalTo(expectedKey)); + assertThat(msg + " docCount", bucket.getDocCount(), equalTo(expectedDocCount)); + } + + private MovAvgModelBuilder randomModelBuilder() { + int rand = randomIntBetween(0,3); + + switch (rand) { + case 0: + return new SimpleModel.SimpleModelBuilder(); + case 1: + return new LinearModel.LinearModelBuilder(); + case 2: + return new SingleExpModel.SingleExpModelBuilder().alpha(randomDouble()); + case 3: + return new DoubleExpModel.DoubleExpModelBuilder().alpha(randomDouble()).beta(randomDouble()); + default: + return new SimpleModel.SimpleModelBuilder(); + } + } + +} diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgUnitTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgUnitTests.java new file mode 100644 index 0000000000000..156f4f873a7e4 --- /dev/null +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgUnitTests.java @@ -0,0 +1,297 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers.moving.avg; + +import com.google.common.collect.EvictingQueue; +import org.elasticsearch.search.aggregations.reducers.movavg.models.*; +import org.elasticsearch.test.ElasticsearchTestCase; +import static org.hamcrest.Matchers.equalTo; +import org.junit.Test; + +public class MovAvgUnitTests extends ElasticsearchTestCase { + + @Test + public void testSimpleMovAvgModel() { + MovAvgModel model = new SimpleModel(); + + int numValues = randomIntBetween(1, 100); + int windowSize = randomIntBetween(1, 50); + + EvictingQueue window = EvictingQueue.create(windowSize); + for (int i = 0; i < numValues; i++) { + + double randValue = randomDouble(); + double expected = 0; + + window.offer(randValue); + + for (double value : window) { + expected += value; + } + expected /= window.size(); + + double actual = model.next(window); + assertThat(Double.compare(expected, actual), equalTo(0)); + } + } + + @Test + public void testSimplePredictionModel() { + MovAvgModel model = new SimpleModel(); + + int windowSize = randomIntBetween(1, 50); + int numPredictions = randomIntBetween(1,50); + + EvictingQueue window = EvictingQueue.create(windowSize); + for (int i = 0; i < windowSize; i++) { + window.offer(randomDouble()); + } + double actual[] = model.predict(window, numPredictions); + + double expected[] = new double[numPredictions]; + for (int i = 0; i < numPredictions; i++) { + for (double value : window) { + expected[i] += value; + } + expected[i] /= window.size(); + window.offer(expected[i]); + } + + for (int i = 0; i < numPredictions; i++) { + assertThat(Double.compare(expected[i], actual[i]), equalTo(0)); + } + } + + @Test + public void testLinearMovAvgModel() { + MovAvgModel model = new LinearModel(); + + int numValues = randomIntBetween(1, 100); + int windowSize = randomIntBetween(1, 50); + + EvictingQueue window = EvictingQueue.create(windowSize); + for (int i = 0; i < numValues; i++) { + double randValue = randomDouble(); + window.offer(randValue); + + double avg = 0; + long totalWeight = 1; + long current = 1; + + for (double value : window) { + avg += value * current; + totalWeight += current; + current += 1; + } + double expected = avg / totalWeight; + double actual = model.next(window); + assertThat(Double.compare(expected, actual), equalTo(0)); + } + } + + @Test + public void testLinearPredictionModel() { + MovAvgModel model = new LinearModel(); + + int windowSize = randomIntBetween(1, 50); + int numPredictions = randomIntBetween(1,50); + + EvictingQueue window = EvictingQueue.create(windowSize); + for (int i = 0; i < windowSize; i++) { + window.offer(randomDouble()); + } + double actual[] = model.predict(window, numPredictions); + double expected[] = new double[numPredictions]; + + for (int i = 0; i < numPredictions; i++) { + double avg = 0; + long totalWeight = 1; + long current = 1; + + for (double value : window) { + avg += value * current; + totalWeight += current; + current += 1; + } + expected[i] = avg / totalWeight; + window.offer(expected[i]); + } + + for (int i = 0; i < numPredictions; i++) { + assertThat(Double.compare(expected[i], actual[i]), equalTo(0)); + } + } + + @Test + public void testSingleExpMovAvgModel() { + double alpha = randomDouble(); + MovAvgModel model = new SingleExpModel(alpha); + + int numValues = randomIntBetween(1, 100); + int windowSize = randomIntBetween(1, 50); + + EvictingQueue window = EvictingQueue.create(windowSize); + for (int i = 0; i < numValues; i++) { + double randValue = randomDouble(); + window.offer(randValue); + + double avg = 0; + boolean first = true; + + for (double value : window) { + if (first) { + avg = value; + first = false; + } else { + avg = (value * alpha) + (avg * (1 - alpha)); + } + } + double expected = avg; + double actual = model.next(window); + assertThat(Double.compare(expected, actual), equalTo(0)); + } + } + + @Test + public void testSinglePredictionModel() { + double alpha = randomDouble(); + MovAvgModel model = new SingleExpModel(alpha); + + int windowSize = randomIntBetween(1, 50); + int numPredictions = randomIntBetween(1,50); + + EvictingQueue window = EvictingQueue.create(windowSize); + for (int i = 0; i < windowSize; i++) { + window.offer(randomDouble()); + } + double actual[] = model.predict(window, numPredictions); + double expected[] = new double[numPredictions]; + + for (int i = 0; i < numPredictions; i++) { + double avg = 0; + boolean first = true; + + for (double value : window) { + if (first) { + avg = value; + first = false; + } else { + avg = (value * alpha) + (avg * (1 - alpha)); + } + } + expected[i] = avg; + window.offer(expected[i]); + } + + for (int i = 0; i < numPredictions; i++) { + assertThat(Double.compare(expected[i], actual[i]), equalTo(0)); + } + } + + @Test + public void testDoubleExpMovAvgModel() { + double alpha = randomDouble(); + double beta = randomDouble(); + MovAvgModel model = new DoubleExpModel(alpha, beta); + + int numValues = randomIntBetween(1, 100); + int windowSize = randomIntBetween(1, 50); + + EvictingQueue window = EvictingQueue.create(windowSize); + for (int i = 0; i < numValues; i++) { + double randValue = randomDouble(); + window.offer(randValue); + + double s = 0; + double last_s = 0; + + // Trend value + double b = 0; + double last_b = 0; + int counter = 0; + + double last; + for (double value : window) { + last = value; + if (counter == 1) { + s = value; + b = value - last; + } else { + s = alpha * value + (1.0d - alpha) * (last_s + last_b); + b = beta * (s - last_s) + (1 - beta) * last_b; + } + + counter += 1; + last_s = s; + last_b = b; + } + + double expected = s + (0 * b) ; + double actual = model.next(window); + assertThat(Double.compare(expected, actual), equalTo(0)); + } + } + + @Test + public void testDoublePredictionModel() { + double alpha = randomDouble(); + double beta = randomDouble(); + MovAvgModel model = new DoubleExpModel(alpha, beta); + + int windowSize = randomIntBetween(1, 50); + int numPredictions = randomIntBetween(1,50); + + EvictingQueue window = EvictingQueue.create(windowSize); + for (int i = 0; i < windowSize; i++) { + window.offer(randomDouble()); + } + double actual[] = model.predict(window, numPredictions); + double expected[] = new double[numPredictions]; + + double s = 0; + double last_s = 0; + + // Trend value + double b = 0; + double last_b = 0; + int counter = 0; + + double last; + for (double value : window) { + last = value; + if (counter == 1) { + s = value; + b = value - last; + } else { + s = alpha * value + (1.0d - alpha) * (last_s + last_b); + b = beta * (s - last_s) + (1 - beta) * last_b; + } + + counter += 1; + last_s = s; + last_b = b; + } + + for (int i = 0; i < numPredictions; i++) { + expected[i] = s + (i * b); + assertThat(Double.compare(expected[i], actual[i]), equalTo(0)); + } + } +} From a03cefcece609ebf5ef5507e0cb6f481c12e1485 Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Wed, 15 Apr 2015 16:33:28 -0400 Subject: [PATCH 53/68] [DOCS] Add documentation for moving average --- .../reducers/images/double_0.2beta.png | Bin 0 -> 70338 bytes .../reducers/images/double_0.7beta.png | Bin 0 -> 73869 bytes .../images/double_prediction_global.png | Bin 0 -> 71898 bytes .../images/double_prediction_local.png | Bin 0 -> 67158 bytes .../reducers/images/linear_100window.png | Bin 0 -> 66459 bytes .../reducers/images/linear_10window.png | Bin 0 -> 71996 bytes .../reducers/images/movavg_100window.png | Bin 0 -> 65152 bytes .../reducers/images/movavg_10window.png | Bin 0 -> 67883 bytes .../reducers/images/simple_prediction.png | Bin 0 -> 68361 bytes .../reducers/images/single_0.2alpha.png | Bin 0 -> 64198 bytes .../reducers/images/single_0.7alpha.png | Bin 0 -> 68747 bytes .../reducers/movavg-reducer.asciidoc | 296 ++++++++++++++++++ 12 files changed, 296 insertions(+) create mode 100644 docs/reference/search/aggregations/reducers/images/double_0.2beta.png create mode 100644 docs/reference/search/aggregations/reducers/images/double_0.7beta.png create mode 100644 docs/reference/search/aggregations/reducers/images/double_prediction_global.png create mode 100644 docs/reference/search/aggregations/reducers/images/double_prediction_local.png create mode 100644 docs/reference/search/aggregations/reducers/images/linear_100window.png create mode 100644 docs/reference/search/aggregations/reducers/images/linear_10window.png create mode 100644 docs/reference/search/aggregations/reducers/images/movavg_100window.png create mode 100644 docs/reference/search/aggregations/reducers/images/movavg_10window.png create mode 100644 docs/reference/search/aggregations/reducers/images/simple_prediction.png create mode 100644 docs/reference/search/aggregations/reducers/images/single_0.2alpha.png create mode 100644 docs/reference/search/aggregations/reducers/images/single_0.7alpha.png create mode 100644 docs/reference/search/aggregations/reducers/movavg-reducer.asciidoc diff --git a/docs/reference/search/aggregations/reducers/images/double_0.2beta.png b/docs/reference/search/aggregations/reducers/images/double_0.2beta.png new file mode 100644 index 0000000000000000000000000000000000000000..64499b9834281744a3566ef1d3a5d597745e82b6 GIT binary patch literal 70338 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 Hj`#loKtPNN literal 0 HcmV?d00001 diff --git a/docs/reference/search/aggregations/reducers/images/double_0.7beta.png b/docs/reference/search/aggregations/reducers/images/double_0.7beta.png new file mode 100644 index 0000000000000000000000000000000000000000..b9f530227d9271a7a889f7e2ae453b44e76f9bea GIT binary patch literal 73869 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

F*ZI&Yc5}NkCYVV=_6D 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

    YBkaA;`w5@bYq8yUrf)dIcb$R3(Bdh4PupD#;*> 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%0AJ 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/reference/search/aggregations/reducers/images/linear_100window.png b/docs/reference/search/aggregations/reducers/images/linear_100window.png new file mode 100644 index 0000000000000000000000000000000000000000..3a4d51ae95603e37953a99ddfd7942e107ba9bf8 GIT binary patch literal 66459 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

    TUXkfG)(e^GcB+ND@+BAFd18jG= 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%0AY+)bmcJDJRauoCoYO6 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 Qn2Pe;g&As<+_n$?4`rN@vj6}9 literal 0 HcmV?d00001 diff --git a/docs/reference/search/aggregations/reducers/images/movavg_100window.png b/docs/reference/search/aggregations/reducers/images/movavg_100window.png new file mode 100644 index 0000000000000000000000000000000000000000..45094ec26817aea638085b466e6e2b0339c2bf7b GIT binary patch literal 65152 zcmZU31yr0%(l#30U4jz`?(XjH8r&J&U4wgYcL?rIa1HM6gS*?$e*f;>ySa1TIcKJ= zx~jYDd8*zHQ;-u!fW?Ic0Rcgfln_w@0Ris;0RfGM{`66zbJm&w0)kCpAuOyQDJ)E+ z-~ceSur>h!(Fjg*hv87&Sa|2VUme~{JJ2*`njpL8nGmL0;F)0+jf;g(GL|3?76}(q z779r;9#IHK7b;Z=k#R>VJ3oK-em}qFJNL=*K0B!Fm|xEBxL@|ncBTix|5*?l3xZAy z;xA}mYz1zkCr)9ex2g>Sit7hyC=jK$ipMN z2I@zHa3011SzMGzex_aEB|y0jA5!ujv)8 z#(6I2c}Ng>QIfb0(46(F3B0YdsE1rc!DV0|k5?e_oCw6Z^F6UUNcVaO@B|Sae!b@! z8q@w6iGTe)M!urN&oxGIZ z*VB3*BTZEI70fB|)aMYKrMCgw-EYSmZ;B{cxgKo3)iK3!hk)#1LC}5Q3vzmWZ&C=- zxR6$?Q={YRUbH#PEh|9uH%o~7kf)WDmwCd7yD7%^d|||<^=%rzO-TCM&f93jFHyYWFKYx>YXk8K1@RgFRURJaTNjRQf-FIoF3w&FCkyTOMp;;x>X!;6 z{VRBe>h)B!2Q~Acal(~12C{CKeNdlsai+%AEhPHjAkF17JK=o)ualsyepo@Ia|jUN z{F^Jh>dc{DD;85wWCEYoyU`?Ge!SG!E!x5eWw8bv!^d$p+Bba+r{6e`Iv=X%44owHn^%qjp z#fC)6YIG2PhbF-BTKwhVZ7QxZHY-W*9ng{B!%PIIVu!xw!EbCJb_x*A8|qx=GicWu z3OgCLLf@rpNmp0X5(D+Cxb{AHcrfVjwiO=ZeIJq-daR&<0}*^C)MRi#R52wr@ZKU! zMXqIEQUPf;Iwg>cUX06aBES|9utgN>{?rW0k1RMq28ZsCI|<>@jjaKqy$OLxRCfjr zhB6Epr}GAhK?l#O&+WyW|1qS&{*nop3MmHSC? zr$oL)6uUc-fq4u{x_hJ=(HfMySC0WFI#_uNzZtqR=s2#y2Q;SF@d6e%!c^cok39B) zMPwy+Z7OuK2B-{-05UnTwj#WOy(03)yaf{*axiXVt45*>pzK72^^qAkFchXdr??o4 z%NqQs)}Ucl!!8dxk)`-acn5ul-Uy-{Y%_3YKv5mRfgg=P5v@F^xhc`ipo4k_cJ`?f zW+5iM=e19H+x?=}&AXkM51KG$;+Os)`}XtY%mXW|7OWtwDl8ls+F z*Kk&_&w?pAa@FJyDA8yRFiAl!J$OAxQc@$jiBySdiJgh|i7kmul-UZM`8v6I#i;qy zxemFo#azP9QDvz|7;{J@ag-vnWL#*y-_5@7^{H)RUsAAwH;^@;G(g=l9`PJm9{C*6 zv!JsCuxPPBndezJn4eAGOpZ+{O~OtcO~#uWSzMbRn}=Fp9byBGfiys{L%Av4Voxb{ z;h21^yoLPx9QgwJB0ypGH$R*Q94G8m98_#6>}8xH?3&1)NU=ztNQ+2zoHm?;RAB0E z>Padh^ByCwF|JuiYH&(a3V6y)>Nx`vV^ytPjd_h*Z9%n4?M)p`okO){tw(K9ty&FY zZDs9c?P1Mhok{gvbyRKsQvYJbBIy!+O;g>3J%dAueY0bM!|E3HmWX}0y_O@v7R~<2 zw$ASMKGP=bR%`d%2C$1qxAqS*T)j~x<#XvnLOC^^j-yydm>ym(x;FLCzaF%$D zSBpiArHk{S6{a_$L8Diuzt&Zgnb&2LcM*uDET(srsZ={lZ*Oidej$HMhJ}YUg{8pI zLT5uyLLUxK3a^s_ld6<@l5ZA)-rTy?d%xL60w$8ZRCf)XVS+0+6wX#2JV7A6S;T_;{A8!(G7r4h)=N3=o{wS!V5b%K3EyprGKTrv*3;3o1o3F)nB23&q95|@j?Sg zr352}p1WO9g-TiliIX;q|1)6^Lgpk_HQboc9|nDdG=_yFdhcOvE> zZGT=5x!LZn=Seg>BG7%=UG*{J@$2I#Bmv}WG7G&brUOP1My1|e$IDgtucRZA+`J4! zYkQH)P}Jx6=<*TuQRyY=87cNkNL@VoIyx(Abt?L<-|oB}V5%s3jGn!FV&}4J=?zJ! zN$O=O<nAeZYdDtRkP=vsa)rIl)co}3MZZ4TY9da*TUE4c%^t7I;c8sy*;0BpIM&> zJ0`s^-MY;;mvSDzZN#okjv6FyT_zLIo9RKeKe3@Q*4}ZQA?(W@QJ$Lr#y-%P(w=Hk z-3i}8+Boby_5fVJ-3C4P6%S7i-}pb?b z9lX$!=%0Ddy)3hybMW6rz1Unoml9g@SJDpA*VAaxMbkpkRnQi{89!?_=Cx1P99465 zV75eSPpl!YmAso=9gmxC?wdk~$t?R|_{D`^U4d;rvx0n=17RR?$M^T3cz@FUW=?$y z(oGN_#Fx9g2~m$AhDIG}G+3u}fjs+ly!0i^tn}D?A4mq|#R_vF$nFm*{=F(PIl_QgRw>wk|<4lM;&_k9MsB ztiBNy8udcwnG%XS>G6K%NM{+K>F{i5t)T_Wp) z&Mk2*e%YgbylQQ~#t_PCa3tZfm^U*fyTOImV|`M0E@1_4xp+qY#P^Z}&~2q)0L{$8 zqjSf!owN__QUD%CAIv_Jk7HzMga(k0DUb5?P=8qW0Lv;#t54TS9eJvL7M`iUz1bZL z(=1&Yjxos&*x%j9wwuOpB;mQscTVYqSbIiG^r>(K0o`M73gC;k{#& zlcM81^X9Q{SAR%z>DbptFF@Z$QA1`#b;gK5omlnO&XaZ!!=>$|XcPaf$tOcBTO(2` z_t-MxITy^U;T+w;gln1xs&%;BA;f zl?b&DSqL>48jI8t%PQg;Yj(0l0ldt*dC-qc@;YR3b zft@Uh9G>n+BP)68{odsAm25xQbdG7By)vr|-=kjAtIDtesT*=)a#cys@pA;M?hUWE zDNt7kP=s0LG&Yvzwq8yhd)8Jm+Qy!%J2%k*6#eq_&ROo5>n7Jad3bBQ(~iaL6%$n( zN5SiU>p63=D|Qc~ed-kAhI*bIQ#dAsnRw-%o(`**nSGuw&q!z$36RR6eCBe>vPfg- z69mt|=V7DY2acP`u~o#^#2Z5!q&_0FB6Lr5I5aaf8?UAJ*Gu8oBL{Qq&SgRhPCa(JEXol9orS60T@h#8n>V7nai& z2$#b-`2uB+k#Kkz#u%?yFj)xLhglmKdQ%(HC^WD&YqfmpE$e^RU)HGBkuFCqZ8&ip znVoYjKSDpB}5uN2kQ?EC~B%be|cMb-Q81-GJEf51t&YiQrY;f?)`fhIE z+wmw+K!2Vk@FEeR-aW{LtwnP%OAgfxq76C_I|;*%h~yq~elSS1VsX3~iVM$&VT`UJ zsgWMk+F^?E96bK=oxCOE5LaA%Zk3ND?g|e>Cs~z}$u6e#-g7$Tdc(N9*_C})(-~REW`}XUN%E76b zurA(O9`Ccq#C=pp>}L+%2b0BplQrm#;}gZwrrC26iW$>9zI0!Gt(s-6_sos0Mp!iz zKN+%#dj6j$8(7B>^15YO*V1+Jo>lFz~5+r^s#ApRS z8ZdL<(glW2xz$r~A?rr^1m_N>5aDQ2WA<1Y?o`uq!2iZ}#k@zI2u12%P~4=%ryQYF zgE0-F4lTb>6n+rGj5ryuiQ`^AYp{LEi=b`yvF4=;@(i{J4iY;3 z`}YV`(S!a^{e&l-WGK{!B!wj7l)UuUI(Mx);q5U2HF*W73DZSaW=V{5J`KNlPmgRo zxmj9S#p_M$lb!qtFgXt{LflAUKX5`!nUC4s(J_K(l&1XC@mKA<%=V51;m946VzvVd ztdZAgs1qatXIf(Cw$~fvi~38ht+ma%qv9Q3;&0?byv^^id@aN>eC0* zAV9yxH7DdF(lYQ(aD$U?drQcpDOEcA~wh*S3l&~@;>cfJQ6pNyD2Rk3#lLLQPQ z?TshR&Mi0i-$LQ_cL}j2HBKOzKeI!5bb|)@(M*Dvc0)o0XvBg_348{EDt`kv3@IQo z8iJGZ3(pas^hC!ck7N>!Q^ZoND5+wgum&RxF5i@7;a{xx<`J;rQxQ9^Mr0T9vr0G9PE2m87)v-U29I*V;^8rg zTuJ)nUh4Vlq!qRe3+kjY2|xGpPfLt)%JV3ys;s|#bDN}`e+ZWhJ#6O;4bCd$cPKVl z%4iyI9Ifp-ncRIC*EG=27`+Bxh3eJuTzy#U;Hl+3_jLA@fBN~z_&f@^_(=>x7v2`} zQNk2v4k-e<3zN|yg>8g}>vd=pJ9ja~2yL(f%^Zz8p(noPE5*!Yar}f`L6d2p8JFpC zhI(7hk7BhPB}xxS-J52zyu_X5UWX>wBE0;h(*g3xrfPUq|5-!b)j=;v`A&JnUFe8M=5<}-$Xj7pH5Zel})xZFIr1_ zzUZ~jhal{+AfEmh7hsG;P^)0No1`M3VZx@!I3vMnxiOTciF8!(s^Pf)hu=s!P;5ik zCoO^IPP`tU^MfV(;f+LV25a)0JDN#&2nZtya};tZiE^NQf&`^>N1#-wRARpW=tG*l zWbtAducxb*b}~C82WB%vFrwm&rqrjdGl?7|O@9ON}?p5w_4&n`& zwMwUDtAbEhlRU@*%P)}P5Ew1({nye^E932zAg3Fc=#71c>Zq~54p29gDU`OBtSJo= zpO-~xwbe5_r`}Q_WJCcYpJnLNptP4YN0%cPIagdcak)Ac5B9FE1p=r=VD0g46t2eZ zi!M0N#4qR(S>Qeqv+iy(oL$ThT~Nl5>KjUL=S3s+S5f52Z6=t@kC2BJ!51uLrtv0t zsWI)SnmROU{cc*lih#O;fn0!1qT{ETrnK!W`w(ZU$){l8nct+;Rd&`mlzDxBac@+% zV5NK%OmM>&2~e`mvKa={xkaA2U9jAwx-A&r7wQfA3}C$m9`+80v53{B|VZ0tT>DS&|RxN&{_v@vlqAab*@wsquk<0bjO z7F-{{|5Z#+LiB%4oUC|BG-MTsgaHmFM67fybPObXutY>eJPyXDTuLHh|L*?rkC(*U z$;pn3p5E2fmClu!4&Y!$&&bKiNzcGU&%{Lg(Sp{|-PXy#jn>wY^#2d?pK(M?9E}_- z?3^qBwnYCL*T4|q?8Hk#@-IPueg6MEP24R0E6LXJ-)em*NdK=IdPX`1`oG5h=*si2 zQZ5AxHxp|O5epj=TgML>e4K1dJpb4JA2t7#__v;#|Lw`l$?^A|f2;X-PagVzDeyOi z{=c>Uuk^!Qe6T$9f0>>S_Nxlk7Z4Bu5J?e16*th6OeilE)w{t7GGbyfvfL&Gp*V%Y z{1QsG>VQgw>Z;1TX3@2-lh)SbBmFg1*Vh%~Hj;ilK?s%{N>zbFC82W|N$A7jm-80D z?+YgPgfX(216O`RrmEE^Z(_i2-flr84(VHdXs z?nEG9e?AIPAP`%Y9Cl#;s6jr2Ac7WuLK}GcW7v7cZkYcq{%9pofaXVpe<+WA3=WD6^1n}s7)XE}D?SJ4{}z9=`tAf~ zAJ-}H6X{RAN#X?hrwVU=|I{BM@O5y$dmxiKXU-6V=MPpoxl3Sw@iLC#6x-;yL$vvN zB7!B?8sEwSySjL*i3_pyq#%s<=M12k{L4s>&3|n_vYIg-pry%yA6A|7$atrz%(b6~ zQZrKAO+Pzg&M|yO%al@h?3@O>DD^LRCTFcO7Nb}Zb?u|ANM*+FftGF4nWELhe_?A`&5c% zQrY#yiq%x7`jd6!%d@+(vb1z$qI^?ikKb4#pIE7KE3WnP&GhvoT4+=Tie@ysIs(sH ztJyz~KheM140u>_1Ld9ub%jf>x^+Ar0E9}4#iNIDzB+=XqUjb#+1Uj)_Qf;1#b*~% zGuovr^XHux@Z&3XI)N4|$4ae_>l@Rh<7L3^m-lIYv-84E(&x#rmh73dGL3L~zJ#;a zbMGg+*$h0^&k|kU4}4tQq;U`HM+rchi9TrK3 za|_kuYO5-Ywfi_bT_EPShFHIa?qNN%XF|m{)()?C+MClYY_5wv@S9onQJ`I0o%!}> zGz$e)#0rJu$82)W`GlHg*r;kqbRDoicDKJB`8Pn~&+$Y@qxKE+2!!pSJ4i6S(>c%HMme9>t9=wU?9?Tzvs%E*I7sFn*Ftw`0nLl9S$UNNIn zr$M6nuckXMjwqM@zSz@5?!!z9x&jEQ0!ZKXAM1+=vH;(@m^XF5pj9}pOOO<`Pp)g7A>}}p9Q!J_sD*zya8S3YtdY$Fu>Q%Tp!Cw%F#L+LXfBxL z?A~>H@}Y4yH*gx$Q{!U|k&)v@tOvsI*htj&I>su zq3#yHzaqAt4oFHt%T3xCj(%T`=}e~MZlLW)|5GuV&xlZn4tZ@x;+@Y_zr*_dHBMSkQyR@i-RyGDSS&lf=WM`BeDPFe>$upC*9vwx5Say) z$scF2gZ~o)41~J(k$p}X-EU58)Kg4tQa$d4z`){>h!?@YdvJ&|2m ze+w8%Tyj_qW>@NJrYbY{=F-)>W%4GNk+AZe0LBTX_CBH5EEhe?5US76v@&sqaZy(| z*y^zHI-atQJU5RacP8C&C_+Y`@tBhI`##Z3)fOKj(d$VGkNW0ucJ&>-0oqHTrb3Gj zTk)6EoeHnHlBsDgJUp(Z4lG_YOY3hk>Fa;EUGR%5HO;*j9bgV9i>5ms-ZvpBrh|ll zWW~nWnrG`i)o}%jEr!EZ{$o#mM>CI~PupHPS=34V(d1pse2669A-!SgNr{%F%` zBZ61K`fB=wPpi;~=T6b#c3;|BSW$+PX`gREU?a^JA!lJ+T>s0A`>b$kkIr<;i?pbg z`gLo`vwr_apo6`!8C!1Wk#DgAec^b8HEX<{$Bn7?yWM;;i`g(}ZNKqkABBDnN*IMB z9^R)3kCFs_m5;DFD^1F}#%84<$h6C9%>Cj){P1o4YvSd~!Xf(z3)k6X)6KNed(i|t zL2=@Z^kE!KmUvX-=i$+&CK*{yPRi(iLUVxF2OaR&A81U<%n>rF8T(ZqzoQEQ{1oYy z2gFuQ{Gzb4FZly^35^2&8dy+3Rwe#b-A42Bb8?1G{&`9*69z_GyWX)s(aCMT0QjF6 zc|i#33)AzKZvSu#KLfn*G)-ar6aD-Iz@Y_@Z9#ZgApdZ7pvchtAJm}tJk?;}PksUd zhCl=&*bI6xiT)?dd&ze1P{W&LSVz8*~Z6R~EI8F@cp14Py8mj+o z;?F=Ho0uXo3HyA%C5U`SuYZI1=Lyh5`5qOOTVEvmm5&V*tVVs8rwTr^?ck>Z+Mlxl z6Y>k*O582*i^PZu6d=WqlK*V==?_KyDZhXSa)cA|{rHEVDTxo7!@}qM6X(x?g<=Kl z?HMsJFdn^4FHr8d4dMU6;@&7?las}>z3*_G&sI}^a~|NoBLVxNN0aG`ZB`nmlGODi z!>FhltsdW=&n<3`=IUL~^}Nf4LlIf*HU+qzPwI5bpLTgzSbF-?frV!LIoZ--Cn_S@ zoeWXy8)|0>ka=kTYq$^Y^$Cmyx=On#8k1H_Bq=G0xM8qy(r^;PF>#u z)Mp}#ceYfSVq|D&66_L7-%Bvt_pL>L9>pn&0j@J!el!o?la$JZr+g${YJ`?b8P0Y$ zSu|hyAF^1$YN*ywlOG=3YL~28e+>3q4n*O7WoJ*;YP6Q@@Vs}K=k~ZM=}b;c6sgc| zn%^HjU2e!vOhl(u(Qe@OdSJjnM_0JsAGi4UdbBIc^CmST(Jd`4)qCFCT5fdm4zJ!I z%*3!-%}=s#c$w{FUwwKQEVm;t@{#_b1rwNwiOJ2Zviwwy(O?EO|6Zap zuyMsU|6>`Zm?wC4+UcO!*PPI9PnKqE+ESU#xMWic$6p?=4|78a^7#Aq_iEqj zQGc`BEUTQZN8&I8qq)y=v~64Zzrq6+tBHFcwmLSZwee7aY22S?GSq9!1j8dZg(B8p zBGqtC);epvht0}e^Wyb7`~By$fna*D z{X&nHK4v4J>(ymJTMOG~D;UfC`Ra+(Q$|LBIC1f8FsW5bf_vV%;{W=5e_k9Pk4jtb za=xCa(QI;UiW$@rv%G9iNOOXS!z9vV{h*U-?e5z49QrZT zkw;|)V+e~$#iJEGJh)!3M(LmQ3=DGbVAh)e*?b3lmR3zZuO7PcKHn!tg{HI3%`~^0r;*}>d?u9eC#R^#g7q$V= zKV7#711Mt1?V?ePMZ#`b8C9m7#bl1)&BwB%JnuG|PX*N0*47VSzTR)hzdqg0e3^cK zobWBN-x<&nrg78Yww^odd_GH{`Yj)(GOlHE7Md^M$VGt}xzxU-%5f0V8-kFJL9NCy zcrsU>j2TcYmsuA44YS1iGiAgeLK=}wlyZr18+sP_QaP7GxHnp%v!OwggnU`83b~>! zS3u=?Ly{>a^C3b8w^o zq%zXFkGq#k{0|>)Zm#E|-eShdTduB8=;GYxp8IIACST=#srXbvud~CXj5zk`=K_&) zAFhGx@-~qyOmD!SLCp~IH_*EJdKD{cPHapp%xKQ#YgKjK!tq$M@w7tkKuzt{B3V4$ z!LrOw#X%&tCezdE$h}G~tC}T3_v87B<%~4hBF+vpxcxUum@ z_hOBrwelB_A^7}-`obp-IjF-d1oIz9%N|05dh~0wlCpAxKA?|wvOZ_?5jy>gzwn

    @w2*xvl&J6h0UMBCHEM>_568 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 literal 0 HcmV?d00001 diff --git a/docs/reference/search/aggregations/reducers/images/simple_prediction.png b/docs/reference/search/aggregations/reducers/images/simple_prediction.png new file mode 100644 index 0000000000000000000000000000000000000000..d74724e1546d5da1fb798f1129f5a93b0f23d075 GIT binary patch literal 68361 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 YPa#MRmA<*>UCsl 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 Oe_}$?f+hSq-v0}UKv3NP literal 0 HcmV?d00001 diff --git a/docs/reference/search/aggregations/reducers/images/single_0.2alpha.png b/docs/reference/search/aggregations/reducers/images/single_0.2alpha.png new file mode 100644 index 0000000000000000000000000000000000000000..d96cf771743e4e3d23b3c42b1fef98c79ae2b0bb GIT binary patch literal 64198 zcmd3N^M7T_(swwqGnv?$33hDTHYXE16Wf^Bwr$(CZQItHbDw+8J!j58@b1t0VXa=> zT~%Gx^{uL2J5WYS7!Dc}8VCpoPELy?17kCNARraLSSKhJ#r64juKSh2-K2e01G;hIYmRaLZ}S|}jDnF7u(5g~1bzZR zLh^k6F?vHXL8yF%3jPvK2u0`T?{4qs*IeiBX>MoxWvz2d>8S@E<&pJGhux`o_l4Z~avazD7j5em!yJ@2cnT>IWPg z{Hws8lt?FgNJqH8He7%*7FYQPbb!7)Be8nusSKcL@z4-q5I_QecZ|4#vK5ut4q+A9sKgPmD<_UV@mg;}k0s-xMzo~>NRN&I%KQ5&u zx>nl5Gpq6|L|j4KF|hFZ?hM9#dAl&T-bKW<>2KeDyT8J37r_cT0+itiAvzD%IiQi1 z(EWN^PTlDJ|2P$)h4XKl0EreUKM;&*OpwpzTHk zMilAa^yO6dxS|_n7H!kiGVHqv*uDSLa>C0TUhv%{?Rz#qeEr%MrRN4XpsMXQ41NR? z{mD?HB8S~L8I;1CvD?)Q&cRGacw9z!ntO$V!|v9er2{vO*S?*#Tg28_{k={S8mi)@ z6hZq6mZoAY(eOdZ$afTP`HhCS!+sCg{al!?ZemWNO*zj?E9?QiM#=Ml)@?_{hA>>cC94t)*rVOO)C5)TuL_p1P5O` zAi2Sg`}X0t${yE_8=W=XPqf{@4YEs61;x) zNWI@5tRNmQanm2sdOWPM+|9P=$vGz?6E0NCHQ`k#%3RQ@$k*d)(Le zo^$sw_a<{5l+FT32JiJXtmPMq+YQ0;wM!VDFTb46JUUOjA)e_ix^JfjTt#RqJ zs0{?344R_uP&XzkE2#+qdzN0i?LR!|w7OaFk8r*ZhzvZIQo;c7KI5stA5b3yW z;w2(hF)u0rHRzt=N`);%X4d1P^YB=}3w3;I0Om&I?IVUk^}?J0bM8P_0aD)pgU7Ev z0|h~vi`*b*fsf+BWPz#b6qJDs;>BoyI`hPw0K)a^-@rHk_t{`X#**JaNq`Xl4(E-J zE#Qle&-4od*|RRm)R=8!ipf^i3|k#9`~5fvWC4>^P30NWO+2_%-kK9lVL=?3-4 zM~nA7L-l~YCdgY>fm{gnix?z{pPZ0!ri4sg!7I7t41of}Hzd1HvBBT?R(TY&aWm60 zKgn(vNEZmAcf`=pk3fib3{}9J0kd~&(O`u6$!}seK$iI)N7lLnhj-gtKw}0Q@LXq+ zL_9DEEN8Ax224~Q$R7kB(AhFJ!M%dK!gGed`4RkJq1{4P3_s|aSn4uc~LlkZpE5NV*%Kt2OG z`&0%sAD-Ox+9SW^bW!E#)^=tgdF4? zgd6lVh!F&bHz7l+g5&`y48rCi;Hh#O%qQs-mvj~KdLq95oTP#%CBN=<6! zyNEIKhi8Aun$Nz^kj??*S>~pH_r!R>uti_NKt>lsU&6>kuMFu55ejh+F%DtIXvWx2 zJV@M0JV}J7-=*c$!!-0y^h*d$08N-qJf}gREw8ewG^%v0%BfJOx~ZnDwyrR#a<0m& zQmTZnDyzDzI;?!G)~}eY2(8Lq>|H2bAX)@e)>n`Jq_Hme*d%P5M%%7Na^5de`@dQ0OxRbM|FIEJXvsl;64!mr5w!?$dcL; z>zo6RES*4`W&LxTMO&?ASM!N|?|IP@*XM870l&0&FN0lTKrqFS%mJlW!TNvel>rK~rtZ>_g&#eI=? zelw-JAv;DlWSgGMo=iJm%`m09)u!-zi>uXb@i|WeRU=Bn{|@GyyXE~5_zn~p0v9)j zRfNrP{`2*<5ORq97JKh`KH(vA3mCgX4XTBc( zD84>~Lfj!P25BaOcz?dkXMWRe^ho8@GONf7l+AO(JE0xHmynw97MFxGtBK7r^2&mk zf|J)}mxE^X(gcc1nG$ugVcIv{!2Y}~*sY;3l(1(!4_p9_2emsv zCqX7xBn2(?&n@51z#!ryV(Y03(b{Og!2`YZoio(N7?`i^a-4GP(>c@DQ*+Xd=|>z? zHf;Addl=>PtA{~{@nb*6Kc(UfO$?B;3p-FhtqA0)8rY#(Dw)jj-F2SivtF_K?L+N% zkB9Fk?agT+Hdx(tJqe}<`#3E*DL$q=etjGU#|3|lX80<4xF!~$n%a~?`5T~p%!#uVL=j1g)}?z_=v;C& zxi%I#R=Fslc*?>{eY2ix&Aye>wtq)zSaY{I_8#nfcipfdT@6lASaE#*VJ2jrZT4;! zW4?B#cy`?b|8YYNRGWW|KQG`g)VHsvZ#3^9uXitEuTgj{oKy|IuEoO649BS2aC?$RXY?~+dxXT}&X(72x4zFk`GXUKw}m|jJqWDR9Mik>RrGMxPt~n>_iiaqs8`13 z)Q9w|`!4`O?K8L8mnFt?7Vg{77xU}qLOe6>GO7VU4W$}&7!^2mDOLWP-m_|5R?Af7 zQ3YEoT4R{{_$uOR!Mpy|@uW+H5NE!Q7QO@;79Jbz9S|RIVgzB3CG@IfsU$BL zvtYA`H@IJ%Y)bC9j7gJyr*c#~NQ6X;MY%@iZf}4fiSZ^7=daL#6h~@cV6KyqrRdL60uzM(ZRe8C(-G7Ym7!JVzt_3d!c?VU= zkt4AO*BjsthXrjJLLNNAfsOSU@zl2Gpujmqu4IcgmZfx?^b#H_El`I{ida9DQnW33 zDrG5_$@b{<@Q^-zx;qvfl6l|LX%`-h@0^Ep$)(`)THo!2HbXQP*~mhz{iPdqLOEjvtQ zbRg*&I5h6)wqo}n?Q=i_0e;NW*%-Pex+s>?;l-gI&dLvK&LC+8Nj1qDi9=5n&-~Lh zw>LW@fvSazgW>w=-g`THXxz$fKi|dfZ{J#WUji(J+pAqYUWnWXj3R4x{CXyLOrHIB z_V(_cuKDX_+$$(KeA+?-yV~ds^49EeDXMIi|vKPh`V&Y;&hE7JtnRn9x!f zgc$-*9kemXvbZ=M4J~IW_qvftmoddLX&h5NyQY@^cn^I^E-ym2Ok9@|k}8jVj+(_~ zbgF&5O@O$9gTPBOqBJ)#vT(I+-8D0nP}g%=*}e($ChL_3*rhq4t?6HDWMQpxPTAx$ zmyVaOANj3$u4T+dEL%Mc_b8JI|Il)2oy5?`OT{X7aj{;xOzm-bc}74fjRuzw;4+eu zmqZvr9mjn>cplUp+qc<>k0>X&CRiU>Cvq2{5}vnvXX zJiYw_VIBc>gS-o+yJ=g)(gcAL8rwCtRkr0uolq(#zr0!s@EpJoOk2MhGU#=iJrO#Vyz*0MpCY z3+Hexnn7$p5|EoFGC!;lKN=ex?VuN0gVBdd62P|raQ4ViA)7uwMxys#-wJf=CV?;nrJC> zaov{X$mpbJ=Vq?aOq<5~z5K+(5&tYBjAE@eJm!4wysh77NVn%YX`aAR|J*K}be)A; z(qm&C+loVm3=(Gs*OgF!VrM@Sx(dbGFg`%lm&$iv=p+z5ID~z~?m;KUl)>g^ATlT$ ziZ-m0uu{BVZJRFKrT>`p7fDrGh6IQx3>jHedcHz#nix@P6n7{)r}dta^O5R1tdGsj zKCa=}aLRDd*xb-0SGp^(2Q8yzmFH=cS>}*_ECsZnTrVMstMa1QVHwg8`H*phh+O-o z^R?dYXwjGYx$|Euc5M!4?%Hs=rSh8YuU4RYnMT>X(dyTg!trXD>g#ppTzkh8PFt@x z3j3#q{F+#+S)9+#ysz7um5ggaRK$KuWlz~7CML;uyVxDvbU=i9TY3sjAaHrw{lZ1|V~BjAL1hcw@j0CUgXX);tdYenh!%;P z4_EVh)S+j=r1lAzbgcQthNv0h?w8r0fRCX{f!1a6W4nTi1$GSG0qq`nJOH6%UUq{V zn|z2|3Ch5i!dG9+W{B<^LoAjeg=|Cu-wU6)_^PKK8%B-i9pWi*DvBmTE7vRkCw+et zA;mr=Qqw%BJu5Uyark0HQ>tXvWmp%w0N8#oLxOm$dX{_XwEmVwSKQ{E$I6#-@H6N< z7;wnsU%!Hp1^0VD_2Qkh5hGC?66O+)l5+yCHSU@;f?C48E3>#~7ykhz(c?M+&Pq?6Bl7!F+M#_GXEiEXgAu|KP^DEvEWnf>zO!c?u=Rvm{K^g%a zVN(K4_H+x%?q%I9+iKgcy-0jOKs7;4M705P(;J=tiu3iHAdx1~#It~}MM6^4Ex|dd zFEB4zDZMVy0-%Y~3hzed1J_L+l~_iXOCL?Y(0k{_8RRn{+f_KoIvCq& zKEgjrKjadBK0>YSZVs&ECGCyiHwNtzXcP<`G#R|@pi6uaH6wR3WlQIn_<0edQRh+0 zPdF$%hAmdR*i|`uji}V3c3zoCBD!QZ`?Nqeqd1GKyxi>jcgG3xxrZRpfWsEn0Kc?c zZtHyg#gzKdy5Xw!lZl;&QB@u7l;La86^L#Pmz9UrR*ovpa~C@o>8FxM+UH^Lg-=3Y zny?n|k0J(8vk1YE?P#>t2~0zjY_9{u=$Q)%x+wjvC`Ksk(OprMU&*E?@}tJBa_S9w z4A~5hQ^RTjGPwSn}DA(c(S=H`Kyz*r06IbMx zn3gkNZ=~WS9Nf>1-n%;LGY{iYi0CcR*7(jLQ9|TFrv0l~ZN#+YJz}UKN^HwX%IYmB zU(^=0JW#8i4}q8?fLy#jUx3i!L#%*kZV(9o2l5*rVhs5uWrmX*#87{ORSd%PI{Z$= zf@I;(JYjNRWXtJ{lkF$k3#%(w*u9_LGcAbBpj{DPp{P^xX1m{JeYRxtDc8Ry&;8H>lLY^f6Nf7Ews>4em3#`iyte9-A3;VlQ*F4@70?enm3g{^25F13bB+kOEVv|tac1JbG%@T}rL2VfSqXsJX` zb;9CmyX105P76vG8YOlW@L*%Q-wl47FL?B}%x@2PpLnfov2vSqy}XSYpPI7nxjK=K zPv@jyqxf9hGaE7c#^_hOQfqlEbS!vmidBcj?9thd?XkMx%XG?AZdvM45T9~#cMB|Q z4C;xoyz)W-26ToEgoTluY$}+XjMXbrb@|D2&olMizU~}T4d`m!B2%iev%%xN|4RFr zf5cIZhsgKi{2LB&=@Qi4s_(wtgH&+>;pwWGP!$B_aM5Qih%$E~@(tq#7UxtWCx zn&qW_!Z&v^v& zZFH@Tt!#}gE%5)CSLcVNoh>IJ;U9$l`u!iD`i{o`qhw+8U$H&}1pM&^Kub*n_-pQu zp&Wm-vdI`b>YJ$u7@O-`*nH67Vr8P^_)q_Tz4;%-zl~J=-$;5UhQE*e+nfK6&J3&L3059TKbQNEgnfAARZu50bT`1;FDA+SB2TdcRWcxJ`gB$CITK6 zFcR`$u+Js<4PN5lv0|5sMzX<*$4~WcrojtK^-86`l!CInKUWrbDxwvZKtqxz`%1?7 zj`DaT5(7u!zdy4_8(Pyh(kF`24UX(MywKRh+l*he{ycLVt4_7sjzBW>^#c>}{QWwD z^s2TF-%Ayh5-TB^-enN|tp|P$6!sW9+7f7p=VU4lu+0YQzvmF?umnN%eHH2rBIW`4 z?TX~_^tK{i5ghaSo$~)bqwv{~Njq61fS~?y@n{msI2y&aaO$>-gqZK$BIlT^SsqR| zM-wl=LwH>Odbuxk&=Yp2Xgo~IoGQ|~a48W(K=m!=3!jhreI_a-@}a?q@id;Q z60zm`w%{Y>b^-7--KjIi>jWT2TsLwA95DPoIuLW_Hcf2UiM^uhy}OkJZ^2 zRXXh!tTUk5BHU(2K!UD8M2>PKpK))Exr{q09nJcfmb2?Us5{!#&sX}+;8IiBw}|lWJw;uf z$20eZMQX2s6D$;k{21&noL>R>G}Tx6?!h{3CstnFJk0BD1)qzA75i(afB3Yn9oCuvR>ov)(5S)l zGA!h5oQipMW!P7wXj(*ZT5~bi;qfHOcp~9JX%}X+8XK*5 zq^qu(`rULuXmoOux-y#nkneok1yxLZO*kM)X!*NU^uH|1pC_=yxKL=f++khLVxc>| zrTgkwGGfzBcgJs6knWN>Tx_p`VUfl>3@IkMtgRb5M_+TUq+a9mb&{*Wk}2`wer?qTG8Mn!u=HX`Mr4nAm%|ZbH?ct-Nd&+{Htp9U zwPp$V-1S_x5D|vEBw3_tUe&cQANyhaw>UHw{lSOS@>{<074IFVflaKH!b-5NNS7_mrPx8OCL=A?F$w+lsQyb1Aq?UIVD@^6F zk)GE$(|L#>9mD=yN2fOJL;|9}{B}REPPML~Tse{*klVymIxuQZwb@`C-bS*IIhbK> zFv&7IcC`J5JZfHu=(^uPM*KNHS(6$Nj-@|lNR>!*{rv|{^tbkxVX(t9Of(bL5IkX& zoqP``(NDQ*TXuWaiCT1f!$Kj^lg6RB!v#=rXbVZ>ULTbj{sY1(NT_?san$V!v(T|s``Rh(PuQTDxdR)mNA)Lun(tkGqZxMVWZ!5%pYzxV02DX(m z^nUT*w<@s=$p^R>`-Xps{tecEpnS1_p}RKi`&qR9;e!A0$X?%&GVnD>Tw4-;r&*NP zkw`zgf}ea%%)`474SD~2$dQ2t)gvH0|Kr>)+Z4LXZkx*{?l2kD%7-}d=Un?zzb}wo1Q7Tpo8vUA{_itG3WAVHr9^mpduJeoa&_?v zkHG&PJmEiph2e63!*jcdtTA6!db&Tydw~Z%(3ve#tkUg=$6&K9m=RVm3%`2YPaj^Y zHPv5lbK8?BRw~UA4#%A;*X9mSnytpd#N2!-y&Cexs5&JkRm4AUe@b#~*;cq6i75S7 z41j|V#QTtJ*%nD69yf3}ozHL5N1<^C*|$$Uwq+5R+y z=jE~=Z`kAQI^FuVc$){($d{_~KD0N=flW**Zl!ojK7ZE;7;JOlD1m7CH!#Nc`bCE5 zXDhB$s9c_wVmGI{y9cxBc)d4z^?JW99!)BlBOXtE=G@rWxTnF#=gnX`H?sC=` zm5^ZMYm$EqkHbdXABGccGsY@hXR&JXbUas@oB$mzJWUhnztw90Ieu9IbIdV1BaNH=>9d^0tDEar4F&Sa(OtNt8i z7EKLE;`5m8B6ck4Jwm&}R*`O>na}SwdxY-Q)y3y@*0{ej5J_~F=x{V6Din@O1%ZGY z^F_ypUbFsdwN~}_PLRiLyf^$08Dkr5_K$R~Pl;137prnd z8jYv(23zmfD6)k@tzo(`FfbxXAitbzemgf0i(2;h893r>^Akg4GKvBwZzR&LUu}5c znH((Ms~hKEHs;w8ukCK^yTR-8X>d1Qn;Osc!ixtBrj$yeP}coJXX0vVNiBCvrctr6 zB$JbqTgOccD)Hdw8=XG6mg{X-FW2KRBh>_E{KGJv_aAy@KINJ4r#Vy2xb>@WyI*y1 zNu%q!peS!4WOq2DbSNu=Fp(v4m3h1EG&r=~UY#N^u18g;|2nxNUejyy?9Eywel!TD zYZ0MT%Q931vUmI2#wC`S1{+IZmCqA5Qk2~Y5B5-l$7aDNlTKscGyPCAco)@3G67^Z zoc4Ss9AtB=^DW&xZ-$^u$;*o?(M?whhQS=UX0ttGXT?a^X|_(czmTK9Oc|zbjgO}q zO{>35Y2IAja8Zk@DdFB_2w)e;|NZj#N@9?KE0INTCrt;WkcEjvF==0Kh^%@}tbUVI z+@zt+{&Y#vV7+a+&CTVRy8d7Lehf(_Q;rP2T|I_?-yc#h56}I9*V@{en1n=-$zsK5 z42Ro&nx(<}CLS#8sozR4T75gjta$6CZ`-x>_7 zHYkP{>pM=D?<6uS58i}?(Q!~saynRcl>vYk-DsKrHvpdV4Y8D!07s7Y>_*@!S-O z8`>G=7;A=%AY?Nl$Dq7vN0K@HY%~^{9V}H_ook!NVW=BAAUjdWq!{>Uz>bdKWBlAr zB@yEq1dQ!008ALnXy)b45<&}_kbHsP&sM&Wwhb5d1_cy09?}T8KD1SE+ zL^LqOQPtr^!*uf(kMg!BlgAI9forzM$1gVCr)xFJU-E(nttB$5ee_$&NGtEcUL z?Sy#O)fQv~pKHG!Dbu;XI6iI#144jS8h+}c=%^hZF+T-(TpX9TTlj8@Mw2E!iBy9T zJfyjtMb4F~o8Fo{b5YzIttIZ{$eEkg)@s1O@e-X#s%yY>>%{+yJvlJ^?`dpyLq=vu zurT0Vv{{_%&c~&Fuue}mhkeZGOIv%R$;oU|^NA>*T{~7Vmvl~t)$NQbH^W~}>(?=D zOcErBT2a8RzoSBTb!H4P{cdyk5|H@#v%`RP4Iy0^s*BorEi6)cNY?~ZUr@t7ihn73 z3zCN-7*$wQRJ5<0d5hF28L`#EoTlMlF}Gc82kU-{ykVZ!(V~W7-bmo@X(|xZjQ$7I zA78ERul&wa20Zcy{y|u>)&$EtR#I*u}h2EkMc@0ML`5a7cmpy9TJ1*`Tt+OL5H;mKs zXVqZ6;YtZFgv;BnZWLz0pZ`J1r(*`kM&E-sY`)Xo&N0~4`Fkv^#cY4&*jlcBL>k~($nCCN2=UskiBJ42LXY@1 zktc7Po{M`Be=C>pJfJ-64TM3HRbD<@;vA!%CVlmAcgPjlH{FdS>l0}{fn_zA4e^3h zuhDvAbBaXGg{vOp#GlU#EOB*6Iw?&joQFFwm<@`v0QDCG3^O?oEeuv^PU+ncNe>*D zvJbb#%Ihy`^;7eo;e&5RG?QQ>LV*e4A3N4PgSbqTG(Kwr*KCcIk&(G`kf7AN9hCLM z{S79)E=VuSj!o%RjH5TXL^o<1>lq|SB0=@psw3# z&hX$%l2E`K=N6=B2L&)#mv;n=4l_Yh8@rLxvagN4@g1AZ>bB;2gmS$*xeGM>P{_!* zp30G;*77#gKUl@Ls!O5bR(pT^HoAp5s54618ghb7) z%5F?2yUmD<=2l*ZLp@#Hf2gB~KDAw2((W6asq^Lyj{Z!aOPfw7V1DX@6KU^(2V z<+BihKCMStH}v2wAp0{*Mg|7g4R+8<59NqP@5BmIS0Ba9^?=hx1nKZ`UdoH&vIuF_ z97!hKcoHWiZEg@y&e4Iud{m$okY(P#Lw3C;-!oi$YF|_#n_rk0f{xPWC^nVt<{G#h zjj`&mYs07fEdc8f1mdkosc9V|u z0;vvqrZ=vv!7f(DBxe4bic&s-u1Hg#7&)M8wvGIR;(g1E&h-&U}-{^s#HCigaB zHg%npuVW?Z#vdTGDaJ!2#2ji$IeL^%^d;S<&Gq$FNtbYyw9P58rRz|#Y2XD^+y7XB z)#7hD1IF?aPqvTk!V8wfMjs_8$tM8gcA6wNPMy#|hE27)u|Fk#y0A!OG7r6JEcdf50#q4orSC3)dPHU#2h!;oJWg!gvf2}44yjmnL)*6%UaM|_|Fw?;Q0u#yj0b(CYW75rm=}j|~5mS|UmE6Zf z*KZ?@%eemX_kbMI5=5Ed+`@O&Zrw6kQrKSX^+2VuLKdR2VGum)+mCPnfzfD_s}Ab__oaBm8~s(!rKgsjVNF zYa~6=Z&Rez_=>+2GWNNHfK=Lm3aL<=cbSZv3R=R=jQxb}s_jl~jpB)g!k4ZP3b8=c zT`Y)+d82X`Q$OyH_$_7b_<$rGLuw8&5CUoZ{g~k%46mvaa4(dtQU$T!qm3~za!7WY zO<80}ClCx_heTRu=q?iwoGX+KpugjjNJbnajhh^mmtj4zj3Njt32!+f?b5n;OBeY! zfoB(-nkU1#wC5-F{omMLT&;6Of z25FXwuYrTUCN3AB&rrV zX&*SkgOk@;C=C6FWY%P?;cSBV#XU8P+MyREaLc=g(#n`Z;NY@RmHPo~y}{8Wf67_F z;q|gr6{nYcNT%VGz}s{s(kYr~+Cq@XTxz4YEj;5 zVuYbo_8Ho+#bTWob#rx$WEu2V81W%0xTRG~C4 zzU9(~;&cQ&8yg6ZSJm}gapMlBv3TPRpHRQxkaRnz0HOo5$!KN0jyQw$mbUN3*M7AR zx~)dHj8}Zr=xyIoF)p_pMZ@p4N|(lV%%hakQz^0F)r0A7mj^2#@M649haEI(9Q-5_ zXe8_KDB_+5Q}qT8Xs5R~U=ac%7hroa1~4bX0J!#Fps_lRHVTZdiO=;i zS7vH`TIip?*^D-=_UqqoW`EWihyKfih%@pg>+|4gXwYejuRKuhAXj27d+kVkHz>5- zrQ^;hKYRb&)4Mcwa*_03xtkM~vwi9ZYW^DK4e|~M{0azu@~B*AwAr0~cT;6TwTqez z=fDS5GaL>v>>qL-|A}T6Rq2uTtvTv>AOS*1+eGI-3KP+kZ&4%82VxOVjG;0g-GcDySUV3n?-i>as>?WDxMO zeO-Gc^R5+ETKaeoL#ID!??qt8P*W1l_3c${xd_cX>dTu$9I?GvR7g1;*r%&O-t{TDVOdF&B(*zn)_A_YsL>#Jt&^f za43f9u5w(Q3+I}?Khv>N243}83PYNaHakeh+}^)wDml(frxhY8A-|A%(GXlHo}tlC z-R!`)hM{AwLk-U*vB{sIQ-*GDkJ#s5m%NB7B{n%n^XO`zfFlflq05^UP(Jyq3Xzcn zwUcJ68rdt{E9#6g{~VxJkXhWjs6E_cj;CZ(9yyPV1`~&)26!k7yIMC-{LEDSz1Su- zOR{`ePaIMTi%BibafkYlvUzvac$i~}BJM=@d8fGCUi3*5VIV=Lb;qhNpYHCKF;6Rm zgwN15mP$Fqo#za5Lew6ROlnF=RqA0LjJjhWZf{?Ua-WtCD7V zeM(-=d(v3|pGUcv+kbdHU8g?pgzU1r&Z+9F2OIxP)L=iM!(gi@CTZLg6FW8T6@^PVfg*iijShXAQxqt=Ls0R8 z$W-yS-(rc?YozLqEaO+f5q*!vo%4|yOFtJbmG<8jpE@=^<3;bBO|7+B^8TP2i#Uz@ zflkK^?fc!qWAB%{-f#`Ks=@Sgi62&SVO?6Shdg54^ac}@FAK28tIz4uhW%jZO10d2 zs{LsYv5PYQAlsEsh^t{)hb9716zZ_ew7>~`T`alQO z_)Sfa<$>?Ol9d*wLcwY{X1^T5?(tFG(u zq*cM)VZOq0LU=*SXPFqW3k;uUivY#Jo-W8(T2LV`?k|kD?)qv zJ^dF<;5oO6@z6r{eRO$?1o77#RR^2dC?D!}eeUG%jDN;YUTVZLR!=#HoLozvO~KeP zD{PFC->&Ml5N5Uxy7EmM0*nXQ(n^yRpWF+y0hKXbTH@JHl~*tTYO7wsIEBOnN-E0! zpR2XI7&7mc@ak#-?(4bXuD!2LnVywx;E|}0 zizC9W#lG=ZPUV$>G60}H4_GR9lBO_H`cm%%NV_HTpk(09n0cZ+0EMZBpNj!w@2x;O z@1;Mo*Uz(24v%7`0Ishy{U%;>D!x*GX3rJj+bzxonEWy`dwj==D9gS^z({rnl7>1 zfk*?|vn=K4Z34xP2iaJ}yTduG{KugzSq@&FlC1>Yohom2*k{q}+vU+leUx{~T%Go# zVIx3e*jLDBuQWvwd{oE+ZPD^5qfjYbwfggnY|Dz)vOG>te^qd8%Pty;WCZN-X#RjK zlKe!9A33d5+RG^QO@MSUuGn|F8&woDQ#uM_zZ&2uWv{7{bu_o`~Du>}nxiN8%QBOehZRk9T1g{wCO@u08<3#w*{la5#Rt z%nj|gjo&AY)ga?qqHZP3D9t$|+QEw!ioqy1?l^L2xm$h(&>82vk4`Zxc_@{Lf_!ZE zSJA1clxqm0&r=bD)g$Qdj;H!R?&0bXT8uS_s$t-$pnY{_guQrRb<~FYRwz|lO1vnw zWAWeBs9XhDfIYq3M#3h3?chby~h&t*b9~ z#jCGBY&PgM{0gZ&@{3Ua(a-r`~xI27YH(g13AD1M#3 zi^et7gX@u=HR+3fSoxBi5r}DGDl`{Ue8SIViya@Fvo-^>E&)Z}u&W)zcj}kVLOz!udUR z+(RS0E3^|-0>Mn;J(EaksU9zHut&E}UG%!>lo5-Dggn!n`)`HN$JkYkd^d4DlE0}o zk`aHcRB^tq88OK6VDPF%)W=*h)D~1`J@uxsM(eix#arzfxy|}tuLMa2!b%pN09**P z&%oxkcKjS9pW0*%+vtt7GD8hptXi{Lj%mli*q_-?F0|xuYBv0em`A0*D-)FuRwE4_c_KAKmFrW)#YT79_m5z`*9_u{QMySgWWBu%~|H#m*>^A@}Q59 zQg6_wdw24fJ=sL1)oi&VHnK%Fd%9|~K3nf}+hNE@3E!V76m!TQl=XwdS$Lw+>89Jt zs_&{Ll&g)>$hx``Nu)EqZz*tP8S0Rjoqv^b@j<*G`4q^}+_av>f^(9_nk-Hk9oogQ zU^DM4Ymi9&a<1fqSyprv#b)fZYG3cAcpK`@fu6~{pdop>gFvD&#{&?3rNIWgBUp3P z^P$zQ-Why#UVr){t}v8UHmWH5>A1g6QGx2eq7Z>68QoHRxDCy6K>R8DwuGBN_L%FD zGAZ4yyw6jT?gVtPD55goYdhJ8U-(0(h>c)zdgZa}r7ntlXX?0e(XeiKa}ix6M=%@> zig?kO#%A@Cd#`ZWCs8wNoGNPljj}UGpTIZOddJ6(*?=J%5kDy|*llGRhs}iaX+aoF zk~3@DroCF#niaD$DLEy_LUyIO_aY|5zZo>J51_qx-tVPFJv`;_zG}nKOcu*RfEy=$ zw}$IS+LXrV8v)sHl*l?>)1P`&KWH+abr=;0& zH)p>V$oG}<#=g!1TRY{oRy5KV7B2}8eI3ysiF~GPgy$1tf)u~{$bHJv^YgE=*0K-X zM*xldA~SaYZz}SzFM%pVut{8&gJ{|;m{S-vS|KY54R2f2u?M9MR`OD-ZN9rZS+=(^ zd%c{~y>ng=ilBeBb^FQ8HYb)#9UiuP<`G=RN8zXVcF4V7qoJ|}~ zzZ@MA;|_fT>t?{}O}@kIqN(ZNKD6PZHrWdDH?l-oR<<|Ml_-RP_K4{n#vDi*5rh5nzIyRjqSCRQAm`HbI@ETQ4HTP;0X&El}xXWBDD z3F(rr?z}|Vu(r#Qxr-fpEp{qe+~>zvfLr{On`^Om_Zzhl_xS0?$AThDs$ z!Fd)v82`F<%HF(pfp*VW#Hi?j%t`y4Y&p6&Al`J8Y4W^gW?UqDBaFFRB&sBDj(ttZ zJy*^$zY)X!Nwj;x=mQ%_(#yEM?!TPrH$L7b(&^7KTVy_DB77OTjW!V1nN+R1pTKxL zHZ0pfNqhbI=T{zGsh&xK_oi#plw91iuNK)jLZAF+8!>9xINcy7GZNE$Ip0R;BoAkwHFb<$fhgJ-Se_f|5amr`E2M0?~r{ev62}T7ug}{E0i| zRqcl1_O#dsvNik%b29JQ#=F^)aaefE4&SwAD&7%Z3HOwdI^&e{SRpq~y{6|~3B>7b zWjiOO8(@&xV20&Ak}+QPeC*2(nb6o~d|WAGd~V%O!ML~8{^3>GLeAxFuK&WbF9op5 z_r-N^Tiid27|GXnr8E2l_o1Gt?Fn4#Aux%=G8vz>E;U!Hccyw_!3MAD@jt<=0bi!= zk1ma^*Qu0S|50NL!zw5A`iA1^K@*gR-BFh`{YPDwH%I6eL1+Wq#p@1rH?Iswz(;m* z{CO!X+#0-mh*LG`S^1gI;~~E|S|3~|{;?JiZE{OcCoFWs^by;lgkan@y|>QhD0>Go zXGs$!C|&zkT90<%?L^%~TWdkY({Pw9aac-TvivCq32<#k)qX;+zy%i&0n}dI6SYh% z6yg8l>aC*U>bj;;oW>h>mk>NS1b26LLh#@g+=IKjy9Rd+?ykXIf;$a==gIqxamKlY zo3-}ZyJyXsRka{AO#|%9Jd;7*5W77AudyT5cprcPankuV^|S@pKITPZU#jSKQA(`& zu?hVn^*15!vRGoRwVO|10`M590( zapPfmykzF|^`d7YU%fsO+k(;QfjK43Gx@0cMb|gi0b;GRVAoZXZpHqZ{=U*=(~e+C z3RTyHHj<77y$D_11I$0w?X7jzz0ULT_$W|4X`W7(nw_lv#zZZZhL6*4E^K<)* z{}G6NNj)m`HW=ajLk#y<0xZjjZ9#{B*1hRR8Pa#X$Y2B5VXUyk{F?;VCWJ{z+aAx4 zDUr$Xke0N5{QySJ)<|Y{fGoQ@ZuU{#584tSnde{dB&2dw+&HHOkbU7$ka%eYbCtX5 z#Gl9t-Wqy&lRg01qb^q;Hd9=9qV^AT?cz70&_+MZz0nViMLKoMr8Q}xaw;UOfqw#m z&L&v*K~cpLS7mdK4LwD$bWR9vQe**uQeXR&LRV9>BEdo+cb1nEj@_o}d21k2xMKPz zp&MjjqIa^O)d;Kj#%d5zle+zbDUMCI#OaEBl`&^lx{d`x?@*-g{BSgv)E7Pe;{~=& zz31B!dP@qMkYStueu?~RsUhU^+XU}y<27ta0(I{=bc_Kq>YuV2trO3u)*Xp?iR?WN zx?8?S{jXa8E%v`P$;r&-_P#uAtNpa_kbD>cY$(C{cP+uNE`TW~ITsts|GhO-0NmlX z3vph3&#b~mTWZb1-pYEVaGPDmjdoE~8Bu)JTb1M3bed-UIXNoNinAqN17^Uizb{ba zT%2R%dAcbl(6aIQlY6z*+UZKO`dF3;+svT5ykPE|vKF4j#%sg+e0^|h2NS>Ec}I)< zzk03>Gf#BC<231(iPzBA#{K5ANHN6wMl|n+Fuwrn>RT;J?0sPvQIExaw^g*0xOdq@})ngl`W@MFfl;$gH(Wh$NKjRyw<84v}E^N zXLr{>ka3C8UlcDfvX+k7c-nob@@|{b@&;g%O@E zeA*heCcwTs(7$e}u6w%DoiVV@m3tGenc4QA$W9Cg z5~X&2OeSNmYJJhwkLT_w?~cYbwAq6@TKxFq=YzzG{0j{3JfzKix+iy|;k^}_Z1^vU z$j6Ii>*`u9UAd;vs*>KJW9s3#(B(Emo`f5lbgF9pxydd`mVrxZ(L%8>+wr|x7gh2q zItJJ2i^&kiYi6Tgg^(bykO(YH- zdd{?A84$Rg3WwCs`F|Z-5GCXS)rr%jCF*ykiox&4!YP73Rr!fLJr9Y}e2q-;*{rbV zoym1^yE+?<1*;F1zjqaoDZpRDHe1ds<)Yj(5Gb9RLt{p+u;cvkH7OE}+{@?5J-@K= zpVX{b2#yn~F$axe0;0*h9Ai2J!5~E%^B9ycw%-&dT2xeYuK)8q)uvE+_HeO%FS=J# z>f)BZ>SA)SRbrN}_qh({U^!9E5>uBpXPoX`&>O5i8#vCXaZ>SvZg zIcNmFOnN?qLM`oFq@2|0lld6>cU>Ot6T70FSXtlG)*l03#LYaRK8t6=fa zB@okQ@B5p!*)}24)>~qxs*+0B)=G>U@Yk`>6?+&bd=(l>sSC|>;+os{fFxA()*qCO zrC&tlm+9FQCmMe49tmq_r>UpWe3z$2fRO|?HAsA@XER+C(ey|?i{$u%FDYB$v_tK= zrlXx0XN}PAxt|oKYU-l)`FO5A>&&oSzR-c*(sY4dz-Hmxq49Mplq7lck}fUV7&n)m zN7X{8#kUp}{jH_VqoVIn^+vC&*R9yCES^*AW%@!C+)B#dtt{*+Y=ValGji^N zPI-LI$HWQmK(uS)GEE-+otxB26f|9L`(5LmCzafaBI$|iaT#_B^~u%R-{x!E9vFI8 zG5@B6WR3`xBfr1UEY*k~_cb@KP^7cqM0f6I1lPXQ=-;fTh}M_(F~Ky7jJ;egxWj*) zd==Af7SP~4fMfc$vMCJ2s>q+EENYdG3iUvn(!q-JXX;&^3u&WZQaBV`t5a;Mwxg&Q zP>3Hh%-mKB9tsd6;|{2$8ja#|joX>H*-yj$FiMETT4Wk?`n9KZ#?gKk+*x1cn!c|q z*q+VEHTm)_l;5;5Oe!SnG8!6TX^G(G&;g~})NIdZr&FalwJ_a|-{r}e)#|aBf7;y-hiPz5_Zmolua4D){2JQ~I zFJi!0}TY!GBWL-iJHaJYutb? z?kYFdh2p<WE<_j^KgRIE5w!L*x z7?as&N=*da&$fLWulGKNMMrS-F)FyU+eA9km(8`lGhV-pDQdqvWhb`;J;w81Ij;4a z$XJCscip2tfg1nl+k*84m`ZE}3yvNcu~u5>cqe}umW4|p;qbh291GVg`yfuOp#qVz znkIBPBs3jQVO<5`?lYGT8_GVa(b+SX1Hypr1oy_Kn53l{+#)Tsc|1&ZWVE=GEn{9Z zf3$^36I%ZR4`lysC%``{!3QYRBMqpieRcFW;NkcLZZ_+UkRf&Id5wB+pCT>)7$tqT z^BcKaoY2;w3BevL#9!SaPQHw|uBccW4zYHVo;3axmt$`Cs(1aiNIaImPpHpgjwYByj33E5#+z#XNbG z=Mzd+`HfaA07jZpk9UzF}pI&`er-tl6YI^^pW;gX;lUO;h8K0|3z_?6GbHbXWn`$1U zuNN4JqDA&Yh`;!Tu^9iWZI8f-cdTU^m0f6pYH}UAa`r_xpFZ5}RLbWXc@|pjzSUj+)%4)IPDac68Kero6>(Fqi%VC-kc&sq|#uB~=jAD(rIFLBDOmq`|)8l0LOO z!Jw2ee_P-x53J|sqX7Hb4?XwNWSM}iyx_k`RQ%@pnLsdpQBgm^IGrav^eRl@Zj8|@@VU%CG$UO;b#Fdg^> zDC{vsgnb*Ru?6)I8(se;L7E2Jyy0Q{XaY3TJvL3H53;rD`F)NcX3YPrgoVKNrKB^y ze|h#g7Bu!qJ1k2#x_ILp(KC$WTvn?SVDAd!pV$HSgXwei-C;z+Ev!1;ra{5*C2pU+ zr)E4}Q3b%vRIm9siBu0TwQd*lVf4Jp#@pJ}m=)UQkg`6drMhtz>pPo%IikVc?o8>p zx@86H(yDx)t?b=|*hJ|~gd(2P5qyn3EFL~b6}ym>=^#*M=?K)5I`8&5ad7D8KRQ$` z%8%cuSV#P6?aO<7Bt|WRXpjZWG{KrpgC{z^j>Ka|^!F!vOf2g=&1&`_6Q=HAKCZho zB>KwKR5sST=u^`JW}g!Um-)BjCDXmnPNpNxo?j1s*wk4%RWfXz%qpEgIg6X9N3eOO zi#P75+#H3k0zz)ulz^ccq&IzEU~x#4jP}ian=2y28ZARflfb&cmf5546;ZaDKV}mYNisMd>-nqHid}+UJBBuS=A2@sm0t%#_E|}L?0arv~8wOY~B_c|dnd^9ExZV=ktz9|ogq;SeA9g?#H)V$S=PY!>s_y&r z@5$oyC(?sBDS|cKt43}gNC+>HepqtK6sAg6Xl$m)rohA1M+_ihb*z!6rgk2AMuYI0 z17*SXNc*Qc7YvpeUydavD0z3b_u#VeX)eL|m`Xnk+V2(}Jg>a&$2gnmv!i4hH-soJ zh8Y#HfF_R8Uy)-=dd}l?9lO2dPl<8q9oX%bhw#6K7ofH8~WSdzBi+X z5>jU}$|ppaKAL`%Fd>ri!m!ySc||wreiG6A79+(HV*n27koRFjl?<2Ks3x#9`m{suL!X z2WfI{;c&}qH;%wy_KO4@o>o5#@juY?VF>j`bFHK zOS;eK4fjK!q#wcd)=$$hDkFf!Go+o4VmlBS<+7zWWvLVxDa={lP}ivbiXE`vylTrD zIRK@vJ^&5ZwXm(@D3kBj+{oTnp4VKn9CF&h{XYR2E~Mwi$|#~s%j8XK89tU#-#0_C zkX%>w)4G6K0ohJ|L-u?Q$6XEANQDQ&-STkv+xaY~%=D@&6X0<7n@`1Yvtig&lVD)2 zbs#z5bJEG)`Mf|((g*NI!-q*i?r%lumy4$xZA#FsSRZ~mQ^)VPm2dc7xyYXWY;=of zpmipK6gjV=E$LJ1cs+p_@#!;)is}brWs%0d`z>vSrAxejkNOd)#Q1FujYit3YvlP~SqxUn zF1g+7n?Ah8xS;UFA!d}m_;LFs%io75$ z6s)L!RvGO&#QZeMzH28$e`hIfF9Ars5AgQJAt!Y(4cAF6o3^uqMq0Xz5r5ke3eXct z==#jorUA2k$k|Eh^uUNq%c(?HuavvG1?I^$N-w8_VLo%&!pHnw?SH2aGOA|7%FIXX zlY~Qq=EGud6p|+guT||3nrC@ebCc3j2ksW3yLhD_B>hj$n{yG_vnx0DM006C7o5-x z(bP6OZgkRr1PEBDHv68Zrs-^C5$Gc9{q>>_Jv2*li?>`y5@oj3Wx-i@>h$X5RioZ$ zA@+IhzK|nl?4$#9|Xbe-UmnkI3oaCfJ5HebuZ#r1oa#|i_MkUwx(#2@bVc7FMD zl=7xi=H!*u-Cyq#NN$A!0T1fFG=Oe-d4_Bl6Fi|eG-JKD4){r|5K!C2cQh811GO(~cRAPZeGIb>){QFh}A^ESzYXPmY^%flZi2-Ht z)fPDzvFd47#QdNjF4EsNKh)CZ6?k#8GA7kyPPt&Vg;Ypw1z)qgI{L8RO*T%v5<)cB z%8%_9lHvOf2L`MoXZuNQSPo2l(Mpe>76#sE0ZRkzlRQfR|L}mX^7`i(LD1R*D1$wB z{&z{(Nnn2ITwk*c9oaqmx_`){cqVEhX#0lfWc4}?sh~be zth!X{_~APlEtqwLhovX1ih$V?ggPgFs~Y)z#wLaHDh^ynpbz3WCS3LKZvVe5fN$N* zRzCfc{Fm2PMaefm^ggCwvf?2X1h{nfk(*kn#My)53fq-7YA zL6S&T3@pWM#@p_dGi^C1KFyn667m_Ri_2)oX8Rr21uz+%q@ty&>fjS7Tbmex3xpuC z1xjrRUYA7KrTa2}^I39+y}@r6tiG=ayRcs6(s~(sFtMcEL&F7$ocY(E9f~^W!ir-P zWeoy+jbTuP@wM$!J9f>n;Ov!hVa?i zoYJF3tMu4ljNO7Xe?Oe?Iw-JnO|2$9@pVxDp;To3^8{c#CaqMX(l6YhOH{p=pNoOE0c+ zD!uM4Xs=?Ou^2($s2Wvxxzi%6dF=G^OlLd8Fy~`Em(OW6k}>HYdFwjIDCt{KXy$nK zF}-#J{JdO2OXVkCy8f^8?M%YNH@~3xWi2p5Q74QY)Tn^ZFhKpQe*TCF zA@bLar6-)9NUR%X5D7GowG$SyS@2!x7_R?Ank%Ej9Ke{0|8LFJF zN$r;YvCDgXQ$EcVF@l%}g!IU70j+X+&d0K`Uiqu9s77RIkKCttBg`* zw~8z!iy`%|7(v-B7&X~) znt?7IJa=hLCy({lePM0Jrib$hO*iQ#`nrnuL^BPoghdrJ2O8+K#FO?o~8 zBAhSMditep&|w*9rF6+!NLv0TNXWQe5{;w87^J@TNqB3iG6IJ^Y_T0@brTExX}#Gy zbI3u^G{1r2{v|RSh3^{6QTj#M*Geb zD>(?gK4!t%PK5P!T$xGW(5vA0kDllNsa$gdkB;?j((G=-MFF1@9>JY>)ao{*ZX}N$ z;2iq@bOUtqxw(&pt(*d*@8Xx~-|dc;@=Ne>E33*?fq$0P7pf7f{QYztryKoV6U=^( zY554xMLLKCR!yllz6EG=H1sWKLn(hB2Xkdvvvl)b$rTCGg^l9E+_QgZ4>4IFv8@&k zXLA;0IU+QClfrq$cV1B79V4Y1o~t#SQKmj)Dn-?9a2@ItNyarn_=o|1`NJm(!lthZ zMMO)!)6~vJhj51r%=53QA;F{;7=HsU8*^SvL=qNnX+aJ>ZKC{P>h#1(rLm^)C_Y?n zRs4_o-x7q}I}`V3IaKry2I`evz9JrYLI;?rHv=ng=8g2yE0%NI3nuH%T#BvUG01=W zWFem7Qy*vBD?H5?YiOzJfWVrOBn3;;oDa>`Kfrh`eS6%X&(H7qx!iuodiZ2%rAgRw zL(}dKom&`lbE?ZzsxgDp{gg{32A;qIrdZ=SmI$m2@0Kfx)x3K5zN>?|zI45QpAC8J z@ZP@xd8CV5D>3`3T5X%=>bFxI`#w3i^~B=h)`ZQkO}hUz00?D~Ltq+WR|Xhk*uaOu zYIM9GQB;HU)SDOhbeynX=13~9bFYGdRU$PYUgDCjkZnDdhQ4wK&bab(X#3?s&upfz z{Yz=oUI6gaHOF%x4lc~^Emg$Azl?3XATEDUb0&1=q-Lk8+I%Z$l&0%p(MO-JdL~Kz z@-_sLM2;oy$1#?QlA78jm;Zui)S{lUcji(zf~OJ+->*)P{K`cB(%Ga#&1*~tRc%M? zL`IIQWN~KO>U-(o+jyAEKjx-fL5T6k*;`AOPH-4=w&r(R_j=&9i+AoJSuKUVh6^r7 z%)Ml9t~WE|44k!?HGlOcLEP%xo(^tDg)DvL9|iecx=BbrpRzG3-QD;jttaAVFcSLD z7G)jl_dNReJTpi~u0d}sqZSu7*UyIg$zFtGQxKb-<1(~+LX;2! z1si2H4QYhq7wCGy|D%2R5R4%88H3bXknZJx3)*v-8s>%s`~(+rZ(3U0Aeq3gWx#VD z63>jb9zWjRX++Qa^M)AnKfVl0^jO6M&J~A z%+yXQPHaLipMoxEf|%hHE+xvYhyHGbQK%;<3_l#M$gyp)Yx+tmW!tP5+86D8T|iY} zyMk${l&Y`D!^WzH4xcJ;kqMo{eBY$rY18Z3bRy}JYVfBo)s z3x(Cpp0${(jXf32BJwXjon6cCa3j}yMXNh#thh-$MM&@qdDt4{y7E8AoWHJtftRW9 zcXN*vKb$%ndHZ@C@Fc%l{PB$U7O<>}z}IP9Hv=PrRN6&>_cLAP<=MeGO{_g%Y&Rs0 zW@f>yMt!%OU|C4#M;Ly9a&HPePgYhq+?J4_7WMnuz(YpqWat?m*t6pX!2tR!M95R3 z*9w;l(O$veXX9%bC@`kF-7Y-Hbb1keoYfx<~%) zf2?AJe16hH6wI;p36!r+!>zrb4K`W1E+CF-l3MW;P0880%+OR`*Y;FG(Vwg~_Ni(- zHN9Rhx{?h!bQf1|d}YexhL?p0^R`myie_@&{LfFV}s2nz!z<8Sn`j z97VPnTST7-%HrAgAzxLr%X9pg^o~R%$oQ8vBeNV)tNB}GaTudgHWk)r%s~e{VB)cD zWG}zeGN#)?u+oMa^xZhk|_B zosQk+-vM?B*x|ZSRq6-3Il7}r{c)SYudz0%MIxB?B7owZ=q}s;M{>#mI6MF{P;I;Y&5*=P!-ioR;3u0gGnESW3=`cd8ckVheVJHIpn0pZgn@o5& zDRvpiwLmIa*YKj3!r3}lRBACnWVm@API(ByW^dCYl<^v2!hJyq0mp0BoP?0L^3K*P zYCsq5y>FIl3^I%SUO6+fB1+kyiDnvH_8^@^CdSpYH)9eM!7pOJ7#OhJEqiga>r8~D zKNpvO(v*m0dI@w~-x|~ZQ1OUQy6>@knC&bc{zC%KWd3MoIB)!)2vErh#@U}Z`76uA z0GkNo$b9~=CHskWIb=@eNzm!1sBS1!DPjP1U?*8@;Q?wjEnx_#ZEY152v{PpB!Hwx z)1BIVW~{6FPWJIYJJkHU>Bi7)HjnnZ`=I~Nn6R~NaWJ`d*=9&&!-1(JsQ4kOwv+yq zWz+4A@sFrNS8hS^gp>8#5$>4;b|T7Ml=@uJsB=IH%HsjWcbsrp0M4Jm>(ZG|uY|g@D&N6wr}cBfZ-#rQ4K|B4-cVp%|Hi)s5b+Zd zhh$(8t2{ASx>iZ^-IA5QyF~MGxFVp05H6j?;wNXR3MGXiDK*!A7ZzRT%o7iG&pH>% zWem2f#wmE>=hj5c9>E+6My20jM}*mq;naHTBaAOxXdU{hJLsrgi_>?D?|+E|wxm{A zwe_qcqkR*Lkx=NT{(O&!`_<~1saEiBH%CkDCpD7Jsc52`n9cY*U;#aR(4CealSfyI zn)~jIhh~b@AQ!(WPZe-Rd_z*)mvLpV`AEqv|v0h^=K{crw$yHt4 zW;u=S+wOg31e^+D{dxh2wYZ*y3|Zy)xe0Uo#(2HHA-lf~aP5H_3HL$AH&hW9YK^`_ z+yPe9ej!-l3C9u5(Xlc^24|2=R-%{|q_X-egMoG;&y)ef8^h;F#=r;1ohGzSxSi6_ zsOr`Jq2Z-XXc#k#_)R>gyxTw8WMIm@=z{v`dOqry{3k#JJSX|XKJ`}_B6JC`m^4Z~ z$uDi1%h<;s&yNd$#}SVsSrV4dF`g(Au6NE8no_@;wBUp-K5R6CA2Q#!*w&t$j5%%8}OvwbsgQK1 zHMy8^gD=r@&r?RBp=grAk4N{&Jm2JNCW3KgoKD-huP9otM8rt>$+SZ#zHDT|AV~FE zri2BiwL!O021>`0;5E8{b9c%!hl#1usVA8*J!_Q1w`q7c8NL$JN>bM{=1C7l+p zMjf%*%{Tt94{oz2jd$`lN2?T5f9Zk~UdX7OoaA{0BV#pX<$4Ry@9I*YmYjE!<{1gR zAK18h>10@e3BUI%>73M}YST*v=W9|d6_Rk?8me@>y#<4SmD-fCaB+*iZ%W89ywL%d z+oH8mH6OgI+w{^UwKJ5%X7kv@zQkp%42^RMN|CK2a92ITvVMS(^J_&xZ0z-qMN-fQ z$ds$~Z6#0b5LGsh&$ec;Q)0@-I5LPZ ztm2b~sd+D|h~fI-7^(HU`ij9ukFabq4T3QJCD8?B>vS9|>B4#A9*1BP7o+3%UB0KK zb(_VXw9==scmnmiQqn!}kf6AEY(Y}63M`;zB#F7(JbE|e(=5ni6cR84{e4F0t6d^T zYqdDyixN3Qn^&0mDjFe*qp3#G)#VSMUKZLFEA)-YP{t&R{lMBtnSI4*eQaM z;HU1RA0=bC6q~R47<77alWda43ijZ!;XN*j1V%#LoCqPlCj2E)ZpUZ{;kYVygNDOu80h!yASx{zBPfL+or9F`&gEH+*;bg z+dh_=k(^U_IDe_6Pr~@p{-|qKHR0TSD&M*5;md5&$wIM33yAnNwiV|#Rt0L4DnTtf z*fd>Qr;0(T>Jpr1`SL4If)FW13Ub0j(krt&><)e9HUpLyNN>UxC}F&(9t zrWk|QlqY(Sc=R7FCQ|0#^!jjU@e!5N+VtI8AJ(qJ``wC(co4n=r%8DiO5HP=>XAs% z#fZFyF#8RA?LokS6RD}p)a>fL@#>vq~;Gg3V%xp2Q!me0^eC= zx}KPpEaH!GiTr)3s8t^T?$T@L$E>di}8hJIlS?Emw3@QIw(6q<)8KSrDn_=wthQDrZdgWs$+9k3X!k zflsGRdgfvQOCb*QT70su{U?Fk77}8dc*Q?e!4CS|bXCH83IqNF z2sMjurwj3?-qvBn4!GS;{_6QKnBF!IM~IC6vP}yhoml;AvdenbXa1cp&b#Rxf_ySP z&FiRj&*rW2>+REgQJ5vF@j{_{#)~M}WcUsgBNAicg&9nLipXKL%H=X5chs9G;Mjs4 zFW6#+>1Iq7>=!%ly3y~ucyA!ZN~W1{omW8f&KwFf1b==}$(D90|0BEKFsIFeD1^6rrh0_PX_lve@IilN;kN+9Vz z;@6WhbO~$Ms!K4d3A5;{1H`(^URsn@o>`gwsSTaC(iR`KZZ`(KT=r|!Z~H+ig&4o? z(@pL^v!3FrbC*9XU_I89i(LEcPW2+cgRFazA=lLO7a>&LstJ7OBkeiYf{vi`GUV zSi;rX<8T{Flj>GCErz>il}njl%e_op)`?#t0N^N6(LxsJ5ia+uak}duI_9~YmfVo( z`tClv?L&*%)3?9&`4I7SOVYK2JTYYSXPloQ%)GpKmCI{eXcC<~gmNyw6SpXR$7zh) z*dw(e(re>S7Jpw9lY;$#29wzerw-&ljs`_9d%@eHxshaEe21}n7ZE9{8kLp(u!342 z6Zk$js-fy~&Jy#XIpx6gg+j0g(Sos=R#|Y5U%n5OSqQ1Iq!73T=2lZZuFa6*K*saw zdu%uBskHJNp|MKrN$<}v{Tn~EA_%)pAU-uoC;T^KskPj)lD?->6*GMabb$2Hg6FEN z+C=MBqDM~5e{tZ8=NjD}(bAc?Z zX2KR1#hVRDk0SDfSzZx_w2RXXnY{-_H+v~$ER@C-nyN59qGe)n!5Kf@$1bb`n`-D znpyJ3s29V-d5zgz8i@e`DKaS-uQYL0LL#c8#$~Yj;jqo~0Soo_hm)Qk>7xC&NKf?I zQe=7ni%arIuDRK>TewBUqk2o6WulkLrX48#!=U@^+oC(qsg1cQQ692SIwc&P{x-Tu54pNIY zmke0a9Kel2I}hhcspdK4@D7yX_;coA%AjavCwA6K14#T7wTK!KLtIZGuDd_X>At&f zPa#O1*<*1daS z4YFz|_xA-isz2%NdxO!yA#JH#55RvLXz{=c(V=HiBuw^DFGZ=)7!1T^E>seElY#TE zNYd?5pcfX>C$ulKvaUs8j7iTcH>8QK{~J;6mnI-H$9?hEtEy?9%6p5(i5Kt;NnZ=L zptRtQ)15F-+BKJ{q*M*jeZdMlBUrU9X@!-;%P5HyNo#O`_$O9K9Z;&> zU^RJWqfGd%r1#vOD%YJ}UG{(n9%fu*CbTH~5Im;HlYp2pP|nR0_E)MLIy;C4hTgT5 z4WnTIgrT(TH|@!W3groAt5gBewYF%|xU0at?4m!wOQo4MnvL8Wm+Opqk+>bGk&%gn z1PzjHGsgI;Z!*fWJvCsAQMXwKJa#^JiC^T^;KgN6NzxA1sAcs0Nurlrnc-#0d~9!< zPxW9yvzBs@j5wx@r{h2FZQk}RS|;Ho#E{O%#pWVljY>&m3p5@~Q^Mf-(tM=-1F5ms z)ZYGs9Kz$MZrDf5o=OyW3j*Te=9WnmLhv@6CbiXe1!bf3Nb3i=}R0k8< zbq`3*1%joXkvRI8TxB+4Im|v>uOFW?z}e)24HSRzKEvWnmXlz-qHFR3a&zk|%~Q^; zhR?Wwfo+Fdq&nKc3>)K(MMYL3ePQKKsr=in+Psct=k!QJO~dP$tk`6bL`$H9xb6~D zDy!rou{9&*MFH>(-dW>HWNV>{bhu9RbVB5Ma*?Ra{E2Fltcvf~9XPc#Y_1zZVzAsL$PclXr795UWf~DNHsN(*l|sf> z_Yk~4dG%LU1L;fhOpqH4S>Lx`y?E|$kO=4lGRzmj>SQdmG6{}xUQLJxqgLrQhXuc# z{ft${omgIAAaDyM*iL`>JYWYix^q_J?OMM0K}vL3r5V7D*!P*MEq_&?{gKK2Y~%CZ zo99K`nkSX+S47jBNfT^LUhuVzIRc7xg<-dEBm0hP!+Eo_7!I1>>h&{n=sgu8^xTIO z_tS63OmFqQ#T86PnEsb{guplLcThz^$5M@O2>4m^jHS@&2yF%HLU*V8iG;;rQ zFu*i{9Si|WbsC4iWbd0A>u4!8smil+M=G=6I}FUb&sSo|L? zmXI`hEf4j2f9tmTtg)fpaB&3LQHE%U2sL(0FbLzJZ^b3U`TtB#?wIcj`e|7?b{rm8 z(7!tc_ki|ZD+L0R%%=0Wz8JqQLxb{>*cFN!>$^W*p>@T+^>Vn^%q6trnGS2=|ZQe`*4+|sDV7M`+> z#>!0mYZXPEQyWP@edv#a0RJy5$aVbKRIZ97Rw67IMkGt9+A2skNZ-O5)i>u*A>->i zVTkFp0&eEZBp|cSn^Ue5Y^~tebVFD%OL70@fH<*LGb2)D8bl* zT*P%uk)w;mtP~x2_C3&qi@+&YR>gDZh7Ye5hDdyDce8ywj^(_6#Z>+b)x4b|T7sB< z_fj`nsURA(_cP>WWF(=4ikAuJs#)&Xbq()WqwaV4A=#7n)R&6MV^$YiPdxu7y`q~& zx8aML1%96;bto$lSpUaotg*{^RNg+j?u2j-Q7&nfy1fArn|*AK3OEbH z?pq|m>_Wh>4gq9A60m-4ccb`ak|#p8gb}ZF%SE(G05d4^^X1xjjYMipZA)?(eR|8> zrV18?Z|DlG@2Wyq5NrL^*EYv;yomI|LvlE(lMZ^;Vun4U8gXFM5Xlq(W2_a=jnH=w zOW^y(M$Lz+gB1&*)JwwO|n*ITiYdGWDzJvj|L{toEi_~6VFgiFy z_g{MBPc7m@0V?wypCA!)KofDmQm&X^qgU1s6? z)J8^+rt-$iyP-V0@#a!(5OEj<8JVpg_%im~29HMUZ#EUKGoJ11U4Jf`tnA)9U0WBd z?pDiD3_fZk4g_z(`PW&p3t=VllaT>qf!Y{yx9?ur2^S+@S)C?F92dQ^m*{z%%(C4N z(jLas+`FdS>H}Dy%#?u&O|z;u$aW*( zvW~GmP`EC2xvOKcyt!Y8!3|`jgJqh z{Gr&8io)C)@P`gkWEfE8zr}COu}X~28sD#>{iSw?nUB55c}^#vU%tG!96Zg#eQveq zX+^NuBD<@)VxKP;NOE`&K zu=c)v(dH!i{Za8-_GYPQIgW1Qss*xeT{a^?tKqjGwb!=<2^YW5Ayg1W!_@kc%e+qu zqZnQ*&dwqFhmkx+nwN(4g1g`>UsgVp)sEBgyf4C2zqJ^RBJ-EP~^!62~ijgieJ$VyUjC9 zgeHHe*sOxgZnM4;j<40z9DUKWKcZ}pe(RMV<4x14pa!Ina(#@5{>J&_8?kp3_Qm~f zpU-{Zbpq*)F-3pl_+!Fx=E@QkMJ`&|Ox9FgoGAJwgb%k5GXqD){morZgymA)l&ROd z0ju>okF;2TKDGZapma+~MHv&c*?D+Ll67A0;tA!x{(J2y3kD-tsNxo*q;|&F6KXAQZtT9q4EGJF!4HOMAH*-Wjcu1g0rlT_UVl0#Bm#41(C3 z4TJ*1HKGSQD5%=h01Z5o;Q>!*#JCt4x7FkY=Qae?ewz45Ax7aArxecOmy!UUK(uen z$rLxu)Q`>5sA|+J_;u~l@;#tf@O3x&P<1bRx51pnZP`xkHv?iewF$^6s$qVJsJxfA zyv=6{3qE}`j_gPXR1}cdKh$K87KUC9n3H5lUqV1HLm7s{b-qo>YdIs$G-U>NtxP^J z?6GO-f}VFU2NkJj63VJ3H6HSv3f^MI%DGS~-Z6Poy54UT{JS&Ps;;y8RrV)qG^bqr zkR=*;4|vr)m8J9Lg~u_&IG9=B<#V6L*Nlu?4~=ax`P)A5m>25tdKBs@fw;`}#AiR~ zaTbaRn%py60**ux1-PDLXSF|L$Nv6MaEw%^duMMKukD1hhKo6#`1qwfFoa$u$Pbm> zW$V~QhqA71bzvZf_-=LFg$k$%wap5h>lAv&(r#4HFu~oANN9fdjge7TZN$G0@uf26 z6yb8q1#m>ws8cy#rql_=^F#RyHjAu$(?YY`+n-lBgze{!g+n_fLcXhx6SiGR3ZT7J z@O!O8^tnpuNR!wR+Y3-1su9MRh|bz5dOX#&F_P!dD~+WvqmZ)2&`kFD^VdQ>9)}iZ z^Tom=1N0^ms@$2JtW|x$ukXgjI8*#4$T?D}9Y(H-fpB@BECM9?66qrZ)@|YC(G+vA z_~3A*$QA&l5<6l=-J}tSeNVcA(v5S6PlXWv`U}c|AgJY@!uRM_*|RhOJ`k(+TY)as zgYNGBEW*&h@W8!xw^4sG7Fir`N$wOS%C`eIGdkLetuxJqmQ?_Vd zlPr!t*N*tu5IdymWCQs?+-f|?oRu> z+Vze}e1eDgPium1ANqNI#(WeJG7S0?l887+M|*mFf?U@n^^-*8vM5G?8GsBkA^e7B z7w40;ddl=l<)28I``}yZ$7V&)c?#g@`{)K;*n?pC%c4eg#+HUt==`iA>RULOX1YSbGc=)W6c(tZ+<)8q$lG( z^Rz4bcB{$CPHM$BSBmN9y2Sik|930a=FVF+Yc4%73XwhkRs3uq;J9nr_VGwyj^l;Rnl6fWE?Wn2nys=78>fdd@6TN(jr=}svL=}zhH?gnX4xgTacu)?CkwXAX-)S+$2pWwr2c;bwS->oZ7jqzSEki8H;)%%vl3!K+bBi^gM7 zBRlsGe>N1)A?&BnpQ`jn&K;2(8y>|V3SbzgjB93X?)w#xc+HujAx1XacrT_yi}}nS z#E+yp z$=;>pimk&H9&I|sNRwLV!M?=#fFpVINlZM-)%6RkDTlVJt<{JLvsisRIHYlkxm0Wn9G=<-x zT2D0qdu9)vmb-qK^XL{h2{xl8VRBCuHT6Fqe?7!!4;%205Vd!%#L$-0fyQc!tF~5GoN5e5S^k)}WJKMnAgWTXwV5ZF& zu6#lMS0?%xa`jnI18VHDdU{5(uNia+#l9^pY+;hSSy#J>GNQc!}b`l_xJ;IDvg)9-~yOT6ryEneynZ`*xD|*&yct_?{Y=*=+rOy5Siq>vH`ap`K z_kgAx+S#Wytj0f?8t>5Wbt6M-*}3AtJ(>1_$L%Q7<`{AN6hR1#@L zzb0BmwbnZ-`KrH(8d$(se+_cCc(BXnikHe~{Mb|@$e^Yy(qn|d=ir@RC|)G~YG(L} z9x0o+3fhkZ+edLfaxe2%3;bsT9iRTTv7{4*x{9+U2BZ}7PVcOA?rb!Yr^d6_Nx%uA zXv74sX@Gv7CiGqCWK>T*PGSPOIqAxoa|bjUPxh<5!xTF^QkQ@LKK|%wyUHZpN9G0* zj9dW;O^weL)ueSA%(ow;#>i7;7^a1lJwc+lVuYZ4~D4r8V3BUXmy_!L|@ z-%oVeC2IQVQylx2z#Uu+HH z$%;>`1q1DIJmEqWcK|B9X;oNzJu=d)@yqmPe7xW-Dl42Kx>2OmftSauRES=Po#<3u zDa@+jB}QR!R&M;nW4)knYUd|s@bm1b@i&H*o=S1|x8VUR!H85E-%veV9&!UWsrV5O$1(|3ps6EOUYB|8eh> zYrJOZZ)i#)=kHV_QOsMy6CMzIFvo;|m|ZPa(j=zWYLMQbDx=Okfk4ob0U+fsJ=Ixg zQbl`I)wRk=?GuRk1Jyp5S@5l?9O}_tKbQ_2>Iw-ox#&)qyRqApOs(xK4%vFT)wfDz z46}q;GQgI7$2*p9=XHbQVPZoj>bpP1?qMz+f8}>pB>_eSO@1b<%NjE?=+aVKmZl=EZ{Y50+~wWbYmGLC69NYM4-YBm<+TRb@`bY0S|?2 zyMxn3%#p~Ka>1jo!JU;hACexDImw8QZoa)HchS_&8o65;8RoPjM8bs_n;HkoNfirF z`&$+@gjj%;?@^DJxutnqKGj}2FGquEwEEaCV0A=p7_rWvT-w#8@*Z6&V|is1JxwJ= z@9bsG(;{dJH`wm8tYm;Ix}w*dEXQr8u&sJq{OdP~9O`e*WO1nr0iUEDAc5V_P`Oul z>bhhL(fd@oys9HblA{%ijyKP*M6A6XmO%|Z9I+BP4LqlQ*6Lq7$#g48vzZosptW|{ zrn`P*uT4s_qu^1)*R;Qwv2FK?WNt+=%LC1o?i6hA4!x~1cstCD%J4BZzSh-r*~Bsz=LO&jS5 z@I0Hhe2T(=YEV!>VyfyJPfIkT5m=w85Zxb`&blbeGb`9iY^=4B(H0rMl=gcGlf|Eu z@#VSC<1M@>MPXW;QM-9x8po%`#HF?RvvKtgn|$K==IGXAer4?~{1UN;ZOie%rz1isXz}1VJ%)$uKC^^XQ!O)V3!FxqYw47S|>Ru{g#v7T|N41OK(88_4B7QP;j2fR@*^Op2EN{QG zt2S05_MZWjlZeOX-)Mp?CyEu@{Sh;=L@XYawSFwo+4jSqEEZ2>0@`qV@sgd14SgaU zs*fz4JQ?_q4s_jJWrj!S661^H;#MV^nFMuAX;7#1pCT9#PA;xL`D{n*FU++R1>rOU z`kJ^nP`mh2P{>8tZpSkPuRhSphrG%dn|C`(7}3cDx%Hs|;WL)q_r8*SOz$w%^d6#V zo1AsNn4o}J06LS(O8Tov*+PiS#hl69)ph}6jW*C+P3ikP_D_>>8@ETOc?F_S-n8dl zPDs=rVJTq0!eOK;Qs2He3xYB1#Y~J0k6Ts=WC=z#S2t{_^Y+WdsdtbCcUNOCsW>X! zobdIf!~>Dw`N!KYnPtY$SuhRk=Ecm8p+E393hso3+vb^^O2|^?!u51bKC7vxau>&L z;T->T5x0@m_BtlHo`W+gO4^6&`ihbg;)0&^QlB?@_r6O~1Yh`@#ucwQ8QX5|Lh2(( zu!puq0_-Z}$5Ioxt`0l!u-+-d%&PU*sXT%;(6WSOiRn_-V)cvh>Bf8`v8*TZ$?Y$d zMqaX}OqYnL!#Rj$(x=KvNdGZRC9Z61PB7uj`l83(tS7-jSIAT8!gixzR${nl%+oA0 z5#uN7jlnE8r5E;N$NfnP`_skmPm;h2V86l2j`XI*@gkSKk31bqGFsOqD$+#ziSgLG zzvVfhpo++%b_^PE4}K{^#7dJ0e4|o+ifh4`=L|XvmL;J_;Vt*_70qD;I`EsvWdHD& z=oc8OvhxGFL8ehceco!=nHqSF-kBP9f)^J44f;@T$_B4z#1|952igctk<_k8L*N9l z9V);=|6Ucd`w=)c7!7yQXE~-}5F1T8cOxEFLLaQVRX!eua2s$veNtBGxHS`VlQjeG z;231XjQ-e6Lk>FpEH`#@CgYCw2a?HbnGh+k-W0)K!(yjfZHRPwd4``}9;)@T1u}t7 zTO`oXZTYf+cXWN!^NcKlpU9$q+d70e!eSRjjlY034=0{Y!fKOZflC?pva7neB~+DR zz$V>2V(oMO6rRnf&dJ^EcGk>_ztaei*&k<3Ni^UG2_aT_OHMp0EJj+99 zzK(S2!pVqtNfQ@K`5n2Bx9huHxZFJoPpvSi)UykhW^gN8a|8AGynDuXq#li5x(vLs zsK`X>R12`r>8`MD@)GV2bFRf55b@LeRI^w0%%B2pBgQU&Rh|DjPFq~_%@h2`AzbZ> z2VOGA^ZQHl7zM?8JREX!U+&VyW152FdhPXyOQ7bp>unpmzaZ{Pu<`3@^y{`;BK4Q# znL>@RrH2mqAnKyG;(%^4N*qN0lURD`tyyxEumUy5us94jFxVQ}?T_v^Xy$EAM)vt% zo;_Z;JsI{sxv2uSSx`bekBuprjr#rO4L5-;yldyaX?~e|YItUM2Ybm56Cg%zyp>S8H%`ohr?K&<$&hT(3Ct~L|3^Mq_d(8p08{|6#Nc7@M@8; z+yK|qPTv|vo4omFTeq8IsK;6vLWvij(u%A$%d+LUK?w{Loavx^z(Pl?ucUrX$YU3= z$3_3(<#z7n$)}x4BWK`XIzIi=_Ah0H?ILfzHI%^|vMMUT?ru5$eO)%iKyDkF{A%}* zq{``gtlv}K)E@j=p9?4FIbv1#6dg#CcM1i|du0Out8n*kpXbw4A>8-s-3$-<11NFJ z@H&cRx0Y(r=kUSbn5{UUJ7$-r&S7uMNg(%LBGruj>=^eW^8;G~Z=IDKG0(6q=!I4p z2aoSFe)xDc?m)eSQ6XP5l2{Yy5Ept6eH+LH*YUV(DAcWZI9Ncr&5M-&MjXmRAYIdrqoqBx^-gHn&-dYQ8Y7 zt$B;pP_$!S=|CFXEw=0_Mn8eGks z$IUxh&J!6lC7OEBD;+vYHOT_aI0Fr(gG!J)Sk=vs(dimhdMHVIK411bX88kX!m?}K z-2Ej_82qAjvu*el5i4=L_<1s=mG1v#0fcuWVHugL6KRoY@HcOKd=wPX0fPq)4v`_7 zg|vjfmkOVq*ui5Q!5jM%47DO@Ib-swSo_;^Eq#6&PF4y;--W+{nGj?c`?t@g6L{@8 zJQc@g$b<9O?*pa5E0XaVUWTOLR&@S&MGCj&vYwjXV!J+&nSW4^ImI6AO5b^&gJ85N zUr$YghbgWQuz0{15&yUXulmdLa2*UV8{s=Fx#6-q*OAJ}2X&;*B}KAgb#Q z%l2^gE|&*rC&bz8RBG@N0r~i^$Cjhq@ z0`nD4d7%f*{dvNa^9y+3)095AZ}N*68xRXhxH35Y8iAwNdrhBkY{^uK5t;c&MvFDR zZGQjpaooXH=BG&tMi6~{;ltx#iwF|(rJHq-DURvD6vK!%)2phP++KV%s-Owq1n9 z*<2qN5<}Y1<)I(iRq;?!=-KV+{JmL+WM7<)P^=apCnZ|p3S;i6VjI7Hy%-YSeGB}Q zDAD6@wWQ3^qn_g+HK+NYu+kh2`X_GiuX~NNL-u8UsgmXV04BL4k?J5;n1bRvT^G)WO*|*P27PlUu4<}p%Igi~<_ltpY zH&ue7sVCWc*`+9N8J=h2@^`%@a?#bK*hafPvW(6u8R=Y}ui*)Jv|1 zbg!uup?K`(Zx+?%QU?n~sJ>zb2uR%jf8BT^qU?2z%5eUR)cm2}I9+{<`(NM%Am6D!WDaayIZVi=oBVRji zuNF6F;UqD2#uo87Y(nAGD*j|FKPP>HJ5NJCD}%&c^Ga9_b*_W~8~z2v&(T`}uS3VB zZ}l{+qfIaOeJ#Ex-V8OlzSxjDbrsyET~?JB%(;>$N97v*?&(C zY#-~|T<({eGGUVH+`&@*Mn^1nI-swg4bua3oDcOyE~-YwyA_9-*^^%cGhVZB?v#hQ z)>7kpzDmtVTb`XqGMyN2>%`P|vVC(ls_)_EEPu~};rHn8|L*kYYqN>Kg8g-KCeSa= z1M_Sie5y%P#o()~=iy^<-1WQ6SY7U{eMh>SAfsU%vklhJCwI@48GBsQph^T4wCAdC zJS>#tZ2~C%VT7DUjWwNjuZmHd+aE?;fMw$VsnE*`Z4gpNG=oGw!}8|t zz3FN(+F#|Y=lQZZ`?E1-hwTK^A$<%dX{SSi`iyiPsv0_NY3kJh_cTq<2Qz?p1BRlW zX{-egIDNAnR{K)B5Banpw;SVFJ@d}rV4)qF^PspO5N}pzgRpoB=BN z+$pDJUaT_Cu7UAq4xhbAYPs(ts$ zFm^{smvxW03{ISsLhpsXkhGlFY8~DIR%kL_P#h;L?0x$)V%~nQC=8FFO~1jWpkW0}#Tl_v@=z~Y zBlUB^(KjQ+*u1HPW4vs?w4}WbG13r}6xa&hX{KD^p-|x{$0h{aZFTfdp1b30TcjbR z%XCwmb+?D`3;#KpXrZ$R4dapb*YeYi_w`Rj9FL1D3p4vJP8zXtmo^)zXmaAkWp|~otUJ| zi4}{Zb99k;{)I2RL$h^XIuUHyq1XcoNhNdQYupe!txb{RDJ@2ci~% z^I`k9)ppC%YC=*x?q2?_vM}c9Cm8nwWOADH?E&0E;qvATgFrv@G`dBuoMu zR`l#>`G7R8xW*@Z;3ch1&1vz07zU$2`GZ2nODD&1E`;aMv%Ltz&@*3pa2rPhX%n$qx@EH0I;36H5 zC;#`{j?K1`GoGU#V6^9TCpe@f4Q_50wkRq8yl9VC^kjIKqM+hvs_0jj-n(QqryXsC zAm|&&bwLTO$9F)NBxhW=08N6?iJ&W{wg19|utrP3rby;b@-KapYWAC45upu`7?-gx zu?x!-wJ&Wc`;0jw>OrocGgTB;?s4@i=yC6$;IY3W0CT?s3W*uqo1)|Jh`oEt{9|!h z4{6P=izUsVS=RcmHpSs>(nuki*XYYdpHlUK-2Q6m5DDJtQ<`II2H$$rk5r8k9%d#0 z?-gLa2`1RQGIX4_F{HnSN!-cY|C>#o&XbeePhXXT4rM9{&hf}XdI~(7e!6cR+)k$! z@j==Y1KY4bSMR;+jg)^PSeY+yS)RJJ zD<-o%c@&?qGgQ(+TJrd}nSA8_?(o6%DrtPXZ>w>IU(+}XsbQ2ri9&w}wFJW-!@JyS zaHgPk&iDR)8*i?1+zvQ?sp1Sk2jJdiq>HVey~>WqO_j+|A3`q9E>DLXmiKWTd+Mu( z3#=`Jv5pAm+xSjNmhdyS8G+Ive;Z{S8z#!w&<&cwuO1avZRa?Wj@Z%CHSO(E-@c&1 zFwJ>BSjGbPTSdek5jq{RfIF zvOA1Fxyo}kA9P-OS6xOrL^ z`t=Oz$sgFaqf&|mAh0){Ln62Ce?H3M&1`9ir~c8-5oPi8QJQBMN+iEsrPV}m#XORK z1~Wjq#!uem;=zOrjvXOnO>5}PPqKaby0jjMuQNR{;OmI^a`8QSuLX zJB<_a5FRG84%Eit|;@@Si#Za&SoEj8du3HsW=C-R8uWc8!)!l)_wob)Xb;&Xt%7?Vr+L_2i zEH!dSMb%Ifr&a+1R8~G z-POXLD6XM{;I%lNc0qXeYFbtj||AavK#SI|yXZ4jO`z^d5Ac@<)CSNr z)7C+rS*&%{!UdsHDtuMctb>fnx$MP|vUUmk(LHpPJBk&Gmy`q`&{&jx#Ak}iVLgmw z1fvmOj7m+uBuAkdwxJUgAbf{`iTf!Y zC-hY_)5u*y{}el_l~m|I&mabv8)P_>`Q=hwm98x&dHBEu>ezmQ9L7c{@;fx7%_WDE z8-qzl?6G>~bKqx@ksy0mwt-tl@ShWklo3iUI5>GQFLYNP^9(&zk((9J-#f!eC~;W* zee$UeH>*zF%G-su4kE+uHp3Lf3Z$#)PwxKLQDy^AO$aeC^Y4AQ8oB2WLqB47sQAh^ ztkPsxM$*`33vmZU&Z_u%3a$sRf1yRkPOR9w?>^;H>G8?2m1pr7XnTg&AB|J*x@Nv$ z7Gp+eDBI;6(~PQtZRp7gZ%zO(NdN*jXd{W0f#@UWm+jwOf8t%jW62n|dN&AaA6mMG z(!C5gXCxkOa>$TbhcygZRY*ze!Y(0of<5k6`3XYrkbn zzbQ^q&6}~maNB`lZ{^?HVC)%dl%7i^UG_L>H&`pzFEz$-(K}aLQG2FEKfj0+RaVV$%SsMzAgv_l)VB_5K=cTZe{1 zo}&fJVS+dZ2VKfv`{bj&{qoU8?wp+r-gF*-zctsQ+oY&247Nbw1=wQGW4NS9c7)Mu z!srro*!}PPr5RV5e1Tv=DwqJR!0H0C zVMa)YjIZ-xh2w;-+eo<#I_ZNf0mp`S%SPdHSr<*8MNO<8h^#*EbDpZgF%)#E`4^B` zI$;1-KvP^SzTjZ%xMu7qFd4(r@3agvL0$&KrCap)w%AoiKw-(_Vc%kj9q&q2(H`Au+{{GX`0ZX zmZ}UK9|Jx>VLh&B7jf?UkEr%@2Bp0)O&O2p#j=5W4_mN`($J zw!a^fLsS}RAK?$7x+r^bJRea|RdytzupVqH_A9$gC6dm5dqGINa4sMUp&zXO%iB4L zKgylT<|8T@a$oL*( zqbQT_Z%2zFWR5>=X;W-8bbUf z;QvuIJ89LLZAfxOW2RK$d4m%uMhsx>r4fWvmjMCT+OvDT_CKZI*ruc==o38an!0eM z8&YOVCfINbC&R%}2*}{>@Dnt@$sKgEQJ+sLlu?g*b=1N^5eO?{MKN$bVXKcKPA_Ly z(ykoTp}C53vw2+>&(Up<5$pQEKfSbWj;+Cf~TL6QVC+YMa_AUd-JjZewmR~zj zUa-Da*!u*M@l*|$cQH5W$6ugAS0{T#*7?#gmr)a&PFms$)U{*sTP=NxN2%sR7V%ygX){N~=X~M;U zEZzXU?%1cli=Y5fOB{2v(G^S=z;TOGgW% zSYpgAscc>@l-e!Xb&=|;mJk!$V3rZr2TLl8=+J1Vt6D-~e})dyl||C0@zl57E0xoN z4)l%M`N9n8lFyPOyKH7zt*E1e6t%ZjHD*F=row(2@PR|2=lmfC(`xg|Xg` zL%4N_-p+0SZ4bRhLmTFF!csS&m+n9uIx=a+R93xqyL2I@359~x=p4+4h?l70>OAIu zJQUflRhxYc97P-ae9wt{8{)q&t3F0L&2-DR+A8ml#}y&l@KWQD$qul3wraryA7v~S zPgP%gXIwi7nfo(vT)NusvcBEO>~7FN^xX>7B*+NY<9E+>pn>wR8p-ROINx3f^XWn@ z^X-w~pZ6FzSj-;ao_O;>0iu6jfX(`Y#-EJOT_T676=H0}B=PbhTuNSluBcg4MQULb zgE8EB@K{n()q9LZck9*cqwLYf7LNTVPcgLN&`xNs{IBo%eNiG}%`D=p_v$8Q1WP%g z4YSB!4$@;}?}=kpLQ> zzYbyYN%gW57;p5pcqt_vqnJ(R_Ha!umz+`EtzB-?TPb~#qY;lTteUftk+}# z#R$D2mpj9)38;>Jatp(MQQ^1dcO9>{#`tCuNAA&@Xb znnJDYg6;qd5F=n{e2u=J&^>kE_PGf10BP(;yRX8Lq0TI6q&P3Vgg=UhQD=iu$aNET ze2bXPb*m>|L=~LP9w3n#d{ji+ISj~rLds6giI2nelkLOZB8a(N`! zt+7!;!%V;jmIK60k1z*yz()}$?DPTsgUDU*(X*`anQG#PBDbWd36b?*%1Sac8Ecvn z(F7mxVhEY?AJm4DYS~OinhK@JDoaYTl5Afx<9<&FFm`3Pspav#9{Ak56ynqk2#vRl zr70Q;gQBe{a8g`)NZsd0^oQ#Pf{A{A1xtX27h#Exkpuwo*(@~8bcnIO6xj$!1*Wht zIhrQ}wOU!4KFW*_gqHHTEYwDJSNI{r9f-zxExq}D>S@thpwI$&zOflUyuQr|G^53x zMC|hc_p${39`k16?PUS!q0HV)dO(_PDNcs{{9ZqK0KDGKY0;?w9aq(z-fxfscC zqcZwSMKD)tD%Scwfy>csv~{9&n2~Kj+U7Jl&2vg;I#q3pyg!PSf;r!usn?u@Z@ST` zse>9^8sM+T>;EY6`Y|ibz#Qep#jw+c_jbMnLp(I5p0xapGQR`OvbTQp{I0@KO5RuZ z0q46!c$U(0`5=$Qj}7-!$*|3b(cknz`wk>2o(|0iD<@<*Sjcqlyqq3PqQxqP_*f%T z=%HN1tA?>C^z2c5z78i%6)aKar{I-Pp<`xhYlGg{2s^W>;g1h+d?og&Z{P3%rvDO$ z=kH>FzS{%FAbP=MMz8P2UM4Zf_QW;q?j_i&^>OJ;hj>Slo}oiYTzZMbj)gi-3mggC zOEemvz9SKST1ri~lP#(PiPTY~3dDR{2$T~;LWaUOahz}nG*9u}k3wHJTAG1Mv@l08 zMaG3sT_)s2>kY}>oCv?MssCri32!i2#faM`@-{I5!lCd*Y-FfacAmJGG zFgx{5p0Ik`yCyF}dEUh!$0piM+F{i?f%q_koP0 zT+t-8Z0y-|@J_==StO#Rui$+;kkRB#RiABqDooY6&!=8}^Ce}ZqfrOI#vocDA~x(K zgeheU%{#f!<7zZv3P^^Qsn@d!Bh6dmZ*I@Wm^JIB7O#h(stPA85s&$kD!p7E|5Yn` zXFhV6YQm(2O#h=?g9#>q8?xlKz~P?5p-V_2Lk{Bk@-qLGV)4U2UP}kgLgj8MQohny zHs7v0Bo2bh?gZgdNf+JlyY6zSx2S}%IALed#-AId&Mpnyc48@&EXAd5T?&R?+>B2KmY|(qU!H#-R;f`&Kjkrt z10oLU>t7{`#F>EH=adD`?af5_^6OK&Bm6su8)?eVR$1#IZu6YEj`Q39UA~_I58=m}B)*xXoHB z%z_8MEPuB;&dxqy5E`BZexEQ#J*#{nhAV?@XkP!WXWsJ|uavbM6VOw*dC?j!RH2v$ zomZaDbVhf(1pMD%P4<92aPPEQWru5uimleS85)6SiJGhTJet(kQpqslr$cFZFadf$ zPZK*#KbFaiYIOuVHrM_OmTGfwoT=ul?MsoitHQA;Q@bwqRuuUw09G9HM@_q02MScQ z%mi$w=6LP29D^T%*AO3s0C}J=Uh#KG!wZ!Xi%~5r39njGcVAF&O zJDp}Cp4vJ$WkzHuA;Ys7+{H~5UEbO=7QtL3D70v^j`@1TtUo!F*nNTyWn!)Yq?Sex zRV3n=$Z9TRANX|FiJ+vXN4}p*kO$A}HlXWRN%GODxP(omOVzLO2f^(sjtGGCP zC8BTiVj!yM3ZY?p4hwqVR}452mgE7lgGHOj<8`S8@v{8!_sO}z2Ifd{r|6u1r~<6`H8LxKr)FKXxmG>D9~VX3ZJ|mfk5HCK21c3Ll{Fx z-yrlr)xPpBh-l>b@YRw))ZRhj@Or5#_fTF1&hbP2m#SsbJ29s!4C~9fHR-c}}8({4F}a{#iyqYNfkP;qsxS)s|>~`(ITk zk8E-I@qDVsDhmo;jQ;Kt$ERooBwfK};@I4J5Y8#+N-!ek0aLhx0Q*Q99)*_lM zVlG9Ipt4%Sx+WiNeLGGw-@zKj@(C+b-Xru*VwSC!dCkrmZ@URJRYmz~sbQbJ;1QJnb0HgJJX@QkZA#bw1{&|IOS_|jx7l?zo zo(DxbqJc9vePgq_Dys!o?uKqKdD^0*4+WZ7d-bgoZnU2GVM4DpT9z&pHCM9e))lHsPKLjr0U8+~MH1o(Xvy6XQGkI=`ZcVVg$V68Xw)6)KTv(KG;clMq41RS zDgV#_RyLzvB6ik1L66LUq{HhLs%`%ymrD!lAlK$`=^fpW$WhjBNpW)$m6VXiZ<{oO z#)dDRbtWBGN6v*h9QNVP(x-Hn3&-^$%+{N8ue^=&iS>VypxG)TLY#t@G0I^&Ju+Zj zr~d(WhOjc0z5Ku`$CgC8gcSTKH*UGE!BUCIRKSACPDuXt*HOVAD{eyO2?nA|t_9g? zkHizJfqm(L6Yje&qqp#mbFn3^9uBsbS`sIEKi*Wm)A<&Tmos)lesYXWQ|TQCT*jI5 z0|&uDjRC`o=0O<0+CB!dS+tEkV`VtRt`i@#%Y?%Ae}6vQDm;?TI(4>B2kOHdGUHrR z`KN8pvOx;H#4G^zTvqyWxy<$2$jXm*KrhjswYe$_G$(U zli68L_GC$-Ur}`oLSJ~29KrZutB)KXeR`Ad^s~a`w8(6!3p!urt|}ybTax*O4gVe^ zi1;bj+qL}57t~Dh^cxa1#GW&#w;ByCNL%zxY>AEl6)jrsVM)0N?{ZNFJ5bWWRNo6N zTW%;{N{dh`nA(!I5z{-nlSV=<>V1ZFFx2Jcn{*o zE%^pe10fw067+h12>WF3ZSx5N79lV)YQsrlVFkC{&Wp30==X|;|1J~rCAaPa$7-GI zknRVIpx8uG*eT(ST?bujEhjiNEtg8TpHr?zF+Gk((v3ei`MSp4uZI@>+PYLuNc3ow z9dGUCT<$c13o_t=asps483i;8frK|q=2@gMja+JQD^^={ZJTgVt1(2Y%p&AEYKJR~ z=K`b*9{@vc97DXKyteAN((LI?izeH_ozTZ&K`g5!_z~%dXVc>pn?2`)1W(oGq}E{v z?>mvJPFN@#`;!V~6Z0uBoOP?LrPaeBIn^e>PYoAj;sbM4wR>QkiGC_u2(;lXFK zOf7DAxx76Lj4-hoS`j|JoFl-5CZgnR>i3>Peq-4#A1pW$}(()x7utHJeM!J&{(*H7z3R}F~_k~2K5uG^e) zEW6bJ}8I}|h%!p*8AdTBExnvr_EIST*<%j4vuh6xE zunj;20x%d+{)MuEh3Y{_J;@k=uo>h+84wa>tA4qkq#HfpB4ZU`XT<l4OGlhW0#tY6>zPmN%rS8#VNJvYOdC!Alwms;a8NO0OG$&DzafLTZqM z@hvID6m5Qh1G0<0>-05l=nVvOm$%F&bnh$2)IiC;&OMC!U~f6TEQH+O5b~U&xCvC9 z68fX=EXvneo9SAi7}R%z1;YY!Gyo?mT$ur@?jUB*Po1}!g2Dv9bt`jO>)iLD)`A$! zS#q2G>!AJSN)n%@s8aoVbT2IOq8#$Kz6fuAu|lTkOR9-p%B$}%PA78(AY~+;EqIzJ zY-vPkC;JFm=o!WV_VFh&ma4mBgYV4|G5l?G*usZMLSL|>r=v`e4=Q4% zt7Q$eRy4>lY{KXWpVeU62hl5I>++X)!v%d<;X3=K1QDfoK^NG-TB1uR%HvJ>%E0n|W9hwbH?&Iv@az zRP%6ma9}4XceiRo$-P|8IEX&R)6mhog~lSKtg z)feG{;Kjq|qwo^p%6O-I>-cMqD0!NjDl+DNmC3%J)z)fw`l6QMEyml#BSzACw-k5# zz8)`J_!;=J$~DR15(wjYw_;ZQdsJYcch{k|Ow1*7&N4||htl5CzR72b8E1S{W)AT7 z;L|w4BY4?H3?^$#Sf870qWp^ouOFRv{N3iySz}!-TQYa1TOp*$I2w%EzJoLrmxpyE zkn=1Bxqsyj$wsOQ_yhgsJ|K!Vr)wb&?NdwH@e&l*goyFlhcT~h+fe2wLoASd1$?{b zI~xXub_0xeTqROfZE(Y)>3WHu#u407^mo@de=;fR;rSo__s9M{t5L+J8IMxO- za_taoAY~B?MnB?^x?GC=-=8f37o#I{VSsx|rCl0hcQbLTfaLkG>Czdqc}EEj)7G!b zeh=IJgKXHMd?ObyP?t(`{T>3z7;#cT=sh%eOKTIpmqEN~&7BhH5{gdN;QN&K;;A=R z(CItlz9qXtI73>6CevKaSx1CQx-^}0TtqhJlcNZ$l3Js18id_Zi&ettp)MLeCk|c1 z_)%Pd;pZu5jsV(d=k*L1vB&qUCF>1@5lZD~BK01SB42Vc(gj<-NP~*HkxDivNE?_x z$mwn*mvJqd|5VJ;l;{~%F-z!!?XBihc@+~fK$Ro^D;@GilGKp-OHKaojh7mAi5WPB z<|Dsk5_=_fVCPYNkh9_en6!D>P0wi$;G>+S(+&`p1dsFUzOsn_t|Jj;q-G~y_q;dQ zsYj9paW@>My#MX{_jOt_+b)i!JidIn$fdxp`m(p*LR0SLTK8lQWe(9uyWqANVvOvF z$;_%)Os)Zx&EkoeFWX<+A9~(YT)W@5bC129)dk9zo!R3|75zxH&=RTB5>r0L|3Lg7CCm#-s~|hNK~rnT2aABSbgQQ-1L5#L z3Q-WCTM!M+LDjqMJbn>f z+q9Igx{Ymwtux0?Sy(Vz$$h;1Pkkgs<_JBehJJH_Htw?v0~qsl=s|r9m1i`NC@Gku z0hDMKQy?__^pSiFarN<>@4wqL@E!HOLdH(}_-3g>(USZw=PhfN8xY89*XL&4cb*dv zdKTw(sG^PH;c(e>g8Cv(N9#!}w!qWi-a3AdaA}jmn$Hao_}gRsLSsLLRCg_W?goZ9 zstGj|VQF^H$+{^fF%X|(`f$vHa(}M8IB4@Is2ReRYHaDDE>!JQ>-u5Qt(X1|URw@7 zKt5zz5eiRuxpaxuzNO(J0#|v)QhsNjCiTF<37(tKIW+IwBN`_tD0_2+ynQGipvNT8 zT_3OaWWaSb_#hL=YZvR`sZ8LK|G;PS{%-WbNn0DSAC=_kAKtDVPmH~U#kKhb+R>BD zId!E5!;|Cd=SnT+l%U=P971&>*@GItpgM5B?(tVdgw|jIclk2d$pWEQ z%00X&Y-vg_m&mKQKx|1`y5Kgq;0Tw3sbGD`b*FdpN3peK_cR*`SUR`#r?o546&KX2 zdnVbmG}e2RQzs2K0oRwW<-$=e2QR@UW<3I^$#mES+WkH|4CsyI=9_{lB1FsN?0pna zw@5xgnf?ERs8i&V)}W2wUh2c;Kf7w#n%xmf$h;qhQ8~w8^#obWrVTW%3BcmJ)77eP z$RZpK>iVZM>NL>|7Kb#`n^XQWvTP!8%7=IKZ>MgBLV0IBN_1_xPh}C1lLh`$BaJX#EypdjS9_t$``U+F%7jCd^?|SLfXDwm1Ci6#DIr?32DGL*zbDOJc_|xLz z*-<~OH@MaMfZ;zG`G{o;RZ1LF09yr#J4yChR`C+re3;iUT3wPCJn{M5XzQ(QEA(pZ z)nV3E7I?O(74as#27VN&fb}wLUsnL^cQmln=A(^4*`pUYHUoxx zP{6N~l(x@HO+&@W9($Ds#bc!X$B_mFk?t<(I&`-vaOiG0B5~;M{PqFw{oVWSH{Sn`u?J(b z*?X?J=9;zEnrlu?{q+sxs=QwtSGcBv?_yfJWH=k^t}f-ByLTgVy8Yy(dUewUKDZ4} z8>X1jFQvc9ev=UK`^FO&ixnxy|CByI(Qqikdt#csVI7F1zzX*4_jrn`HZ{}oz$e2s zveXd}zQreL*(vFty!?`SH&IX?alfEdWzMhKA3mn2e7$!$9vXFDU3*EFXgM(s7c6c+ zl`|M_{~q}gIFGGLTF~KJf@Gj6%{@qD~xNY>`Q)NpPn}0yt=(^LoI<42g$eld2C%sM)F+ z)*#^be&2Uj)Fw#bxNvOzoMlLecOPT@a;GymRz216 z9n%YEn@pP91uGJHswK4dsG_eNh7hN#&mm-Vk>|;znOxu0#>LG^eS&Sh5iV5(kOb#9 zr%77*G+)Uv_JhU3@u=>#)V%DUJCo$Q3Xtb#HuV>_>aWC>yYquTD+H7M1Zn#sBc05z z#*PkkaRNuwW8_FyXnd;fZBl-6Wo71BqtcFOFFdjBD{|ymn!*z%3>E3F@LKmePw-#Q zyIP)kMD&HA7c@A(VtZpvIGDYoh(0x+tud`mo-*-X`}6~u>{2@#nq*zCOE>|VKX{tP z@WmmGGH_TqxP$`d)H$5b&_VR3-yw%KUS5uoiVFE zvYLw76tf(f4-Z3Bo1KgKbk^VEJHhWA<#+#a)dp$9_Esa8J&p6V*wSUD5=0)H{r<|L zR0YHW0P#1{BFgwb_rQ5{xr{Vk6czg8$$~8!2R#XqY8cNx^%*lgZm zO?q^9#LUIdzL^j`{N_OLDm3SHgr-RQk+4OQK09#>>&eg-e3j)Qh!Cj}X4}hwTr(Xz zJImS2UI_M|ot}lg1TRDjPiTiCj#L5z`9TyI@A+X(3}l5B5hkv8;0Fk%1!*DYuTD8y zcoxaMmJaX{5eiU9d+2A%e&Kdqb>yp|t)y%smUPw1-%Ny=4hlHl^P8=r_{PzZpNpRr ziOB;&CvRip81F-F#d3TCnq@zrgVV9lV|s%;P|UE>z(A>)kbneWVGvsUHKkN##%R#v z+*S~wz=3TVtjq?gLKOT=ocD-LXe5HLG}e5xrl^2yxqNvFmD%c-seFogrdt)vCie5N zKs%UEmd`JLjS+4_WSl+}pQSonq)k?tZ?oAWb-EQoDmF@+Y0CJjy(JX&HGHFyDb*0T zu^ZM3rx+{sDXs%_LyzCBWQwG9)>Y>Dt2^yty%Fw6fIQskS>cCjNB^PFZxN~`_fMlE zAt)g{Gui?)0G3Mk73O6*wRcwpDvaeK-)5$~qEHnJZxXhSruxqEHGvc+cpzSXan5NC zf~K<N8bM!(!Fq=DC)>EFu| zmds~=x~fWEU47RBazaW#R_py|Otx{0I^=Kl$q4}n99+`(WBm?IQw`YRX1wkq=U*z} z(tXU*P<7tPe|PZM2e}JkW|_Ai_k}um4+vxSsQ_&a?TJ}Wp1oJA=X=yzI@8hNJc^g^ z1sMamYdDpUItn)BQuQ21yon6ErjE}XFyBf#X!(TUHvaO+Yn7j)rI{F9JtO>E4!ac# zoHfY+$k?(BLRDyKk?V27aeD0$lWrt>?=-G%lNXh_C6DeQ$#UAPofo)&r|(I6zZHZ` zxC+DrMn0TAdYAeuwQ;9O=4@ZszXZSx+iv>KC0?D56tf-x1MRb&7)ch zKom~&^YuuZjMBu%&4x|jf(5D%@xAO>uRb3lsZs8MrVRyese;h5!zOw`Iq|v{EKfsk zEPXJ2nqnv?w%vkePy?FDFGR`{^JRf5d%0xt^wrM+2Y&9f;*WKVs;VInWik%`5P!)E zr?)Wnful7sWV-kMmFva@xqk1^XypbNYk!=MK<^%air?qXfzgtcEwAJHj6Y9BBNC4Q z7l9YTL=mUIMB3tT0yHF5I;M-Mo=PC`J=~Zh3HzG5_fKq8fxJ{PpMea{8NL{X^UKr> zyh$uDGWH(yk*{wOXq;1%V3pVJa=x>C(l^Z0(1UZque9WZkk`7m?(*dz!2mZU)s6lK z#m#{X_NQTcK(v#?$!jwsXHI#biW${&q-u1oxxC1?8Ebd~ivsuKKrrXk$4n+Mhwt=f zb+#yNE6b&3VpM!FUx7aVMohQ8kBHWvpcb@;5mwmNziq-8bH#u7c6wd2_9Kd0%S)&? zEL2>(gYRKP+h+gft=q8+v?}y{XK)^e!Grs8D6qO2MuzHuzs+$k*s#CeprYDed4J;_ zMSWgqmeBz3sL>#!-YfjE7OA8(OS83LYkB$?14@|-=(x}QVWDQ{ywJfD^VUZQ`cXYX zRnox^K@+36spS-r3>X&`lfc7MswMod3C8|>O-G!AsCb9xKY3e7B>N>f+nA1u%XvfZ zm+~vUVtRmKWv+p|6WqZm04zyPVCt z#y0+t!Wt=WHG}JB^iUqdbf}Z2T<|w+5RZ{lfmc8U_GtiJi$tPyS;ak1t4!WA=w$Vq z8N*RbeT*{BCCFJef636A4!?iH4+mnb8-B=F! z7UDvz?~|J-zf=7{pWg%*Zb-bb->dtK-+Wf85!yMPe>y6n{?P&xG~lq#yY|t8P8jLJ zqpMlKQ2x_kOXT&(y}JNr3HN{vvR=5xz6^q-4$^9C5&Znd1)p0Xn^Shr$HS@x1CJ@o7h!lY2 zgsn6hulRZ^9h1-Z#Z+2^-(3dWq+9$T`|Aiv+0d*Q2Q7Q|W)TH2-=bcvWf5?V(wrMN zE)(^Ii+n|k?3#Xkse?bfHh;3hY+iuLmKN(r*d3YQ^Tk_7IHB)}T%O%3Ep+@e@fxIo z8sJbkaP|e+TkJ^vQe}|+)epEM86$AWpX*#(8AzvkgwAAl<8~o9*b9}KMkLtgQ`);= z#2+B4Gt`EQ=iJd-L08^j)El@6uhR%oqs_>%7F>2C1QV!DG48d$u2`^Dd`x@;{m&GHp1Qey0K0|rHM+=7H@JgZLR;eG+BUL} zh0iBEwuEe3Jm@qk%$u+5Z#@76^bZ68^tkcQjns%CC3t}vrdcE3Z58ubSBlo>_SubN zeFnUzrTd}qwj3?KGO`(kVDX8itJcFH{V6Ppo3fQR6v{$KYF;-q@+PotfD@ZLyATJlA|L`Y;QIbLUh{alnjWUi6lIpl%cl#W#$xK3+eK5bF3d1NfFFgId| z@F8UCLh5EUVt@>*McxHC*x#zWJzeK&6Mr*(xS18w4B#VMJ+u^^+L?g=Bc9FgF7Kf1 z*ajbMbYDU=zu}=rIF42FbI~bP$uY-a+I8jdv{p@$I;YoBIPTsMc-)Y07hu2>BR@i8sip+CESsvF*-%wHn8B30ePw4wnz2V%9z$9(5;RY>H zcToIrR!&scTCX7|Pkw&p>!HPn0N;(eCVlaEYx*UZ=u)q{P{3cTiXjgS44!Cn#!$nH(+_}s{Y z_dl^uOP>Piad(d7TtA$1Kk5`Jj~Xq_+JI?2ljf!%FddaW14TES9(9a~eRTEkJ{{BH zBet~3y6;ym{*UM+asNBy+;0V5+=`|E)`9u3GCoh>ZRF35(2+<}@%fZgM4K0r(?uu7 zbQWKqd=mI?08ln8*>I{74m!;?+zZRwzo=_tJLxJYTIG12&cr15rGk{68`u0Pe^C6v zkw$nGiR;u%>Wv1v73|yJX|)7An+mIg#e9|Kq?P-uti@pmJ$G$uPx(UB7&<~yBYISs zMMrGk-Ydm~y^oW*iNX9!Q~c2`UX)0lrz6&(A0ho5uWeQ$Sc){?G?J#*w-o)M-7N)g ze)sx`M9E|%;tHoc`y;}CfFSfp1fbcI42Xhe5qkdLmkAkQc7hi=8gH2#!F7LK5kY{y zqN&&0VEHe^1wfHlkqK|zSm%FX6J~($GA7aVo&O7wL;~1}S~#)&Q$pw;xOf9R`HJVC zJo!JM13W;yHfcww>))F6@2=4h1K3MCwN7aN1J-Q^z1tDyjK^cl;p@rUKX%8FahP@-Luae5i8#Z$BOs z$Zbw#_Yv>@|9lLhLPawBB-%1h_b*PlG36r&IXHwcAY8hC@+@#qh3Z|AdUf8vZ5)3GfeBE%VemZNGw=L5= zd1E}fY_~{lG5$@r0l3wOcdRbw?%Gt@GS8DU6=eqE7)C>1-hOxwn1vkmMv`yZ8-O|z&N!nl zrf1`tK`H8bF2%FYBs33mw)@Wsi%j&SBeIUv1WDf||BH z#&gq~lWoR33uf=C^Bsf1rjF?ktlqJ1L(W0i*5z6ZabXBOD1kwaI4?i3J{~S1(craj#R7mWXaMYm34$UbvsMXrzk;5*ySKKE=e+VE;R<=PV=lc#nA8f z@H%tz#-X@vnw@pr%iVr<9B!c3hXq#hgs#0FPY0ULT7`(6mPYM*JGqn&j@;C>w4O}E zKE8^vERK*aZq#5|xvFhA{p~jSd04ncMJN8+=K_2x#B0P7S#RNX3ZAt49Yt2KQ)7}{ zrdxlGqZ{#g9S=HTp3^OxfpS^WppEg2Bl7XO6kjJUw>G^#KUtU>yn&(Pb;bIWAVc$+ z_EVUvK#w5F774ieJo=!Qm<~8|K&#&o6#ia2f6-n1 z;3ml>E2#EPNEDEBP2gBuYV}AZBC(l!!#*>;pL^r@w`*!=ST`glk#(IxQ?=>2^sa#8 z!)EgltrY9IW0#X-Ibdj($_{u1s;3g+V+O=ArSf~r4Rq()v(g9XJQ>w zkU6FRvC9!Slq#*je(&&W+{zFtG;AkN%%7Iyur?$ij!Q|iDTq3c@>7CFza`6VDeO74 zmPcYphLSPg>!nz82`{wEdlTbLoi3xY{aI&itQNDc?Df5HGbQVm6z#u120foip}UG*`Xpa(s8n*4=1_M+5+5}?C!?hG zgqsw}O(c2U?)8xe9otDhG-8-vx*hJojv;Kgh1=}Z_<1*hg1#f-m^0sGz?%MolMN`! z23yQw(>r-qYtv&x%-@i>!4|1-S)BAFV`|iJ<$M~;c6-E8u-f*q7CIdl%K94-%9UerIm%t-W|?!^Y{9}$Fyt93_} zaWDN;C<~FO?+77dvm}p#r^FMQuxC{FT=a1|R(uzQBr=G~1mhO7MsNo+Xht8zv=)^} z$H+G$_tH*Z+vm(aL19GswqDxHWDv;f{Zd<_^ZkB&czXK^`G!|rMd9lqe|TrmDz8%Q zq{%u?a>R}f*sRrv7(9G8Pvb@Zm)!CM*o*IBB&u?rxRc4%8oQCWpZ&s9Xi)d(Dh@>&1I7t0U*ywofT0T|2Bjn-BZU zsfPqQ&?|{|G^y|Bg4gMliYH=u2tl!9MMH+xgGx~MY0N5Z@vdg=2-<0Qq7~ZPqL8c6 zh9Kl=v!-q4I99ZWlY8X>?mbP6Us|`Fn=IHCu=5Hdu3}|ln@oB$X7_?T*o?kEs~s{) z%W}&RIQl^V)jSS3VAWAKzs^H>=~5Wg!_sIt;YCBQ$XsP}|weD!aA;T2jc)Onk^vgW6c5SuUZ@bpuIx+evblq>+2U3l> z=U@J6Vo@%%&NQyiowEZ{_SyRwc$BGL?fk}8FDa^a&^XqZUH_0d*VowdL+mHK%rJD9 zyLbZ*vy!EcbcVu7EPoJ#XBJusiyTA6KL%fT@>gkR6!lDdNTJKugV;8)k2zCV<~{f; zx0Cg(Bi&lhPgQHd$-JyG!>C_b&~zgzA>o5_ABA8+(%!qNo~EHMg3DCw4ftUj9CvDYRm!75)HdgPC)`xbm?VlPT)GRk^$GagIcyr=f8{6(?Ocy3B_@|WW9Yio?8Bcg{S%=&8k zu8Oj*2?nEe?Y*nj%IAH%ADhU_ed~hsYzp;DoL)lE=6-IU#?W!!TcvNy?5P^AlOWA| zzUN^@F>w)K>-d-zR>Rmq@ol<+eoknq%faN{uMg zG2QFh@6OaDT0kclv^K6j;8tUIOtq#iz1VcQKDyWrP!&9x&Lbv0vEN(N2t5vgUo|ml zZzmh^bM2u0z_q0dW6bF1(U&P|d-_Eh2o!XKVrmkR?8EmKj6~v`vJc3IlovNE%XUu` zpm{UTn1Vz&^~@;8p#?{!Su(Ybb!6PS!#${rbc7S8vZK6g_G=33aQ+{qYI*YY)BDEV z1rlXTny`54P3f>}9*B$i!eKq|2f=U~6RUmbgpRkfVKt(B1} zpwRQF(EH`OjBhzT7p;fw7qzcL;@2Zm-oHCwBSd8y? zzN-?}@GC>58pMgLM&+%>{L}yr4ZSm3yH_1@Lv!WYsaJH9hI{qmhi9833nZtPPF~Ev zxVdzCmhxFv*?yp=*k2t{pKq>QyO~>lLkTPlYv`pn0QNKX`Z@h+En7Oh0zo?? zhrGX!-Ee-qT-ox{GV>_c>F+)0U2c6>rg@~g42r=TQK{+ZVaGFJm<^`doM|p80Rv-v zKE28U_5_P_8SAnLH?ee>T>@$#gO$v`Gt()S>i-XL8)b#UFScC5k0>>;ReG|PS53aKdl)T2Qk z2V7lPZd_7u)YdrNyTPgcH7ImXb#*(jlm;>aW~ z=EBPmPfpst@Mcq9Z-_4M@GLf^PZCa+2znb3V2mwXCc`dn2n| z$w@o(nR(;2_oKy8wh9^JiTIj+US~h>dzNom4v(*L7hng-O z*c)%T8ywCkvZL`ehu{0eII2B4X3El^+}R)E$Q!u&<#1gS&5Wskp@^_}zLs!^TJ+J~ z8CzqE;p}tFnyvBb2#mq2q0-KMusSi`mg;bt{;<{=$aCtk>lF30R192>1-a_bgny2P z-6)~rz2PB}y5$F>vb9(X*l{yO|8q;*4fPecT#(d(y){d5j$N4wrFt}Jx#ysT7R;W0 z?@Z|UIN2nj^K{TP!xR2x_TKZKFx=m`HOVr_)j=VyP4>L{MBbom>@pT8J^l_9nT>5N zF4^jk;ACOVLdB{D9=qC%KD6Lde^RjGW*53FpvH8?CRtx3v1RzlrD@t)1u}Z&LX~=u zs(qyjzL4+M>JMeLerc_F8Ki5S2&taQx|#{tjjg>_Gd%M=vXGwbNP1C?TOh=y3$xfe zdvtv_fY~|kI6PXnaOfUPO3O;bXhvq0m>V|CY?m0?AWZ85Q~z3Qz19>@x;s<4Hu}Sz zo4s0vvbQHb!F749VxS1VjgG_Iw@yo-N0kKneEv)X30uJn`9azpw3}NhL$nj;Ps&y1 zlP~`D9C!sp(;~tXj(x27kJk~mB3;-o@Z}~UhHd`=7Jdh1UW{=@;@@p}Au})mpbYC{ za5Mfi`xi)WG32|S2=ihF$7Qsc+9j2mm())n1DQlW);?!bkGLg67Czt)i7+BU!i*12UO`x z>?Tc&IlEZv(7G7j5~76~zl7C?uk7nhLnXOr=mR@YW6W>(TVFX#x`lI|q8u5X?ya=v zT$QJOE9L&eAH){LL-hMAcayBDT1g@$Clx;EG7Z+{b^yaOgp}85wUQ*@z(yk$gpLv#t+5`#7xYMzqt_9lXP% z)tN%p7RLR|Qo|d=jG|q8h)(bA+CGv7tEBtl$G%a++*wyvRX@uT?5!o8BvVT9RS3$2 zW(==32OVqLR&Z!VhdM3sEgU3=6eQTPmzdq}4mK4u@qCcuxYWt!%(tPIZ0Jc|f z{{_$O#3pLlzU2V#k%zxVRz@~8s}QWx(o2L1YB9@{9dENTpq^ds82G{ILW8fe5EDr- za>jaZg=)emlDw74}6e+dY-wF z2fJ)1VQbkdYUp6P?vU^a^ZI$p5))|nDU+13;U+;mNtt~K97vcHG=9GxDBkn%Dt0H3 zb2V)CBugeh#klLJqX5~>B1SD>W~e?~lvs)Gh#W$19wHNYQqtm8@PtF#bVJeKhWtyMgP4ClLj9y#l1e#BQXL zff*3RtoqnsgeclXq77Mh=wnoibf}5HOyN7hm!iQR84h2RKB&laJrWEs#sawY#*(*dXQjzsu^b#N#AS1IR{)9KM3q0OI{N+j@?q{&16tx% zq#G?UpYAH;(&Zqh;Ma4_d(L29vtwwd)hHj&q2j~Ed<>(BB+p|C0sE6XTbOSKv-mv% zj6JGJPx-bV2Fd!#MOdp;`e$nUmpEp1k%^giC@ku&gip#59bF&U3OKsl8(Xo%7?_*& z9_waQh6Lenw_j;T8{wk&f0IStx3P=1{yE%y>bPN2QhaZ@sHHYnu(0bAO-fdPm)T@i zPM?|UxiYQpEAvpwOLC$^L#-vNJT$^vC8TGHDRzxl#-Y)ps)W!jE#I+oYrE1jKhz)W zi1!?G)Hv7}4Y!uUY|=r7S}Clr4tQmzED1_*5zq3CNfft8Z0PLDM6jF129MM}y&&wj%PwcOCg3UG;Tlh?8` zGn?!Q_0=S=H|~7d24fugPyUYn)$k>LF5xc;QYhPGVA9#}?v)uvn_x-Xj+{_5{{iNV zHd2Gm`etdgD3PkCYZP+|;)h1-%?__rp*sE+Sb_B;fIq`URj*Nino%-R;Sgc<-e z#{13Y?eUL&?=>ByBX7PP_6U4Ess@_6_+SJ%Z0yk-^oTcFaU@6;wB75~i6@E4ZT$3m zSKJ3Hn~L5(`pL%`^h*ziq&e<&-OTh9-!r5oGmIIY&~{}uTfXwgp&J_ff_6+O(1@Oh z`fEl1vw*Gv%tD#&Dzw4f-8=<+fv3o!O$X0Ia|Ub#N+q45vW;o#J*#m+Y_-|rW{7emuUze^rCRVWdhY$ecC^bS2ZXMll8s(@-@`C0oF z3Ad{1rr45HtzR|9cGGAkimX1Ew@Q^qK~=?YF5`Kix}!EnNu}NADn%TFMrW3WvQf^v z?xa|)n{{kTM4Vp)p1#58CALBLH%vJ6!mHM%)l}3L*r(C9Gr~jm6=ZWf{TgK=x*C%F ztP~Et?9==eLG54H_u1zcnZ<0ivT86MZFgwleo1weQgtsYj#=d=@ytbn7Q82Kv|RN~`)KjUA8j_BhYNOe7RGlhDY<_;^xr`@ zs5I$(yKhXHoFYP@c8B|qyO77QvoLq9JnU2S4Iz%+Z=UGI-kJD~qE&m3XPVbv9MmNC za-FO%$Y8KQ3oZT8o$lmK6hmSpwWr@-HS^-~PE%WdjJYt)1FZtdW?kMzF{EPBnB))@ zF$=zkrRZ)!nSP9VeaFHm>jw6N`@g=}e088ne04)19Hw89;o7M|Qt*7?y8`CedNWUt z?@il>Cq|Q{3ZK(uZ4o5rNZ+|!@Yz-;?eYC%hBJu(5A#6#2lkEh+-i^;PqW1h+orVe zbi_dY_RZTtKscab2Y|~K*%bKh)&J>x#>pe0f8Wo8mi@=YTt$0~N$*Zn>hpgBdyxtN zgDXcG{>NV(!T~(SGnNl%e<=I+mwG|~u>Ksn?i&jK4=)k#|66BX;HSvT#vlacf9@)9 dE6KX!Y3B5NwE>Ak;|}m6`9@9*@>ZD?Qk4)H52Uj5^LOv}^K1TdpDgdQgUa^#Bv7@fiGZj1*Qt}ETs zE8LCq+|cuoAo5})aqXZv>sJ%FTW3)Zxrjo`z(C$_fyi^B5a%xU#2z4B>mk4sM0oi1 zo@;1~ug^#V>+do0O`ZH-$j&?6&FNMktB!GTPzA%E1mp(Z^}xV;-fwE)N>%txM2;(% zNUzoQ32kbFOOaQRcg^hl4Ll(PE^im-H+o3QTw=MvM?rG@puQ|XbPwv} zrS!g@*83Q1qI#@gOi83ZhhQ(g_1oAnrq+R#IN(2_x>NnBMb65S!Mw>HIb!0ks{s(TJls zSWo6U)df73>EN^hY`x!X5Zr7GB_-$fK7g&~9{F-Krt-lV9xyfaDhB z0-`k3b>BJiR`dH?*Nx%2@h7I86!k6$%UjX@Uww?g^TCtu-NxjN4Wy_3g!jG-%oP8x zzvbTVfvd>JO9I>$#6b;UTDTR_7ZT0+sZ%F{VDvGOqHq=dk+Pt1ZJ`-9+*tdC6;8SopyJev|1%FUpapuZf@bl}{& zk(w?xBvV$Sg9O?&?VYYAULM}2;wodalJwr~+Y@|Pi0rF4ps#uH8XJh614Qx$JJ$IP ze`yVVKN+${-(_e?S69;#2lcDC_C9!cFl_g>6B*@uACw$?te}Gf5qc)nWOPJSF(Wnf z-XcszuH{%#0ckcqC6J3=jLU5zz!DU+Llp1&)C?+sEYwd1hwhIv3E|m=r2(S734usd zcLolIG9R}|#fca%h{Fk2+byO59WI3340Gm(GYLZAKd^}ngbdtdL&a6zL`#8^Hb4j< z!V?X`B4Q7NLJj&&tSO1T40kA06w9^?(IULAfQ*iaAB>vA3dD0pX$4CXY0BjWqTHY# z1?mZ%=V$_X>cayRRj9<#F{Pl%gO$WBb7d47OJ1q$XNgrPlo}8HJMW8m>mP0p;n{W5kXQR^GyEhOP`cj%)A%jp=o|fW?V06THqN zk9}YjUCCXW3Z1M0Dgz^c%+74B2(MtTh zxSB}F8vdx(pkqNBtEs0H3*$N%`I=Okp zsQJ^mj=8YK+#)VfWvNG>=a5L^s6=PUxY2sU%)|D6t8HUlQnG~yy zmlTIcOg?7bLjHY@d;y@yzA)Rs5BmYz8EX|A6-x?h8M_FpCbB0|Jklr9GLi$k4f`M! zn7W&Kl8VT($HZrXV;+(koD!7+o-&hq&WOZRRqIe=QR7})P_0sXQ%6_lSZ!77SzA=A zR)bhuS$kP~So2tCT0K`CRhz%mw^*@Ax&)|cs+;)A=veZ#*{Q&Bbqi}t^lSK6EhmC4 zy8V-Fo!#wy=1thG)~>l>wt=zHilNhi%)YfjzLDOpy5xam%826c2c!VBDhzuNdj@;l zb6!HqY+`-Rjn5r+9SyoYZ6~gM=Vi-m!}6Qz`}f-u+xA=8n42h!Xq+?(#Ec}8#3gh< zQg^*-5eH34jX@z`fkM<|Bx8AXoI|}s@C7bPURcN=aU4k;sN&C(+LBHSe~T(CDH)+r z2|D?o5_sg5RA;n$MC(nsb=szGlV^Ery{IavTBscgU^DIwfHwxui`c zT_j)Q)nXB28RC5CMF7ThXaHrvwXT}Xye_-Et6(%$F~CixQtd4LSM#sp7xKqsSa?`7 zSjx{@=-UoNkwqF)8ndKVq!*~b z)zYsoY#3U;Z%B4gaWQgEb1~EfUsGALX^-$UermgEzoor7KtP9&#$v@af|rG7 zfe(#pj$Dlzj@pk(iZqT~ktva}kzq*t#qpViJbgYrcQkU0d2BreE)6PmK?4MiG(sjq zQQB}sXr z=>BI+hf-ut69wp-*ul9b6s%-AHW>+ zPQ)Cf?a%8WH#^+*Jc(sT1bQrcs6J*qQa_GB5x-<56ono}1@-^#A1eYD_3g**w@9qVP-r1G{<3qi!MCWhI=17m z^u5w}|7snoCNYC{ONk(_nmzAA<2KK${7r4GaMIWP+v^GE zneBADd*8(BX(_a#4vg5GMNBit_S(+i5-=x_Ky1uVPE!$>eON!>p){l zd#XuwCwvEK&NgKuO)1f3DGIqpVLJuTLaO|FeJ<5P zt=lz%yw)o{^(geQ6)k(`i`H<@vnO)|D};eaVl+@?a2A1U=LoMKfC`^w#+BhE%|$5g z4A-B|p8GF$8^1=b-WmeCk_3&M5*!w;s_%G#dxx@DHJ58U1II{@F;J|KdWcfccQ7@) z1v2{xeW5-GxUlw-R1u@Rc(|XDPn~}QOFSc$er_`*aaQb5Tq43`eK(|(BQwpU`_hp< zow1z6?tFB5c*v4H)0wS1s=?iq(vs4U7RW!5*!CRIEq4FQ_^|~tDLD-`TbH1jS&3DT zSG!gLR^ONgjdr2qObNw<^mspWxTDOz>F{iDt)T_e^WDb9L)QB=zjgj;9|{TQOyH*# zojr{mgL~py{IX~LSk>BojS-afz;ME4F<)j(c7rRQ=lZ1XT*3Ar~1RXCshM$bv&c;S z?al7!cg@nJp%~Nbfc@Ql3;}iTukTX#w{O4pUPA39I_tjqzL0toTg27x2LGPgwR#TO z-QT}^x)y0t@TsQb4eW^e-pjo`dx?tP17SFOBYDJ6Vwye=A{3ITtNugzCih*j) zyux?KBqzndb>_|c^_TiXnrr*MK6(NAHi{ZD6ROMS2-JyHZ|yv3M{ykbUdlF!aZP?1 zV%ZweQn|;LVXwJhJ`I=Xc4i#2G*GR>>K;%8(j>+-QHu+vL-_C3Q!N8@;CP{j`%(GL1d%o-TXm_3m$!Dd< z@yFYz1*k-*eaJ$nfzVi_mRL3s58;KLPrdfyWMZxO^AYjU7@R#WiS!}VKkl&Z7L`_? zcQP-V&}z|0vRrRxcV>*uI|=|Bc+5=MGn?NivMgqgVIUS5u?mzlr4UY?QADiR=-L-{S_ zlx2}d(I*I=fzLz6;|ETg$+1<$*Tfrx8>Bv>^r8$;4A^utbQ`aw_t#6|*TV;M>n>$c zai_PKP`0r!H>i6s##_$yoUKr3QAs`H+m+kiR0ULO(3y$BDsiILR+>0gY2?f5&SN)w z@TrkZGV7!BbVon)Y-DVXPgw9?Goo~oR{MB8esO)%Y@2>efFObRBJA1ot4BbrRTQpI zxDUbY`b!R(8F^@7mgK^SPV!h%M1q^Dg5qzQJ1SFJp0euFUrO`}*Ndd((W-QMh|=yJgujYBA`3@iG zbaOypem0UZ5jb@ubK0 z>y666skw+Q?phw-v**NpRC_EwC*OnV;=bt`^v3atVrkRtISJ*ASss77ufA5zvetX% z##SS&8j7C`*+jiS)T|~5vsEnbH7E!j1PI-Cki{~P*&!)E<`Rh5_wSweP(fr_?qmlh zpLhk)2T4kFLUb^2}%+q ze=Njk1wR_HaN;lohEBQH({Ll}M*0Nj4x|uaYtmx$SQ+h9({sX)W4U46qfUe(buB1v zQsGezQ>nq21jnekT zDUeUX(>NgJYd=DtIeA)^26t*wElvX4?I2@Hh-$( z=m>;7Bv0BKOPZZqZt%Z_!tLu6W>0FIK(ct|fb#4D4f3O#1TpJ^gb2`x1(g!S2ZAaa zfE$Gr5E&1`N%@86NKAU6ot^8xX{D8ATZd zALUj9Pxtc<&+p^fs@(3_X}CyzKti`dPepfv@HQD+pvI>@Cr)Kdwel+w{v{*(#XH3_ zt^a#bx>|N)svSTVtsT*uG7zDcDn7N6xsWA-Nx+!v_wcCJm|5SQKVNv@pkhzy5Z6#r zx9zCN7|XC%^7$x(zK<=an!kJiQt&ve*LRBu*zl={9XDgLi}+ckn`vie_teiz*e!;S za={YeF^Sws`sLr$^Vdl$>>3u-No5j#?&Y7B80VDdQC3yi7#O%uQq4bve+fPO#T6Qy zRVd(CY`T=uG}bs$+j%m%`!J?ysGl)%4ZaH1tK+r$u-497%XjYO;wAs|^O5O!1ak3{ zID{^|9pa;;8O$6~1av0`lVb||Fdg^n;0RXkVu~@^Ks%ZR8c#w`d<`|_%w%!=ghN4- z*>7`hv*Qf)wwxcuYB@?&o{+jX&189rJIlR}O|V6{`AMfuo@aFH$)#Lc_dorM6q{04 zm6q98a$j%cl4aa{&Mn@1x|(tilhH_7>@n7b&!N#GmBD60>bRVw^p$-R8K8bTSCLmX z+0ngdE$R89*FGPDaKwUm`G39uV}$iiSJ+?Ef#e@Z9|zt zX=}-v(g5*!S(H{=J&Q}~Ee%3O6fp8x29O4&y{tL19J$D~;>Lx;-M)CRcXcfoKr0IS z757HrYV^M7g6mA;0)WU0_lcNoca!n#Vt(*~Duz_wNP0Uj8mX^}GEZ(Z!9sqRJhTYD zU@0?=FX5XS^Ny;SW24r1)9O_O)D;Zm0&Eh40Npf|T}Ro+bY`0T3Wi?!O-h|*XN`lI z*Y_9q#$^lE%16NjH~f+IN;X-xL-uvOl6#>w3+ zsG=pfANI=X3o!)986F5Oc6z$CSb92cpJeUjC%=8a%y-wua~v&@s|~wcxti`~-}iwl z{b!L;cP&BEppW~5OiL9FXAM~yZex2}2165jBU1)M8HTl0}<$SM$t*gKjMu`#eRFp}`Y5)l#cI+~bqD~XE# ztNX`4J`xLOX9sQoz|GB#!HtE%-q9Su#Kpx0U}Od`Gt+;xpm*}Hb2fCRw{s%>e}nvc z98pszV@FE|XG?oKqQAy9G_rSb<|85bYofn@|8Je9?w0?V$b0f9?NK^Pd_2tEc9Fda`hF{&&y+s`*z> zUcg@h{FgxgPh0<2`k^j?Cee>yXykEV3*?;c#I(vR*2#kZv^#g(a<7v+9dfY~Bn&@7j z+&nmj`~4&c0`cb~NC`r$LFw;?3opH> ztRZv|YnD^rSOKdqDnp8;2r(BrzveO^BIX19+>FuMPhZx$w|+aEV9-r1CW2@R z###PDPkw&5Fs!@eQ$MOYEw};^2&c4Jv)(TD^I4hebN5^@GcIW-a@;t+$p-h^u^x_F z^Xfr|aMi=Tq2x&ap)zC#a!})(A~5z()1$#nGqO#(VrWkfBgD8s5Wc6?GmPG6)h5op zL^NSs{22qXK&Dmd*HkwpTgDV$=V2z+Zn^F-xj*+OX9wnCZeVIO-B-#O7sujF-mc)4 z*1o{kNB4_vhi_=S4_~l2l-kq~Krgpge^CljIOirNAMU(4;;iKxMCQgD;nU7h@2qvt z+RY+gLMgu&Dc{q;eJRPC`AzabW!_-8iPr7P=R{P-%(LeMT|`UwV@>7PyE^%)ebPcy zsj4mY>59&lzJs0N0nJJ{~V#hxOot5|D>zku~d`G^1KdLm(=fE3ly_j~$Gy|_MxdeY6Aa@*4 z5&F4gHaV;9%+4Z7-dZdv_wG7k?V682rA@}lw|Ve&J~M1wnb2hf zQ!Ij^XMp`_4fbRP@bJk+Ly&LSi9Vi4wr{M-gMgIE$AuN=(=L#*_7lF=$0@gx7hh}Y z!-M-N`CjY8ncCiZ0Da7-GwD6@P$^K)_e)8!hA=&zba8u|kpg|ioD+`Cc!3$(`^elhW55)>?^DJL+;vIo8FG93TFgu2 zSLYe~A9Z(4{yv=EJWXtqtz!~EfbN{`WjPf|=UMVh&+~cFkWhPxz5YDvie?T>BN}>{ z{~w0wSB!Q6;2i)f@efM9ne5PA%+MHXmIo9L-xBhLpdRCvbPL3kN#!qPbLy!*T;;m~ z6G(MSe7dMgQ(rIZ8g91S4hAQY#nfd^S4@_~J9RwIH)3Xd^_NK5nq(Q$yh~1|5(0wN zma)Y10XRfeCYkiib68$A+?c!ud3bfvkgLl#uS$M)2E=a_0vlp(>%S5qHw~8hJ3~q) zK6mD3N1uxMkcS>tbn;30`aGF>rb52RKz$1(hDf&qMf}t5y4s+z5KL7@dUC0fPC}oL z&fmyiEncJYsrY{~sVdf(G2=VmC8EcF2e3}+Y~tgYj=PC zOSnX>4LDS&>1yiy@6m;W{5u$ptihE?NcnF+Wf`C)^NTTb2iE)9q%O=VzCuHVoeA83 zg4KFwof`L@0-w zJzMJZ;`aa`Pw4qQCTj^t)P)a_`|Qi~odYEC0-u*r&N4Io*8Gr^wfjfOCf+~%38VjH z2?08pOw<^hwcGRWi2|s%M|NEfsmHm=DV${ zIb}6JRNYL5LF2d&lshA&i$G;O$7j8-yiZX|NK=!XNj;6AKIeEXg`GL40Zcz7!Ag@x}v0%XbFD}_cfannxhF&&USf?(Kbfnk6LHyqaAYh0nB~|u0 zSHn_e73HM)-J1&9Dvh!qi?tOm0sXTr(IzQ`0pcks2kOW_&=GR=|6ea6i08qm-^~AT zD(NsPwS>-MV!wR~8o!`8C^Qua6_(!#kG7R_>u%{LQtKqWKDo@BPN>#+WTG zj>h7(4lvzD-5{rl#|`?&KKhRf3PN{(`SRsvPF-Ko+q+$Y-eVvd-}z!wsI0=4`oMO% z{tN86`9xOX!-G2xr$Y{Uh`{TO^3!gDyiP4*lT|?QB+uMtW2R$8K zq~{HgST^44{!A7H1w|>Rah8FDBUwsHii(>%Ej}S(Z_#)lntUph%F@!(d4Cl9Xr>tH z&?y0PR1Xo4Q*foxmL;?0?dh1Bh9+Fk_tk~_>o$b<^%w`)K9k3li88)9Ub8Cn`05J* zK0GX`0LHiD(P@3`SMUNg_YrMti_UkfR^K6XSeJgQDX2e<&Y2mEjg9SQ!uKr!77k99 zmXO&5dd=e?)A?+b%k!wTSmxk1P}{C0I0O;js$ngS&01!E#4{bx@euHKQ!FsGpzkY- z%V9SwKjJ1G@K|F!kfx*E?#Z_1vY_j{V%wAou|+Bo{Sb?saY_DqF3G6twjjEEF?~nj z2ljFWEME{PTI&M`hL|GzrFV5h{lh@~y2dkk3PE`7*Ll;poG9Ag-=4|ptA({`M~lTY zYfWIH@VMgjJP*may`NYiAs`g2PXhu2P1_!?4`v`Sbp+p^7CI$UOyGpHE!;k~b!FpC z?T$3ahB%A^AiP2gajO5ajX4JRBJ{9io?WP1EZ5LHzm8@%1%XQ)x~}Jzv5m1RE7B`J z?a5pnu0!9~38y%|KS4EsC7ir|kA_o{X@4#}MewIX;06cHpIS6Y6;7uKzv%qz>p3($ zoL5#xLtTwXz?%|eKr53Lj7;}rQIQE0fo@Hpkc~eLVx$E#NJ>tY_E%3~gqyKa6pz9o z2iG4$G6U$gxh4hFArHF9)&M0SCQ$e1g>&Y|%E!_go+s&r9me7asn%jnvjx24-WYy# zg|1|I2U;AGFwb5N_<&Pw>Co8voD<1*8tNp76%`e+Kia$z6BBp8kVgM`l5=KICLyJ~ zw|XYj63Z)F!-Gauw6vr=JUq}bF(pC~@#7bUhldN09L&s$UjADA+E#C$8G8j--6 zddVuf)U^q*%t8QW>v`7+my21(S&i0i+Q!)&N{%( zx~Ha;>7T$R6eg@o-S=a23$PtUL;__}Sq*z2mYeJeVF*`KQm~dgyxUk=e1N&37B|4D zX}YJ4_csog!)YUytxyP1^UP^4tAi!#;n0uZ_&OLM3`)SXr_=pqJ`*(SFjtn5exZ|WZR$h_^~9BpDs z75y=o__pyJ+|cZTIU5{rlZRW@>DQr*-EWs*f4b!?89`Co`H1Z7?3?@cliCrBkstI$ zlH&r;OA{sPReD$Q&?CZiw0YuiPG-b(aJgzJOxjkIB(YfE`tRwMgX03kXxAg-xTLDOA~`YUPkoa;-7Ahi`Cth+x%_Y z({UnsDvqO1B`pFfEWh)(QeYwcd7^n^{E!*S%gYOk)F3)g#i091%B~w0^k`*cKAbqM zw}0@D<_~vT8(rvn6%d7KXhdJ%^Q19f26x(UA;9l(Rlu5yVKz+?oPa#6t4IXV7C`g# zk7OD^L=+%jq0>_Py-3Z%x2ky?7Lek-QUp!hY`Z!M`{#nbh4@YOhqH>u#l=m>ESO`M z84{me!Zb3X{4*~>c0lq60xj<0PeK%N`BK*yIYa(2IWmZUG#LnSGpVI9-Jc!^1hfhD zg8@)`!NU7<6@odWA9?n+S42lgIPi%Nu^}fQ{z1@9-P`_!H6)!9at$8(tmLffQex z%YtC2N4ngIt_!uk&$g-9|HL#up;6syEN7d<)_PsXk+Q7TU6`>JL_0ZjgT4m$cSRm7 zW60s}&ZIoh=26I{{*R3A2>z7gHbwTd*FVSaAA5|Qh1Tt~rj1+sxDPsflHS_wFh9@X zQW*iMu0Eip1>5!g&yOtsJ>2mT2V2BqJAPvt07aN9LExJ`XprL~eO{Po`d*}e$1gd- zdnv%JL{qR0a>R8@@w9U~Lp8V^mgRz)W-!r8n1TS!DZ06H{cJhfT@1`hu-2;Q(aVxW zDy+2sNJ5`nlF&r4LK8Kjii1 zKDje59wp*&nYqkh^H~4cT8~4jL7J8P7oK%BG{f=aTfP zL2p>^>H|#Z?kwLJMiJy{DZv!sW3d#`2tkcJ-t@XaNQ3n}o^jvw9?QPB-ujs9f&ZZ= z-39y$`NX=)#Sv^GK0mtTS{qR|a?;Der?LCv9ChRsuM0@csR%vmKOvDnKBJV&4JxQh zhlEImpW}-lJPzITAGf(D3euQ=Us4$9sm={hLNr$r6QiH2Cj4sh4FFp;l7Fu6BmMMw zVRo5)rjfH0kspS*Indvj6WIX+RJWv^wSba73WuU?Z9Jfwvy<(6@Rp7(Wo3rbZ*N@j zgskTqu_?}n+Ux9e!Oi7IFr%Dfx(m4pGCdSBCn>4f8v;o zJ|HYsEe@UDM#;U|q4mvcJ9{Oqkc-Im_d#%e+C!r1Jaiy<95UG5j$?6hP%14gO^l7* zG@w<10zD_MZU<87I$q&xp|59SxDm?3&CDkthlM`h<|MsKQ9W}|a50E^2^_D#>vOm+z30CAv>)RAFD%Mo zLG`0(W?6UnxkL$Fp!yj#({LrBx8>%bK*v^g>*{q{Gw|aUqrftgl%%bEr>Of?X5oKK z90V*g7=*Pub5G7VlV8Fha}Y%*7g7Y+D_r@2Mm{QeT$xfH-LSylJtfP(1O6ZH|8cyW z)xahp+~#>=T)YH8joB1SuX+}p=WNo_7h?$Io@2pi+7z5in7`ZA4uZ&)=p9HvexCbL z#ZelG^gjV=GaVua=5FjcT=kI?l#dT}uhjb|wYMtti?#8<%(T!9T~!AzT(^#^`BBl# zkMb?L|5?HVGpI#?_v4w7Ex$BTe%h=52&*!1Q!v5I%}5Ws z|0oVdfONyV_m1Zhha$K{^uA+9(QL<43FQAkHh#&E{2FOpg>L8`29H5NBC#Bv3uC|% zw8nj^2$HQ`UT=mTMS<4cTeOHfS#{PdR%Z}J!@tW#;=g%XsrpOo} zp{}J5GJ+?a>Ioba6O9T*%Rn(rEJTe`+XF!9T$TW2uah?DVY} zV6XUdZu8Tjoh|9n8!!5v?mEqLd)T;lGl6dHUj(!tS#?+Ytk@28sklS|0++C-^d%I= z4q)^zbKtB3xl3J?YBT3qJd*uHY&Hgh5W$M%VtSXft>@c6)DoiOzpfiiqCxu!rb7?j zNJfi??8iMpSm~P~EE3zGxwHO@xb~NmGqC>RNlCfMHSz)K>&{UNQ((G`JY8<QL-F#>DesWIZwI`4TCVVl6y(%TH*5o_=i_mM?q=R;+15G6uni`xWXv2|# z|66wnFp0C)b{f1x6-ft@&8A`yLGD+HJbkN`&p_+EjBn-Ee2zdezROo3&zn3LjN_Mw z@%O2b75SP)Y4on;Imk}$Foxp=jk!gKuqHhnXZ7Ot5t!Gr=2WcpWZuW>wCbDk0So7~ms z7j)BO4rA`{j_5#L)@*yCDC5o+;o>?te0l8e%*epw)?b}9s%dP@cscS?c1xJ&ihpvp z%$fDJ9NS-cV7k2>IMnEcXg#0ikD^S+7eZTwhvpCX;QcwZbZ0-zv^`%X%b@J*C~N?QS-q-I&^s#NEt3G=CjbhkB9=YgBEZ>#}tIuBH@-o19iZ+om|Tl}Hx*XCNh3@JDi zY?DwcYSVAK%&pDYdmY!7?0pvPxTOK>zPm9u^bNC!P@^xO`P^n4W_aQ0&~V{jEy51r zYwni2S`BFntpxa*^0S`Szw+HSDv4!$kc+T*I$)skq(mgF6myP#;lCug8mPVL)7>@F z+%|ITe#q_o7uBpmuAo(%0l*Btr;N)})F@ap*6lWHAk=YS1{L@rjC*6Dq4pFVWkn6* zq-reV2Fw4%x^_!It9FBm@ta~?_PIP)FpWhFx~uFaGA6gxZzoZQE6^%3m(?W&RSW^4 z!3M7)yrE+(SPeXQIttJs%wc7#<;7B4zihxM6Z(+71uu(>H+cQCak}K5o)KTd@#(qwP- z^TYbHT}x`{&KYbW_vJLG3pr6Mv+}xDOAFtKg&DnX8SJkql5TH1C~2!K0plI60(>?3 zr66=TpK8Wk^sU%*`>$+4!mSLGA z+q1?}pi)}lp(Duha|4F^4Co*t1T_y;eOMigGD_ukgy_AWmn_S09BFH!+vdQsH`OjT zNub>^{4+@e^l?c<($%JPA2n_GJ?5RuND)V!;9Y;aV}bu$o~{-Mk{1!+bZl1b%+B7@ z_$gjHm&|5V1Shnr6N~w^o^Ze+hq6R!HWU8B+`N*vDBW#1Okoe2HgFGLbrxAix^Rpe z-bj<STRWO7r(E-EB_m%+H)7=0a~+%4vFgQL~)_Lz0D}fPo!S33#9v( zjQ*Nf%ToAHWaUCk8VIDH)`CV zTmj#d{mbb{gg$2~mZ)v1Ba4m`VQMd7kt zB8a`=Jh%9}Yf$Z@G!dWcT;I)sCfezlk-jC4tY^se&>rMukyllI%XhP{7pG@^m*Ggo z-jcNAgax}O(y~Z0171RKe|t_hWX2N@6-4Wd7f$bQJ_7VI+Vax*Aye1ir7gUs6kEym zNVU-CBi6BfLV1c74hL2D)dWb@xMI7NC}A*K*)z`S1vg%T$#%4OLlg3~)W=lS>1q;(auXA*&eXL!z18noij&8KoBTU=e7rnrS;7#X2#b|jvvalk24mA31)*i^2qPE~k4N;7q7^chs5O%->tEO^H=7ewi%@7*>?<;{W zF$fAbd?GKsz8{t>6R(0CFAtDYH3nMdbjHMsVu4SRO-8+&?vTak+k?yFk(KlT1>v2( zy6kg?O1X3($70aEveNWP&#Fp=j1WN7K`@7z@<#rK^`?sXA64o1mMtN#=EM*y6R>PT zT@?`g02vBwSb%=2CuiBvDcWf1ScBjvO4tY<6MEXZ4(6!3RwC>oB?_DmnEn5hJV8mM zlV+lc|0N^}FjK<0?#EHhimoQYVe=4OqAy467idtji_q49A`vaQ>f_ATCT(WTI7`MoxfyFamok0|)DVHPa8E>6}IQ!1SP zhmS}rlA_m>xc1-syMV^2nL5KMX$XVAFR)$DbriD^z&T+ZRm*JsGc11}BFJ=ge%MJ= z;FIz)Wbhck3wR)SB;Z*RI$rTQBkT;kV-ZCn+H?eChRu&-$up(aP8{fmcYI7S@}r!v z$sr%mBPE{X*2>z(msdn}ghIAG2qi0aebPTe^gsms9ntiJa}>}ppPY-6A`X-`hT6kC zu`D0iW(FU8JFnu!n%&Zh(33;kCR<--&NAV}lRt3g4&euA1_eb4R>YuZzWLQ{I}35I zP&7*`Z?@QAW!UqpkuK9Usc2bMzlto#Y%2OCfXYSS+ear-LhO#I9d`Y18k#pl z@C#qMh`ObRyKw8Ix3a|ane~C<<@s45CBrYlF@BZ8kw*sHp3H_7YGoaYa-BA3b_K0| z&GLwAd=U^~hXIFvDxygSu#MybX<3@2+95@U9C*LUB1G_BmAX&l@J;if7vNH5svW)N z<@ndc9jCLQwa4YxTmrgDKcw1Y8@dG^h9u)Fq9UyS)<>4mrUv8zID`6jPnZJv@9$Wo zD4-;g#cCufzc<@kY|bK#;=01WsezeTFxaMoCh837D6UFx;lOJc(Mok6DOSN+vft7t$9SkVC(}0+%RUE^>GNVn*Hfu}~oJNMcpXEe)M!-z%NacJQq1 z^;*=`Z82-ZSh7~ALb;OhzZ+n2y(Oxcb#m1+@SZPLjpiiF;!;bre%*!6E46yTZS*lW zcGmg(w$_2eAAIjcJ4cYO?Ld1-2BlOp6~D4^!)kQx*m3Sb_-oC}s+=dkb-HC(!;*x? zb9vCFKJ*BuVsvdhNMFUrp>6vt@CgcZ1)@Mej$OddDpIMOo0EImm6URJaYTQ&J}_U= z-Wbgaai6H?c|~~_Jb_ebW{LT*^JP&CO?FI81}i_>>Ew8178&aAgF^6SvsRBr-NA~% z5wdDx18_doo~oa2lw-r$j|;;_=SY52_!(1P^`4OCZn9HHsLVljfAdjAx0LVzjDRwrHs94H4U9`o~YMtLhT;g1^_?%}|Fro0}|1u|*fiR*?WPh;eX_%TBFQ_~MLU(ofuayW7i1i9d|$_U+P|P9I$BofuqnW}F-PS$NM&xmU$xEgoM*aEp)Oz>$d4JK*c)zJ2K2k26I zI~Tl3WQzJau6bu!qd2-kUx(2}tCnig`Hf;m+`bw9U;ILcydboEZ**l|%ml5kQjFHu zQVN>i&N95HFm&D_In$sAqR7>ioBqBz-hw)l+losi=8PXj!1V6uZ z;SIn4;KG|rm9`79Wue4ImzMtIn{ReE^DSt^%fd`~>2VCe_DJ{OelqT!kh=YE%BMcF zE5yhYyMfV`k1zgCXobhoam2fj7odF@cf-`S;@U7>{c*1XogS<@=k^^)1si;ytv$UOQQ!t9Gv-; z-{$I;WzBJ!k-FATfwdV(d8+8)>e_|mkD#PBaC;Mat-4bE-Epj&9iom4F1#%t5Ks^9BdNV=xY`o-eF!#od{o#th7 zoH;)}hry4=0}t>cxsd((|M7H9fpM+vI+!%JlZI`~#`Z8o;8rm=0?_L=Uz z|8qYV^L?}C#iP~6<;v+z8^%ki4{sqsD1Q45er5#iJfhaetTE*f{ywvGz!F^g_+4$|7idLn@WsgU-4IM`@X-@fD@wvDu zduHY5uMiaf#T)}Fjzk2Y#hqsI4)SCGGw=|jk1Rfx`I*xISA;#{Bw2I?s$lsp(H_! z@at|*e&WX|5ebJ-5l;h0mL7>hVN#vB3mH|8rF`CTg#zHsVni$h5FEax*4eOWNX8r# zGOqG#O9~9hKT90d%g;9pPhhd7)YX<5esmeKjF=j6N5o#@DVYq1 zXJ9(%tOTpn-Y8m`%F>+}U&JA9wszksI1BY@_q%S266cLI>X61BS|m_ICaO?cIvoPK z+lY4cMzXO!%9gY~zyGaA8u9zOQbp~T=>Gu1o>%Bp>h6i(Q$ey_lNsHn#!~P&3!=0a zX|f5LPJ$;$VCW|wEbZ zn7bk(@d~A!_Iy_&=$qY%P9C8z*sq&(_2ZJj4PA>-w}tPj%xXjQfCOm zH$k6$x*{T7=txg~<1RpaX*=7MU2U|et+iUZ{q>fRaf)z9y52NUp9%}cs{W;fikV#d zU8ZgbP2e~&Lg_lmR@)PE)qphXP8J?8T2I&gY9_0h@g?tFnhA6M2OxJ|-#h?!ok z?iz8@DgLZ_4xVxSHXw%H@Ue_}ypEQ{IoiP!&EJy(bw(|%deAle^(p)tMYR@JO7vQ- zZ)pD!?Dpk$*5=epg6UlRYY>O^vB6&f*zS4Pv1#Hz4Rg!NAp$N(*Lor&+D%q=(lRe{PnmjJ&5JU{}fBN_{q=uYX zXv-J8yB*quG>ye7R1O^vX8;)$^Iz$M7Sv&Q?M|w^VLQu*e5dJl*7yHj4}sSs-bwyCNc`&oo;N$Xo|Z=QM&c@d{P!E@P6)k*V{LEIyjk)Y|Q1w)pympNnXC8<<_6b33z z={zTpgdn6fS&ENn(%O3hi4YXKHYDnXR@2+}Uz;7zOr%wa<2cJ%%pJ@|RjbTKnYmJe z0Gbwe82Ag`_^J#{K~VM~@}$H~ziD1Ri=b}TQ*@156V}I2Ft9Gcp1ZA4Wr1DzHf$!k zYl2zQQb!?TvPnAW^*TV z_No6jR$U2zy*A8=DgE*^&p6yhzh+FH#ICBS2_HF9_kL(95=%H>+E6+mpG;$phVCV! zQWL`@@n!9^POQq0HrY;6n0nG`o*p};ll0kAJaHM#B>IU^Df$8tQje04F3A}Jw626e zv1voG$dQk0yCbf1HH1p%FHY&Q9>_a&3xzZ1s0cA(!xuTGC+X*-WtI~a=pq{{QxdWD z(8YAQzdNmDZDwH5r1x>>`}$rY#U6+mEXtX2x-CwvO`2Fdf}Xh+oOgt075IK>>(KQ{ zq46HI@&5It#_etnMxyJJtifrRt2Bva($*BGqdAo>L&yOn}BP*?qPFojKBSevxij$Lv$`_O5baUZdJCN+yy z>8UKeM9KB<4-H@bDu+qm&XXYX)4IkB_g$Fl&4z}1)${ZsJ#y<-C=gquerk9VZ7q|o1ZjZ0`ktnP<-#xSd=66gumO>d3p}q)Vwn5D32(j zXIB92OW>@49U--RlMc6Nm*jX0n?z;qL_<9_W(?-P4_FRKO&*(cC5@oDGz~;g=8_ZP zJyuzukv{F}ih~?fPeOFHd7mVV`I$mtN;TsX_C_#@B^5WyBLz)||GXMR)_2vKJIJbo zK?e{<`Q%7!>PCkRgeDq#Ez9Y8gw7*WMGA9XH^RGTTGs_P!-L>B17 zskN^kewp3N*%WD>=#DLx=?b)IWr%x=l%FFT5c{ISg#7}a%ipWq@Z1?-Vm+rXAONQR zVkwVI#@8bVEjC-I;xDF+&Kl_AJ=jc{(si<3!ozmxBt`#<1?!@Tq2aROfi0SAMM#0Q zdc>11T`#om;mx77pw;RI3IDd3QGtC>9cRWn;Y^Xi^lCkkN z7NxkdHLvm}5|ojI=F5J+GjzCIDMrPBDxAKem0ZFw<`Z$+NZl=2X&;Az8tk|`?tm+- z;c9Lp!&%S7N`-6P@qV)n^ReD%a!{u>wYOT|CBA)jgE83`LoSw3$mTdzJ{zQGXvbPM zE?$7xoS7`i=HrN2^ospUR|;w$Z&OtFD+QrpKeqeBGpm_p4`G{LAIQjhf|S$x4!Iw< z-|5pPf6PzSsrZZ`dA4n9Qmp^Q< zhOB!g9>=3aTGQjBFDNcxr6KXfXgk%GY4_b4p&rw?Og3wREQGW&1-@pe2``Rg3#Ux% ztScge7*)ivd?rFT)H8TB;5=4x4~|B$6U{KS zg;6x#V&{q?9npi$Fda7>+>UZ~yHf&gp`mDwi=b=m4O?GYhzJLDT# z_?ptTlR5e`V5dP46}@fn7r1SwJO@l;OkDMJb(cG5GC;Rol4T_%Wc`S1jOqi6 zeD`p4P$%2mDXEPU*Ms4-Vk_u@ zo2SzbZN~BD{5V?}P!(V@E7+YTt(+M$*DYD|_9>%;<-G27m#V_Jy^}MWW1^y{?!*ld z5@g{dVBRbEXvXZiYk5I)H`H1j1H_A~$q1zr?zH<=JVif{STl;pWy#WV7K6%ZtEv+_ zM}I}L{0=juLX;1LPGQ=`sm7ruD1397CwtWuPA_yaE;)X`g)~cQqWYM{s#L7EDGWn@ z6;UkXeHDDRsewnPm)^6)FcyzF{!Sc_|L;2Ygb#`>uSR`pm8X?iIp^Ojgjld2;ClP) zO8>rn>ctRRWJV6C4SHpDAcV(=w@m7h0AjSayARWwziX7|p=-C#ML*Nn1Bwso!fr|x zG!i_+tHPS`2J9`j<%@(5@TtFyqM_1Lg%Ne6;s2@_9g}%I+s>YG%x5s6)#_s_Dj>`VPL5ij!0=@BN}gus0`(;02=AfC zqjC6mCee-cFfmNjpU|GcihLLl4Pt6&@a;T8))$NR;mT1J${$UPeI&K z@}maWawhh17M+~*abMPhjHQdkS^K{pZ+r{AK`@!wy4@wZ2QH~W9;1uiUNMFpevoAojXV}~d)7#L zMGhNf~^8LWqwHk~cK2?ILNg2tP?I>mgpxRQ={+j@VV zYgFCAMk{+YmVaY%4h8wl2>>c%rjP;IZ0%tLJd|ENq0<4J24We8R+UO00vAXvff(v^ z#2ikR4ypL1QUS7AkL!@-b0e^`l(cPWZ4;1}|Ncl_{Cv$llqPOUi3@Lj%JBD;Qu+6p zy$6TF#UXFTi=$ZI`dl8{p26q#aY$l63{SA0-9$DwMygxJmMqFh` z^7;OJAW=*Ccc?hZ1Oy7Fh6WLhV()JljuNPmkrW6aRu@e--=yI#ho1%;XnYPW8+krW zkn*z-5b!D*Ya+q3H_pr*x&9Oaex%ypKtrUfI-<0v+I!gRDD527)_9g=#Jr7S(s~XvGa^X_{%jM9bSxoGjQK(KvmO4^y$wU|&}jW`Ee{OSVtw5&!7n5XR0` zGAzmIZ?^=3{m%b*lkPkhSnm1QE1VS%rLd=o;pIdPW)$&;JqNjJ;1cKKQ0Z|c_sDec zx%&+GsOv45w9aTFn8dXdJn6TB;Sz~4R0nwY(?Cyvev!z3Ra#q&0Ou5PA~r%@h{1Hm zfgidG*Bxm4Kpy;Vpr^djzP0hyH6J^v!Ew%))XC&tbyubMfQjAqgWrN(c)&1k--yUd zoGOiaY&9*qkuRCo=-C7gF>gF210Zt)xzJj=$(}CT=#6qoO9JFhid?7NOvwnls4vG? zN(GK`6h5-)#c(0t;UrFRFR$p!$5XlWn@_9d^Z(| zT7irdvAw_eKJm{%4U>gh#9c_7f!U{)+5fTt1Y1*36j+NgtwG0(V7yeIdt)Xo%x%{5 zA9LW#%DfVv#WbjB>Dka&cN)z7v23L&iifpMi_PIwuiq!RWdu>>=k)VrOgYem_5BErqCOMCnq#fhCy`R18$nv2*h4%t%GG25xPC z74&JBvM^BBL71#{5a~3fyRsm4kEYmkvfZZmw-~{QID&!EK&Lt@A(rRLjGD;Sae|19 zZfLm_H_i!F;Wol^hm_tahOI`yySm4=Puz%8}ai0JrG z{dnrNV2+m1+HvXHB8mnmoZ>VA&aX^kzuyX$Bk_uMfj4-;wt!WdfE?RP!bEd5;>O4& z0&A5`+neExXL=v1{ORvG_Em#NFUUt8-UqLK`0a#UW2*s`IqU2biyHIqfbN;%o1J|1 zs&&84)6egtXBvkbS41HL-J6s4d_JNV`87rWEpCvQLrJ~1(ReKstI@pk#^{wMy$gw_ zMGHYyEyip;nSEvU0LYI00tz>d;~Egfpau?#ry@$Cno3|pPM8pl4&)4#oKu0f84{NV z3bjN^D4nM`Nku~s%VNDO-^eZTYUw&#Wk@HDDkY45uG<26AFJKZGf}jn`csQ8DVo^# zn|{6coy&3ww^?!Ozg2>-9eVE<*OlU0E;NQi%=M)JTD4UEV!FA%OHwfko~pmC$GGRv zki`$$%&o5Pz;$m~?kqSGnz)7{#V7MKj+Xs7nyZ+)sPcV2G47-uRrek(S>_ByyHNm$ zty0<9K?7DK^t?VmH=2|XQDyHTFf4D2wMEn1dm$YlBvGri8ehApf5^NiNXqfEdVj)y zati$Gro3_on>iQ$qh#=HF-$%P55fVcc_x<05%RP0Q&JOfzbf-^Avj#?q3#fI{>jB! z!xAP?9)SQfP+A^EF?0gy>|(ozp1_(jN#a<~IY#DSc~Ys^ z3ff|Q&`PU!%+uZ0I`Sy3e;rK)be_+v7Ql_p zDH0BIGRs@{8ua~%nG;IQ{JNbj<7%W2@z`RErv!pO(!u4gUx!Dc8N-qfwz(SU3A#%r zzjAN@f#qywsfms0x8VocWlO`RrFK5Va!8`3ejBLb-7G-hbNy2vnomx)Dd>~$O|!c5 zaH8|`d`o!n9rVsbr_JIt@Qw+{4@26L@y+d|0z39uBg4s1G%n*cRy=Pi27SKti9-KJ z9o#~vGNlSrI>ip348bd6QY06uFshg<3zrp!bieDE`?(c7yUY%88mqW(RY#e44@Emu zOXp28Fy`eQxR}?Sn~|MY(2Wl@^4!GV?!{37pOT@2ADRtB*9QVX7C{>dQ`-drjLyi5 z0-Q`r!aHkVbF#^+tf9xsVvhnjFJm)|rr@Gz>_<28x?}0Hlk6K;qa_$@@EO(UV2VA2 zk8WSO-nH*xeunBWR0i~jspUrbsb*?ICC_8PoqT-M)qjn88X$!;##KZyU+KNZe5{$T zUSOU~X+F5{Zcq8Gn3Kq$F~|N8dqKO%-@;NuFoD9csaV?tv%Y`-a4jg(==(V(g2e%` zqFFOd1u7yOQ48v(Cv~S6AaKD2Xy`FBKTw^K3YlTs*%^`-;+(_$bi$0f6Qil=vZ&o% z8gLZv-WtCA`*`eXwG!JTGb*GDMH#PFUsS*!gxHKZT&7=yKe@Q<4vz~B^GhqRrc!SG zYa{7lgx-1SnZwqiFo=Q2kb!3RjU_~V}n|n$uj_s8tk9? zm*@-UOMWv715WS}MGq7;R8ldg2|c1K6sw4+&hAx#-^p5gr|?s^IytePKhKHeY|TRq z&j2)J0>Q(E?f*>DWONg^EHT^CUKf1KX|4do{*gQk+op8IJ(3*8yt2QuKEkdl(1y%<XWc-qD~)-+8)jr!3krjjJ! z=EYNg*?|Ie2WtejHs;gBMiUusU5%k|_{xek2Q~aCJMMB~$n*qJpytZk*^6jC$rj`jtr{LWnZ^ z`veaeU9zWj4t;Y);xA=N;ybMj6~P!nH>PBXVcM+(53n0RX9?+}N-w@5%vTSS>Pb@q zV@Aaz;{;J;_^{;qcH?psu`K#|mG)SEs@C98(VHbzU?yMO@Hb8CfQSuYpfbW`oo;Ml z7$Br%+UcobC&k+53~6m(=>ma0ufbK*dejQPx?Y;Vfz-7< z{@H}t_+cxN?!gQjUL<=M=BGOQ;s+~k5xDCr>duqlOIKCAHyA;lM^+NCWnVThmMCo- z`d`*k3^DL@CZ$(v+`Y^46}9O66<=zj9%?Mo4*-g0Uec1^Q1eRv&dX|B;Rkh<)C6-eR+)Jz9T}ESDED!!B*?&D3WE<#X zrTtJwi%jVW;X)5hOfCin`O#Mp#kOilrDY`75Hw>s@>;Td#gs?I(7n&m=hW@?&V7!O zXmQRvtzGqI>Pu$%oxOKf6A0z~;qa-2jOoFP)a1!-0T8vQM_Jo0f5bLMQHQ2Bh*)Fk zVROpUQi~3rf^8d~(|`UW(BKiz)p*~FM$qTQM5GukP)EWa%cF?|9*9c|0c7%5qu zlIAX1w8tU!h!{rW&=wBT;gEpNPPyNHWJA~*+^@BLSYFl6INe{Qaya5W+grlZN|p}5 zXc?#HZXgQOL+t#KsxpK`j_l20GMIcIqm)#NFah|*Bxd#H{d}j@gnMghAK>pKU2_Q4 ztKOJ-5IUN=Mc&3^L0b71`KgU}DWLg?sdmqta`1@ZZ=k+I24)OBUc2 z8q!e2^1oJz4Tx63MBcx?{6gza$t9b*yf2_;460_el@7)LOf_rc>nyNLcEm*O#XE1@ zh%cTp{*n*&vAe1ED6QOAGG_8a!8}COyKxb^-l`K;e?pooX2cM?F2AI>!^#W2W#wB27RpbfjShR+*fbaat0Z(nQ?PorM{I#W`KFq~Z&H@h zE=M~87Rjmv71zlNj+wo5NuEpv_88#O>bL1m3wpL8Lj-6~;T3sA0-O0BdRRx??L;8+ zLmL~7N)Vb3D(Ny97poA^T|gU-^lttEicPbItp8-ioH2A;|FP{1GjJ;IZgT1fl3i1F=%mJ0XuLWG#B9OhLfc)X2igSv)?BA3ywgkQ=7+*0JE!|A^ z!~(CdOUZ>wC8RtMjzZld+I>p_2K{`yYeYRkd_+1@$ z6{^4VwZTjx(pxOlSkR6GQ0w-(Mlk-|4v)-nPg3ZtXyHKL$vT$7@Qz| zg$i5ijzS^tE7bk2wjS*&LQ%_iw%^;~U$JT4()Sj{9`73)lN%^mLN+jD9HYG)mpv^P z=`f})ay}Dpad3;o7yV;cnT`cMa5?j+p;RVz=Mb$a@@;7;@b_@nlZ|uX|L$TyKUNxI zSF*GRcSGnVZ$cj=hnKT^S>cWi<-Jl2ANW{_kkx1jLe!gG|63`ZfQgJl53MEuyS9GayL0enV%2-LEeJ((Y&pPJ_ z1BoZd#{&WcTg^UB%F6PXiMf*!Ms;ZZBwaz`{C+OjN5sMxJcIKYoX!@;DLgRNp1_XA zA4aqx0@?KGyn99=wSCkr_Yd%_<4zm9{oZAl>QW>8P8cw}S?ZRVEBi#0{<16D4y}T~L7fz+u$pcb&rY{w`E(vbj zwx|;=lp(P?yudqCrETE22??v9;a3%XNPI87tIVk3ka&HC_;B@XYJjZ#Cc%ef{i3&+ zWX|fUkRbJV@%UTHSU~oh_W<0)X}Q1ZxQ7#WrT%#%`Q?R4y*cfo_ENN2LL8;NiHPv* zmQ9`Ee^NqM04TQaYyfj_RbXL>L_Vmzed>6nSwK;c5_*O}N!j7+yCO9&LD4JDAv1>Y zc~$?-`Z68%yw$pLS(w<*&dhc0ms=!YZ{^+-(ZjhQA`DPQdy$`6wwIVRVO(9)>8X81Z@>)KpwU z5K_b6kr-9NZaQq_I^&Wa=)L?H+C2_3TacWVRkp6@hHb{+mhqFn z@D7w6Xl)cW;}FAsNYq~ml8WL9|F`6bfic(__`P&xU;6O?&shvSr<(T{d-HKRwRC}u z{V(`m@yvf(G(V)ud{-b5`KblxUDZa+r(y#C`TM947ZW4z^++75m&e_(mUM@6z zMdepG=ZibmVbFNC=pwpr-Qqc3j+SO3azv5~LJZ|zWa6feo(?V1X;Ik^siFEGYS24r zu6c1yJ1%^&#Mx$T{whZk64FeyvX{?uU6%9JT97XX=|5dHR|H?y%F|uppD8?0Gv_%w zW~oAV6OCI~jP9XAio2gh5Z*?0#xylYQmHnhShRvkdgm|^^z7z{Ec&W)*DE@IXCq0Z z^Uh@7xvj9}({?X|5NAHwrl0Abvl%LN%`1HO@Ejnl$y5=?kYI`+1m>t!Tm1lKJII<>Ep}7a3wdr*GT{;!;7@&25 z?X;SK*lKY^ELE4UAFwa8(^2E)oNwwIj3JtNe$B{o6Hyy1mSdQJOkto5#`t(PD{{2T za^@7cxgVF|tscFx1~u#iWEoW$I}|%me|2c}aUAu9m&Vx*7=;RjJZY<&WpX6kM8uFA z6m)Vise%)k-{1%KJ|*eX(G22@0~uR`2XEmcq@;oxa$g;8yTl7DUVoi^$Qp9gFZVTU z9Z$%FQso2}tT8-MynL)8wRT>G=@)n`g9ct6vJdblCg~ASZ9zv#5AlNBe?eR?_7=gmKtkk?KlI>zRTUzzwA9 zs~{Gx#o-;GVB6na9n_zYZXKSm&()v(fo)WF{ql_yLk)v^IZ{0_#D}X8$K)3X-Xi?MSi<}=MzsJ| zSKc8U!-QMt!$wo8RPj2E#fl3tZEDb#T@eKBFC`QGhejcqe`NrwNJtnateMMFu(Pab zZIhJK6;AHL852&$0n75~^NP)2tyYrWy|#xpmp^bpYsb>U4ol!-2dhMy*H%(QCU$!? zi64T=nI+Wocp7#aHNwQEiPGnlIokvjCxMwDON^JWCiC(DCf$j_pX-&;sh)#QLW)#U zi-xbP>%C0cJ^Ud^p$8#3)7l8#&XngIdW)Ca5V?*PM=6D=R5H(Oq%-? z3l-qrKkO^GY$qe6ZuMjbRZaq5uYXG?zWi)tq-st||4&^6q+sq^P>d>AiWWK3QpiHG zXJ=n7BcoF@+J7T~5*H!b^0K%b5N9tQ?6Z{sz1Rw1XF#HcrVTx9aWTf&m-^#M?%(_00&ts9QY1>QN4M9FgAs#MTKY|f0=kK}5Y^gAe5kVbtVwO~#Uxbi?4^4iS zWk5?Hd7JHlc(jS)VIpIp|Mp(h61o$?=2m#JcL@A5)McK-qW0lB%_@x2DobU&-2=#% zir3H&Zrinvrgo9qowLKhf`|QGEYn;Zg!bb^g*3x#NKF)8$ANDoxm! zvI6^iMUTKFvmtjcUGHvXKq#Ckyfqx>_08?e$M_jWVG`Y~3{sY!qB*Usb|Fhsqhw`Q zm}%}E>w$*ApNX$dG43q4rkc;8n&)a-d(v>nIb$IU`AVy2;OJc0Ky)sqTR1hh97-%F z#$n`cUS-AK<7F&QIN?={|7nH?T$`EX?i9pt%mTeo>ghbZ?xx)9-2G}eS8b&`gi2G~ z@W+qL%kHLvtOj1~Xz6#2^x1M)TJQP5tpfCP?9_-5!N-4zt_S2Ljd&ULc`LAr=VXNA z1-HsSQl!(n!1C{^gK0Kd>Cv^Hn8-{WJusx^%x?kF{HLoXalDcj%b628iZQ5kgtMMe z)7*H7giGPvckf-95a992Jpi*JM%bM_yI^Jxdv?6D39X78#T3gZrGAc$=tPYg3!3*d z!#nXQ7R((pli{TK)K%j{eM!4%-PCT?+IU|NG}|rxR<{lx!?V&xrS@_Q`K4{y+59RB zKZVG7_dXM-#=Jp+o>?vkIi&aR8Kj{S7p^a1DnqeQfj;msrp=k&>8!Xl#QDWmFT4R9ODv7Z}Ls{$1N~~cUXz4NqWN@)7TO9!-R?mLR z{VU>H%-w?ZJ1^T(oB_fo*ZdmL=_iTM1*rdvAzIT06t+s1K1}jPL*SrQlMv1zK<5yq z_oQpp=Jd9L32&?&2%C9SqNmYHdK@j^AYyt+p?LQOC|P_zkr(X@Slc}%2?!K+h^3`% zMB2eq6X2;~O^;AqO zR&Hnu!lPGm8uK^#S*ClJpp`b3{EFvi2=iQjfpNqx_SEzUXftHo!vRO9Yrdn6d4LL> zk((T4 z`$A))ReH_qU~vB|-~K)FG{uIxDRhu8Qeb%fn_|ikwA@P5~m%C z`p4Ors|+UIIC#^xgw@Gp01{`S&-ATJwEUc7Rycu>E4;FK$HMHn22eu7OPd;C6h3ZF z+s?RZf4T09iW0xYv37jhBeLk^!yq!8UEQ= zf6Ns-RkG03PBjkkt%{N!!#LYvk%^x6AMTEi5sV@Bz%8le;V^(b8W0}lTeE@E>J>!D zB|orarx0f{&pmJJ8+9&@_p4P@`WP0zE5Rm8)Ol6`s!FCPMR!T;l@aFGOwUT(i@^w6 zip;5sw?HX=D?kbc@Rcl9McGV3)0d8Y=-JI8B9G|nUL?43Sl!1Q_AHa}-J!ro+o%eu zj1POiKV8b8iB zG{}rqT6U_|=I*F!aW$EL*OUfC#*nc<04qYdBJZ~$t&$l>ZbsiIQb~V7o-_%PNjoG? z<3HA=&82Kp6Zez`}8H-BbLnU;DBnw4}#_ zyx)&93u(U}8$8}3(CztUP@zq9fg4^qlhE*XR!CWI@0EL%7@!KLxzBt=s&=zJuuuPp zsxJubfbcehb`FxS%eqe|XYrKzQ08(JYlrZ#enj^}B2s0}!<}CfzBKUm*2sv0L&tM@ z)!&JVu;7)_8`4yKNSP=DiHLLij3^n?Q8!AEs`~I2c^24lp~y9KJ-< z8H#3mb|26DYs)B2;EJ4cDiyKkS~jON7&;}4yQ*v03-1xD`sIHaSxL1s$zmswjG zJ&Q#^K2Q<*xRj@O_kcVBpE)XE%LmTwH7upUU_x*F@nVt>LDJwoyw5~PT;_(UbFVH^ za&T!SFN0>^!}@QWm>&?UukZ(_4R1jmMo04H%=F&%efM`$Hb^Ni}=khdq9bx9IWC#PRJp8It zbdWK#H?WEWHSnquh5grM^*WX1QYAQKlLw7a zkdpn9%(I*=4qlxpsfarYuZ&S_A($}&XCq?YW;;l&W;eb(o*Q4dWs>;k!u%C$m?x^UbFvx(66AAHD z9!fFNGC7{!Hj^4f>JC&IdtiFO!j}x|HTd$p(Kf_uaj+Y|%LJ?MCyP&r=Bd6wIfj>g#bQHT z%Bmc05fD&FU_OlU%!0&6Je6v?9-zVwDK|xl0DXjX+Kc_On*0n_s=rJ;UA&yJ>v2UB z^=bJH_g{z#7`I=j{FBLm=GRQD%?7Sj?d~wUi_mk$V&Inba}k2mhc^4G(;0Olw*G)OTUm1(RliVhrt<6DBdQ|>n;On;E<^!2E@!O@V`cFVqUa)+^7>&TEm&H zR4=1T#20D|F%4a4eEaQ$;D_N?+eT(5vlUQsw5qz18`*n}cKvzg1?{PPpY%Bl<`+o) z{W}qr-}l(Sn#&f#2pl1ZxsjS*l?y6pD>A2%V@n^BoRx#eY`dXotLEC_NO{C9wclu= z0L$NF0rm0l@v%u~-}ihbE?q8n_xCG@S8q1hb7VI2FlqWUxO4hWnuI3;{ykTU9K+C`wpz-khfP zy!h}g>u6vJZ}haTVkzXwd*y1u(%N&flXL~w6c@VK7mYuocrBqtp|0NL zx^|iyD&Qs6nBkQR`Wwaopy{1--O+zFmtM%nfK)Y@a8SXZ?J(qtvlpZVwn>C<7eU)| zkD#W20a;~z1FW5QA|xY#sU)|e0nl}E6#@}M<8LHWZgJ|-cfShd*MF}LGWgV9{M9Zl zFSaLa$~v&YL8-j*N#=S8OJ6kwRAIaNLRv>ibtrgfkq(({HSi)Ve6K+ZR5}$L|5f{% zXGtps9JkN32evk1&Y;5>fCVZ7asd|^aIRkj7IxR6uw?Ru)E})};e zz?5#|pbXKF@+7F~K&I442&xisF?F&<8DG(6W0ZB_`rKX){6W4^R85_lMmjZaU%NUY zr5+GKX5=66<)&Do7?>lbRBWuij|>t?Dhik__P<8@))U9hdIE(TPBso+M0^h*Y@B<^ z`4vv-F4&;$#C(MkcEToEzE4=~P2*A6e0(HB19L3JJO0dtYDqI-naNnzxe7dI5>RG| zTXE)+TY2fdNcbyX%oz)M$*8g3Itq>bzIcLn5cS<3##cGox>0LDnJIir8i8CJ`3bFI z>b8iEOxX9!^xI+k&XjxP@TLb6ADyDW-LFVQdN3J(-_Tm6mwan_Ab})crw`$2FRA^wPMSD^_7dHs>Jc(M?|} zL=KBBIbZ&fd}H_$SG#?W^D4t{K~LPqSDQ$q-8Wp_@&e{~>R6y;e7*veatfXvtS-*} z$SqQsU`mMegjb&90&XB_8<8TOhM0<+!d7O zmRG`TU{eFZg-%ozRQ!%v*XhN%1{WeDmec&f?CI4L>JIC^e#)kpy9SbxG zDq`jv31aN=*_YkxJLh7}r|axzs93N!yNL|DUsYhaN-~8DGLp>QV6?+fcVvK$X`Hof zeTI)xUih%rlWZuajl{93kO}ABlQiU5ZB;}{_61SZ5fDmrl9oQ)xw?xmw3roaq})^| zj6;4t!F){L^q@)lTDHs(9fjbvOb7}L9!W6_Qah>U+cojkhr$q&mP#ETPiaiYnYEEZ z*W9-BrWbWrPbs{!%_jH{Ls|5O!S7%n0OlP~Px)7he6(d&fx zcQAc@zzZXoK9EE5>c}j( z%<|qW=H72A6bWnZ82TgJ#g9GJ)zO(>%#pgVK{xPh#&&yW+z6}u*P6B+dbEy}e^oD5 z2p18cDw_>)e<)huevh*kcD1GL617Dl`H(q6ukcQ0ib-8gE_eA5PoxJ6Y;v!-pGd)% z`v;w!JR3%3(0nQMZ`y5#NDyF81J(gwUpY#RB8BS}Z_nsW1in4BkaF5Q)ffW^{NRX; zMUAbieU7IXG_oTmmtJzP#6|UbJ<|pwUT!yS86cgZoFK!C7ZTy<;UpI?R=`3#gG#GQ z{}q#3thP$RiQM=O3KN?fvHvT;UCloZfFpr$?*Hk1>$P+<&A;?KO?LG=XVdR)49DRF z+^ijrv-u+Ek`S$U!;O>c?ma{wKuMr$!fIaiXjbaqK`Wd3JP=T0He!7>mrx(;-G)@B z$tCyvPPv88rVPK%1_F)$T7&j8RxgaSri7A;@CHSAOfK_?u zjW_G6?`)AbA!NnF!sENf^W0U3;1eM|bCRn)yK|S@`#ao(SY27w5o_Nt1-3Tw#0{9v z`JdkSexUum2R3{)y*(QO?xd|z(XDr|V+*FKMFM|M#J?cfP5yy>d>+OkZ0njtY-qq+1AAlqNtDVY`RKNAZYZ2US)Qp?5*mbd`(xuBfs z`|Gy4ufBCvD|qf~^o7{hon6<&MdR>^Vggu@?vyXZG#qvT{pTg{)R-8} zPaA^Jv>ck$U$PxHAGd=DsskH&Kajb8P(Y2>pT{PdY7iB3-8LT`U z5M+5L9CSBFa{+}|2MLzA5xSNxj?JRlv>>L?ycn}(TiS?6j=@F~H;gbg7&Cg$3-Irp zW>yM=-Y_G4DE*8I_c*J9QgR{wp%R6Hh+fUr9z!f_0^cU)u^G6z!2<5hWRo8V88gpR zPkc7kjcy-AN)(gi)W7Go8VRTnSI-DCUW$KgpN&pXpwFkj40Tw`1Mwt_mu!edo)Om8 z)w+5JFPGq|VI2bJ9IC9>r29j-Dji&sE2*td0f#WaCL#RAgLv0$kxxtpCm)}o?LH<4 zcJkAcwxOv>Ke8H;e3~=%?g&S%S__-U4f)N&9y0m(OvU8-cGBVmKkWhV#vpy0rIrmOynBPLd>B|@HGXo1`x z!+peW1pYn|>QEV@yn^L+!8LHUmBYcGQJQ88oEyx@JMzn5p!Q+KgBV!R1FD;#R$Z}+ zG`cln)y~2iD+(h2$JSTIMfC=4!?JYk(%rqZq;z*9-Jt@~-QC?Gox;)}-3=-!rF4gM zO1|s=d7f|Yr}ORn&dfb=%{4Rk8C@<$H>$v;ot8qSzzc%ZFnySN1!P9b4QzboRo97z zG`8LQyx&K5YWRv!yeEo+zOCH3hAD}-)aErRwRV5Ej=na&JyZwN~5d1|M7aE11bCC z-va(m*ly2b*OINS@yfS*&lcf#3|NG;u;H-a;XwujqqR`yj3H7Gihj2DswRr&FWxVy zQ^b>Je|TDdIFKK1Vy)^JSJ-sz*|uv3rPK22LN*nRF?V>GaM@H8*35yQLW4%CV&|dk zljR0DyD=K@Jqf*9bb~%@n9&V-MDT=iW)ZJU*FJp`SpE1)81Ne~1~XvL*vl)VCViGR zdE-s4m4uaWAD&SX_DN7XxHWA# zPYy8=AJS4<9XX${ZN~K#z(QA6mqGATb z-K>3%Se8&HC_n4-a#ZuW`JH;+g!(*PV@&O=bU|+)JDd$_LpCnvHxjCFjy8GRC5~6R z9*LE2mTOq~*#AND&4&)V+e0O6`lWB40?K4UE21^7khmaJV2=aS0IG;V%hCBT&MMlv z5l6_dY}y7b-lK%3EIM!AdDEfgu-qmkecOt;=sK^o*j6@Fbd+%^t3sIVOp zS}-i}8Y&*{Er)-}H~gqepVjr_soOt}z4mjsWepEmgHnzD$K|g{8z~pk=-pd5@$9uB z^uHFdML?4p!u5ooa(TyV{kKMP5#Wc{#{}OFcAEj+z@RWB`ZgU`G6nUdC)dE5;+-P` z?Ves^cLLq4iw`wzl6EHbNE27)vVHGB(_ZqxmRRbt{q&BBX)m(Vs2~e{Qof}&Yx^ih zb}yE{4Pu*5OOuaEMc497*nj#xc_uCeQyrQ6?w^wd4maIzXL+61OepbiA3C@E{cTl~ zAUBmrJxdEHAB~DfCx*9b4sf22h?}u+&)_0JHQD{>BP~71lX_y1{z)(0HZ=e|Zy6x% zZ&4>&?LrAauwZZZqk4ys*;b12wzyN(MW~X{ll6Be^Q0$I@$iV&N z6bg4>6UbZ6Y63zbBQ}iGmlcMpR0q4!M>|rgv2#bl=F&-R#4dytOIo8`rpt);$K>r3LA^eRM;%*dH>~~uMmUxcLEck&41Yz+r zhWXeXq5u+l0lJ@*uJEmPnl#4WAR$eEGdDE@lAj=BAvk~iT>Q1Y!m#9mE~U{H_qfMb zTLRc?f6|+?U|3xkwmx>Pz2r%TppVJfaiMie9*0L^cL)GHkw&7yvJu}hpEkKYu-z{V zPjd#L8qM_X7jr{QjDL&pXViQ!cHi-5G5x4Gi4T%bENyGWrER9p7G}`Peb})`E~3#9 zQKi-{9AeQL78`pl)V)Ek`|2Z@r;3t{p443x#nH$NNP8pT4L1UmD@N2yPDB0jSl`C> zPm=4&-Sa^hJF}x+c_^Du07NfW4qMjRU5|@yl?eICu|mK~(e?}ys417NN)qcx&+4sl z8h##d4|hR(v087S$O9|7R%+**@n067I8#%?q7O!JDJNZu)c6FOM?><3lMy+5QPx4J z95?U#K*yb6lnivSKhI`pY>x$mhsj4fZmt1Q=4IQJ8mO1#ri-jgzV@qgN^A!r5t0s# zrDPgD!`O9shsI55?~&=L^>8?DYH@ld7Ero~xX`HxRxf8TNo2qFZ_ku`TBTb&8$ua- zH7E-OqgI}eNyVDIKf-A9bt5uUb!`(I!?qYzj_}<}2qhxE+!z|%tZhdFAV(o~5aAne zqew@@5GomogS=MRvL*TR#dXBkz|I3ZXe#V(@*PD}tBO#8Gj0HTwD`}yA`7E=gd@1I zn1x>snd7BOA>*MgC;;7GGkN-IK6I9OV4D=CRa8ZSPASsR3F`f@Zw_l%h{F7%i%g}M$_+ehK7ZuRBI9i;WfHBjV-^`~^evynR zae@`t!Q8)vw$4Pf;6$+I9Vqrk%m+)A?3rZk_X9)MMcE0Z{QH|M^mar*0S|AX1-iPihW-+fg?9bXTNU$svLAuEH7{~toPcul?Ynd9AiUOq-S23 zDlgfBM(JHt;0vd98(^fQ?ZM&c9x!U@qv~vI#eb%zN`ohJb^ieQo|u9$GddyoF6Hwx z#>0(MxclVyYxyH_nH;)d@*LvgciOp@Q0mXN$%^${EsoBV_)7I6l7eC32H77_rieqT zUEs)s4L*UryiXxS8`wUOs#Y+3vUksU%%kN8$ zV;VKPM7N8PB75;Ss<_%Ev9DnP43nj6+W{p?fv&?x!|&;u$m+X{!jvTkh36x=fNfWE zkPTCKUb}zCS}*|#`Ydhb4Fh&RqVFY+qb$Sy3V!a?m&JHrv7_cF%L_&$A@Ue784E7FYP2D+?sc_-d60cV(u2H78)I zVb9h2=z4)9<0AXS7S~o7KEAz83GSW$dQYQS+98`0)J^}l*e9zsE=UEm#IY4zhVS)q za6nC{=@>yyo!F4q<;VFx4$muf#VftGJ89Z=f( z?5*JR%v90E_sK{M2JGKIefyqDM?g;j$T0npzjbte4r_y!piFoDX!x+uk& z3^EeK+fkj%(&G?VJnjF?ZT0!l*x4=pw+4y$J$PYJH<>}fIeKyf1m_PD+{uzpbTd}v z)sj?x!&vcP&N8H~;L`Hq*E9d{kK0;3!m)`@1F@VWK`TZ^Vq88nmg!ox#$Nkr5SQQN zZb_-e!Fl7OrLZ!d(t7^5MS+P0n;ZACU=a;d@5nJMY=}=o=OFTZedcWWV55;BMg(t1 zO=WagTM}XzOM!}{m%oo(T&5H&Z23W=rI`%l;$ZKseT}HINSt_}WRS_JZ3i|A487+N z9CFPRjP(1Tn4+lvsn~F#+IyEqAKtk+Jdj6#DLIsAJ4>tJ8Q=;5*`(k_94PVTLY9nH zJ{hZ)$`#)U#!P6LRjc$rf=j~Ib`4v+Wq19Taugv2 zwf-a-3B+4#@)rjuA$^+p*xXX-8l3|(>MN^T~~TF1X2XPG%bsx9OvfX zQ~hc_uJ#y~^wuOQo>PK0t6lUUfI&N2iU}JtQYwZav`7}jv-F{*yh}J71d5K$ogE+Y z*QLG+)QJ~eRgp1uFnElXjeLn5@|Xa{;l3N=UmyM(&J4zwn{f1yr5kzE#0iKK@0Ad* z!(sIiV}i5TwxEWe`vi2V4(x3&dhRaS3fE*>JuN86uyqoEmdI`@;V7`lbMJ=j4=b#i zzQ3e{ak!_EeK-^0^0P+Ntxt!uC`n~@#xh7dEliaLe}oNUT34KiMO@IX^#&SAH$%zh zCV=kx!mn(~iKSTYI7=SCWtlN;uXo494z{LIRH~@*;)ZZ}wxfbSb?>Nv*w@c|zc(X6 z^HCy6VtkqlDQMNgYw$;REHrALYN|gA6d8J(lan#)=kw6#2F>5D$>SMDX`B?_m(X`xf&_vz3$;z0~4-y*1b2i0v<1dT>AxDeED9tW>io+Mt7EyxB^j)K7Cl~UH5UDuP@Vj_qg zCePvbp*EV?UiWi@?{5_KF!7-|4StOeEVvYsQXM#d^e_Nuwt?g+e5&MzZnruq!dz*?;7}$vgsE6uN^?TDOPf#-x(J?J> zfw5f8lD7EakiK~0P?m0JVCaVK8>5*~0ytZdU1@L^Xo{y~i;yfjKE~@819oBrW<(bA zdIf2tHR2Jtdy@2a#jAw6{ zyFVW%p`5pIh$mo^75U2y0Wc{-l@6+Uv8Bq{JqM&rPt!kaKTDXdZDqeWlVl#$$Ql)g z@5`jec?R%M8`!6`!VY&@4Kxh}uD4y1lL_7<`d$8Atbe^KxxYjFBj`nd_SG#RfLrwR zJtgS=Dt)`Y*Cl|-B@wc&7VbTQ*%%z>jwgEQq258}d+ zfykOuXSB!;@>!C%=Umz=S7)d7yQ0 z7f4Lahy$Q5SzQPwMC_G33!9te?p9Q3{ld)VS)Pnjo#a?6#=ITv9urlUVs0R6soO}e zM=9+W2X?Xr`mprBt1uh%E$E*?Uv|3v#E>iO3;)`#5RP$8(9IeC_Hv z8I!oDI8R^0{tl|arhcTrJxunIa4Ww!LX&y5#pN{qPw z*BubRgMT_ZVSl&We%UuIPb9{U$dIT+5QwY!K|I8$^dta2&{YA*V}rywF2Y>(i;Hi8 zh?BV+c6U&mcl%ipX{uZNnt)@*URnb=m{8eSiinhTr6LZ0&owTHl!nTsqGPjd)qD)r zorultf`+DSNcLzyjiuP4yRd5VF!3-Peb%LZ7GVEECfz!RU{e2Zl3H)c!9vW0{(kh> zoejTTU{t~S{mEJhlBGL8A%TK5y;GEiD9TJ$a3T?9vm~UU^gGBNj!uYz;4m4lxQXk1 zl7)?@iW5l|nDEJAmn-gWxRRB*Egu>VHUoB0!&H@mU%RXX>iXDO*5> zsUd%l9A{ilf0k=ibD8<6H_J+d&*m1O?O*#FpWgm!{iC9wm9dQHK8~L#;hKy4sm^pA zIu^U}%1P2gxG>Jb4>EL8_RTjvwcv$6Vo1L;EUN1>qGT5Ryv_bf55`0<(EE4}`-)pf z)mAPVMqvNKhLMOmv=!MI<=8${0>mwmN{&1*bDQ$_B@jVz%HP^XY=@kM4;fR&#DVRw zOv55&i&GSJ%yJiNv$=FI{QgXD)}L;2CFz^mtqbE@#)?Qy&v$@z9DbvtQhyWw z!OVKE9HY>G8$wCPNL&-E28%sGt~4+xjFrua4@meEImO?LkZBRoZ#ElUgz)w)z-({O z)x7!sG&gk9L?1gC{fBNcG1!r>tgG*xpQgb+QrUGC?CPB;B9j_u>%zu2dP~k7esdv7 zr12kZ>fH;#GWAx>d1js1NH}RMGtUEujdMa-aTz##ypKp@PzSS25&YDEo_Cfmhkx%D z^ksZYxi-UI=c|pp_*{DW!LvKYrNJ{S5;|-1yQA<1GZsqD=|g);Q?GBIXwgaKGd^^L z={cF^@)3j}Z+F6%wKrjQ_>~bR3KZ^dC!4354gR@0SUkc$G|dej)xz!2_Z=>VZlOIl z9ct>&XvL39!JD+X)NxCf*a^gjf(x(ZZ@v^3(}iWJCYG)mHjJr=H5A=HRo#GwY=2qV zTnnuk1pqg`XiG0n=CS8Hny&IApN{#NGSyP1ZG~c6&l|6<<}b>wA$Iz!d-Ks0`wRDO zv|t@vss#@ckqTRGdn`Tv#JTt_qOZmJs$2g(<^omLc1!CYjHv#HaYZI#3;PEV9)AON z%jgZBdjMP~Uw}>Sdg!=|t0FQ$h$1Vt)k9y=G>upPz-Ef|rswb@(D_?sDdop%TsPr2 zq^7DW2bzR%eH&{|Ikf82GbPSWEb<4L;aOh@B?kjucDCu5oi(ra1=8{Lb2J3Vmh( zX(qWoz)u+TSIw~tb&NZYY!7=m^`bp+gplCHo?rX5m(qqnnBUqsg+M5-Mw`uEM9AVH zY6PH)G4o{%wTK?Q3HkFo?C}(&Fp>yb^%ZqUkmYJzapyBra1kGDawOo zDnKyCr!3!#b9Dh<11#y%^Z~ZeC*nQQ`1d2)$Sk!vwT!4JuX zYrUrxJAAlJIL14Va7sy7M{xF>fyYSMh*f+i74K^h8tp1|{^<+T#3H9U+258O#`zMy z)r?|@NVqVnhNU~(r{pac{?Wk5UXA9Sjk71fvvpOvJo+2|-;dpoKSM2G|8B9|xLYr3 z)kpT>QoflXohUoToT&lqqI(;B0T$sAFvu@*u4P$-nNtzR!ks2!Ybjnuzyj<8=|5Is zNjfHwDSSxiG$_01*Kzk>URysiZ!z!v^-sxo-bFnjPtzs3^pC99ONBG_ zyT5VdDYbyT;t*iX&sSjtU3Hrq0^R3%0dmWm3qhEU4-f!~wHrqS)(k6=Rri|v;^H7I zbKSKGTRd_DG0#SAoycuhd|Ks~yFcFd-?~l}IraBuXSmS{4jXP|F+$@#Rq*{V9xlrb zEPr1AS;yl=&j0*O-&gVS<@-+X=#+^@RR-GdD=5{CGJKZK9$ONm$YrR?W-z>}j`gz` z5GAR&l1Tf$fA%C>-P8!<@QtFG1}D3E5%nhBAkBRXxhgrU7_m#=F#adH+JB+rIi%sW%;|KMztHSsl38j6yt~K!V8|$e zL?h3}O$C>WDVwzTPEUy!Cyqh?kI`scgHX>*tHCrpn;+?nz5yxFrh1xf+o#GI$mXNa zxlCF#q@`{8bpm9AS^NDiK zeA*Mo`dMO*K%DOcZ-r9*a0Tq|xfIIibuxBV}mr6LcXMOK(~3^DX0vs2pn@O;=R*+j$?mmw-nu{W~Xp zm~NeDfIHlII+4Gi&WP`lBYwU8qVsaZiL_niSNEmD`?we-Dl-!Yy9=zKDCuk=bJ;m{ zrJQiD*lFZlF@LWVo@epZ)F@1lb5w^R>DN3i9-uHqwjZhX_H%U~WneHGwf9Ehd@GFJasnGC zGbhZNpw<7%T$B?HgD>WX)7k0-yb?S)fquR&Hcfv$Rro4j&{7$&3FE(^%gTR?s z9E9>qD0|);{ytI~r{MJA8?Zo?OiX%2gE_8cWFPr{lKn*|DZ^Yf3WQrSZ3&-R6Affw zH}}4O#Il0392t|a;9L6N&a)_Ye=6@V*n9g>^oWCV2^3-w5lhWh!uIhYoV;&ni3*@4 z>K9T|d5fwhKa&v4{KL88+CU1VA&;FT2nH za=I$rb*j^#E_eW%g%qQr=)VNf)y?=A$=}Fn^`eG7wtKeg!PzCfVi2raf7OP;{BwY4 zY|W<2%3_0-vTdna(tM5EPYwwrIiax&#E(w35_a`9Te+3QA8?ZcK#LMi4%#sPpsyvVSeS ztQta?q9m_5Jf)p0Q@So)!&NK#YPJqR~-8RK#*xo7$7UyIy=Xft<#D#J8JW5S-32uTPqx3<^Dng3CMFE^>ib96Gsg( zPEhiF|6FOfV!m!)CQgT?!-<-Ln~|EPII0q6R5b2RRBNVcV(}`Acs(5xzAD?xgR~lk z`uUdqbGtOU%a6B$-MNVv+x}o)cD=S4Lv^seUHtC1mO>Wx3D~d~J4{M?ujvDx`x!B| zaK`!GvmcRZMf_YcDPkBm?izSrdL+O`=2xp|o;$JZs-euyWqM~?xUQK?_4PHj1!dQw zMp~1;EaVicAYHe?gIH-EQhcM+p zdwT9l&nxl$wc&Y*GdEO1%1W;mbuI5tH>~+teRW^Puihu=9~%O5L-=3{Mb4ZvDq$M3 z;-9?-(q123P~5zA1{?hRDjVB)00R?2f-(xTIAe9HF?#Xj?79ngyBSrHo%YdwGO2_h z-9p_swF8fQ21YytzB#el-_9=sFkSJa84_;ow42t|=iwUT= zU04*K!rvonq=8h=e#3QSp8nNW6W@g%;l(wW!^Z%dyI8^rRtOk07cy$H#x-iq1F0|H z&HsEechi=Wps^CH04~K7JpCwvYm*Y8sB|uC)aCsjBFz}PN(2G^p8T{aC$g1^%d>pN|Lp#TU zo!7saLvd^+C&mr~<4S1oB{Y4j4g=q;k=n~^sPD-uj?2PXADHmd+@T+8Y~lA!4&oeU zP@)ot4ql(-!2d|z{nfXKc=dXsG<+*yf}>h)3}0!xwtQch66R>|$21#f@l*DcMtzc% zn@0T$h>Me7QE&Zeh9$`(1qiQ`jb`3>+>Oznn_Ox+RKm@%!Pzk_9a_<&cZ##@pAY%(-%gr zymPb-j=WD!at5$V0m{rR@(GXqN)_m4i$f<)ZsREq6qb!}?};m^&Lm8;#3XTSt46)Q z!44@d=neJs4;-WG9~Jc%d8wRKf~cAZN1Nvr1^uIuJwbP3OX#L}&cgL?Rt4Qf_D{OtYGKkOGAYR@Z}q`U&es%@2P9c zo=iL}BSRBbHHC7@}yEx%&?3 z!n^%mOTdk2AF&j!WWWER%o9Eg6Z~m=nNi7nc@%m6h{q7z>Oil_s#4Z9_@5w{AlQ_~ z;^dzYHaVpYA3H>zX8wWGJjSAasUGnTVS|nDlOkz8NyCVs!ux#t`wF&K&s|{c`Gnr; zt}DZ_@u~{VK)RDcy%YhxJ-9j&HF9aLw`qj_o+-N3Z%;g%dV$PNtdF0+A3g9zhouieL<8PrEnfJzu64C$ocGCRMCQa+0%XO_ti!K-kN%Hx zWCU#4|3b{0j**VZ2O+DmDqUlKLqLDHP<`?BzKR|QQYvw>9bqetGJotmu%y|(zh4gus7cf>dZ0xkLDPqlCX@Ao_R~-{*D+ z-s$1Yv2WBm?||e4+d26YqQ{z{jmeZ~Z9k9vePixI28O3mb^@e5>-Hceu}xq*8Ib`3 zu4D|^4Kw&XTwSDB+botrWZj6x{x6IC^9E4>rHN1|^ymGC!zCdh7juTqpP^1$^-R`- zts%XeJT^Fbmw*h55RejSJ0iR@T$}i<>KH#<)AUk9GuS1d39TcayZWhWaSA&YIVzpn zW$I5V8Ept@DaK>SrMz;pLE8Ty3;}uoiIUR*!reetzYBh{&)ZK&BAhc#2r(?X4dz<= zo1PV92%ZzlK&BT@-)-w4+|W&I!^r{D+X5g{3S_tG!!W7Q#eA*F5I9_s-b>D;QhrSNUI7LqG-JHi)he3Ad`S v2ifMTK-q?gp5K>TZ}@3a_AzPil${I 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 R6%6zzE+Q>lCaCN8{{UMu0S5p8 literal 0 HcmV?d00001 diff --git a/docs/reference/search/aggregations/reducers/movavg-reducer.asciidoc b/docs/reference/search/aggregations/reducers/movavg-reducer.asciidoc new file mode 100644 index 0000000000000..d9759629b75ff --- /dev/null +++ b/docs/reference/search/aggregations/reducers/movavg-reducer.asciidoc @@ -0,0 +1,296 @@ +[[search-aggregations-reducers-movavg-reducer]] +=== Moving Average Aggregation + +Given an ordered series of data, the Moving Average aggregation will slide a window across the data and emit the average +value of that window. For example, given the data `[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]`, we can calculate a simple moving +average with windows size of `5` as follows: + +- (1 + 2 + 3 + 4 + 5) / 5 = 3 +- (2 + 3 + 4 + 5 + 6) / 5 = 4 +- (3 + 4 + 5 + 6 + 7) / 5 = 5 +- etc + +Moving averages are a simple method to smooth sequential data. Moving averages are typically applied to time-based data, +such as stock prices or server metrics. The smoothing can be used to eliminate high frequency fluctuations or random noise, +which allows the lower frequency trends to be more easily visualized, such as seasonality. + +==== Syntax + +A `moving_avg` aggregation looks like this in isolation: + +[source,js] +-------------------------------------------------- +{ + "movavg": { + "buckets_path": "the_sum", + "model": "double_exp", + "window": 5, + "gap_policy": "insert_zero", + "settings": { + "alpha": 0.8 + } + } +} +-------------------------------------------------- + +.`moving_avg` Parameters +|=== +|Parameter Name |Description |Required |Default + +|`buckets_path` |The path to the metric that we wish to calculate a moving average for |Required | +|`model` |The moving average weighting model that we wish to use |Optional |`simple` +|`gap_policy` |Determines what should happen when a gap in the data is encountered. |Optional |`insert_zero` +|`window` |The size of window to "slide" across the histogram. |Optional |`5` +|`settings` |Model-specific settings, contents which differ depending on the model specified. |Optional | +|=== + + +`moving_avg` aggregations must be embedded inside of a `histogram` or `date_histogram` aggregation. They can be +embedded like any other metric aggregation: + +[source,js] +-------------------------------------------------- +{ + "my_date_histo":{ <1> + "date_histogram":{ + "field":"timestamp", + "interval":"day", + "min_doc_count": 0 <2> + }, + "aggs":{ + "the_sum":{ + "sum":{ "field": "lemmings" } <3> + }, + "the_movavg":{ + "moving_avg":{ "buckets_path": "the_sum" } <4> + } + } + } +} +-------------------------------------------------- +<1> A `date_histogram` named "my_date_histo" is constructed on the "timestamp" field, with one-day intervals +<2> We must specify "min_doc_count: 0" in our date histogram that all buckets are returned, even if they are empty. +<3> A `sum` metric is used to calculate the sum of a field. This could be any metric (sum, min, max, etc) +<4> Finally, we specify a `moving_avg` aggregation which uses "the_sum" metric as it's input. + +Moving averages are built by first specifying a `histogram` or `date_histogram` over a field. You can then optionally +add normal metrics, such as a `sum`, inside of that histogram. Finally, the `moving_avg` is embedded inside the histogram. +The `buckets_path` parameter is then used to "point" at one of the sibling metrics inside of the histogram. + +A moving average can also be calculated on the document count of each bucket, instead of a metric: + +[source,js] +-------------------------------------------------- +{ + "my_date_histo":{ + "date_histogram":{ + "field":"timestamp", + "interval":"day", + "min_doc_count": 0 + }, + "aggs":{ + "the_movavg":{ + "moving_avg":{ "buckets_path": "_count" } <1> + } + } + } +} +-------------------------------------------------- +<1> By using `_count` instead of a metric name, we can calculate the moving average of document counts in the histogram + +==== Models + +The `moving_avg` aggregation includes four different moving average "models". The main difference is how the values in the +window are weighted. As data-points become "older" in the window, they may be weighted differently. This will +affect the final average for that window. + +Models are specified using the `model` parameter. Some models may have optional configurations which are specified inside +the `settings` parameter. + +===== Simple + +The `simple` model calculates the sum of all values in the window, then divides by the size of the window. It is effectively +a simple arithmetic mean of the window. The simple model does not perform any time-dependent weighting, which means +the values from a `simple` moving average tend to "lag" behind the real data. + +[source,js] +-------------------------------------------------- +{ + "the_movavg":{ + "moving_avg":{ + "buckets_path": "the_sum", + "model" : "simple" + } +} +-------------------------------------------------- + +A `simple` model has no special settings to configure + +The window size can change the behavior of the moving average. For example, a small window (`"window": 10`) will closely +track the data and only smooth out small scale fluctuations: + +[[movavg_10window]] +.Moving average with window of size 10 +image::images/movavg_10window.png[] + +In contrast, a `simple` moving average with larger window (`"window": 100`) will smooth out all higher-frequency fluctuations, +leaving only low-frequency, long term trends. It also tends to "lag" behind the actual data by a substantial amount: + +[[movavg_100window]] +.Moving average with window of size 100 +image::images/movavg_100window.png[] + + +==== Linear + +The `linear` model assigns a linear weighting to points in the series, such that "older" datapoints (e.g. those at +the beginning of the window) contribute a linearly less amount to the total average. The linear weighting helps reduce +the "lag" behind the data's mean, since older points have less influence. + +[source,js] +-------------------------------------------------- +{ + "the_movavg":{ + "moving_avg":{ + "buckets_path": "the_sum", + "model" : "linear" + } +} +-------------------------------------------------- + +A `linear` model has no special settings to configure + +Like the `simple` model, window size can change the behavior of the moving average. For example, a small window (`"window": 10`) +will closely track the data and only smooth out small scale fluctuations: + +[[linear_10window]] +.Linear moving average with window of size 10 +image::images/linear_10window.png[] + +In contrast, a `linear` moving average with larger window (`"window": 100`) will smooth out all higher-frequency fluctuations, +leaving only low-frequency, long term trends. It also tends to "lag" behind the actual data by a substantial amount, +although typically less than the `simple` model: + +[[linear_100window]] +.Linear moving average with window of size 100 +image::images/linear_100window.png[] + +==== Single Exponential + +The `single_exp` model is similar to the `linear` model, except older data-points become exponentially less important, +rather than linearly less important. The speed at which the importance decays can be controlled with an `alpha` +setting. Small values make the weight decay slowly, which provides greater smoothing and takes into account a larger +portion of the window. Larger valuers make the weight decay quickly, which reduces the impact of older values on the +moving average. This tends to make the moving average track the data more closely but with less smoothing. + +The default value of `alpha` is `0.5`, and the setting accepts any float from 0-1 inclusive. + +[source,js] +-------------------------------------------------- +{ + "the_movavg":{ + "moving_avg":{ + "buckets_path": "the_sum", + "model" : "single_exp", + "settings" : { + "alpha" : 0.5 + } + } +} +-------------------------------------------------- + + + +[[single_0.2alpha]] +.Single Exponential moving average with window of size 10, alpha = 0.2 +image::images/single_0.2alpha.png[] + +[[single_0.7alpha]] +.Single Exponential moving average with window of size 10, alpha = 0.7 +image::images/single_0.7alpha.png[] + +==== Double Exponential + +The `double_exp` model, sometimes called "Holt's Linear Trend" model, incorporates a second exponential term which +tracks the data's trend. Single exponential does not perform well when the data has an underlying linear trend. The +double exponential model calculates two values internally: a "level" and a "trend". + +The level calculation is similar to `single_exp`, and is an exponentially weighted view of the data. The difference is +that the previously smoothed value is used instead of the raw value, which allows it to stay close to the original series. +The trend calculation looks at the difference between the current and last value (e.g. the slope, or trend, of the +smoothed data). The trend value is also exponentially weighted. + +Values are produced by multiplying the level and trend components. + +The default value of `alpha` and `beta` is `0.5`, and the settings accept any float from 0-1 inclusive. + +[source,js] +-------------------------------------------------- +{ + "the_movavg":{ + "moving_avg":{ + "buckets_path": "the_sum", + "model" : "double_exp", + "settings" : { + "alpha" : 0.5, + "beta" : 0.5 + } + } +} +-------------------------------------------------- + +In practice, the `alpha` value behaves very similarly in `double_exp` as `single_exp`: small values produce more smoothing +and more lag, while larger values produce closer tracking and less lag. The value of `beta` is often difficult +to see. Small values emphasize long-term trends (such as a constant linear trend in the whole series), while larger +values emphasize short-term trends. This will become more apparently when you are predicting values. + +[[double_0.2beta]] +.Double Exponential moving average with window of size 100, alpha = 0.5, beta = 0.2 +image::images/double_0.2beta.png[] + +[[double_0.7beta]] +.Double Exponential moving average with window of size 100, alpha = 0.5, beta = 0.7 +image::images/double_0.7beta.png[] + +=== Prediction + +All the moving average model support a "prediction" mode, which will attempt to extrapolate into the future given the +current smoothed, moving average. Depending on the model and parameter, these predictions may or may not be accurate. + +Predictions are enabled by adding a `predict` parameter to any moving average aggregation, specifying the nubmer of +predictions you would like appended to the end of the series. These predictions will be spaced out at the same interval +as your buckets: + +[source,js] +-------------------------------------------------- +{ + "the_movavg":{ + "moving_avg":{ + "buckets_path": "the_sum", + "model" : "simple", + "predict" 10 + } +} +-------------------------------------------------- + +The `simple`, `linear` and `single_exp` models all produce "flat" predictions: they essentially converge on the mean +of the last value in the series, producing a flat: + +[[simple_prediction]] +.Simple moving average with window of size 10, predict = 50 +image::images/simple_prediction.png[] + +In contrast, the `double_exp` model can extrapolate based on local or global constant trends. If we set a high `beta` +value, we can extrapolate based on local constant trends (in this case the predictions head down, because the data at the end +of the series was heading in a downward direction): + +[[double_prediction_local]] +.Double Exponential moving average with window of size 100, predict = 20, alpha = 0.5, beta = 0.8 +image::images/double_prediction_local.png[] + +In contrast, if we choose a small `beta`, the predictions are based on the global constant trend. In this series, the +global trend is slightly positive, so the prediction makes a sharp u-turn and begins a positive slope: + +[[double_prediction_global]] +.Double Exponential moving average with window of size 100, predict = 20, alpha = 0.5, beta = 0.1 +image::images/double_prediction_global.png[] \ No newline at end of file From 2a74f2ce0f8f963e0111be1c3c33b9d2201dc5c3 Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Wed, 22 Apr 2015 18:40:34 -0400 Subject: [PATCH 54/68] [TESTS] randomize metric type, better naming, fix gap handling - Randomizes the metric type between min/max/avg. Should have identical behavior, but good to test - Fixes improper handling of gaps due to a bug in the production of the "expected" dataset. Due to this fix, randomization of gap policy was re-enabled - Bunch of renaming to be more descriptive and less verbose --- .../reducers/moving/avg/MovAvgTests.java | 348 +++++++++++------- 1 file changed, 205 insertions(+), 143 deletions(-) diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java index 9c3a6f2341990..d6fd7750346b4 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java @@ -21,7 +21,6 @@ import com.google.common.collect.EvictingQueue; - import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.search.SearchPhaseExecutionException; import org.elasticsearch.action.search.SearchResponse; @@ -30,22 +29,21 @@ import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram; import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram.Bucket; +import org.elasticsearch.search.aggregations.metrics.ValuesSourceMetricsAggregationBuilder; import org.elasticsearch.search.aggregations.reducers.BucketHelpers; import org.elasticsearch.search.aggregations.reducers.SimpleValue; import org.elasticsearch.search.aggregations.reducers.movavg.models.*; import org.elasticsearch.test.ElasticsearchIntegrationTest; -import org.hamcrest.Matchers; import org.junit.Test; import java.util.ArrayList; import java.util.List; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; -import static org.elasticsearch.search.aggregations.AggregationBuilders.histogram; -import static org.elasticsearch.search.aggregations.AggregationBuilders.sum; -import static org.elasticsearch.search.aggregations.reducers.ReducerBuilders.smooth; +import static org.elasticsearch.search.aggregations.AggregationBuilders.*; +import static org.elasticsearch.search.aggregations.reducers.ReducerBuilders.movingAvg; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; -import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.*; import static org.hamcrest.core.IsNull.notNullValue; @ElasticsearchIntegrationTest.SuiteScopeTest @@ -62,16 +60,16 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { static BucketHelpers.GapPolicy gapPolicy; static long[] docCounts; - static long[] valueCounts; - static Double[] simpleMovAvgCounts; - static Double[] linearMovAvgCounts; - static Double[] singleExpMovAvgCounts; - static Double[] doubleExpMovAvgCounts; + static long[] docValues; + static Double[] simpleDocCounts; + static Double[] linearDocCounts; + static Double[] singleDocCounts; + static Double[] doubleDocCounts; - static Double[] simpleMovAvgValueCounts; - static Double[] linearMovAvgValueCounts; - static Double[] singleExpMovAvgValueCounts; - static Double[] doubleExpMovAvgValueCounts; + static Double[] simpleDocValues; + static Double[] linearDocValues; + static Double[] singleDocValues; + static Double[] doubleDocValues; @Override public void setupSuiteScopeCluster() throws Exception { @@ -83,13 +81,14 @@ public void setupSuiteScopeCluster() throws Exception { numValueBuckets = randomIntBetween(6, 80); numFilledValueBuckets = numValueBuckets; windowSize = randomIntBetween(3,10); - gapPolicy = BucketHelpers.GapPolicy.INSERT_ZEROS; // TODO randomBoolean() ? BucketHelpers.GapPolicy.IGNORE : BucketHelpers.GapPolicy.INSERT_ZEROS; - + gapPolicy = randomBoolean() ? BucketHelpers.GapPolicy.IGNORE : BucketHelpers.GapPolicy.INSERT_ZEROS; + + docCounts = new long[numValueBuckets]; - valueCounts = new long[numValueBuckets]; + docValues = new long[numValueBuckets]; for (int i = 0; i < numValueBuckets; i++) { docCounts[i] = randomIntBetween(0, 20); - valueCounts[i] = randomIntBetween(1,20); //this will be used as a constant for all values within a bucket + docValues[i] = randomIntBetween(1,20); //this will be used as a constant for all values within a bucket } // Used for the gap tests @@ -104,14 +103,12 @@ public void setupSuiteScopeCluster() throws Exception { this.setupLinear(); this.setupSingle(); this.setupDouble(); - - - + for (int i = 0; i < numValueBuckets; i++) { for (int docs = 0; docs < docCounts[i]; docs++) { builders.add(client().prepareIndex("idx", "type").setSource(jsonBuilder().startObject() .field(SINGLE_VALUED_FIELD_NAME, i * interval) - .field(SINGLE_VALUED_VALUE_FIELD_NAME, 1).endObject())); + .field(SINGLE_VALUED_VALUE_FIELD_NAME, docValues[i]).endObject())); } } @@ -120,11 +117,13 @@ public void setupSuiteScopeCluster() throws Exception { } private void setupSimple() { - simpleMovAvgCounts = new Double[numValueBuckets]; + simpleDocCounts = new Double[numValueBuckets]; EvictingQueue window = EvictingQueue.create(windowSize); for (int i = 0; i < numValueBuckets; i++) { - double thisValue = docCounts[i]; - window.offer(thisValue); + if (docCounts[i] == 0 && gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { + continue; + } + window.offer((double)docCounts[i]); double movAvg = 0; for (double value : window) { @@ -132,13 +131,26 @@ private void setupSimple() { } movAvg /= window.size(); - simpleMovAvgCounts[i] = movAvg; + simpleDocCounts[i] = movAvg; } window.clear(); - simpleMovAvgValueCounts = new Double[numValueBuckets]; + simpleDocValues = new Double[numValueBuckets]; for (int i = 0; i < numValueBuckets; i++) { - window.offer((double)docCounts[i]); + if (docCounts[i] == 0) { + // If there was a gap in doc counts and we are ignoring, just skip this bucket + if (gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { + continue; + } else if (gapPolicy.equals(BucketHelpers.GapPolicy.INSERT_ZEROS)) { + // otherwise insert a zero instead of the true value + window.offer(0.0); + } else { + window.offer((double) docValues[i]); + } + } else { + //if there are docs in this bucket, insert the regular value + window.offer((double) docValues[i]); + } double movAvg = 0; for (double value : window) { @@ -146,7 +158,7 @@ private void setupSimple() { } movAvg /= window.size(); - simpleMovAvgValueCounts[i] = movAvg; + simpleDocValues[i] = movAvg; } @@ -154,14 +166,13 @@ private void setupSimple() { private void setupLinear() { EvictingQueue window = EvictingQueue.create(windowSize); - linearMovAvgCounts = new Double[numValueBuckets]; + linearDocCounts = new Double[numValueBuckets]; window.clear(); for (int i = 0; i < numValueBuckets; i++) { - double thisValue = docCounts[i]; - if (thisValue == -1) { - thisValue = 0; + if (docCounts[i] == 0 && gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { + continue; } - window.offer(thisValue); + window.offer((double)docCounts[i]); double avg = 0; long totalWeight = 1; @@ -172,15 +183,27 @@ private void setupLinear() { totalWeight += current; current += 1; } - linearMovAvgCounts[i] = avg / totalWeight; + linearDocCounts[i] = avg / totalWeight; } window.clear(); - linearMovAvgValueCounts = new Double[numValueBuckets]; + linearDocValues = new Double[numValueBuckets]; for (int i = 0; i < numValueBuckets; i++) { - double thisValue = docCounts[i]; - window.offer(thisValue); + if (docCounts[i] == 0) { + // If there was a gap in doc counts and we are ignoring, just skip this bucket + if (gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { + continue; + } else if (gapPolicy.equals(BucketHelpers.GapPolicy.INSERT_ZEROS)) { + // otherwise insert a zero instead of the true value + window.offer(0.0); + } else { + window.offer((double) docValues[i]); + } + } else { + //if there are docs in this bucket, insert the regular value + window.offer((double) docValues[i]); + } double avg = 0; long totalWeight = 1; @@ -191,19 +214,18 @@ private void setupLinear() { totalWeight += current; current += 1; } - linearMovAvgValueCounts[i] = avg / totalWeight; + linearDocValues[i] = avg / totalWeight; } } private void setupSingle() { EvictingQueue window = EvictingQueue.create(windowSize); - singleExpMovAvgCounts = new Double[numValueBuckets]; + singleDocCounts = new Double[numValueBuckets]; for (int i = 0; i < numValueBuckets; i++) { - double thisValue = docCounts[i]; - if (thisValue == -1) { - thisValue = 0; + if (docCounts[i] == 0 && gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { + continue; } - window.offer(thisValue); + window.offer((double)docCounts[i]); double avg = 0; double alpha = 0.5; @@ -217,14 +239,27 @@ private void setupSingle() { avg = (value * alpha) + (avg * (1 - alpha)); } } - singleExpMovAvgCounts[i] = avg ; + singleDocCounts[i] = avg ; } - singleExpMovAvgValueCounts = new Double[numValueBuckets]; + singleDocValues = new Double[numValueBuckets]; window.clear(); for (int i = 0; i < numValueBuckets; i++) { - window.offer((double)docCounts[i]); + if (docCounts[i] == 0) { + // If there was a gap in doc counts and we are ignoring, just skip this bucket + if (gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { + continue; + } else if (gapPolicy.equals(BucketHelpers.GapPolicy.INSERT_ZEROS)) { + // otherwise insert a zero instead of the true value + window.offer(0.0); + } else { + window.offer((double) docValues[i]); + } + } else { + //if there are docs in this bucket, insert the regular value + window.offer((double) docValues[i]); + } double avg = 0; double alpha = 0.5; @@ -238,21 +273,20 @@ private void setupSingle() { avg = (value * alpha) + (avg * (1 - alpha)); } } - singleExpMovAvgCounts[i] = avg ; + singleDocValues[i] = avg ; } } private void setupDouble() { EvictingQueue window = EvictingQueue.create(windowSize); - doubleExpMovAvgCounts = new Double[numValueBuckets]; + doubleDocCounts = new Double[numValueBuckets]; for (int i = 0; i < numValueBuckets; i++) { - double thisValue = docCounts[i]; - if (thisValue == -1) { - thisValue = 0; + if (docCounts[i] == 0 && gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { + continue; } - window.offer(thisValue); + window.offer((double)docCounts[i]); double s = 0; double last_s = 0; @@ -281,14 +315,27 @@ private void setupDouble() { last_b = b; } - doubleExpMovAvgCounts[i] = s + (0 * b) ; + doubleDocCounts[i] = s + (0 * b) ; } - doubleExpMovAvgValueCounts = new Double[numValueBuckets]; + doubleDocValues = new Double[numValueBuckets]; window.clear(); for (int i = 0; i < numValueBuckets; i++) { - window.offer((double)docCounts[i]); + if (docCounts[i] == 0) { + // If there was a gap in doc counts and we are ignoring, just skip this bucket + if (gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { + continue; + } else if (gapPolicy.equals(BucketHelpers.GapPolicy.INSERT_ZEROS)) { + // otherwise insert a zero instead of the true value + window.offer(0.0); + } else { + window.offer((double) docValues[i]); + } + } else { + //if there are docs in this bucket, insert the regular value + window.offer((double) docValues[i]); + } double s = 0; double last_s = 0; @@ -317,7 +364,7 @@ private void setupDouble() { last_b = b; } - doubleExpMovAvgValueCounts[i] = s + (0 * b) ; + doubleDocValues[i] = s + (0 * b) ; } } @@ -332,8 +379,8 @@ public void simpleSingleValuedField() { .addAggregation( histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) - .subAggregation(movingAvg("movingAvg") + .subAggregation(randomMetric("the_metric", SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(movingAvg("movavg_counts") .window(windowSize) .modelBuilder(new SimpleModel.SimpleModelBuilder()) .gapPolicy(gapPolicy) @@ -342,7 +389,7 @@ public void simpleSingleValuedField() { .window(windowSize) .modelBuilder(new SimpleModel.SimpleModelBuilder()) .gapPolicy(gapPolicy) - .setBucketsPaths("the_sum")) + .setBucketsPaths("the_metric")) ).execute().actionGet(); assertSearchResponse(response); @@ -356,13 +403,13 @@ public void simpleSingleValuedField() { for (int i = 0; i < numValueBuckets; ++i) { Histogram.Bucket bucket = buckets.get(i); checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, docCounts[i]); - SimpleValue docCountMovAvg = bucket.getAggregations().get("movingAvg"); + SimpleValue docCountMovAvg = bucket.getAggregations().get("movavg_counts"); assertThat(docCountMovAvg, notNullValue()); - assertThat(docCountMovAvg.value(), equalTo(simpleMovAvgCounts[i])); + assertThat(docCountMovAvg.value(), equalTo(simpleDocCounts[i])); SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); assertThat(valuesMovAvg, notNullValue()); - assertThat(valuesMovAvg.value(), equalTo(simpleMovAvgCounts[i])); + assertThat(valuesMovAvg.value(), equalTo(simpleDocValues[i])); } } @@ -377,8 +424,8 @@ public void linearSingleValuedField() { .addAggregation( histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) - .subAggregation(movingAvg("movingAvg") + .subAggregation(randomMetric("the_metric", SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(movingAvg("movavg_counts") .window(windowSize) .modelBuilder(new LinearModel.LinearModelBuilder()) .gapPolicy(gapPolicy) @@ -387,7 +434,7 @@ public void linearSingleValuedField() { .window(windowSize) .modelBuilder(new LinearModel.LinearModelBuilder()) .gapPolicy(gapPolicy) - .setBucketsPaths("the_sum")) + .setBucketsPaths("the_metric")) ).execute().actionGet(); assertSearchResponse(response); @@ -401,13 +448,13 @@ public void linearSingleValuedField() { for (int i = 0; i < numValueBuckets; ++i) { Histogram.Bucket bucket = buckets.get(i); checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, docCounts[i]); - SimpleValue docCountMovAvg = bucket.getAggregations().get("movingAvg"); + SimpleValue docCountMovAvg = bucket.getAggregations().get("movavg_counts"); assertThat(docCountMovAvg, notNullValue()); - assertThat(docCountMovAvg.value(), equalTo(linearMovAvgCounts[i])); + assertThat(docCountMovAvg.value(), equalTo(linearDocCounts[i])); SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); assertThat(valuesMovAvg, notNullValue()); - assertThat(valuesMovAvg.value(), equalTo(linearMovAvgCounts[i])); + assertThat(valuesMovAvg.value(), equalTo(linearDocValues[i])); } } @@ -422,8 +469,8 @@ public void singleExpSingleValuedField() { .addAggregation( histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) - .subAggregation(movingAvg("movingAvg") + .subAggregation(randomMetric("the_metric", SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(movingAvg("movavg_counts") .window(windowSize) .modelBuilder(new SingleExpModel.SingleExpModelBuilder().alpha(0.5)) .gapPolicy(gapPolicy) @@ -432,7 +479,7 @@ public void singleExpSingleValuedField() { .window(windowSize) .modelBuilder(new SingleExpModel.SingleExpModelBuilder().alpha(0.5)) .gapPolicy(gapPolicy) - .setBucketsPaths("the_sum")) + .setBucketsPaths("the_metric")) ).execute().actionGet(); assertSearchResponse(response); @@ -446,13 +493,13 @@ public void singleExpSingleValuedField() { for (int i = 0; i < numValueBuckets; ++i) { Histogram.Bucket bucket = buckets.get(i); checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, docCounts[i]); - SimpleValue docCountMovAvg = bucket.getAggregations().get("movingAvg"); + SimpleValue docCountMovAvg = bucket.getAggregations().get("movavg_counts"); assertThat(docCountMovAvg, notNullValue()); - assertThat(docCountMovAvg.value(), equalTo(singleExpMovAvgCounts[i])); + assertThat(docCountMovAvg.value(), equalTo(singleDocCounts[i])); SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); assertThat(valuesMovAvg, notNullValue()); - assertThat(valuesMovAvg.value(), equalTo(singleExpMovAvgCounts[i])); + assertThat(valuesMovAvg.value(), equalTo(singleDocValues[i])); } } @@ -467,8 +514,8 @@ public void doubleExpSingleValuedField() { .addAggregation( histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) - .subAggregation(movingAvg("movingAvg") + .subAggregation(randomMetric("the_metric", SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(movingAvg("movavg_counts") .window(windowSize) .modelBuilder(new DoubleExpModel.DoubleExpModelBuilder().alpha(0.5).beta(0.5)) .gapPolicy(gapPolicy) @@ -477,7 +524,7 @@ public void doubleExpSingleValuedField() { .window(windowSize) .modelBuilder(new DoubleExpModel.DoubleExpModelBuilder().alpha(0.5).beta(0.5)) .gapPolicy(gapPolicy) - .setBucketsPaths("the_sum")) + .setBucketsPaths("the_metric")) ).execute().actionGet(); assertSearchResponse(response); @@ -491,13 +538,13 @@ public void doubleExpSingleValuedField() { for (int i = 0; i < numValueBuckets; ++i) { Histogram.Bucket bucket = buckets.get(i); checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, docCounts[i]); - SimpleValue docCountMovAvg = bucket.getAggregations().get("movingAvg"); + SimpleValue docCountMovAvg = bucket.getAggregations().get("movavg_counts"); assertThat(docCountMovAvg, notNullValue()); - assertThat(docCountMovAvg.value(), equalTo(doubleExpMovAvgCounts[i])); + assertThat(docCountMovAvg.value(), equalTo(doubleDocCounts[i])); SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); assertThat(valuesMovAvg, notNullValue()); - assertThat(valuesMovAvg.value(), equalTo(doubleExpMovAvgCounts[i])); + assertThat(valuesMovAvg.value(), equalTo(doubleDocValues[i])); } } @@ -509,12 +556,12 @@ public void testSizeZeroWindow() { .addAggregation( histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) - .subAggregation(movingAvg("movingAvg") + .subAggregation(randomMetric("the_metric", SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(movingAvg("movavg_counts") .window(0) .modelBuilder(new SimpleModel.SimpleModelBuilder()) .gapPolicy(gapPolicy) - .setBucketsPaths("the_sum")) + .setBucketsPaths("the_metric")) ).execute().actionGet(); fail("MovingAvg should not accept a window that is zero"); @@ -531,13 +578,13 @@ public void testBadParent() { client() .prepareSearch("idx") .addAggregation( - range("histo").field(SINGLE_VALUED_FIELD_NAME).addRange(0,10) - .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) - .subAggregation(movingAvg("movingAvg") + range("histo").field(SINGLE_VALUED_FIELD_NAME).addRange(0, 10) + .subAggregation(randomMetric("the_metric", SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(movingAvg("movavg_counts") .window(0) .modelBuilder(new SimpleModel.SimpleModelBuilder()) .gapPolicy(gapPolicy) - .setBucketsPaths("the_sum")) + .setBucketsPaths("the_metric")) ).execute().actionGet(); fail("MovingAvg should not accept non-histogram as parent"); @@ -554,8 +601,8 @@ public void testNegativeWindow() { .addAggregation( histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) - .subAggregation(movingAvg("movingAvg") + .subAggregation(randomMetric("the_metric", SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(movingAvg("movavg_counts") .window(-10) .modelBuilder(new SimpleModel.SimpleModelBuilder()) .gapPolicy(gapPolicy) @@ -578,12 +625,12 @@ public void testNoBucketsInHistogram() { .addAggregation( histogram("histo").field("test").interval(interval).minDocCount(0) .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) - .subAggregation(movingAvg("movingAvg") + .subAggregation(randomMetric("the_metric", SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(movingAvg("movavg_counts") .window(windowSize) .modelBuilder(new SimpleModel.SimpleModelBuilder()) .gapPolicy(gapPolicy) - .setBucketsPaths("the_sum")) + .setBucketsPaths("the_metric")) ).execute().actionGet(); assertSearchResponse(response); @@ -603,13 +650,13 @@ public void testZeroPrediction() { .addAggregation( histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) - .subAggregation(movingAvg("movingAvg") + .subAggregation(randomMetric("the_metric", SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(movingAvg("movavg_counts") .window(windowSize) .modelBuilder(randomModelBuilder()) .gapPolicy(gapPolicy) .predict(0) - .setBucketsPaths("the_sum")) + .setBucketsPaths("the_metric")) ).execute().actionGet(); fail("MovingAvg should not accept a prediction size that is zero"); @@ -626,13 +673,13 @@ public void testNegativePrediction() { .addAggregation( histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(sum("the_sum").field(SINGLE_VALUED_VALUE_FIELD_NAME)) - .subAggregation(movingAvg("movingAvg") + .subAggregation(randomMetric("the_metric", SINGLE_VALUED_VALUE_FIELD_NAME)) + .subAggregation(movingAvg("movavg_counts") .window(windowSize) .modelBuilder(randomModelBuilder()) .gapPolicy(gapPolicy) .predict(-10) - .setBucketsPaths("the_sum")) + .setBucketsPaths("the_metric")) ).execute().actionGet(); fail("MovingAvg should not accept a prediction size that is negative"); @@ -655,12 +702,12 @@ public void testGiantGap() { .addAggregation( histogram("histo").field("gap_test").interval(interval).minDocCount(0) .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(sum("the_sum").field(GAP_FIELD)) - .subAggregation(movingAvg("movingAvg") + .subAggregation(randomMetric("the_metric", GAP_FIELD)) + .subAggregation(movingAvg("movavg_counts") .window(windowSize) .modelBuilder(randomModelBuilder()) .gapPolicy(gapPolicy) - .setBucketsPaths("the_sum")) + .setBucketsPaths("the_metric")) ).execute().actionGet(); assertSearchResponse(response); @@ -671,12 +718,12 @@ public void testGiantGap() { List buckets = histo.getBuckets(); assertThat(buckets.size(), equalTo(numValueBuckets)); - double lastValue = ((SimpleValue)(buckets.get(0).getAggregations().get("movingAvg"))).value(); + double lastValue = ((SimpleValue)(buckets.get(0).getAggregations().get("movavg_counts"))).value(); assertThat(Double.compare(lastValue, 0.0d), greaterThanOrEqualTo(0)); double currentValue; for (int i = 1; i < numValueBuckets - 2; i++) { - currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movingAvg"))).value(); + currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movavg_counts"))).value(); // Since there are only two values in this test, at the beginning and end, the moving average should // decrease every step (until it reaches zero). Crude way to check that it's doing the right thing @@ -687,7 +734,7 @@ public void testGiantGap() { } // The last bucket has a real value, so this should always increase the moving avg - currentValue = ((SimpleValue)(buckets.get(numValueBuckets - 1).getAggregations().get("movingAvg"))).value(); + currentValue = ((SimpleValue)(buckets.get(numValueBuckets - 1).getAggregations().get("movavg_counts"))).value(); assertThat(Double.compare(lastValue, currentValue), equalTo(-1)); } @@ -698,19 +745,19 @@ public void testGiantGap() { public void testGiantGapWithPredict() { MovAvgModelBuilder model = randomModelBuilder(); - int numPredictions = randomIntBetween(0, 10); + int numPredictions = randomIntBetween(1, 10); SearchResponse response = client() .prepareSearch("idx") .addAggregation( histogram("histo").field("gap_test").interval(interval).minDocCount(0) .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(sum("the_sum").field(GAP_FIELD)) - .subAggregation(movingAvg("movingAvg") + .subAggregation(randomMetric("the_metric", GAP_FIELD)) + .subAggregation(movingAvg("movavg_counts") .window(windowSize) .modelBuilder(model) .gapPolicy(gapPolicy) .predict(numPredictions) - .setBucketsPaths("the_sum")) + .setBucketsPaths("the_metric")) ).execute().actionGet(); assertSearchResponse(response); @@ -721,12 +768,12 @@ public void testGiantGapWithPredict() { List buckets = histo.getBuckets(); assertThat(buckets.size(), equalTo(numValueBuckets + numPredictions)); - double lastValue = ((SimpleValue)(buckets.get(0).getAggregations().get("movingAvg"))).value(); + double lastValue = ((SimpleValue)(buckets.get(0).getAggregations().get("movavg_counts"))).value(); assertThat(Double.compare(lastValue, 0.0d), greaterThanOrEqualTo(0)); double currentValue; for (int i = 1; i < numValueBuckets - 2; i++) { - currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movingAvg"))).value(); + currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movavg_counts"))).value(); // Since there are only two values in this test, at the beginning and end, the moving average should // decrease every step (until it reaches zero). Crude way to check that it's doing the right thing @@ -737,15 +784,15 @@ public void testGiantGapWithPredict() { } // The last bucket has a real value, so this should always increase the moving avg - currentValue = ((SimpleValue)(buckets.get(numValueBuckets - 1).getAggregations().get("movingAvg"))).value(); + currentValue = ((SimpleValue)(buckets.get(numValueBuckets - 1).getAggregations().get("movavg_counts"))).value(); assertThat(Double.compare(lastValue, currentValue), equalTo(-1)); // Now check predictions for (int i = numValueBuckets; i < numValueBuckets + numPredictions; i++) { // Unclear at this point which direction the predictions will go, just verify they are - // not null, and that we don't have the_sum anymore - assertThat((buckets.get(i).getAggregations().get("movingAvg")), notNullValue()); - assertThat((buckets.get(i).getAggregations().get("the_sum")), nullValue()); + // not null, and that we don't have the_metric anymore + assertThat((buckets.get(i).getAggregations().get("movavg_counts")), notNullValue()); + assertThat((buckets.get(i).getAggregations().get("the_metric")), nullValue()); } } @@ -763,12 +810,12 @@ public void testLeftGap() { filter("filtered").filter(new RangeFilterBuilder("gap_test").from(1)).subAggregation( histogram("histo").field("gap_test").interval(interval).minDocCount(0) .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(sum("the_sum").field(GAP_FIELD)) - .subAggregation(movingAvg("movingAvg") + .subAggregation(randomMetric("the_metric", GAP_FIELD)) + .subAggregation(movingAvg("movavg_counts") .window(windowSize) .modelBuilder(randomModelBuilder()) .gapPolicy(gapPolicy) - .setBucketsPaths("the_sum")) + .setBucketsPaths("the_metric")) ) ).execute().actionGet(); @@ -789,7 +836,7 @@ public void testLeftGap() { double currentValue; double lastValue = 0.0; for (int i = 0; i < numValueBuckets - 1; i++) { - currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movingAvg"))).value(); + currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movavg_counts"))).value(); assertThat(Double.compare(lastValue, currentValue), lessThanOrEqualTo(0)); lastValue = currentValue; @@ -808,13 +855,13 @@ public void testLeftGapWithPrediction() { filter("filtered").filter(new RangeFilterBuilder("gap_test").from(1)).subAggregation( histogram("histo").field("gap_test").interval(interval).minDocCount(0) .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(sum("the_sum").field(GAP_FIELD)) - .subAggregation(movingAvg("movingAvg") + .subAggregation(randomMetric("the_metric", GAP_FIELD)) + .subAggregation(movingAvg("movavg_counts") .window(windowSize) .modelBuilder(randomModelBuilder()) .gapPolicy(gapPolicy) .predict(numPredictions) - .setBucketsPaths("the_sum")) + .setBucketsPaths("the_metric")) ) ).execute().actionGet(); @@ -835,7 +882,7 @@ public void testLeftGapWithPrediction() { double currentValue; double lastValue = 0.0; for (int i = 0; i < numValueBuckets - 1; i++) { - currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movingAvg"))).value(); + currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movavg_counts"))).value(); assertThat(Double.compare(lastValue, currentValue), lessThanOrEqualTo(0)); lastValue = currentValue; @@ -844,9 +891,9 @@ public void testLeftGapWithPrediction() { // Now check predictions for (int i = numValueBuckets; i < numValueBuckets + numPredictions; i++) { // Unclear at this point which direction the predictions will go, just verify they are - // not null, and that we don't have the_sum anymore - assertThat((buckets.get(i).getAggregations().get("movingAvg")), notNullValue()); - assertThat((buckets.get(i).getAggregations().get("the_sum")), nullValue()); + // not null, and that we don't have the_metric anymore + assertThat((buckets.get(i).getAggregations().get("movavg_counts")), notNullValue()); + assertThat((buckets.get(i).getAggregations().get("the_metric")), nullValue()); } } @@ -864,12 +911,12 @@ public void testRightGap() { filter("filtered").filter(new RangeFilterBuilder("gap_test").to((interval * (numValueBuckets - 1) - interval))).subAggregation( histogram("histo").field("gap_test").interval(interval).minDocCount(0) .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(sum("the_sum").field(GAP_FIELD)) - .subAggregation(movingAvg("movingAvg") + .subAggregation(randomMetric("the_metric", GAP_FIELD)) + .subAggregation(movingAvg("movavg_counts") .window(windowSize) .modelBuilder(randomModelBuilder()) .gapPolicy(gapPolicy) - .setBucketsPaths("the_sum")) + .setBucketsPaths("the_metric")) ) ).execute().actionGet(); @@ -888,9 +935,9 @@ public void testRightGap() { assertThat(buckets.size(), equalTo(numValueBuckets)); double currentValue; - double lastValue = ((SimpleValue)(buckets.get(0).getAggregations().get("movingAvg"))).value(); + double lastValue = ((SimpleValue)(buckets.get(0).getAggregations().get("movavg_counts"))).value(); for (int i = 1; i < numValueBuckets - 1; i++) { - currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movingAvg"))).value(); + currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movavg_counts"))).value(); assertThat(Double.compare(lastValue, currentValue), greaterThanOrEqualTo(0)); lastValue = currentValue; @@ -909,13 +956,13 @@ public void testRightGapWithPredictions() { filter("filtered").filter(new RangeFilterBuilder("gap_test").to((interval * (numValueBuckets - 1) - interval))).subAggregation( histogram("histo").field("gap_test").interval(interval).minDocCount(0) .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(sum("the_sum").field(GAP_FIELD)) - .subAggregation(movingAvg("movingAvg") + .subAggregation(randomMetric("the_metric", GAP_FIELD)) + .subAggregation(movingAvg("movavg_counts") .window(windowSize) .modelBuilder(randomModelBuilder()) .gapPolicy(gapPolicy) .predict(numPredictions) - .setBucketsPaths("the_sum")) + .setBucketsPaths("the_metric")) ) ).execute().actionGet(); @@ -934,9 +981,9 @@ public void testRightGapWithPredictions() { assertThat(buckets.size(), equalTo(numValueBuckets + numPredictions)); double currentValue; - double lastValue = ((SimpleValue)(buckets.get(0).getAggregations().get("movingAvg"))).value(); + double lastValue = ((SimpleValue)(buckets.get(0).getAggregations().get("movavg_counts"))).value(); for (int i = 1; i < numValueBuckets - 1; i++) { - currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movingAvg"))).value(); + currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movavg_counts"))).value(); assertThat(Double.compare(lastValue, currentValue), greaterThanOrEqualTo(0)); lastValue = currentValue; @@ -945,9 +992,9 @@ public void testRightGapWithPredictions() { // Now check predictions for (int i = numValueBuckets; i < numValueBuckets + numPredictions; i++) { // Unclear at this point which direction the predictions will go, just verify they are - // not null, and that we don't have the_sum anymore - assertThat((buckets.get(i).getAggregations().get("movingAvg")), notNullValue()); - assertThat((buckets.get(i).getAggregations().get("the_sum")), nullValue()); + // not null, and that we don't have the_metric anymore + assertThat((buckets.get(i).getAggregations().get("movavg_counts")), notNullValue()); + assertThat((buckets.get(i).getAggregations().get("the_metric")), nullValue()); } } @@ -962,13 +1009,13 @@ public void testPredictWithNoBuckets() { // Filter so we are above all values filter("filtered").filter(new RangeFilterBuilder("gap_test").from((interval * (numValueBuckets - 1) + interval))).subAggregation( histogram("histo").field("gap_test").interval(interval).minDocCount(0) - .subAggregation(sum("the_sum").field(GAP_FIELD)) - .subAggregation(movingAvg("movingAvg") + .subAggregation(randomMetric("the_metric", GAP_FIELD)) + .subAggregation(movingAvg("movavg_counts") .window(windowSize) .modelBuilder(randomModelBuilder()) .gapPolicy(gapPolicy) .predict(numPredictions) - .setBucketsPaths("the_sum")) + .setBucketsPaths("the_metric")) ) ).execute().actionGet(); @@ -1014,5 +1061,20 @@ private MovAvgModelBuilder randomModelBuilder() { return new SimpleModel.SimpleModelBuilder(); } } + + private ValuesSourceMetricsAggregationBuilder randomMetric(String name, String field) { + int rand = randomIntBetween(0,3); + + switch (rand) { + case 0: + return min(name).field(field); + case 2: + return max(name).field(field); + case 3: + return avg(name).field(field); + default: + return avg(name).field(field); + } + } } From e08e45cee8eeda7e4f8f865048ef25429988521d Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Wed, 22 Apr 2015 18:42:47 -0400 Subject: [PATCH 55/68] [DOCS] Add link to movavg page --- docs/reference/search/aggregations/reducer.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/reference/search/aggregations/reducer.asciidoc b/docs/reference/search/aggregations/reducer.asciidoc index 5b3bff11c1898..75ac8b9a49ae6 100644 --- a/docs/reference/search/aggregations/reducer.asciidoc +++ b/docs/reference/search/aggregations/reducer.asciidoc @@ -1,3 +1,4 @@ [[search-aggregations-reducer]] include::reducer/derivative.asciidoc[] +include::reducer/movavg-reducer.asciidoc[] From 1a1ddceb47f7842241de43cf668112da67f07ea7 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Thu, 23 Apr 2015 09:42:05 +0100 Subject: [PATCH 56/68] Muted failing MovAvgTests --- .../reducers/moving/avg/MovAvgTests.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java index d6fd7750346b4..bab47aadb8085 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java @@ -21,6 +21,8 @@ import com.google.common.collect.EvictingQueue; + +import org.apache.lucene.util.LuceneTestCase.AwaitsFix; import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.search.SearchPhaseExecutionException; import org.elasticsearch.action.search.SearchResponse; @@ -32,7 +34,11 @@ import org.elasticsearch.search.aggregations.metrics.ValuesSourceMetricsAggregationBuilder; import org.elasticsearch.search.aggregations.reducers.BucketHelpers; import org.elasticsearch.search.aggregations.reducers.SimpleValue; -import org.elasticsearch.search.aggregations.reducers.movavg.models.*; +import org.elasticsearch.search.aggregations.reducers.movavg.models.DoubleExpModel; +import org.elasticsearch.search.aggregations.reducers.movavg.models.LinearModel; +import org.elasticsearch.search.aggregations.reducers.movavg.models.MovAvgModelBuilder; +import org.elasticsearch.search.aggregations.reducers.movavg.models.SimpleModel; +import org.elasticsearch.search.aggregations.reducers.movavg.models.SingleExpModel; import org.elasticsearch.test.ElasticsearchIntegrationTest; import org.junit.Test; @@ -40,13 +46,22 @@ import java.util.List; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; -import static org.elasticsearch.search.aggregations.AggregationBuilders.*; +import static org.elasticsearch.search.aggregations.AggregationBuilders.avg; +import static org.elasticsearch.search.aggregations.AggregationBuilders.filter; +import static org.elasticsearch.search.aggregations.AggregationBuilders.histogram; +import static org.elasticsearch.search.aggregations.AggregationBuilders.max; +import static org.elasticsearch.search.aggregations.AggregationBuilders.min; +import static org.elasticsearch.search.aggregations.AggregationBuilders.range; import static org.elasticsearch.search.aggregations.reducers.ReducerBuilders.movingAvg; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.IsNull.notNullValue; @ElasticsearchIntegrationTest.SuiteScopeTest +@AwaitsFix(bugUrl = "Gap test logic seems to fail a lot of the time on CI build") public class MovAvgTests extends ElasticsearchIntegrationTest { private static final String SINGLE_VALUED_FIELD_NAME = "l_value"; From 0ff4827e55457d802ca6110d62e4b91816a09087 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Thu, 23 Apr 2015 10:44:23 +0100 Subject: [PATCH 57/68] Fix MaxBucketReducer to use gapPolicy Also moved gapPolicy and format ParseField constants to common class --- .../search/aggregations/reducers/Reducer.java | 3 + .../bucketmetrics/MaxBucketBuilder.java | 11 ++++ .../bucketmetrics/MaxBucketParser.java | 6 +- .../bucketmetrics/MaxBucketReducer.java | 15 +++-- .../reducers/derivative/DerivativeParser.java | 7 +-- .../reducers/movavg/MovAvgParser.java | 5 +- .../aggregations/reducers/MaxBucketTests.java | 61 +++++++++++++++++++ 7 files changed, 93 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java index 5ec45064c7f7f..8daa4d6180a84 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/Reducer.java @@ -47,6 +47,9 @@ public static interface Parser { public static final ParseField BUCKETS_PATH = new ParseField("buckets_path"); + public static final ParseField FORMAT = new ParseField("format"); + public static final ParseField GAP_POLICY = new ParseField("gap_policy"); + /** * @return The reducer type this parser is associated with. */ diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketBuilder.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketBuilder.java index eb04617e548ff..7fbcd54f789c4 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketBuilder.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketBuilder.java @@ -20,13 +20,16 @@ package org.elasticsearch.search.aggregations.reducers.bucketmetrics; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; import org.elasticsearch.search.aggregations.reducers.ReducerBuilder; +import org.elasticsearch.search.aggregations.reducers.derivative.DerivativeParser; import java.io.IOException; public class MaxBucketBuilder extends ReducerBuilder { private String format; + private GapPolicy gapPolicy; public MaxBucketBuilder(String name) { super(name, MaxBucketReducer.TYPE.name()); @@ -37,11 +40,19 @@ public MaxBucketBuilder format(String format) { return this; } + public MaxBucketBuilder gapPolicy(GapPolicy gapPolicy) { + this.gapPolicy = gapPolicy; + return this; + } + @Override protected XContentBuilder internalXContent(XContentBuilder builder, Params params) throws IOException { if (format != null) { builder.field(MaxBucketParser.FORMAT.getPreferredName(), format); } + if (gapPolicy != null) { + builder.field(DerivativeParser.GAP_POLICY.getPreferredName(), gapPolicy.getName()); + } return builder; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketParser.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketParser.java index 2a9dab3b6bdb1..7d773747a8d04 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketParser.java @@ -22,6 +22,7 @@ import org.elasticsearch.common.ParseField; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.SearchParseException; +import org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.reducers.ReducerFactory; import org.elasticsearch.search.aggregations.support.format.ValueFormat; @@ -46,6 +47,7 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon String currentFieldName = null; String[] bucketsPaths = null; String format = null; + GapPolicy gapPolicy = GapPolicy.IGNORE; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { @@ -55,6 +57,8 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon format = parser.text(); } else if (BUCKETS_PATH.match(currentFieldName)) { bucketsPaths = new String[] { parser.text() }; + } else if (GAP_POLICY.match(currentFieldName)) { + gapPolicy = GapPolicy.parse(context, parser.text()); } else { throw new SearchParseException(context, "Unknown key for a " + token + " in [" + reducerName + "]: [" + currentFieldName + "]."); @@ -86,7 +90,7 @@ public ReducerFactory parse(String reducerName, XContentParser parser, SearchCon formatter = ValueFormat.Patternable.Number.format(format).formatter(); } - return new MaxBucketReducer.Factory(reducerName, bucketsPaths, formatter); + return new MaxBucketReducer.Factory(reducerName, bucketsPaths, gapPolicy, formatter); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketReducer.java index e209684797c61..b325697568e67 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketReducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/bucketmetrics/MaxBucketReducer.java @@ -61,6 +61,7 @@ public MaxBucketReducer readResult(StreamInput in) throws IOException { }; private ValueFormatter formatter; + private GapPolicy gapPolicy; public static void registerStreams() { ReducerStreams.registerStream(STREAM, TYPE.stream()); @@ -69,8 +70,10 @@ public static void registerStreams() { private MaxBucketReducer() { } - protected MaxBucketReducer(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter, Map metaData) { + protected MaxBucketReducer(String name, String[] bucketsPaths, GapPolicy gapPolicy, @Nullable ValueFormatter formatter, + Map metaData) { super(name, bucketsPaths, metaData); + this.gapPolicy = gapPolicy; this.formatter = formatter; } @@ -90,7 +93,7 @@ public InternalAggregation doReduce(Aggregations aggregations, ReduceContext con List buckets = multiBucketsAgg.getBuckets(); for (int i = 0; i < buckets.size(); i++) { Bucket bucket = buckets.get(i); - Double bucketValue = BucketHelpers.resolveBucketValue(multiBucketsAgg, bucket, bucketsPath, GapPolicy.IGNORE); + Double bucketValue = BucketHelpers.resolveBucketValue(multiBucketsAgg, bucket, bucketsPath, gapPolicy); if (bucketValue != null) { if (bucketValue > maxValue) { maxBucketKeys.clear(); @@ -110,25 +113,29 @@ public InternalAggregation doReduce(Aggregations aggregations, ReduceContext con @Override public void doReadFrom(StreamInput in) throws IOException { formatter = ValueFormatterStreams.readOptional(in); + gapPolicy = GapPolicy.readFrom(in); } @Override public void doWriteTo(StreamOutput out) throws IOException { ValueFormatterStreams.writeOptional(formatter, out); + gapPolicy.writeTo(out); } public static class Factory extends ReducerFactory { private final ValueFormatter formatter; + private final GapPolicy gapPolicy; - public Factory(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter) { + public Factory(String name, String[] bucketsPaths, GapPolicy gapPolicy, @Nullable ValueFormatter formatter) { super(name, TYPE.name(), bucketsPaths); + this.gapPolicy = gapPolicy; this.formatter = formatter; } @Override protected Reducer createInternal(Map metaData) throws IOException { - return new MaxBucketReducer(name, bucketsPaths, formatter, metaData); + return new MaxBucketReducer(name, bucketsPaths, gapPolicy, formatter, metaData); } @Override diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java index c4d3aa2a22978..cfca5c60978a6 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/derivative/DerivativeParser.java @@ -19,9 +19,9 @@ package org.elasticsearch.search.aggregations.reducers.derivative; -import org.elasticsearch.common.ParseField; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.SearchParseException; +import org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.reducers.ReducerFactory; import org.elasticsearch.search.aggregations.support.format.ValueFormat; @@ -32,13 +32,8 @@ import java.util.ArrayList; import java.util.List; -import static org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; - public class DerivativeParser implements Reducer.Parser { - public static final ParseField FORMAT = new ParseField("format"); - public static final ParseField GAP_POLICY = new ParseField("gap_policy"); - @Override public String type() { return DerivativeReducer.TYPE.name(); diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgParser.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgParser.java index c1cdadf91ea44..5d79b1d1e7a3a 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgParser.java @@ -23,6 +23,7 @@ import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.SearchParseException; +import org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; import org.elasticsearch.search.aggregations.reducers.Reducer; import org.elasticsearch.search.aggregations.reducers.ReducerFactory; import org.elasticsearch.search.aggregations.reducers.movavg.models.MovAvgModel; @@ -37,12 +38,8 @@ import java.util.List; import java.util.Map; -import static org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; - public class MovAvgParser implements Reducer.Parser { - public static final ParseField FORMAT = new ParseField("format"); - public static final ParseField GAP_POLICY = new ParseField("gap_policy"); public static final ParseField MODEL = new ParseField("model"); public static final ParseField WINDOW = new ParseField("window"); public static final ParseField SETTINGS = new ParseField("settings"); diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/MaxBucketTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/MaxBucketTests.java index 48d93766bfca8..84e559e497082 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/MaxBucketTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/MaxBucketTests.java @@ -26,6 +26,7 @@ import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.search.aggregations.bucket.terms.Terms.Order; import org.elasticsearch.search.aggregations.metrics.sum.Sum; +import org.elasticsearch.search.aggregations.reducers.BucketHelpers.GapPolicy; import org.elasticsearch.search.aggregations.reducers.bucketmetrics.InternalBucketMetricValue; import org.elasticsearch.test.ElasticsearchIntegrationTest; import org.junit.Test; @@ -244,6 +245,66 @@ public void testMetric_asSubAgg() throws Exception { List termsBuckets = terms.getBuckets(); assertThat(termsBuckets.size(), equalTo(interval)); + for (int i = 0; i < interval; ++i) { + Terms.Bucket termsBucket = termsBuckets.get(i); + assertThat(termsBucket, notNullValue()); + assertThat((String) termsBucket.getKey(), equalTo("tag" + (i % interval))); + + Histogram histo = termsBucket.getAggregations().get("histo"); + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + + List maxKeys = new ArrayList<>(); + double maxValue = Double.NEGATIVE_INFINITY; + for (int j = 0; j < numValueBuckets; ++j) { + Histogram.Bucket bucket = buckets.get(j); + assertThat(bucket, notNullValue()); + assertThat(((Number) bucket.getKey()).longValue(), equalTo((long) j * interval)); + if (bucket.getDocCount() != 0) { + Sum sum = bucket.getAggregations().get("sum"); + assertThat(sum, notNullValue()); + if (sum.value() > maxValue) { + maxValue = sum.value(); + maxKeys = new ArrayList<>(); + maxKeys.add(bucket.getKeyAsString()); + } else if (sum.value() == maxValue) { + maxKeys.add(bucket.getKeyAsString()); + } + } + } + + InternalBucketMetricValue maxBucketValue = termsBucket.getAggregations().get("max_bucket"); + assertThat(maxBucketValue, notNullValue()); + assertThat(maxBucketValue.getName(), equalTo("max_bucket")); + assertThat(maxBucketValue.value(), equalTo(maxValue)); + assertThat(maxBucketValue.keys(), equalTo(maxKeys.toArray(new String[maxKeys.size()]))); + } + } + + @Test + public void testMetric_asSubAggWithInsertZeros() throws Exception { + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation( + terms("terms") + .field("tag") + .order(Order.term(true)) + .subAggregation( + histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) + .extendedBounds((long) minRandomValue, (long) maxRandomValue) + .subAggregation(sum("sum").field(SINGLE_VALUED_FIELD_NAME))) + .subAggregation(maxBucket("max_bucket").setBucketsPaths("histo>sum").gapPolicy(GapPolicy.INSERT_ZEROS))) + .execute().actionGet(); + + assertSearchResponse(response); + + Terms terms = response.getAggregations().get("terms"); + assertThat(terms, notNullValue()); + assertThat(terms.getName(), equalTo("terms")); + List termsBuckets = terms.getBuckets(); + assertThat(termsBuckets.size(), equalTo(interval)); + for (int i = 0; i < interval; ++i) { Terms.Bucket termsBucket = termsBuckets.get(i); assertThat(termsBucket, notNullValue()); From 114d10e5a96b071121ac7862e176b1e15112aadb Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Thu, 23 Apr 2015 17:51:17 -0400 Subject: [PATCH 58/68] [TEST] Restructure MovAvgTests to be more generic, robust --- .../reducers/ReducerTestHelpers.java | 131 ++ .../reducers/moving/avg/MovAvgTests.java | 1049 ++++++++--------- 2 files changed, 648 insertions(+), 532 deletions(-) create mode 100644 src/test/java/org/elasticsearch/search/aggregations/reducers/ReducerTestHelpers.java diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/ReducerTestHelpers.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/ReducerTestHelpers.java new file mode 100644 index 0000000000000..8496b93e7eabf --- /dev/null +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/ReducerTestHelpers.java @@ -0,0 +1,131 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.reducers; + + +import org.elasticsearch.search.aggregations.metrics.ValuesSourceMetricsAggregationBuilder; +import org.elasticsearch.search.aggregations.metrics.avg.AvgBuilder; +import org.elasticsearch.search.aggregations.metrics.max.MaxBuilder; +import org.elasticsearch.search.aggregations.metrics.min.MinBuilder; +import org.elasticsearch.search.aggregations.metrics.sum.SumBuilder; +import org.elasticsearch.test.ElasticsearchTestCase; + +import java.util.ArrayList; + +/** + * Provides helper methods and classes for use in Reducer tests, such as creating mock histograms or computing + * simple metrics + */ +public class ReducerTestHelpers extends ElasticsearchTestCase { + + /** + * Generates a mock histogram to use for testing. Each MockBucket holds a doc count, key and document values + * which can later be used to compute metrics and compare against the real aggregation results. Gappiness can be + * controlled via parameters + * + * @param interval Interval between bucket keys + * @param size Size of mock histogram to generate (in buckets) + * @param gapProbability Probability of generating an empty bucket. 0.0-1.0 inclusive + * @param runProbability Probability of extending a gap once one has been created. 0.0-1.0 inclusive + * @return + */ + public static ArrayList generateHistogram(int interval, int size, double gapProbability, double runProbability) { + ArrayList values = new ArrayList<>(size); + + boolean lastWasGap = false; + + for (int i = 0; i < size; i++) { + MockBucket bucket = new MockBucket(); + if (randomDouble() < gapProbability) { + // start a gap + bucket.count = 0; + bucket.docValues = new double[0]; + + lastWasGap = true; + + } else if (lastWasGap && randomDouble() < runProbability) { + // add to the existing gap + bucket.count = 0; + bucket.docValues = new double[0]; + + lastWasGap = true; + } else { + bucket.count = randomIntBetween(1, 50); + bucket.docValues = new double[bucket.count]; + for (int j = 0; j < bucket.count; j++) { + bucket.docValues[j] = randomDouble() * randomIntBetween(-20,20); + } + lastWasGap = false; + } + + bucket.key = i * interval; + values.add(bucket); + } + + return values; + } + + /** + * Simple mock bucket container + */ + public static class MockBucket { + public int count; + public double[] docValues; + public long key; + } + + /** + * Computes a simple agg metric (min, sum, etc) from the provided values + * + * @param values Array of values to compute metric for + * @param metric A metric builder which defines what kind of metric should be returned for the values + * @return + */ + public static double calculateMetric(double[] values, ValuesSourceMetricsAggregationBuilder metric) { + + if (metric instanceof MinBuilder) { + double accumulator = Double.MAX_VALUE; + for (double value : values) { + accumulator = Math.min(accumulator, value); + } + return accumulator; + } else if (metric instanceof MaxBuilder) { + double accumulator = Double.MIN_VALUE; + for (double value : values) { + accumulator = Math.max(accumulator, value); + } + return accumulator; + } else if (metric instanceof SumBuilder) { + double accumulator = 0; + for (double value : values) { + accumulator += value; + } + return accumulator; + } else if (metric instanceof AvgBuilder) { + double accumulator = 0; + for (double value : values) { + accumulator += value; + } + return accumulator / values.length; + } + + return 0.0; + } +} diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java index bab47aadb8085..c92f0b1cc2efc 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java @@ -33,6 +33,7 @@ import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram.Bucket; import org.elasticsearch.search.aggregations.metrics.ValuesSourceMetricsAggregationBuilder; import org.elasticsearch.search.aggregations.reducers.BucketHelpers; +import org.elasticsearch.search.aggregations.reducers.ReducerTestHelpers; import org.elasticsearch.search.aggregations.reducers.SimpleValue; import org.elasticsearch.search.aggregations.reducers.movavg.models.DoubleExpModel; import org.elasticsearch.search.aggregations.reducers.movavg.models.LinearModel; @@ -40,10 +41,10 @@ import org.elasticsearch.search.aggregations.reducers.movavg.models.SimpleModel; import org.elasticsearch.search.aggregations.reducers.movavg.models.SingleExpModel; import org.elasticsearch.test.ElasticsearchIntegrationTest; +import org.hamcrest.Matchers; import org.junit.Test; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.search.aggregations.AggregationBuilders.avg; @@ -59,329 +60,247 @@ import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.IsNull.notNullValue; +import static org.hamcrest.core.IsNull.nullValue; @ElasticsearchIntegrationTest.SuiteScopeTest @AwaitsFix(bugUrl = "Gap test logic seems to fail a lot of the time on CI build") public class MovAvgTests extends ElasticsearchIntegrationTest { - private static final String SINGLE_VALUED_FIELD_NAME = "l_value"; - private static final String SINGLE_VALUED_VALUE_FIELD_NAME = "v_value"; + private static final String INTERVAL_FIELD = "l_value"; + private static final String VALUE_FIELD = "v_value"; private static final String GAP_FIELD = "g_value"; static int interval; - static int numValueBuckets; - static int numFilledValueBuckets; + static int numBuckets; static int windowSize; + static double alpha; + static double beta; static BucketHelpers.GapPolicy gapPolicy; + static ValuesSourceMetricsAggregationBuilder metric; + static List mockHisto; - static long[] docCounts; - static long[] docValues; - static Double[] simpleDocCounts; - static Double[] linearDocCounts; - static Double[] singleDocCounts; - static Double[] doubleDocCounts; + static Map> testValues; - static Double[] simpleDocValues; - static Double[] linearDocValues; - static Double[] singleDocValues; - static Double[] doubleDocValues; - @Override - public void setupSuiteScopeCluster() throws Exception { - createIndex("idx"); - createIndex("idx_unmapped"); - List builders = new ArrayList<>(); - - interval = 5; - numValueBuckets = randomIntBetween(6, 80); - numFilledValueBuckets = numValueBuckets; - windowSize = randomIntBetween(3,10); - gapPolicy = randomBoolean() ? BucketHelpers.GapPolicy.IGNORE : BucketHelpers.GapPolicy.INSERT_ZEROS; - - - docCounts = new long[numValueBuckets]; - docValues = new long[numValueBuckets]; - for (int i = 0; i < numValueBuckets; i++) { - docCounts[i] = randomIntBetween(0, 20); - docValues[i] = randomIntBetween(1,20); //this will be used as a constant for all values within a bucket - } + enum MovAvgType { + SIMPLE ("simple"), LINEAR("linear"), SINGLE("single"), DOUBLE("double"); - // Used for the gap tests - builders.add(client().prepareIndex("idx", "type").setSource(jsonBuilder().startObject() - .field("gap_test", 0) - .field(GAP_FIELD, 1).endObject())); - builders.add(client().prepareIndex("idx", "type").setSource(jsonBuilder().startObject() - .field("gap_test", (numValueBuckets - 1) * interval) - .field(GAP_FIELD, 1).endObject())); + private final String name; - this.setupSimple(); - this.setupLinear(); - this.setupSingle(); - this.setupDouble(); - - for (int i = 0; i < numValueBuckets; i++) { - for (int docs = 0; docs < docCounts[i]; docs++) { - builders.add(client().prepareIndex("idx", "type").setSource(jsonBuilder().startObject() - .field(SINGLE_VALUED_FIELD_NAME, i * interval) - .field(SINGLE_VALUED_VALUE_FIELD_NAME, docValues[i]).endObject())); - } + MovAvgType(String s) { + name = s; } - indexRandom(true, builders); - ensureSearchable(); + public String toString(){ + return name; + } } - private void setupSimple() { - simpleDocCounts = new Double[numValueBuckets]; - EvictingQueue window = EvictingQueue.create(windowSize); - for (int i = 0; i < numValueBuckets; i++) { - if (docCounts[i] == 0 && gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { - continue; - } - window.offer((double)docCounts[i]); + enum MetricTarget { + VALUE ("value"), COUNT("count"); - double movAvg = 0; - for (double value : window) { - movAvg += value; - } - movAvg /= window.size(); + private final String name; - simpleDocCounts[i] = movAvg; + MetricTarget(String s) { + name = s; } - window.clear(); - simpleDocValues = new Double[numValueBuckets]; - for (int i = 0; i < numValueBuckets; i++) { - if (docCounts[i] == 0) { - // If there was a gap in doc counts and we are ignoring, just skip this bucket - if (gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { - continue; - } else if (gapPolicy.equals(BucketHelpers.GapPolicy.INSERT_ZEROS)) { - // otherwise insert a zero instead of the true value - window.offer(0.0); - } else { - window.offer((double) docValues[i]); - } - } else { - //if there are docs in this bucket, insert the regular value - window.offer((double) docValues[i]); - } + public String toString(){ + return name; + } + } - double movAvg = 0; - for (double value : window) { - movAvg += value; - } - movAvg /= window.size(); - simpleDocValues[i] = movAvg; + @Override + public void setupSuiteScopeCluster() throws Exception { + createIndex("idx"); + createIndex("idx_unmapped"); + List builders = new ArrayList<>(); - } - } + interval = 5; + numBuckets = randomIntBetween(6, 80); + windowSize = randomIntBetween(3, 10); + alpha = randomDouble(); + beta = randomDouble(); - private void setupLinear() { - EvictingQueue window = EvictingQueue.create(windowSize); - linearDocCounts = new Double[numValueBuckets]; - window.clear(); - for (int i = 0; i < numValueBuckets; i++) { - if (docCounts[i] == 0 && gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { - continue; - } - window.offer((double)docCounts[i]); + gapPolicy = randomBoolean() ? BucketHelpers.GapPolicy.IGNORE : BucketHelpers.GapPolicy.INSERT_ZEROS; + metric = randomMetric("the_metric", VALUE_FIELD); + mockHisto = ReducerTestHelpers.generateHistogram(interval, numBuckets, randomDouble(), randomDouble()); - double avg = 0; - long totalWeight = 1; - long current = 1; + testValues = new HashMap<>(8); - for (double value : window) { - avg += value * current; - totalWeight += current; - current += 1; + for (MovAvgType type : MovAvgType.values()) { + for (MetricTarget target : MetricTarget.values()) { + setupExpected(type, target); } - linearDocCounts[i] = avg / totalWeight; } - window.clear(); - linearDocValues = new Double[numValueBuckets]; - - for (int i = 0; i < numValueBuckets; i++) { - if (docCounts[i] == 0) { - // If there was a gap in doc counts and we are ignoring, just skip this bucket - if (gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { - continue; - } else if (gapPolicy.equals(BucketHelpers.GapPolicy.INSERT_ZEROS)) { - // otherwise insert a zero instead of the true value - window.offer(0.0); - } else { - window.offer((double) docValues[i]); - } - } else { - //if there are docs in this bucket, insert the regular value - window.offer((double) docValues[i]); + for (ReducerTestHelpers.MockBucket mockBucket : mockHisto) { + for (double value : mockBucket.docValues) { + builders.add(client().prepareIndex("idx", "type").setSource(jsonBuilder().startObject() + .field(INTERVAL_FIELD, mockBucket.key) + .field(VALUE_FIELD, value).endObject())); } + } - double avg = 0; - long totalWeight = 1; - long current = 1; + // Used for specially crafted gap tests + builders.add(client().prepareIndex("idx", "gap_type").setSource(jsonBuilder().startObject() + .field(INTERVAL_FIELD, 0) + .field(GAP_FIELD, 1).endObject())); - for (double value : window) { - avg += value * current; - totalWeight += current; - current += 1; - } - linearDocValues[i] = avg / totalWeight; - } + builders.add(client().prepareIndex("idx", "gap_type").setSource(jsonBuilder().startObject() + .field(INTERVAL_FIELD, 49) + .field(GAP_FIELD, 1).endObject())); + + indexRandom(true, builders); + ensureSearchable(); } - private void setupSingle() { + /** + * Calculates the moving averages for a specific (model, target) tuple based on the previously generated mock histogram. + * Computed values are stored in the testValues map. + * + * @param type The moving average model to use + * @param target The document field "target", e.g. _count or a field value + */ + private void setupExpected(MovAvgType type, MetricTarget target) { + ArrayList values = new ArrayList<>(numBuckets); EvictingQueue window = EvictingQueue.create(windowSize); - singleDocCounts = new Double[numValueBuckets]; - for (int i = 0; i < numValueBuckets; i++) { - if (docCounts[i] == 0 && gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { - continue; - } - window.offer((double)docCounts[i]); - double avg = 0; - double alpha = 0.5; - boolean first = true; + for (ReducerTestHelpers.MockBucket mockBucket : mockHisto) { + double metricValue; + double[] docValues = mockBucket.docValues; - for (double value : window) { - if (first) { - avg = value; - first = false; - } else { - avg = (value * alpha) + (avg * (1 - alpha)); - } - } - singleDocCounts[i] = avg ; - } - - singleDocValues = new Double[numValueBuckets]; - window.clear(); - - for (int i = 0; i < numValueBuckets; i++) { - if (docCounts[i] == 0) { + // Gaps only apply to metric values, not doc _counts + if (mockBucket.count == 0 && target.equals(MetricTarget.VALUE)) { // If there was a gap in doc counts and we are ignoring, just skip this bucket if (gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { + values.add(null); continue; } else if (gapPolicy.equals(BucketHelpers.GapPolicy.INSERT_ZEROS)) { // otherwise insert a zero instead of the true value - window.offer(0.0); + metricValue = 0.0; } else { - window.offer((double) docValues[i]); + metricValue = ReducerTestHelpers.calculateMetric(docValues, metric); } + } else { - //if there are docs in this bucket, insert the regular value - window.offer((double) docValues[i]); + // If this isn't a gap, or is a _count, just insert the value + metricValue = target.equals(MetricTarget.VALUE) ? ReducerTestHelpers.calculateMetric(docValues, metric) : mockBucket.count; } - double avg = 0; - double alpha = 0.5; - boolean first = true; - - for (double value : window) { - if (first) { - avg = value; - first = false; - } else { - avg = (value * alpha) + (avg * (1 - alpha)); - } + window.offer(metricValue); + switch (type) { + case SIMPLE: + values.add(simple(window)); + break; + case LINEAR: + values.add(linear(window)); + break; + case SINGLE: + values.add(singleExp(window)); + break; + case DOUBLE: + values.add(doubleExp(window)); + break; } - singleDocValues[i] = avg ; - } + } + testValues.put(type.toString() + "_" + target.toString(), values); } - private void setupDouble() { - EvictingQueue window = EvictingQueue.create(windowSize); - doubleDocCounts = new Double[numValueBuckets]; - - for (int i = 0; i < numValueBuckets; i++) { - if (docCounts[i] == 0 && gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { - continue; - } - window.offer((double)docCounts[i]); - - double s = 0; - double last_s = 0; - - // Trend value - double b = 0; - double last_b = 0; - - double alpha = 0.5; - double beta = 0.5; - int counter = 0; + /** + * Simple, unweighted moving average + * + * @param window Window of values to compute movavg for + * @return + */ + private double simple(Collection window) { + double movAvg = 0; + for (double value : window) { + movAvg += value; + } + movAvg /= window.size(); + return movAvg; + } - double last; - for (double value : window) { - last = value; - if (counter == 1) { - s = value; - b = value - last; - } else { - s = alpha * value + (1.0d - alpha) * (last_s + last_b); - b = beta * (s - last_s) + (1 - beta) * last_b; - } + /** + * Linearly weighted moving avg + * + * @param window Window of values to compute movavg for + * @return + */ + private double linear(Collection window) { + double avg = 0; + long totalWeight = 1; + long current = 1; + + for (double value : window) { + avg += value * current; + totalWeight += current; + current += 1; + } + return avg / totalWeight; + } - counter += 1; - last_s = s; - last_b = b; + /** + * Single exponential moving avg + * + * @param window Window of values to compute movavg for + * @return + */ + private double singleExp(Collection window) { + double avg = 0; + boolean first = true; + + for (double value : window) { + if (first) { + avg = value; + first = false; + } else { + avg = (value * alpha) + (avg * (1 - alpha)); } - - doubleDocCounts[i] = s + (0 * b) ; } + return avg; + } - doubleDocValues = new Double[numValueBuckets]; - window.clear(); - - for (int i = 0; i < numValueBuckets; i++) { - if (docCounts[i] == 0) { - // If there was a gap in doc counts and we are ignoring, just skip this bucket - if (gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { - continue; - } else if (gapPolicy.equals(BucketHelpers.GapPolicy.INSERT_ZEROS)) { - // otherwise insert a zero instead of the true value - window.offer(0.0); - } else { - window.offer((double) docValues[i]); - } + /** + * Double exponential moving avg + * @param window Window of values to compute movavg for + * @return + */ + private double doubleExp(Collection window) { + double s = 0; + double last_s = 0; + + // Trend value + double b = 0; + double last_b = 0; + + int counter = 0; + + double last; + for (double value : window) { + last = value; + if (counter == 1) { + s = value; + b = value - last; } else { - //if there are docs in this bucket, insert the regular value - window.offer((double) docValues[i]); + s = alpha * value + (1.0d - alpha) * (last_s + last_b); + b = beta * (s - last_s) + (1 - beta) * last_b; } - double s = 0; - double last_s = 0; + counter += 1; + last_s = s; + last_b = b; + } - // Trend value - double b = 0; - double last_b = 0; + return s + (0 * b) ; + } - double alpha = 0.5; - double beta = 0.5; - int counter = 0; - double last; - for (double value : window) { - last = value; - if (counter == 1) { - s = value; - b = value - last; - } else { - s = alpha * value + (1.0d - alpha) * (last_s + last_b); - b = beta * (s - last_s) + (1 - beta) * last_b; - } - counter += 1; - last_s = s; - last_b = b; - } - - doubleDocValues[i] = s + (0 * b) ; - } - } /** * test simple moving average on single value field @@ -390,11 +309,11 @@ private void setupDouble() { public void simpleSingleValuedField() { SearchResponse response = client() - .prepareSearch("idx") + .prepareSearch("idx").setTypes("type") .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) - .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(randomMetric("the_metric", SINGLE_VALUED_VALUE_FIELD_NAME)) + histogram("histo").field(INTERVAL_FIELD).interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numBuckets - 1))) + .subAggregation(metric) .subAggregation(movingAvg("movavg_counts") .window(windowSize) .modelBuilder(new SimpleModel.SimpleModelBuilder()) @@ -413,33 +332,40 @@ public void simpleSingleValuedField() { assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List buckets = histo.getBuckets(); - assertThat(buckets.size(), equalTo(numValueBuckets)); - - for (int i = 0; i < numValueBuckets; ++i) { - Histogram.Bucket bucket = buckets.get(i); - checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, docCounts[i]); - SimpleValue docCountMovAvg = bucket.getAggregations().get("movavg_counts"); - assertThat(docCountMovAvg, notNullValue()); - assertThat(docCountMovAvg.value(), equalTo(simpleDocCounts[i])); - - SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); - assertThat(valuesMovAvg, notNullValue()); - assertThat(valuesMovAvg.value(), equalTo(simpleDocValues[i])); + assertThat("Size of buckets array is not correct.", buckets.size(), equalTo(mockHisto.size())); + + List expectedCounts = testValues.get(MovAvgType.SIMPLE.toString() + "_" + MetricTarget.COUNT.toString()); + List expectedValues = testValues.get(MovAvgType.SIMPLE.toString() + "_" + MetricTarget.VALUE.toString()); + + Iterator actualIter = buckets.iterator(); + Iterator expectedBucketIter = mockHisto.iterator(); + Iterator expectedCountsIter = expectedCounts.iterator(); + Iterator expectedValuesIter = expectedValues.iterator(); + + while (actualIter.hasNext()) { + assertValidIterators(expectedBucketIter, expectedCountsIter, expectedValuesIter); + + Histogram.Bucket actual = actualIter.next(); + ReducerTestHelpers.MockBucket expected = expectedBucketIter.next(); + Double expectedCount = expectedCountsIter.next(); + Double expectedValue = expectedValuesIter.next(); + + assertThat("keys do not match", ((Number) actual.getKey()).longValue(), equalTo(expected.key)); + assertThat("doc counts do not match", actual.getDocCount(), equalTo((long)expected.count)); + + assertBucketContents(actual, expectedCount, expectedValue); } } - /** - * test linear moving average on single value field - */ @Test public void linearSingleValuedField() { SearchResponse response = client() - .prepareSearch("idx") + .prepareSearch("idx").setTypes("type") .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) - .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(randomMetric("the_metric", SINGLE_VALUED_VALUE_FIELD_NAME)) + histogram("histo").field(INTERVAL_FIELD).interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numBuckets - 1))) + .subAggregation(metric) .subAggregation(movingAvg("movavg_counts") .window(windowSize) .modelBuilder(new LinearModel.LinearModelBuilder()) @@ -458,41 +384,48 @@ public void linearSingleValuedField() { assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List buckets = histo.getBuckets(); - assertThat(buckets.size(), equalTo(numValueBuckets)); - - for (int i = 0; i < numValueBuckets; ++i) { - Histogram.Bucket bucket = buckets.get(i); - checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, docCounts[i]); - SimpleValue docCountMovAvg = bucket.getAggregations().get("movavg_counts"); - assertThat(docCountMovAvg, notNullValue()); - assertThat(docCountMovAvg.value(), equalTo(linearDocCounts[i])); - - SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); - assertThat(valuesMovAvg, notNullValue()); - assertThat(valuesMovAvg.value(), equalTo(linearDocValues[i])); + assertThat("Size of buckets array is not correct.", buckets.size(), equalTo(mockHisto.size())); + + List expectedCounts = testValues.get(MovAvgType.LINEAR.toString() + "_" + MetricTarget.COUNT.toString()); + List expectedValues = testValues.get(MovAvgType.LINEAR.toString() + "_" + MetricTarget.VALUE.toString()); + + Iterator actualIter = buckets.iterator(); + Iterator expectedBucketIter = mockHisto.iterator(); + Iterator expectedCountsIter = expectedCounts.iterator(); + Iterator expectedValuesIter = expectedValues.iterator(); + + while (actualIter.hasNext()) { + assertValidIterators(expectedBucketIter, expectedCountsIter, expectedValuesIter); + + Histogram.Bucket actual = actualIter.next(); + ReducerTestHelpers.MockBucket expected = expectedBucketIter.next(); + Double expectedCount = expectedCountsIter.next(); + Double expectedValue = expectedValuesIter.next(); + + assertThat("keys do not match", ((Number) actual.getKey()).longValue(), equalTo(expected.key)); + assertThat("doc counts do not match", actual.getDocCount(), equalTo((long)expected.count)); + + assertBucketContents(actual, expectedCount, expectedValue); } } - /** - * test single exponential moving average on single value field - */ @Test - public void singleExpSingleValuedField() { + public void singleSingleValuedField() { SearchResponse response = client() - .prepareSearch("idx") + .prepareSearch("idx").setTypes("type") .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) - .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(randomMetric("the_metric", SINGLE_VALUED_VALUE_FIELD_NAME)) + histogram("histo").field(INTERVAL_FIELD).interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numBuckets - 1))) + .subAggregation(metric) .subAggregation(movingAvg("movavg_counts") .window(windowSize) - .modelBuilder(new SingleExpModel.SingleExpModelBuilder().alpha(0.5)) + .modelBuilder(new SingleExpModel.SingleExpModelBuilder().alpha(alpha)) .gapPolicy(gapPolicy) .setBucketsPaths("_count")) .subAggregation(movingAvg("movavg_values") .window(windowSize) - .modelBuilder(new SingleExpModel.SingleExpModelBuilder().alpha(0.5)) + .modelBuilder(new SingleExpModel.SingleExpModelBuilder().alpha(alpha)) .gapPolicy(gapPolicy) .setBucketsPaths("the_metric")) ).execute().actionGet(); @@ -503,41 +436,48 @@ public void singleExpSingleValuedField() { assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List buckets = histo.getBuckets(); - assertThat(buckets.size(), equalTo(numValueBuckets)); - - for (int i = 0; i < numValueBuckets; ++i) { - Histogram.Bucket bucket = buckets.get(i); - checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, docCounts[i]); - SimpleValue docCountMovAvg = bucket.getAggregations().get("movavg_counts"); - assertThat(docCountMovAvg, notNullValue()); - assertThat(docCountMovAvg.value(), equalTo(singleDocCounts[i])); - - SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); - assertThat(valuesMovAvg, notNullValue()); - assertThat(valuesMovAvg.value(), equalTo(singleDocValues[i])); + assertThat("Size of buckets array is not correct.", buckets.size(), equalTo(mockHisto.size())); + + List expectedCounts = testValues.get(MovAvgType.SINGLE.toString() + "_" + MetricTarget.COUNT.toString()); + List expectedValues = testValues.get(MovAvgType.SINGLE.toString() + "_" + MetricTarget.VALUE.toString()); + + Iterator actualIter = buckets.iterator(); + Iterator expectedBucketIter = mockHisto.iterator(); + Iterator expectedCountsIter = expectedCounts.iterator(); + Iterator expectedValuesIter = expectedValues.iterator(); + + while (actualIter.hasNext()) { + assertValidIterators(expectedBucketIter, expectedCountsIter, expectedValuesIter); + + Histogram.Bucket actual = actualIter.next(); + ReducerTestHelpers.MockBucket expected = expectedBucketIter.next(); + Double expectedCount = expectedCountsIter.next(); + Double expectedValue = expectedValuesIter.next(); + + assertThat("keys do not match", ((Number) actual.getKey()).longValue(), equalTo(expected.key)); + assertThat("doc counts do not match", actual.getDocCount(), equalTo((long)expected.count)); + + assertBucketContents(actual, expectedCount, expectedValue); } } - /** - * test double exponential moving average on single value field - */ @Test - public void doubleExpSingleValuedField() { + public void doubleSingleValuedField() { SearchResponse response = client() - .prepareSearch("idx") + .prepareSearch("idx").setTypes("type") .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) - .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(randomMetric("the_metric", SINGLE_VALUED_VALUE_FIELD_NAME)) + histogram("histo").field(INTERVAL_FIELD).interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numBuckets - 1))) + .subAggregation(metric) .subAggregation(movingAvg("movavg_counts") .window(windowSize) - .modelBuilder(new DoubleExpModel.DoubleExpModelBuilder().alpha(0.5).beta(0.5)) + .modelBuilder(new DoubleExpModel.DoubleExpModelBuilder().alpha(alpha).beta(beta)) .gapPolicy(gapPolicy) .setBucketsPaths("_count")) .subAggregation(movingAvg("movavg_values") .window(windowSize) - .modelBuilder(new DoubleExpModel.DoubleExpModelBuilder().alpha(0.5).beta(0.5)) + .modelBuilder(new DoubleExpModel.DoubleExpModelBuilder().alpha(alpha).beta(beta)) .gapPolicy(gapPolicy) .setBucketsPaths("the_metric")) ).execute().actionGet(); @@ -548,18 +488,28 @@ public void doubleExpSingleValuedField() { assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List buckets = histo.getBuckets(); - assertThat(buckets.size(), equalTo(numValueBuckets)); - - for (int i = 0; i < numValueBuckets; ++i) { - Histogram.Bucket bucket = buckets.get(i); - checkBucketKeyAndDocCount("Bucket " + i, bucket, i * interval, docCounts[i]); - SimpleValue docCountMovAvg = bucket.getAggregations().get("movavg_counts"); - assertThat(docCountMovAvg, notNullValue()); - assertThat(docCountMovAvg.value(), equalTo(doubleDocCounts[i])); - - SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); - assertThat(valuesMovAvg, notNullValue()); - assertThat(valuesMovAvg.value(), equalTo(doubleDocValues[i])); + assertThat("Size of buckets array is not correct.", buckets.size(), equalTo(mockHisto.size())); + + List expectedCounts = testValues.get(MovAvgType.DOUBLE.toString() + "_" + MetricTarget.COUNT.toString()); + List expectedValues = testValues.get(MovAvgType.DOUBLE.toString() + "_" + MetricTarget.VALUE.toString()); + + Iterator actualIter = buckets.iterator(); + Iterator expectedBucketIter = mockHisto.iterator(); + Iterator expectedCountsIter = expectedCounts.iterator(); + Iterator expectedValuesIter = expectedValues.iterator(); + + while (actualIter.hasNext()) { + assertValidIterators(expectedBucketIter, expectedCountsIter, expectedValuesIter); + + Histogram.Bucket actual = actualIter.next(); + ReducerTestHelpers.MockBucket expected = expectedBucketIter.next(); + Double expectedCount = expectedCountsIter.next(); + Double expectedValue = expectedValuesIter.next(); + + assertThat("keys do not match", ((Number) actual.getKey()).longValue(), equalTo(expected.key)); + assertThat("doc counts do not match", actual.getDocCount(), equalTo((long)expected.count)); + + assertBucketContents(actual, expectedCount, expectedValue); } } @@ -567,11 +517,11 @@ public void doubleExpSingleValuedField() { public void testSizeZeroWindow() { try { client() - .prepareSearch("idx") + .prepareSearch("idx").setTypes("type") .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) - .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(randomMetric("the_metric", SINGLE_VALUED_VALUE_FIELD_NAME)) + histogram("histo").field(INTERVAL_FIELD).interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numBuckets - 1))) + .subAggregation(randomMetric("the_metric", VALUE_FIELD)) .subAggregation(movingAvg("movavg_counts") .window(0) .modelBuilder(new SimpleModel.SimpleModelBuilder()) @@ -581,9 +531,7 @@ public void testSizeZeroWindow() { fail("MovingAvg should not accept a window that is zero"); } catch (SearchPhaseExecutionException exception) { - //Throwable rootCause = exception.unwrapCause(); - //assertThat(rootCause, instanceOf(SearchParseException.class)); - //assertThat("[window] value must be a positive, non-zero integer. Value supplied was [0] in [movingAvg].", equalTo(exception.getMessage())); + // All good } } @@ -591,10 +539,10 @@ public void testSizeZeroWindow() { public void testBadParent() { try { client() - .prepareSearch("idx") + .prepareSearch("idx").setTypes("type") .addAggregation( - range("histo").field(SINGLE_VALUED_FIELD_NAME).addRange(0, 10) - .subAggregation(randomMetric("the_metric", SINGLE_VALUED_VALUE_FIELD_NAME)) + range("histo").field(INTERVAL_FIELD).addRange(0, 10) + .subAggregation(randomMetric("the_metric", VALUE_FIELD)) .subAggregation(movingAvg("movavg_counts") .window(0) .modelBuilder(new SimpleModel.SimpleModelBuilder()) @@ -604,7 +552,7 @@ public void testBadParent() { fail("MovingAvg should not accept non-histogram as parent"); } catch (SearchPhaseExecutionException exception) { - // All good + // All good } } @@ -612,11 +560,11 @@ public void testBadParent() { public void testNegativeWindow() { try { client() - .prepareSearch("idx") + .prepareSearch("idx").setTypes("type") .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) - .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(randomMetric("the_metric", SINGLE_VALUED_VALUE_FIELD_NAME)) + histogram("histo").field(INTERVAL_FIELD).interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numBuckets - 1))) + .subAggregation(randomMetric("the_metric", VALUE_FIELD)) .subAggregation(movingAvg("movavg_counts") .window(-10) .modelBuilder(new SimpleModel.SimpleModelBuilder()) @@ -636,11 +584,11 @@ public void testNegativeWindow() { public void testNoBucketsInHistogram() { SearchResponse response = client() - .prepareSearch("idx") + .prepareSearch("idx").setTypes("type") .addAggregation( histogram("histo").field("test").interval(interval).minDocCount(0) - .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(randomMetric("the_metric", SINGLE_VALUED_VALUE_FIELD_NAME)) + .extendedBounds(0L, (long) (interval * (numBuckets - 1))) + .subAggregation(randomMetric("the_metric", VALUE_FIELD)) .subAggregation(movingAvg("movavg_counts") .window(windowSize) .modelBuilder(new SimpleModel.SimpleModelBuilder()) @@ -657,15 +605,41 @@ public void testNoBucketsInHistogram() { assertThat(buckets.size(), equalTo(0)); } + @Test + public void testNoBucketsInHistogramWithPredict() { + int numPredictions = randomIntBetween(1,10); + SearchResponse response = client() + .prepareSearch("idx").setTypes("type") + .addAggregation( + histogram("histo").field("test").interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numBuckets - 1))) + .subAggregation(randomMetric("the_metric", VALUE_FIELD)) + .subAggregation(movingAvg("movavg_counts") + .window(windowSize) + .modelBuilder(new SimpleModel.SimpleModelBuilder()) + .gapPolicy(gapPolicy) + .setBucketsPaths("the_metric") + .predict(numPredictions)) + ).execute().actionGet(); + + assertSearchResponse(response); + + InternalHistogram histo = response.getAggregations().get("histo"); + assertThat(histo, notNullValue()); + assertThat(histo.getName(), equalTo("histo")); + List buckets = histo.getBuckets(); + assertThat(buckets.size(), equalTo(0)); + } + @Test public void testZeroPrediction() { try { client() - .prepareSearch("idx") + .prepareSearch("idx").setTypes("type") .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) - .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(randomMetric("the_metric", SINGLE_VALUED_VALUE_FIELD_NAME)) + histogram("histo").field(INTERVAL_FIELD).interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numBuckets - 1))) + .subAggregation(randomMetric("the_metric", VALUE_FIELD)) .subAggregation(movingAvg("movavg_counts") .window(windowSize) .modelBuilder(randomModelBuilder()) @@ -676,7 +650,7 @@ public void testZeroPrediction() { fail("MovingAvg should not accept a prediction size that is zero"); } catch (SearchPhaseExecutionException exception) { - // All Good + // All Good } } @@ -684,11 +658,11 @@ public void testZeroPrediction() { public void testNegativePrediction() { try { client() - .prepareSearch("idx") + .prepareSearch("idx").setTypes("type") .addAggregation( - histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).minDocCount(0) - .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(randomMetric("the_metric", SINGLE_VALUED_VALUE_FIELD_NAME)) + histogram("histo").field(INTERVAL_FIELD).interval(interval).minDocCount(0) + .extendedBounds(0L, (long) (interval * (numBuckets - 1))) + .subAggregation(randomMetric("the_metric", VALUE_FIELD)) .subAggregation(movingAvg("movavg_counts") .window(windowSize) .modelBuilder(randomModelBuilder()) @@ -705,7 +679,7 @@ public void testNegativePrediction() { /** * This test uses the "gap" dataset, which is simply a doc at the beginning and end of - * the SINGLE_VALUED_FIELD_NAME range. These docs have a value of 1 in the `g_field`. + * the INTERVAL_FIELD range. These docs have a value of 1 in GAP_FIELD. * This test verifies that large gaps don't break things, and that the mov avg roughly works * in the correct manner (checks direction of change, but not actual values) */ @@ -713,12 +687,11 @@ public void testNegativePrediction() { public void testGiantGap() { SearchResponse response = client() - .prepareSearch("idx") + .prepareSearch("idx").setTypes("gap_type") .addAggregation( - histogram("histo").field("gap_test").interval(interval).minDocCount(0) - .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(randomMetric("the_metric", GAP_FIELD)) - .subAggregation(movingAvg("movavg_counts") + histogram("histo").field(INTERVAL_FIELD).interval(1).minDocCount(0).extendedBounds(0L, 49L) + .subAggregation(min("the_metric").field(GAP_FIELD)) + .subAggregation(movingAvg("movavg_values") .window(windowSize) .modelBuilder(randomModelBuilder()) .gapPolicy(gapPolicy) @@ -731,26 +704,38 @@ public void testGiantGap() { assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List buckets = histo.getBuckets(); - assertThat(buckets.size(), equalTo(numValueBuckets)); + assertThat("Size of buckets array is not correct.", buckets.size(), equalTo(50)); - double lastValue = ((SimpleValue)(buckets.get(0).getAggregations().get("movavg_counts"))).value(); + double lastValue = ((SimpleValue)(buckets.get(0).getAggregations().get("movavg_values"))).value(); assertThat(Double.compare(lastValue, 0.0d), greaterThanOrEqualTo(0)); double currentValue; - for (int i = 1; i < numValueBuckets - 2; i++) { - currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movavg_counts"))).value(); - - // Since there are only two values in this test, at the beginning and end, the moving average should - // decrease every step (until it reaches zero). Crude way to check that it's doing the right thing - // without actually verifying the computed values. Should work for all types of moving avgs and - // gap policies - assertThat(Double.compare(lastValue, currentValue), greaterThanOrEqualTo(0)); - lastValue = currentValue; + for (int i = 1; i < 49; i++) { + SimpleValue current = buckets.get(i).getAggregations().get("movavg_values"); + if (current != null) { + currentValue = current.value(); + + // Since there are only two values in this test, at the beginning and end, the moving average should + // decrease every step (until it reaches zero). Crude way to check that it's doing the right thing + // without actually verifying the computed values. Should work for all types of moving avgs and + // gap policies + assertThat(Double.compare(lastValue, currentValue), greaterThanOrEqualTo(0)); + lastValue = currentValue; + } } - // The last bucket has a real value, so this should always increase the moving avg - currentValue = ((SimpleValue)(buckets.get(numValueBuckets - 1).getAggregations().get("movavg_counts"))).value(); - assertThat(Double.compare(lastValue, currentValue), equalTo(-1)); + + SimpleValue current = buckets.get(49).getAggregations().get("movavg_values"); + assertThat(current, notNullValue()); + currentValue = current.value(); + + if (gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { + // if we are ignoring, movavg could go up (double_exp) or stay the same (simple, linear, single_exp) + assertThat(Double.compare(lastValue, currentValue), lessThanOrEqualTo(0)); + } else if (gapPolicy.equals(BucketHelpers.GapPolicy.INSERT_ZEROS)) { + // If we insert zeros, this should always increase the moving avg since the last bucket has a real value + assertThat(Double.compare(lastValue, currentValue), equalTo(-1)); + } } /** @@ -758,21 +743,19 @@ public void testGiantGap() { */ @Test public void testGiantGapWithPredict() { - - MovAvgModelBuilder model = randomModelBuilder(); int numPredictions = randomIntBetween(1, 10); + SearchResponse response = client() - .prepareSearch("idx") + .prepareSearch("idx").setTypes("gap_type") .addAggregation( - histogram("histo").field("gap_test").interval(interval).minDocCount(0) - .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) - .subAggregation(randomMetric("the_metric", GAP_FIELD)) - .subAggregation(movingAvg("movavg_counts") + histogram("histo").field(INTERVAL_FIELD).interval(1).minDocCount(0).extendedBounds(0L, 49L) + .subAggregation(min("the_metric").field(GAP_FIELD)) + .subAggregation(movingAvg("movavg_values") .window(windowSize) - .modelBuilder(model) + .modelBuilder(randomModelBuilder()) .gapPolicy(gapPolicy) - .predict(numPredictions) - .setBucketsPaths("the_metric")) + .setBucketsPaths("the_metric") + .predict(numPredictions)) ).execute().actionGet(); assertSearchResponse(response); @@ -781,32 +764,43 @@ public void testGiantGapWithPredict() { assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List buckets = histo.getBuckets(); - assertThat(buckets.size(), equalTo(numValueBuckets + numPredictions)); + assertThat("Size of buckets array is not correct.", buckets.size(), equalTo(50 + numPredictions)); - double lastValue = ((SimpleValue)(buckets.get(0).getAggregations().get("movavg_counts"))).value(); + double lastValue = ((SimpleValue)(buckets.get(0).getAggregations().get("movavg_values"))).value(); assertThat(Double.compare(lastValue, 0.0d), greaterThanOrEqualTo(0)); double currentValue; - for (int i = 1; i < numValueBuckets - 2; i++) { - currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movavg_counts"))).value(); - - // Since there are only two values in this test, at the beginning and end, the moving average should - // decrease every step (until it reaches zero). Crude way to check that it's doing the right thing - // without actually verifying the computed values. Should work for all types of moving avgs and - // gap policies - assertThat(Double.compare(lastValue, currentValue), greaterThanOrEqualTo(0)); - lastValue = currentValue; + for (int i = 1; i < 49; i++) { + SimpleValue current = buckets.get(i).getAggregations().get("movavg_values"); + if (current != null) { + currentValue = current.value(); + + // Since there are only two values in this test, at the beginning and end, the moving average should + // decrease every step (until it reaches zero). Crude way to check that it's doing the right thing + // without actually verifying the computed values. Should work for all types of moving avgs and + // gap policies + assertThat(Double.compare(lastValue, currentValue), greaterThanOrEqualTo(0)); + lastValue = currentValue; + } } - // The last bucket has a real value, so this should always increase the moving avg - currentValue = ((SimpleValue)(buckets.get(numValueBuckets - 1).getAggregations().get("movavg_counts"))).value(); - assertThat(Double.compare(lastValue, currentValue), equalTo(-1)); + SimpleValue current = buckets.get(49).getAggregations().get("movavg_values"); + assertThat(current, notNullValue()); + currentValue = current.value(); + + if (gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { + // If we ignore missing, there will only be two values in this histo, so movavg will stay the same + assertThat(Double.compare(lastValue, currentValue), equalTo(0)); + } else if (gapPolicy.equals(BucketHelpers.GapPolicy.INSERT_ZEROS)) { + // If we insert zeros, this should always increase the moving avg since the last bucket has a real value + assertThat(Double.compare(lastValue, currentValue), equalTo(-1)); + } // Now check predictions - for (int i = numValueBuckets; i < numValueBuckets + numPredictions; i++) { + for (int i = 50; i < 50 + numPredictions; i++) { // Unclear at this point which direction the predictions will go, just verify they are // not null, and that we don't have the_metric anymore - assertThat((buckets.get(i).getAggregations().get("movavg_counts")), notNullValue()); + assertThat((buckets.get(i).getAggregations().get("movavg_values")), notNullValue()); assertThat((buckets.get(i).getAggregations().get("the_metric")), nullValue()); } } @@ -818,22 +812,19 @@ public void testGiantGapWithPredict() { */ @Test public void testLeftGap() { - SearchResponse response = client() - .prepareSearch("idx") + .prepareSearch("idx").setTypes("gap_type") .addAggregation( - filter("filtered").filter(new RangeFilterBuilder("gap_test").from(1)).subAggregation( - histogram("histo").field("gap_test").interval(interval).minDocCount(0) - .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + filter("filtered").filter(new RangeFilterBuilder(INTERVAL_FIELD).from(1)).subAggregation( + histogram("histo").field(INTERVAL_FIELD).interval(1).minDocCount(0).extendedBounds(0L, 49L) .subAggregation(randomMetric("the_metric", GAP_FIELD)) - .subAggregation(movingAvg("movavg_counts") + .subAggregation(movingAvg("movavg_values") .window(windowSize) .modelBuilder(randomModelBuilder()) .gapPolicy(gapPolicy) .setBucketsPaths("the_metric")) - ) - - ).execute().actionGet(); + )) + .execute().actionGet(); assertSearchResponse(response); @@ -842,44 +833,42 @@ public void testLeftGap() { assertThat(filtered.getName(), equalTo("filtered")); InternalHistogram histo = filtered.getAggregations().get("histo"); - assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List buckets = histo.getBuckets(); - assertThat(buckets.size(), equalTo(numValueBuckets)); + assertThat("Size of buckets array is not correct.", buckets.size(), equalTo(50)); + + double lastValue = 0; double currentValue; - double lastValue = 0.0; - for (int i = 0; i < numValueBuckets - 1; i++) { - currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movavg_counts"))).value(); + for (int i = 0; i < 50; i++) { + SimpleValue current = buckets.get(i).getAggregations().get("movavg_values"); + if (current != null) { + currentValue = current.value(); - assertThat(Double.compare(lastValue, currentValue), lessThanOrEqualTo(0)); - lastValue = currentValue; + assertThat(Double.compare(lastValue, currentValue), lessThanOrEqualTo(0)); + lastValue = currentValue; + } } - } @Test - public void testLeftGapWithPrediction() { - - int numPredictions = randomIntBetween(0, 10); - + public void testLeftGapWithPredict() { + int numPredictions = randomIntBetween(1, 10); SearchResponse response = client() - .prepareSearch("idx") + .prepareSearch("idx").setTypes("gap_type") .addAggregation( - filter("filtered").filter(new RangeFilterBuilder("gap_test").from(1)).subAggregation( - histogram("histo").field("gap_test").interval(interval).minDocCount(0) - .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + filter("filtered").filter(new RangeFilterBuilder(INTERVAL_FIELD).from(1)).subAggregation( + histogram("histo").field(INTERVAL_FIELD).interval(1).minDocCount(0).extendedBounds(0L, 49L) .subAggregation(randomMetric("the_metric", GAP_FIELD)) - .subAggregation(movingAvg("movavg_counts") + .subAggregation(movingAvg("movavg_values") .window(windowSize) .modelBuilder(randomModelBuilder()) .gapPolicy(gapPolicy) - .predict(numPredictions) - .setBucketsPaths("the_metric")) - ) - - ).execute().actionGet(); + .setBucketsPaths("the_metric") + .predict(numPredictions)) + )) + .execute().actionGet(); assertSearchResponse(response); @@ -888,26 +877,29 @@ public void testLeftGapWithPrediction() { assertThat(filtered.getName(), equalTo("filtered")); InternalHistogram histo = filtered.getAggregations().get("histo"); - assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List buckets = histo.getBuckets(); - assertThat(buckets.size(), equalTo(numValueBuckets + numPredictions)); + assertThat("Size of buckets array is not correct.", buckets.size(), equalTo(50 + numPredictions)); + + double lastValue = 0; double currentValue; - double lastValue = 0.0; - for (int i = 0; i < numValueBuckets - 1; i++) { - currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movavg_counts"))).value(); + for (int i = 0; i < 50; i++) { + SimpleValue current = buckets.get(i).getAggregations().get("movavg_values"); + if (current != null) { + currentValue = current.value(); - assertThat(Double.compare(lastValue, currentValue), lessThanOrEqualTo(0)); - lastValue = currentValue; + assertThat(Double.compare(lastValue, currentValue), lessThanOrEqualTo(0)); + lastValue = currentValue; + } } // Now check predictions - for (int i = numValueBuckets; i < numValueBuckets + numPredictions; i++) { + for (int i = 50; i < 50 + numPredictions; i++) { // Unclear at this point which direction the predictions will go, just verify they are // not null, and that we don't have the_metric anymore - assertThat((buckets.get(i).getAggregations().get("movavg_counts")), notNullValue()); + assertThat((buckets.get(i).getAggregations().get("movavg_values")), notNullValue()); assertThat((buckets.get(i).getAggregations().get("the_metric")), nullValue()); } } @@ -919,22 +911,19 @@ public void testLeftGapWithPrediction() { */ @Test public void testRightGap() { - SearchResponse response = client() - .prepareSearch("idx") + .prepareSearch("idx").setTypes("gap_type") .addAggregation( - filter("filtered").filter(new RangeFilterBuilder("gap_test").to((interval * (numValueBuckets - 1) - interval))).subAggregation( - histogram("histo").field("gap_test").interval(interval).minDocCount(0) - .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + filter("filtered").filter(new RangeFilterBuilder(INTERVAL_FIELD).to(1)).subAggregation( + histogram("histo").field(INTERVAL_FIELD).interval(1).minDocCount(0).extendedBounds(0L, 49L) .subAggregation(randomMetric("the_metric", GAP_FIELD)) - .subAggregation(movingAvg("movavg_counts") + .subAggregation(movingAvg("movavg_values") .window(windowSize) .modelBuilder(randomModelBuilder()) .gapPolicy(gapPolicy) .setBucketsPaths("the_metric")) - ) - - ).execute().actionGet(); + )) + .execute().actionGet(); assertSearchResponse(response); @@ -943,44 +932,46 @@ public void testRightGap() { assertThat(filtered.getName(), equalTo("filtered")); InternalHistogram histo = filtered.getAggregations().get("histo"); - assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List buckets = histo.getBuckets(); - assertThat(buckets.size(), equalTo(numValueBuckets)); + assertThat("Size of buckets array is not correct.", buckets.size(), equalTo(50)); + + + SimpleValue current = buckets.get(0).getAggregations().get("movavg_values"); + assertThat(current, notNullValue()); + + double lastValue = current.value(); double currentValue; - double lastValue = ((SimpleValue)(buckets.get(0).getAggregations().get("movavg_counts"))).value(); - for (int i = 1; i < numValueBuckets - 1; i++) { - currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movavg_counts"))).value(); + for (int i = 1; i < 50; i++) { + current = buckets.get(i).getAggregations().get("movavg_values"); + if (current != null) { + currentValue = current.value(); - assertThat(Double.compare(lastValue, currentValue), greaterThanOrEqualTo(0)); - lastValue = currentValue; + assertThat(Double.compare(lastValue, currentValue), greaterThanOrEqualTo(0)); + lastValue = currentValue; + } } - } @Test - public void testRightGapWithPredictions() { - - int numPredictions = randomIntBetween(0, 10); - + public void testRightGapWithPredict() { + int numPredictions = randomIntBetween(1, 10); SearchResponse response = client() - .prepareSearch("idx") + .prepareSearch("idx").setTypes("gap_type") .addAggregation( - filter("filtered").filter(new RangeFilterBuilder("gap_test").to((interval * (numValueBuckets - 1) - interval))).subAggregation( - histogram("histo").field("gap_test").interval(interval).minDocCount(0) - .extendedBounds(0L, (long) (interval * (numValueBuckets - 1))) + filter("filtered").filter(new RangeFilterBuilder(INTERVAL_FIELD).to(1)).subAggregation( + histogram("histo").field(INTERVAL_FIELD).interval(1).minDocCount(0).extendedBounds(0L, 49L) .subAggregation(randomMetric("the_metric", GAP_FIELD)) - .subAggregation(movingAvg("movavg_counts") + .subAggregation(movingAvg("movavg_values") .window(windowSize) .modelBuilder(randomModelBuilder()) .gapPolicy(gapPolicy) - .predict(numPredictions) - .setBucketsPaths("the_metric")) - ) - - ).execute().actionGet(); + .setBucketsPaths("the_metric") + .predict(numPredictions)) + )) + .execute().actionGet(); assertSearchResponse(response); @@ -989,75 +980,69 @@ public void testRightGapWithPredictions() { assertThat(filtered.getName(), equalTo("filtered")); InternalHistogram histo = filtered.getAggregations().get("histo"); - assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List buckets = histo.getBuckets(); - assertThat(buckets.size(), equalTo(numValueBuckets + numPredictions)); + assertThat("Size of buckets array is not correct.", buckets.size(), equalTo(50 + numPredictions)); + + + SimpleValue current = buckets.get(0).getAggregations().get("movavg_values"); + assertThat(current, notNullValue()); + + double lastValue = current.value(); double currentValue; - double lastValue = ((SimpleValue)(buckets.get(0).getAggregations().get("movavg_counts"))).value(); - for (int i = 1; i < numValueBuckets - 1; i++) { - currentValue = ((SimpleValue)(buckets.get(i).getAggregations().get("movavg_counts"))).value(); + for (int i = 1; i < 50; i++) { + current = buckets.get(i).getAggregations().get("movavg_values"); + if (current != null) { + currentValue = current.value(); - assertThat(Double.compare(lastValue, currentValue), greaterThanOrEqualTo(0)); - lastValue = currentValue; + assertThat(Double.compare(lastValue, currentValue), greaterThanOrEqualTo(0)); + lastValue = currentValue; + } } // Now check predictions - for (int i = numValueBuckets; i < numValueBuckets + numPredictions; i++) { + for (int i = 50; i < 50 + numPredictions; i++) { // Unclear at this point which direction the predictions will go, just verify they are // not null, and that we don't have the_metric anymore - assertThat((buckets.get(i).getAggregations().get("movavg_counts")), notNullValue()); + assertThat((buckets.get(i).getAggregations().get("movavg_values")), notNullValue()); assertThat((buckets.get(i).getAggregations().get("the_metric")), nullValue()); } } - @Test - public void testPredictWithNoBuckets() { - - int numPredictions = randomIntBetween(0, 10); - - SearchResponse response = client() - .prepareSearch("idx") - .addAggregation( - // Filter so we are above all values - filter("filtered").filter(new RangeFilterBuilder("gap_test").from((interval * (numValueBuckets - 1) + interval))).subAggregation( - histogram("histo").field("gap_test").interval(interval).minDocCount(0) - .subAggregation(randomMetric("the_metric", GAP_FIELD)) - .subAggregation(movingAvg("movavg_counts") - .window(windowSize) - .modelBuilder(randomModelBuilder()) - .gapPolicy(gapPolicy) - .predict(numPredictions) - .setBucketsPaths("the_metric")) - ) - - ).execute().actionGet(); - assertSearchResponse(response); - - InternalFilter filtered = response.getAggregations().get("filtered"); - assertThat(filtered, notNullValue()); - assertThat(filtered.getName(), equalTo("filtered")); - - InternalHistogram histo = filtered.getAggregations().get("histo"); - - assertThat(histo, notNullValue()); - assertThat(histo.getName(), equalTo("histo")); - List buckets = histo.getBuckets(); - assertThat(buckets.size(), equalTo(0)); + private void assertValidIterators(Iterator expectedBucketIter, Iterator expectedCountsIter, Iterator expectedValuesIter) { + if (!expectedBucketIter.hasNext()) { + fail("`expectedBucketIter` iterator ended before `actual` iterator, size mismatch"); + } + if (!expectedCountsIter.hasNext()) { + fail("`expectedCountsIter` iterator ended before `actual` iterator, size mismatch"); + } + if (!expectedValuesIter.hasNext()) { + fail("`expectedValuesIter` iterator ended before `actual` iterator, size mismatch"); + } } + private void assertBucketContents(Histogram.Bucket actual, Double expectedCount, Double expectedValue) { + // This is a gap bucket + SimpleValue countMovAvg = actual.getAggregations().get("movavg_counts"); + if (expectedCount == null) { + assertThat("[_count] movavg is not null", countMovAvg, nullValue()); + } else { + assertThat("[_count] movavg is null", countMovAvg, notNullValue()); + assertThat("[_count] movavg does not match expected ["+countMovAvg.value()+" vs "+expectedCount+"]", + Math.abs(countMovAvg.value() - expectedCount) <= 0.000001, equalTo(true)); + } - private void checkBucketKeyAndDocCount(final String msg, final Histogram.Bucket bucket, final long expectedKey, - long expectedDocCount) { - if (expectedDocCount == -1) { - expectedDocCount = 0; + // This is a gap bucket + SimpleValue valuesMovAvg = actual.getAggregations().get("movavg_values"); + if (expectedValue == null) { + assertThat("[value] movavg is not null", valuesMovAvg, Matchers.nullValue()); + } else { + assertThat("[value] movavg is null", valuesMovAvg, notNullValue()); + assertThat("[value] movavg does not match expected ["+valuesMovAvg.value()+" vs "+expectedValue+"]", Math.abs(valuesMovAvg.value() - expectedValue) <= 0.000001, equalTo(true)); } - assertThat(msg, bucket, notNullValue()); - assertThat(msg + " key", ((Number) bucket.getKey()).longValue(), equalTo(expectedKey)); - assertThat(msg + " docCount", bucket.getDocCount(), equalTo(expectedDocCount)); } private MovAvgModelBuilder randomModelBuilder() { @@ -1069,9 +1054,9 @@ private MovAvgModelBuilder randomModelBuilder() { case 1: return new LinearModel.LinearModelBuilder(); case 2: - return new SingleExpModel.SingleExpModelBuilder().alpha(randomDouble()); + return new SingleExpModel.SingleExpModelBuilder().alpha(alpha); case 3: - return new DoubleExpModel.DoubleExpModelBuilder().alpha(randomDouble()).beta(randomDouble()); + return new DoubleExpModel.DoubleExpModelBuilder().alpha(alpha).beta(beta); default: return new SimpleModel.SimpleModelBuilder(); } From a218d59ce1ebb53c93ffb049d1d921fb0609711e Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Thu, 23 Apr 2015 17:51:43 -0400 Subject: [PATCH 59/68] Fix bug where MovAvgReducer would allow NaN's to "corrupt" the moving avg --- .../search/aggregations/reducers/movavg/MovAvgReducer.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgReducer.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgReducer.java index 4bd2ff4c50a83..8b9f73ebf55db 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgReducer.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/MovAvgReducer.java @@ -115,10 +115,9 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext Double thisBucketValue = resolveBucketValue(histo, bucket, bucketsPaths()[0], gapPolicy); currentKey = bucket.getKey(); - if (thisBucketValue != null) { + if (!(thisBucketValue == null || thisBucketValue.equals(Double.NaN))) { values.offer(thisBucketValue); - // TODO handle "edge policy" double movavg = model.next(values); List aggs = new ArrayList<>(Lists.transform(bucket.getAggregations().asList(), FUNCTION)); From 8435d9226f41c97658ca5c16cdca71fc82474dd5 Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Thu, 23 Apr 2015 19:13:27 -0400 Subject: [PATCH 60/68] Fix bug in GiantGapWithPrediction, due to "slow start" of double exp --- .../aggregations/reducers/moving/avg/MovAvgTests.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java index c92f0b1cc2efc..eaedfe4e597cb 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java @@ -20,9 +20,9 @@ package org.elasticsearch.search.aggregations.reducers.moving.avg; +import com.carrotsearch.randomizedtesting.annotations.Seed; import com.google.common.collect.EvictingQueue; -import org.apache.lucene.util.LuceneTestCase.AwaitsFix; import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.search.SearchPhaseExecutionException; import org.elasticsearch.action.search.SearchResponse; @@ -58,12 +58,10 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.lessThanOrEqualTo; -import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.IsNull.notNullValue; import static org.hamcrest.core.IsNull.nullValue; @ElasticsearchIntegrationTest.SuiteScopeTest -@AwaitsFix(bugUrl = "Gap test logic seems to fail a lot of the time on CI build") public class MovAvgTests extends ElasticsearchIntegrationTest { private static final String INTERVAL_FIELD = "l_value"; @@ -789,8 +787,8 @@ public void testGiantGapWithPredict() { currentValue = current.value(); if (gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { - // If we ignore missing, there will only be two values in this histo, so movavg will stay the same - assertThat(Double.compare(lastValue, currentValue), equalTo(0)); + // if we are ignoring, movavg could go up (double_exp) or stay the same (simple, linear, single_exp) + assertThat(Double.compare(lastValue, currentValue), lessThanOrEqualTo(0)); } else if (gapPolicy.equals(BucketHelpers.GapPolicy.INSERT_ZEROS)) { // If we insert zeros, this should always increase the moving avg since the last bucket has a real value assertThat(Double.compare(lastValue, currentValue), equalTo(-1)); From 26189ee2e62afd819b793f5e6f3a6f7e0382775b Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Fri, 24 Apr 2015 22:38:43 -0400 Subject: [PATCH 61/68] Rename helpers to follow naming conventions --- ...stHelpers.java => ReducerHelperTests.java} | 2 +- .../reducers/moving/avg/MovAvgTests.java | 31 +++++++++---------- 2 files changed, 16 insertions(+), 17 deletions(-) rename src/test/java/org/elasticsearch/search/aggregations/reducers/{ReducerTestHelpers.java => ReducerHelperTests.java} (98%) diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/ReducerTestHelpers.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/ReducerHelperTests.java similarity index 98% rename from src/test/java/org/elasticsearch/search/aggregations/reducers/ReducerTestHelpers.java rename to src/test/java/org/elasticsearch/search/aggregations/reducers/ReducerHelperTests.java index 8496b93e7eabf..0b0f720344fd2 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/ReducerTestHelpers.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/ReducerHelperTests.java @@ -33,7 +33,7 @@ * Provides helper methods and classes for use in Reducer tests, such as creating mock histograms or computing * simple metrics */ -public class ReducerTestHelpers extends ElasticsearchTestCase { +public class ReducerHelperTests extends ElasticsearchTestCase { /** * Generates a mock histogram to use for testing. Each MockBucket holds a doc count, key and document values diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java index eaedfe4e597cb..cd6ac6cf490cb 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java @@ -20,7 +20,6 @@ package org.elasticsearch.search.aggregations.reducers.moving.avg; -import com.carrotsearch.randomizedtesting.annotations.Seed; import com.google.common.collect.EvictingQueue; import org.elasticsearch.action.index.IndexRequestBuilder; @@ -33,7 +32,7 @@ import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram.Bucket; import org.elasticsearch.search.aggregations.metrics.ValuesSourceMetricsAggregationBuilder; import org.elasticsearch.search.aggregations.reducers.BucketHelpers; -import org.elasticsearch.search.aggregations.reducers.ReducerTestHelpers; +import org.elasticsearch.search.aggregations.reducers.ReducerHelperTests; import org.elasticsearch.search.aggregations.reducers.SimpleValue; import org.elasticsearch.search.aggregations.reducers.movavg.models.DoubleExpModel; import org.elasticsearch.search.aggregations.reducers.movavg.models.LinearModel; @@ -75,7 +74,7 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { static double beta; static BucketHelpers.GapPolicy gapPolicy; static ValuesSourceMetricsAggregationBuilder metric; - static List mockHisto; + static List mockHisto; static Map> testValues; @@ -124,7 +123,7 @@ public void setupSuiteScopeCluster() throws Exception { gapPolicy = randomBoolean() ? BucketHelpers.GapPolicy.IGNORE : BucketHelpers.GapPolicy.INSERT_ZEROS; metric = randomMetric("the_metric", VALUE_FIELD); - mockHisto = ReducerTestHelpers.generateHistogram(interval, numBuckets, randomDouble(), randomDouble()); + mockHisto = ReducerHelperTests.generateHistogram(interval, numBuckets, randomDouble(), randomDouble()); testValues = new HashMap<>(8); @@ -134,7 +133,7 @@ public void setupSuiteScopeCluster() throws Exception { } } - for (ReducerTestHelpers.MockBucket mockBucket : mockHisto) { + for (ReducerHelperTests.MockBucket mockBucket : mockHisto) { for (double value : mockBucket.docValues) { builders.add(client().prepareIndex("idx", "type").setSource(jsonBuilder().startObject() .field(INTERVAL_FIELD, mockBucket.key) @@ -166,7 +165,7 @@ private void setupExpected(MovAvgType type, MetricTarget target) { ArrayList values = new ArrayList<>(numBuckets); EvictingQueue window = EvictingQueue.create(windowSize); - for (ReducerTestHelpers.MockBucket mockBucket : mockHisto) { + for (ReducerHelperTests.MockBucket mockBucket : mockHisto) { double metricValue; double[] docValues = mockBucket.docValues; @@ -180,12 +179,12 @@ private void setupExpected(MovAvgType type, MetricTarget target) { // otherwise insert a zero instead of the true value metricValue = 0.0; } else { - metricValue = ReducerTestHelpers.calculateMetric(docValues, metric); + metricValue = ReducerHelperTests.calculateMetric(docValues, metric); } } else { // If this isn't a gap, or is a _count, just insert the value - metricValue = target.equals(MetricTarget.VALUE) ? ReducerTestHelpers.calculateMetric(docValues, metric) : mockBucket.count; + metricValue = target.equals(MetricTarget.VALUE) ? ReducerHelperTests.calculateMetric(docValues, metric) : mockBucket.count; } window.offer(metricValue); @@ -336,7 +335,7 @@ public void simpleSingleValuedField() { List expectedValues = testValues.get(MovAvgType.SIMPLE.toString() + "_" + MetricTarget.VALUE.toString()); Iterator actualIter = buckets.iterator(); - Iterator expectedBucketIter = mockHisto.iterator(); + Iterator expectedBucketIter = mockHisto.iterator(); Iterator expectedCountsIter = expectedCounts.iterator(); Iterator expectedValuesIter = expectedValues.iterator(); @@ -344,7 +343,7 @@ public void simpleSingleValuedField() { assertValidIterators(expectedBucketIter, expectedCountsIter, expectedValuesIter); Histogram.Bucket actual = actualIter.next(); - ReducerTestHelpers.MockBucket expected = expectedBucketIter.next(); + ReducerHelperTests.MockBucket expected = expectedBucketIter.next(); Double expectedCount = expectedCountsIter.next(); Double expectedValue = expectedValuesIter.next(); @@ -388,7 +387,7 @@ public void linearSingleValuedField() { List expectedValues = testValues.get(MovAvgType.LINEAR.toString() + "_" + MetricTarget.VALUE.toString()); Iterator actualIter = buckets.iterator(); - Iterator expectedBucketIter = mockHisto.iterator(); + Iterator expectedBucketIter = mockHisto.iterator(); Iterator expectedCountsIter = expectedCounts.iterator(); Iterator expectedValuesIter = expectedValues.iterator(); @@ -396,7 +395,7 @@ public void linearSingleValuedField() { assertValidIterators(expectedBucketIter, expectedCountsIter, expectedValuesIter); Histogram.Bucket actual = actualIter.next(); - ReducerTestHelpers.MockBucket expected = expectedBucketIter.next(); + ReducerHelperTests.MockBucket expected = expectedBucketIter.next(); Double expectedCount = expectedCountsIter.next(); Double expectedValue = expectedValuesIter.next(); @@ -440,7 +439,7 @@ public void singleSingleValuedField() { List expectedValues = testValues.get(MovAvgType.SINGLE.toString() + "_" + MetricTarget.VALUE.toString()); Iterator actualIter = buckets.iterator(); - Iterator expectedBucketIter = mockHisto.iterator(); + Iterator expectedBucketIter = mockHisto.iterator(); Iterator expectedCountsIter = expectedCounts.iterator(); Iterator expectedValuesIter = expectedValues.iterator(); @@ -448,7 +447,7 @@ public void singleSingleValuedField() { assertValidIterators(expectedBucketIter, expectedCountsIter, expectedValuesIter); Histogram.Bucket actual = actualIter.next(); - ReducerTestHelpers.MockBucket expected = expectedBucketIter.next(); + ReducerHelperTests.MockBucket expected = expectedBucketIter.next(); Double expectedCount = expectedCountsIter.next(); Double expectedValue = expectedValuesIter.next(); @@ -492,7 +491,7 @@ public void doubleSingleValuedField() { List expectedValues = testValues.get(MovAvgType.DOUBLE.toString() + "_" + MetricTarget.VALUE.toString()); Iterator actualIter = buckets.iterator(); - Iterator expectedBucketIter = mockHisto.iterator(); + Iterator expectedBucketIter = mockHisto.iterator(); Iterator expectedCountsIter = expectedCounts.iterator(); Iterator expectedValuesIter = expectedValues.iterator(); @@ -500,7 +499,7 @@ public void doubleSingleValuedField() { assertValidIterators(expectedBucketIter, expectedCountsIter, expectedValuesIter); Histogram.Bucket actual = actualIter.next(); - ReducerTestHelpers.MockBucket expected = expectedBucketIter.next(); + ReducerHelperTests.MockBucket expected = expectedBucketIter.next(); Double expectedCount = expectedCountsIter.next(); Double expectedValue = expectedValuesIter.next(); From 31f26ec1152ce652151c70801ac7af298cd91b30 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Mon, 27 Apr 2015 17:10:03 +0100 Subject: [PATCH 62/68] review comment fixes --- .../search/aggregations/AggregatorFactories.java | 12 ------------ .../aggregations/InternalMultiBucketAggregation.java | 9 --------- .../aggregations/bucket/BucketsAggregator.java | 9 ++++----- .../bucket/histogram/InternalHistogram.java | 4 +--- .../aggregations/bucket/range/InternalRange.java | 10 ++++------ .../search/aggregations/reducers/BucketHelpers.java | 4 ++-- .../reducers/bucketmetrics/MaxBucketParser.java | 2 +- .../reducers/derivative/DerivativeParser.java | 2 +- .../aggregations/reducers/movavg/MovAvgParser.java | 2 +- .../aggregations/reducers/DateDerivativeTests.java | 1 - .../reducers/moving/avg/MovAvgTests.java | 8 ++++---- 11 files changed, 18 insertions(+), 45 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java index 843180960802a..4bbc8ba662c76 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java @@ -160,14 +160,6 @@ public AggregatorFactories build() { return new AggregatorFactories(factories.toArray(new AggregatorFactory[factories.size()]), orderedReducers); } - /* - * L ← Empty list that will contain the sorted nodes while there are - * unmarked nodes do select an unmarked node n visit(n) function - * visit(node n) if n has a temporary mark then stop (not a DAG) if n is - * not marked (i.e. has not been visited yet) then mark n temporarily - * for each node m with an edge from n to m do visit(m) mark n - * permanently unmark n temporarily add n to head of L - */ private List resolveReducerOrder(List reducerFactories, List aggFactories) { Map reducerFactoriesMap = new HashMap<>(); for (ReducerFactory factory : reducerFactories) { @@ -184,10 +176,6 @@ private List resolveReducerOrder(List reducerFac ReducerFactory factory = unmarkedFactories.get(0); resolveReducerOrder(aggFactoryNames, reducerFactoriesMap, orderedReducers, unmarkedFactories, temporarilyMarked, factory); } - List orderedReducerNames = new ArrayList<>(); - for (ReducerFactory reducerFactory : orderedReducers) { - orderedReducerNames.add(reducerFactory.getName()); - } return orderedReducers; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java b/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java index 856b96979f23f..db2ac49bf38a5 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java +++ b/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java @@ -98,13 +98,4 @@ public Object getProperty(String containingAggName, List path) { return aggregation.getProperty(path.subList(1, path.size())); } } - - public static abstract class Factory { - - public abstract String type(); - - public abstract A create(List buckets, A prototype); - - public abstract B createBucket(InternalAggregations aggregations, B prototype); - } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/BucketsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/BucketsAggregator.java index 93fa360b113c6..041c15a5dc110 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/BucketsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/BucketsAggregator.java @@ -43,8 +43,7 @@ public abstract class BucketsAggregator extends AggregatorBase { private final BigArrays bigArrays; private IntArray docCounts; - public BucketsAggregator(String name, AggregatorFactories factories, - AggregationContext context, Aggregator parent, + public BucketsAggregator(String name, AggregatorFactories factories, AggregationContext context, Aggregator parent, List reducers, Map metaData) throws IOException { super(name, factories, context, parent, reducers, metaData); bigArrays = context.bigArrays(); @@ -113,11 +112,11 @@ public final int bucketDocCount(long bucketOrd) { */ protected final InternalAggregations bucketAggregations(long bucket) throws IOException { final InternalAggregation[] aggregations = new InternalAggregation[subAggregators.length]; - for (int i = 0; i < subAggregators.length; i++) { + for (int i = 0; i < subAggregators.length; i++) { aggregations[i] = subAggregators[i].buildAggregation(bucket); - } + } return new InternalAggregations(Arrays.asList(aggregations)); - } + } /** * Utility method to build empty aggregations of the sub aggregators. diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java index 1fb919558d547..5c10e0d3ad418 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java @@ -234,7 +234,7 @@ public static void writeTo(EmptyBucketInfo info, StreamOutput out) throws IOExce } - public static class Factory extends InternalMultiBucketAggregation.Factory, B> { + public static class Factory { protected Factory() { } @@ -249,13 +249,11 @@ public InternalHistogram create(String name, List buckets, InternalOrder o return new InternalHistogram<>(name, buckets, order, minDocCount, emptyBucketInfo, formatter, keyed, this, reducers, metaData); } - @Override public InternalHistogram create(List buckets, InternalHistogram prototype) { return new InternalHistogram<>(prototype.name, buckets, prototype.order, prototype.minDocCount, prototype.emptyBucketInfo, prototype.formatter, prototype.keyed, this, prototype.reducers(), prototype.metaData); } - @Override public B createBucket(InternalAggregations aggregations, B prototype) { return (B) new Bucket(prototype.key, prototype.docCount, prototype.getKeyed(), prototype.formatter, this, aggregations); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java index 1bf62b9abb64f..db0ccee33e5d2 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java @@ -225,7 +225,7 @@ public void writeTo(StreamOutput out) throws IOException { } } - public static class Factory> extends InternalMultiBucketAggregation.Factory { + public static class Factory> { public String type() { return TYPE.name(); @@ -236,18 +236,16 @@ public R create(String name, List ranges, @Nullable ValueFormatter formatter, return (R) new InternalRange<>(name, ranges, formatter, keyed, reducers, metaData); } - - public B createBucket(String key, double from, double to, long docCount, InternalAggregations aggregations, boolean keyed, @Nullable ValueFormatter formatter) { + public B createBucket(String key, double from, double to, long docCount, InternalAggregations aggregations, boolean keyed, + @Nullable ValueFormatter formatter) { return (B) new Bucket(key, from, to, docCount, aggregations, keyed, formatter); } - @Override public R create(List ranges, R prototype) { return (R) new InternalRange<>(prototype.name, ranges, prototype.formatter, prototype.keyed, prototype.reducers(), prototype.metaData); - } + } - @Override public B createBucket(InternalAggregations aggregations, B prototype) { return (B) new Bucket(prototype.getKey(), prototype.from, prototype.to, prototype.getDocCount(), aggregations, prototype.keyed, prototype.formatter); diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java index f6cdd8ca1f960..4ac1bff7cfbbf 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/BucketHelpers.java @@ -53,7 +53,7 @@ public class BucketHelpers { * "ignore": empty buckets will simply be ignored */ public static enum GapPolicy { - INSERT_ZEROS((byte) 0, "insert_zeros"), IGNORE((byte) 1, "ignore"); + INSERT_ZEROS((byte) 0, "insert_zeros"), SKIP((byte) 1, "skip"); /** * Parse a string GapPolicy into the byte enum @@ -172,7 +172,7 @@ public static Double resolveBucketValue(InternalMultiBucketAggregation settings = null; String model = "simple"; diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java index ede94abd97336..b1ac6756f1e7c 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/DateDerivativeTests.java @@ -51,7 +51,6 @@ import static org.hamcrest.core.IsNull.nullValue; @ElasticsearchIntegrationTest.SuiteScopeTest -//@AwaitsFix(bugUrl = "Fix factory selection for serialisation of Internal derivative") public class DateDerivativeTests extends ElasticsearchIntegrationTest { private DateTime date(int month, int day) { diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java index cd6ac6cf490cb..ae0f89ae8687a 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java @@ -121,7 +121,7 @@ public void setupSuiteScopeCluster() throws Exception { alpha = randomDouble(); beta = randomDouble(); - gapPolicy = randomBoolean() ? BucketHelpers.GapPolicy.IGNORE : BucketHelpers.GapPolicy.INSERT_ZEROS; + gapPolicy = randomBoolean() ? BucketHelpers.GapPolicy.SKIP : BucketHelpers.GapPolicy.INSERT_ZEROS; metric = randomMetric("the_metric", VALUE_FIELD); mockHisto = ReducerHelperTests.generateHistogram(interval, numBuckets, randomDouble(), randomDouble()); @@ -172,7 +172,7 @@ private void setupExpected(MovAvgType type, MetricTarget target) { // Gaps only apply to metric values, not doc _counts if (mockBucket.count == 0 && target.equals(MetricTarget.VALUE)) { // If there was a gap in doc counts and we are ignoring, just skip this bucket - if (gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { + if (gapPolicy.equals(BucketHelpers.GapPolicy.SKIP)) { values.add(null); continue; } else if (gapPolicy.equals(BucketHelpers.GapPolicy.INSERT_ZEROS)) { @@ -726,7 +726,7 @@ public void testGiantGap() { assertThat(current, notNullValue()); currentValue = current.value(); - if (gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { + if (gapPolicy.equals(BucketHelpers.GapPolicy.SKIP)) { // if we are ignoring, movavg could go up (double_exp) or stay the same (simple, linear, single_exp) assertThat(Double.compare(lastValue, currentValue), lessThanOrEqualTo(0)); } else if (gapPolicy.equals(BucketHelpers.GapPolicy.INSERT_ZEROS)) { @@ -785,7 +785,7 @@ public void testGiantGapWithPredict() { assertThat(current, notNullValue()); currentValue = current.value(); - if (gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { + if (gapPolicy.equals(BucketHelpers.GapPolicy.SKIP)) { // if we are ignoring, movavg could go up (double_exp) or stay the same (simple, linear, single_exp) assertThat(Double.compare(lastValue, currentValue), lessThanOrEqualTo(0)); } else if (gapPolicy.equals(BucketHelpers.GapPolicy.INSERT_ZEROS)) { From 935144a064484830c42fe4ab0548357a397489f0 Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Mon, 27 Apr 2015 14:32:20 -0400 Subject: [PATCH 63/68] review comment fixes --- .../aggregations/reducers/movavg/models/MovAvgModel.java | 4 ---- .../reducers/movavg/models/MovAvgModelBuilder.java | 1 - 2 files changed, 5 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModel.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModel.java index d798887c836aa..b244587c9b276 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModel.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModel.java @@ -63,10 +63,6 @@ public double[] predict(Collection values, int numPredicti return predictions; } - // nocommit - // I don't like that it creates a new queue here - // The alternative to this is to just use `values` directly, but that would "consume" values - // and potentially change state elsewhere. Maybe ok? Collection predictionBuffer = EvictingQueue.create(values.size()); predictionBuffer.addAll(values); diff --git a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelBuilder.java b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelBuilder.java index 96bc9427de3d9..a8f40d474ac2a 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelBuilder.java +++ b/src/main/java/org/elasticsearch/search/aggregations/reducers/movavg/models/MovAvgModelBuilder.java @@ -29,5 +29,4 @@ * average models are used by the MovAvg reducer */ public interface MovAvgModelBuilder extends ToXContent { - public abstract XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException; } From bf9739d0f0bd8fa5c4f1a78208a65b46b5268bda Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Mon, 27 Apr 2015 14:40:04 -0400 Subject: [PATCH 64/68] [DOCS] review comment fixes --- docs/reference/search/aggregations.asciidoc | 2 +- docs/reference/search/aggregations/reducer.asciidoc | 1 + .../reducer/derivative-aggregation.asciidoc | 8 +++++--- .../{reducers => reducer}/images/double_0.2beta.png | Bin .../{reducers => reducer}/images/double_0.7beta.png | Bin .../images/double_prediction_global.png | Bin .../images/double_prediction_local.png | Bin .../images/linear_100window.png | Bin .../images/linear_10window.png | Bin .../images/movavg_100window.png | Bin .../images/movavg_10window.png | Bin .../images/simple_prediction.png | Bin .../images/single_0.2alpha.png | Bin .../images/single_0.7alpha.png | Bin .../reducer/max-bucket-aggregation.asciidoc | 2 +- .../{reducers => reducer}/movavg-reducer.asciidoc | 3 ++- 16 files changed, 10 insertions(+), 6 deletions(-) rename docs/reference/search/aggregations/{reducers => reducer}/images/double_0.2beta.png (100%) rename docs/reference/search/aggregations/{reducers => reducer}/images/double_0.7beta.png (100%) rename docs/reference/search/aggregations/{reducers => reducer}/images/double_prediction_global.png (100%) rename docs/reference/search/aggregations/{reducers => reducer}/images/double_prediction_local.png (100%) rename docs/reference/search/aggregations/{reducers => reducer}/images/linear_100window.png (100%) rename docs/reference/search/aggregations/{reducers => reducer}/images/linear_10window.png (100%) rename docs/reference/search/aggregations/{reducers => reducer}/images/movavg_100window.png (100%) rename docs/reference/search/aggregations/{reducers => reducer}/images/movavg_10window.png (100%) rename docs/reference/search/aggregations/{reducers => reducer}/images/simple_prediction.png (100%) rename docs/reference/search/aggregations/{reducers => reducer}/images/single_0.2alpha.png (100%) rename docs/reference/search/aggregations/{reducers => reducer}/images/single_0.7alpha.png (100%) rename docs/reference/search/aggregations/{reducers => reducer}/movavg-reducer.asciidoc (99%) diff --git a/docs/reference/search/aggregations.asciidoc b/docs/reference/search/aggregations.asciidoc index 74784c110a96b..7f08161637552 100644 --- a/docs/reference/search/aggregations.asciidoc +++ b/docs/reference/search/aggregations.asciidoc @@ -125,7 +125,7 @@ experimental[] Reducer aggregations work on the outputs produced from other aggregations rather than from document sets, adding information to the output tree. There are many different types of reducer, each computing different information from -other aggregations, but these type can broken down into two families: +other aggregations, but these types can broken down into two families: _Parent_:: A family of reducer aggregations that is provided with the output of its parent aggregation and is able diff --git a/docs/reference/search/aggregations/reducer.asciidoc b/docs/reference/search/aggregations/reducer.asciidoc index 75ac8b9a49ae6..d460fd5e450c9 100644 --- a/docs/reference/search/aggregations/reducer.asciidoc +++ b/docs/reference/search/aggregations/reducer.asciidoc @@ -1,4 +1,5 @@ [[search-aggregations-reducer]] include::reducer/derivative.asciidoc[] +include::reducer/max-bucket-aggregation.asciidoc[] include::reducer/movavg-reducer.asciidoc[] diff --git a/docs/reference/search/aggregations/reducer/derivative-aggregation.asciidoc b/docs/reference/search/aggregations/reducer/derivative-aggregation.asciidoc index f1fa8b44043b4..8369d0c1ba068 100644 --- a/docs/reference/search/aggregations/reducer/derivative-aggregation.asciidoc +++ b/docs/reference/search/aggregations/reducer/derivative-aggregation.asciidoc @@ -13,7 +13,8 @@ The following snippet calculates the derivative of the total monthly `sales`: "sales_per_month" : { "date_histogram" : { "field" : "date", - "interval" : "month" + "interval" : "month", + "min_doc_count" : 0 }, "aggs": { "sales": { @@ -64,7 +65,7 @@ And the following may be the response: { "key_as_string": "2015/03/01 00:00:00", "key": 1425168000000, - "doc_count": 2, + "doc_count": 2, <3> "sales": { "value": 375 }, @@ -81,6 +82,7 @@ And the following may be the response: <1> No derivative for the first bucket since we need at least 2 data points to calculate the derivative <2> Derivative value units are implicitly defined by the `sales` aggregation and the parent histogram so in this case the units would be $/month assuming the `price` field has units of $. +<3> The number of documents in the bucket are represented by the `doc_count` value ==== Second Order Derivative @@ -179,7 +181,7 @@ There are a couple of reasons why the data output by the enclosing histogram may on the enclosing histogram or with a query matching only a small number of documents) Where there is no data available in a bucket for a given metric it presents a problem for calculating the derivative value for both -the current bucket and the next bucket. In the derivative reducer aggregation has a `gap policy` parameter to define what the behavior +the current bucket and the next bucket. In the derivative reducer aggregation has a `gap_policy` parameter to define what the behavior should be when a gap in the data is found. There are currently two options for controlling the gap policy: _ignore_:: diff --git a/docs/reference/search/aggregations/reducers/images/double_0.2beta.png b/docs/reference/search/aggregations/reducer/images/double_0.2beta.png similarity index 100% rename from docs/reference/search/aggregations/reducers/images/double_0.2beta.png rename to docs/reference/search/aggregations/reducer/images/double_0.2beta.png diff --git a/docs/reference/search/aggregations/reducers/images/double_0.7beta.png b/docs/reference/search/aggregations/reducer/images/double_0.7beta.png similarity index 100% rename from docs/reference/search/aggregations/reducers/images/double_0.7beta.png rename to docs/reference/search/aggregations/reducer/images/double_0.7beta.png diff --git a/docs/reference/search/aggregations/reducers/images/double_prediction_global.png b/docs/reference/search/aggregations/reducer/images/double_prediction_global.png similarity index 100% rename from docs/reference/search/aggregations/reducers/images/double_prediction_global.png rename to docs/reference/search/aggregations/reducer/images/double_prediction_global.png diff --git a/docs/reference/search/aggregations/reducers/images/double_prediction_local.png b/docs/reference/search/aggregations/reducer/images/double_prediction_local.png similarity index 100% rename from docs/reference/search/aggregations/reducers/images/double_prediction_local.png rename to docs/reference/search/aggregations/reducer/images/double_prediction_local.png diff --git a/docs/reference/search/aggregations/reducers/images/linear_100window.png b/docs/reference/search/aggregations/reducer/images/linear_100window.png similarity index 100% rename from docs/reference/search/aggregations/reducers/images/linear_100window.png rename to docs/reference/search/aggregations/reducer/images/linear_100window.png diff --git a/docs/reference/search/aggregations/reducers/images/linear_10window.png b/docs/reference/search/aggregations/reducer/images/linear_10window.png similarity index 100% rename from docs/reference/search/aggregations/reducers/images/linear_10window.png rename to docs/reference/search/aggregations/reducer/images/linear_10window.png diff --git a/docs/reference/search/aggregations/reducers/images/movavg_100window.png b/docs/reference/search/aggregations/reducer/images/movavg_100window.png similarity index 100% rename from docs/reference/search/aggregations/reducers/images/movavg_100window.png rename to docs/reference/search/aggregations/reducer/images/movavg_100window.png diff --git a/docs/reference/search/aggregations/reducers/images/movavg_10window.png b/docs/reference/search/aggregations/reducer/images/movavg_10window.png similarity index 100% rename from docs/reference/search/aggregations/reducers/images/movavg_10window.png rename to docs/reference/search/aggregations/reducer/images/movavg_10window.png diff --git a/docs/reference/search/aggregations/reducers/images/simple_prediction.png b/docs/reference/search/aggregations/reducer/images/simple_prediction.png similarity index 100% rename from docs/reference/search/aggregations/reducers/images/simple_prediction.png rename to docs/reference/search/aggregations/reducer/images/simple_prediction.png diff --git a/docs/reference/search/aggregations/reducers/images/single_0.2alpha.png b/docs/reference/search/aggregations/reducer/images/single_0.2alpha.png similarity index 100% rename from docs/reference/search/aggregations/reducers/images/single_0.2alpha.png rename to docs/reference/search/aggregations/reducer/images/single_0.2alpha.png diff --git a/docs/reference/search/aggregations/reducers/images/single_0.7alpha.png b/docs/reference/search/aggregations/reducer/images/single_0.7alpha.png similarity index 100% rename from docs/reference/search/aggregations/reducers/images/single_0.7alpha.png rename to docs/reference/search/aggregations/reducer/images/single_0.7alpha.png diff --git a/docs/reference/search/aggregations/reducer/max-bucket-aggregation.asciidoc b/docs/reference/search/aggregations/reducer/max-bucket-aggregation.asciidoc index ca6f274d18974..a93c7ed803636 100644 --- a/docs/reference/search/aggregations/reducer/max-bucket-aggregation.asciidoc +++ b/docs/reference/search/aggregations/reducer/max-bucket-aggregation.asciidoc @@ -34,7 +34,7 @@ The following snippet calculates the maximum of the total monthly `sales`: -------------------------------------------------- <1> `bucket_paths` instructs this max_bucket aggregation that we want the maximum value of the `sales` aggregation in the -"sales_per_month` date histogram. +`sales_per_month` date histogram. And the following may be the response: diff --git a/docs/reference/search/aggregations/reducers/movavg-reducer.asciidoc b/docs/reference/search/aggregations/reducer/movavg-reducer.asciidoc similarity index 99% rename from docs/reference/search/aggregations/reducers/movavg-reducer.asciidoc rename to docs/reference/search/aggregations/reducer/movavg-reducer.asciidoc index d9759629b75ff..a01141f0fecf9 100644 --- a/docs/reference/search/aggregations/reducers/movavg-reducer.asciidoc +++ b/docs/reference/search/aggregations/reducer/movavg-reducer.asciidoc @@ -71,7 +71,7 @@ embedded like any other metric aggregation: <1> A `date_histogram` named "my_date_histo" is constructed on the "timestamp" field, with one-day intervals <2> We must specify "min_doc_count: 0" in our date histogram that all buckets are returned, even if they are empty. <3> A `sum` metric is used to calculate the sum of a field. This could be any metric (sum, min, max, etc) -<4> Finally, we specify a `moving_avg` aggregation which uses "the_sum" metric as it's input. +<4> Finally, we specify a `moving_avg` aggregation which uses "the_sum" metric as its input. Moving averages are built by first specifying a `histogram` or `date_histogram` over a field. You can then optionally add normal metrics, such as a `sum`, inside of that histogram. Finally, the `moving_avg` is embedded inside the histogram. @@ -121,6 +121,7 @@ the values from a `simple` moving average tend to "lag" behind the real data. "buckets_path": "the_sum", "model" : "simple" } + } } -------------------------------------------------- From 891dfee0d605cec09366add40971438fab46ad77 Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Wed, 29 Apr 2015 15:06:58 +0200 Subject: [PATCH 65/68] Fix some indentation issues. --- .../bucket/SingleBucketAggregator.java | 2 +- .../children/ParentToChildrenAggregator.java | 2 +- .../bucket/filter/FilterAggregator.java | 8 +- .../bucket/filters/FiltersAggregator.java | 10 +-- .../bucket/geogrid/GeoHashGridAggregator.java | 5 +- .../bucket/global/GlobalAggregator.java | 2 +- .../bucket/histogram/HistogramAggregator.java | 6 +- .../bucket/histogram/InternalHistogram.java | 2 +- .../bucket/missing/MissingAggregator.java | 4 +- .../bucket/nested/NestedAggregator.java | 80 +++++++++---------- .../nested/ReverseNestedAggregator.java | 30 +++---- .../bucket/range/RangeAggregator.java | 18 ++--- ...balOrdinalsSignificantTermsAggregator.java | 2 +- .../SignificantLongTermsAggregator.java | 7 +- .../SignificantStringTermsAggregator.java | 2 +- .../bucket/terms/DoubleTermsAggregator.java | 2 +- .../bucket/terms/LongTermsAggregator.java | 49 ++++++------ .../bucket/terms/StringTermsAggregator.java | 2 +- .../bucket/terms/TermsAggregatorFactory.java | 3 +- .../metrics/avg/AvgAggregator.java | 19 +++-- .../geobounds/GeoBoundsAggregator.java | 10 +-- .../metrics/geobounds/InternalGeoBounds.java | 4 +- .../metrics/max/MaxAggregator.java | 16 ++-- .../metrics/min/MinAggregator.java | 18 ++--- .../AbstractPercentilesAggregator.java | 4 +- .../PercentileRanksAggregator.java | 3 +- .../percentiles/PercentilesAggregator.java | 5 +- .../scripted/InternalScriptedMetric.java | 2 +- .../metrics/stats/StatsAggegator.java | 44 +++++----- .../stats/extended/InternalExtendedStats.java | 3 +- .../metrics/sum/SumAggregator.java | 20 ++--- .../valuecount/ValueCountAggregator.java | 6 +- 32 files changed, 189 insertions(+), 201 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/SingleBucketAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/SingleBucketAggregator.java index 202f02c4a22e9..2e032640f98c5 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/SingleBucketAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/SingleBucketAggregator.java @@ -33,7 +33,7 @@ public abstract class SingleBucketAggregator extends BucketsAggregator { protected SingleBucketAggregator(String name, AggregatorFactories factories, - AggregationContext aggregationContext, Aggregator parent, + AggregationContext aggregationContext, Aggregator parent, List reducers, Map metaData) throws IOException { super(name, factories, aggregationContext, parent, reducers, metaData); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/children/ParentToChildrenAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/children/ParentToChildrenAggregator.java index da4c2622331e1..0a8a136d160f0 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/children/ParentToChildrenAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/children/ParentToChildrenAggregator.java @@ -65,7 +65,7 @@ public class ParentToChildrenAggregator extends SingleBucketAggregator { public ParentToChildrenAggregator(String name, AggregatorFactories factories, AggregationContext aggregationContext, Aggregator parent, String parentType, Filter childFilter, Filter parentFilter, - ValuesSource.Bytes.WithOrdinals.ParentChild valuesSource, + ValuesSource.Bytes.WithOrdinals.ParentChild valuesSource, long maxOrd, List reducers, Map metaData) throws IOException { super(name, factories, aggregationContext, parent, reducers, metaData); this.parentType = parentType; diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterAggregator.java index da728f1ee044f..6459ff832157a 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterAggregator.java @@ -48,7 +48,7 @@ public FilterAggregator(String name, org.apache.lucene.search.Filter filter, AggregatorFactories factories, AggregationContext aggregationContext, - Aggregator parent, List reducers, + Aggregator parent, List reducers, Map metaData) throws IOException { super(name, factories, aggregationContext, parent, reducers, metaData); this.filter = filter; @@ -61,12 +61,12 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, // no need to provide deleted docs to the filter final Bits bits = DocIdSets.asSequentialAccessBits(ctx.reader().maxDoc(), filter.getDocIdSet(ctx, null)); return new LeafBucketCollectorBase(sub, null) { - @Override + @Override public void collect(int doc, long bucket) throws IOException { - if (bits.get(doc)) { + if (bits.get(doc)) { collectBucket(sub, doc, bucket); } - } + } }; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/FiltersAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/FiltersAggregator.java index 931ead734fb1f..913d844cb6a11 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/FiltersAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/FiltersAggregator.java @@ -61,7 +61,7 @@ static class KeyedFilter { private final boolean keyed; public FiltersAggregator(String name, AggregatorFactories factories, List filters, boolean keyed, AggregationContext aggregationContext, - Aggregator parent, List reducers, Map metaData) + Aggregator parent, List reducers, Map metaData) throws IOException { super(name, factories, aggregationContext, parent, reducers, metaData); this.keyed = keyed; @@ -78,14 +78,14 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, bits[i] = DocIdSets.asSequentialAccessBits(ctx.reader().maxDoc(), filters[i].filter.getDocIdSet(ctx, null)); } return new LeafBucketCollectorBase(sub, null) { - @Override + @Override public void collect(int doc, long bucket) throws IOException { - for (int i = 0; i < bits.length; i++) { - if (bits[i].get(doc)) { + for (int i = 0; i < bits.length; i++) { + if (bits[i].get(doc)) { collectBucket(sub, doc, bucketOrd(bucket, i)); } + } } - } }; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridAggregator.java index c2c646f570259..36448a103c1b2 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashGridAggregator.java @@ -51,9 +51,8 @@ public class GeoHashGridAggregator extends BucketsAggregator { private final LongHash bucketOrds; public GeoHashGridAggregator(String name, AggregatorFactories factories, ValuesSource.Numeric valuesSource, - int requiredSize, - int shardSize, AggregationContext aggregationContext, Aggregator parent, List reducers, Map metaData) - throws IOException { + int requiredSize, int shardSize, AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException { super(name, factories, aggregationContext, parent, reducers, metaData); this.valuesSource = valuesSource; this.requiredSize = requiredSize; diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/global/GlobalAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/global/GlobalAggregator.java index edecdd749ddf3..acc1464d34987 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/global/GlobalAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/global/GlobalAggregator.java @@ -53,7 +53,7 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, public void collect(int doc, long bucket) throws IOException { assert bucket == 0 : "global aggregator can only be a top level aggregator"; collectBucket(sub, doc, bucket); - } + } }; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/HistogramAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/HistogramAggregator.java index 63325c12aad8a..44342366b3f95 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/HistogramAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/HistogramAggregator.java @@ -57,14 +57,12 @@ public class HistogramAggregator extends BucketsAggregator { private final InternalHistogram.Factory histogramFactory; private final LongHash bucketOrds; - private SortedNumericDocValues values; public HistogramAggregator(String name, AggregatorFactories factories, Rounding rounding, InternalOrder order, boolean keyed, long minDocCount, @Nullable ExtendedBounds extendedBounds, @Nullable ValuesSource.Numeric valuesSource, @Nullable ValueFormatter formatter, - InternalHistogram.Factory histogramFactory, - AggregationContext aggregationContext, - Aggregator parent, List reducers, Map metaData) throws IOException { + InternalHistogram.Factory histogramFactory, AggregationContext aggregationContext, + Aggregator parent, List reducers, Map metaData) throws IOException { super(name, factories, aggregationContext, parent, reducers, metaData); this.rounding = rounding; diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java index 5c10e0d3ad418..9e35ddb97b343 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java @@ -191,7 +191,7 @@ public void writeTo(StreamOutput out) throws IOException { public ValueFormatter getFormatter() { return formatter; - } + } public boolean getKeyed() { return keyed; diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/missing/MissingAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/missing/MissingAggregator.java index eb81c6a5ec119..b60c851023894 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/missing/MissingAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/missing/MissingAggregator.java @@ -44,8 +44,8 @@ public class MissingAggregator extends SingleBucketAggregator { private final ValuesSource valuesSource; public MissingAggregator(String name, AggregatorFactories factories, ValuesSource valuesSource, - AggregationContext aggregationContext, - Aggregator parent, List reducers, Map metaData) throws IOException { + AggregationContext aggregationContext, Aggregator parent, List reducers, + Map metaData) throws IOException { super(name, factories, aggregationContext, parent, reducers, metaData); this.valuesSource = valuesSource; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregator.java index 459802f62a3b0..3356c08966753 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregator.java @@ -68,58 +68,58 @@ public LeafBucketCollector getLeafCollector(final LeafReaderContext ctx, final L this.parentFilter = null; // In ES if parent is deleted, then also the children are deleted. Therefore acceptedDocs can also null here. DocIdSet childDocIdSet = childFilter.getDocIdSet(ctx, null); - if (DocIdSets.isEmpty(childDocIdSet)) { - childDocs = null; - } else { - childDocs = childDocIdSet.iterator(); - } + if (DocIdSets.isEmpty(childDocIdSet)) { + childDocs = null; + } else { + childDocs = childDocIdSet.iterator(); + } return new LeafBucketCollectorBase(sub, null) { - @Override + @Override public void collect(int parentDoc, long bucket) throws IOException { - // here we translate the parent doc to a list of its nested docs, and then call super.collect for evey one of them so they'll be collected + // here we translate the parent doc to a list of its nested docs, and then call super.collect for evey one of them so they'll be collected - // if parentDoc is 0 then this means that this parent doesn't have child docs (b/c these appear always before the parent doc), so we can skip: - if (parentDoc == 0 || childDocs == null) { - return; - } - if (parentFilter == null) { - // The aggs are instantiated in reverse, first the most inner nested aggs and lastly the top level aggs - // So at the time a nested 'nested' aggs is parsed its closest parent nested aggs hasn't been constructed. - // So the trick is to set at the last moment just before needed and we can use its child filter as the - // parent filter. - - // Additional NOTE: Before this logic was performed in the setNextReader(...) method, but the the assumption - // that aggs instances are constructed in reverse doesn't hold when buckets are constructed lazily during - // aggs execution + // if parentDoc is 0 then this means that this parent doesn't have child docs (b/c these appear always before the parent doc), so we can skip: + if (parentDoc == 0 || childDocs == null) { + return; + } + if (parentFilter == null) { + // The aggs are instantiated in reverse, first the most inner nested aggs and lastly the top level aggs + // So at the time a nested 'nested' aggs is parsed its closest parent nested aggs hasn't been constructed. + // So the trick is to set at the last moment just before needed and we can use its child filter as the + // parent filter. + + // Additional NOTE: Before this logic was performed in the setNextReader(...) method, but the the assumption + // that aggs instances are constructed in reverse doesn't hold when buckets are constructed lazily during + // aggs execution Filter parentFilterNotCached = findClosestNestedPath(parent()); - if (parentFilterNotCached == null) { - parentFilterNotCached = NonNestedDocsFilter.INSTANCE; - } - parentFilter = context.searchContext().bitsetFilterCache().getBitDocIdSetFilter(parentFilterNotCached); + if (parentFilterNotCached == null) { + parentFilterNotCached = NonNestedDocsFilter.INSTANCE; + } + parentFilter = context.searchContext().bitsetFilterCache().getBitDocIdSetFilter(parentFilterNotCached); BitDocIdSet parentSet = parentFilter.getDocIdSet(ctx); - if (DocIdSets.isEmpty(parentSet)) { - // There are no parentDocs in the segment, so return and set childDocs to null, so we exit early for future invocations. - childDocs = null; - return; - } else { - parentDocs = parentSet.bits(); - } - } + if (DocIdSets.isEmpty(parentSet)) { + // There are no parentDocs in the segment, so return and set childDocs to null, so we exit early for future invocations. + childDocs = null; + return; + } else { + parentDocs = parentSet.bits(); + } + } - final int prevParentDoc = parentDocs.prevSetBit(parentDoc - 1); - int childDocId = childDocs.docID(); - if (childDocId <= prevParentDoc) { - childDocId = childDocs.advance(prevParentDoc + 1); - } + final int prevParentDoc = parentDocs.prevSetBit(parentDoc - 1); + int childDocId = childDocs.docID(); + if (childDocId <= prevParentDoc) { + childDocId = childDocs.advance(prevParentDoc + 1); + } - for (; childDocId < parentDoc; childDocId = childDocs.nextDoc()) { + for (; childDocId < parentDoc; childDocId = childDocs.nextDoc()) { collectBucket(sub, childDocId, bucket); } - } + } }; } - + @Override public InternalAggregation buildAggregation(long owningBucketOrdinal) throws IOException { return new InternalNested(name, bucketDocCount(owningBucketOrdinal), bucketAggregations(owningBucketOrdinal), reducers(), diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/ReverseNestedAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/ReverseNestedAggregator.java index b64abf55b109a..5644c6acf1f52 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/ReverseNestedAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/ReverseNestedAggregator.java @@ -72,29 +72,29 @@ protected LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final Leaf // must belong to parent docs that is alive. For this reason acceptedDocs can be null here. BitDocIdSet docIdSet = parentFilter.getDocIdSet(ctx); final BitSet parentDocs; - if (DocIdSets.isEmpty(docIdSet)) { + if (DocIdSets.isEmpty(docIdSet)) { return LeafBucketCollector.NO_OP_COLLECTOR; - } else { - parentDocs = docIdSet.bits(); - } + } else { + parentDocs = docIdSet.bits(); + } final LongIntOpenHashMap bucketOrdToLastCollectedParentDoc = new LongIntOpenHashMap(32); return new LeafBucketCollectorBase(sub, null) { - @Override + @Override public void collect(int childDoc, long bucket) throws IOException { - // fast forward to retrieve the parentDoc this childDoc belongs to - final int parentDoc = parentDocs.nextSetBit(childDoc); - assert childDoc <= parentDoc && parentDoc != DocIdSetIterator.NO_MORE_DOCS; + // fast forward to retrieve the parentDoc this childDoc belongs to + final int parentDoc = parentDocs.nextSetBit(childDoc); + assert childDoc <= parentDoc && parentDoc != DocIdSetIterator.NO_MORE_DOCS; if (bucketOrdToLastCollectedParentDoc.containsKey(bucket)) { - int lastCollectedParentDoc = bucketOrdToLastCollectedParentDoc.lget(); - if (parentDoc > lastCollectedParentDoc) { + int lastCollectedParentDoc = bucketOrdToLastCollectedParentDoc.lget(); + if (parentDoc > lastCollectedParentDoc) { collectBucket(sub, parentDoc, bucket); - bucketOrdToLastCollectedParentDoc.lset(parentDoc); - } - } else { + bucketOrdToLastCollectedParentDoc.lset(parentDoc); + } + } else { collectBucket(sub, parentDoc, bucket); bucketOrdToLastCollectedParentDoc.put(bucket, parentDoc); - } - } + } + } }; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/RangeAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/RangeAggregator.java index 14fe9ddd3bca4..d6d961a59982d 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/RangeAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/RangeAggregator.java @@ -105,7 +105,7 @@ public RangeAggregator(String name, List ranges, boolean keyed, AggregationContext aggregationContext, - Aggregator parent, List reducers, + Aggregator parent, List reducers, Map metaData) throws IOException { super(name, factories, aggregationContext, parent, reducers, metaData); @@ -140,15 +140,15 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final LeafBucketCollector sub) throws IOException { final SortedNumericDoubleValues values = valuesSource.doubleValues(ctx); return new LeafBucketCollectorBase(sub, values) { - @Override + @Override public void collect(int doc, long bucket) throws IOException { - values.setDocument(doc); - final int valuesCount = values.count(); - for (int i = 0, lo = 0; i < valuesCount; ++i) { - final double value = values.valueAt(i); + values.setDocument(doc); + final int valuesCount = values.count(); + for (int i = 0, lo = 0; i < valuesCount; ++i) { + final double value = values.valueAt(i); lo = collect(doc, value, bucket, lo); - } - } + } + } private int collect(int doc, double value, long owningBucketOrdinal, int lowBound) throws IOException { int lo = lowBound, hi = ranges.length - 1; // all candidates are between these indexes @@ -267,7 +267,7 @@ public Unmapped(String name, ValueFormat format, AggregationContext context, Aggregator parent, - InternalRange.Factory factory, List reducers, + InternalRange.Factory factory, List reducers, Map metaData) throws IOException { super(name, context, parent, reducers, metaData); diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/GlobalOrdinalsSignificantTermsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/GlobalOrdinalsSignificantTermsAggregator.java index 7e16dc2907389..492167f173531 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/GlobalOrdinalsSignificantTermsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/GlobalOrdinalsSignificantTermsAggregator.java @@ -50,7 +50,7 @@ public class GlobalOrdinalsSignificantTermsAggregator extends GlobalOrdinalsStri public GlobalOrdinalsSignificantTermsAggregator(String name, AggregatorFactories factories, ValuesSource.Bytes.WithOrdinals.FieldData valuesSource, BucketCountThresholds bucketCountThresholds, - IncludeExclude.OrdinalsFilter includeExclude, AggregationContext aggregationContext, Aggregator parent, + IncludeExclude.OrdinalsFilter includeExclude, AggregationContext aggregationContext, Aggregator parent, SignificantTermsAggregatorFactory termsAggFactory, List reducers, Map metaData) throws IOException { super(name, factories, valuesSource, null, bucketCountThresholds, includeExclude, aggregationContext, parent, diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTermsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTermsAggregator.java index f67c533956c40..329f5f566f568 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTermsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTermsAggregator.java @@ -46,10 +46,9 @@ public class SignificantLongTermsAggregator extends LongTermsAggregator { public SignificantLongTermsAggregator(String name, AggregatorFactories factories, ValuesSource.Numeric valuesSource, @Nullable ValueFormat format, - BucketCountThresholds bucketCountThresholds, - AggregationContext aggregationContext, - Aggregator parent, SignificantTermsAggregatorFactory termsAggFactory, IncludeExclude.LongFilter includeExclude, - List reducers, Map metaData) throws IOException { + BucketCountThresholds bucketCountThresholds, AggregationContext aggregationContext, + Aggregator parent, SignificantTermsAggregatorFactory termsAggFactory, IncludeExclude.LongFilter includeExclude, + List reducers, Map metaData) throws IOException { super(name, factories, valuesSource, format, null, bucketCountThresholds, aggregationContext, parent, SubAggCollectionMode.DEPTH_FIRST, false, includeExclude, reducers, metaData); diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTermsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTermsAggregator.java index 2423f22845144..a49f18734eecb 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTermsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTermsAggregator.java @@ -50,7 +50,7 @@ public class SignificantStringTermsAggregator extends StringTermsAggregator { public SignificantStringTermsAggregator(String name, AggregatorFactories factories, ValuesSource valuesSource, BucketCountThresholds bucketCountThresholds, IncludeExclude.StringFilter includeExclude, AggregationContext aggregationContext, Aggregator parent, - SignificantTermsAggregatorFactory termsAggFactory, List reducers, Map metaData) + SignificantTermsAggregatorFactory termsAggFactory, List reducers, Map metaData) throws IOException { super(name, factories, valuesSource, null, bucketCountThresholds, includeExclude, aggregationContext, parent, diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTermsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTermsAggregator.java index ea98734b94e1a..9250495524ed6 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTermsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTermsAggregator.java @@ -43,7 +43,7 @@ public class DoubleTermsAggregator extends LongTermsAggregator { public DoubleTermsAggregator(String name, AggregatorFactories factories, ValuesSource.Numeric valuesSource, @Nullable ValueFormat format, - Terms.Order order, BucketCountThresholds bucketCountThresholds, + Terms.Order order, BucketCountThresholds bucketCountThresholds, AggregationContext aggregationContext, Aggregator parent, SubAggCollectionMode collectionMode, boolean showTermDocCountError, IncludeExclude.LongFilter longFilter, List reducers, Map metaData) throws IOException { super(name, factories, valuesSource, format, order, bucketCountThresholds, aggregationContext, parent, collectionMode, diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTermsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTermsAggregator.java index ef1150f1d7efa..ea32e388fe68d 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTermsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTermsAggregator.java @@ -65,7 +65,7 @@ public LongTermsAggregator(String name, AggregatorFactories factories, ValuesSou this.longFilter = longFilter; bucketOrds = new LongHash(1, aggregationContext.bigArrays()); } - + @Override public boolean needsScores() { return (valuesSource != null && valuesSource.needsScores()) || super.needsScores(); @@ -80,30 +80,30 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final LeafBucketCollector sub) throws IOException { final SortedNumericDocValues values = getValues(valuesSource, ctx); return new LeafBucketCollectorBase(sub, values) { - @Override - public void collect(int doc, long owningBucketOrdinal) throws IOException { - assert owningBucketOrdinal == 0; - values.setDocument(doc); - final int valuesCount = values.count(); - - long previous = Long.MAX_VALUE; - for (int i = 0; i < valuesCount; ++i) { - final long val = values.valueAt(i); - if (previous != val || i == 0) { - if ((longFilter == null) || (longFilter.accept(val))) { - long bucketOrdinal = bucketOrds.add(val); - if (bucketOrdinal < 0) { // already seen - bucketOrdinal = - 1 - bucketOrdinal; + @Override + public void collect(int doc, long owningBucketOrdinal) throws IOException { + assert owningBucketOrdinal == 0; + values.setDocument(doc); + final int valuesCount = values.count(); + + long previous = Long.MAX_VALUE; + for (int i = 0; i < valuesCount; ++i) { + final long val = values.valueAt(i); + if (previous != val || i == 0) { + if ((longFilter == null) || (longFilter.accept(val))) { + long bucketOrdinal = bucketOrds.add(val); + if (bucketOrdinal < 0) { // already seen + bucketOrdinal = - 1 - bucketOrdinal; collectExistingBucket(sub, doc, bucketOrdinal); - } else { + } else { collectBucket(sub, doc, bucketOrdinal); - } + } + } + + previous = val; + } } - - previous = val; } - } - } }; } @@ -152,7 +152,7 @@ public InternalAggregation buildAggregation(long owningBucketOrdinal) throws IOE list[i] = bucket; otherDocCount -= bucket.docCount; } - + runDeferredCollections(survivingBucketOrds); //Now build the aggs @@ -160,13 +160,12 @@ public InternalAggregation buildAggregation(long owningBucketOrdinal) throws IOE list[i].aggregations = bucketAggregations(list[i].bucketOrd); list[i].docCountError = 0; } - + return new LongTerms(name, order, formatter, bucketCountThresholds.getRequiredSize(), bucketCountThresholds.getShardSize(), bucketCountThresholds.getMinDocCount(), Arrays.asList(list), showTermDocCountError, 0, otherDocCount, reducers(), metaData()); } - - + @Override public InternalAggregation buildEmptyAggregation() { return new LongTerms(name, order, formatter, bucketCountThresholds.getRequiredSize(), bucketCountThresholds.getShardSize(), diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTermsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTermsAggregator.java index f0bbcbef92490..6f80142da27ff 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTermsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTermsAggregator.java @@ -51,7 +51,7 @@ public class StringTermsAggregator extends AbstractStringTermsAggregator { public StringTermsAggregator(String name, AggregatorFactories factories, ValuesSource valuesSource, Terms.Order order, BucketCountThresholds bucketCountThresholds, - IncludeExclude.StringFilter includeExclude, AggregationContext aggregationContext, + IncludeExclude.StringFilter includeExclude, AggregationContext aggregationContext, Aggregator parent, SubAggCollectionMode collectionMode, boolean showTermDocCountError, List reducers, Map metaData) throws IOException { diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorFactory.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorFactory.java index 664211bc74c16..e12e4227fdfab 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorFactory.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorFactory.java @@ -243,8 +243,7 @@ protected Aggregator doCreateInternal(ValuesSource valuesSource, AggregationCont } return new DoubleTermsAggregator(name, factories, (ValuesSource.Numeric) valuesSource, config.format(), order, bucketCountThresholds, aggregationContext, parent, collectMode, - showTermDocCountError, longFilter, reducers, - metaData); + showTermDocCountError, longFilter, reducers, metaData); } if (includeExclude != null) { longFilter = includeExclude.convertToLongFilter(); diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/avg/AvgAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/avg/AvgAggregator.java index 3f0035330b892..6bcf5902233d6 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/avg/AvgAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/avg/AvgAggregator.java @@ -53,8 +53,7 @@ public class AvgAggregator extends NumericMetricsAggregator.SingleValue { ValueFormatter formatter; public AvgAggregator(String name, ValuesSource.Numeric valuesSource, @Nullable ValueFormatter formatter, - AggregationContext context, - Aggregator parent, List reducers, Map metaData) throws IOException { + AggregationContext context, Aggregator parent, List reducers, Map metaData) throws IOException { super(name, context, parent, reducers, metaData); this.valuesSource = valuesSource; this.formatter = formatter; @@ -75,22 +74,22 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final LeafBucketCollector sub) throws IOException { if (valuesSource == null) { return LeafBucketCollector.NO_OP_COLLECTOR; - } + } final BigArrays bigArrays = context.bigArrays(); final SortedNumericDoubleValues values = valuesSource.doubleValues(ctx); return new LeafBucketCollectorBase(sub, values) { - @Override + @Override public void collect(int doc, long bucket) throws IOException { counts = bigArrays.grow(counts, bucket + 1); sums = bigArrays.grow(sums, bucket + 1); - values.setDocument(doc); - final int valueCount = values.count(); + values.setDocument(doc); + final int valueCount = values.count(); counts.increment(bucket, valueCount); - double sum = 0; - for (int i = 0; i < valueCount; i++) { - sum += values.valueAt(i); - } + double sum = 0; + for (int i = 0; i < valueCount; i++) { + sum += values.valueAt(i); + } sums.increment(bucket, sum); } }; diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/geobounds/GeoBoundsAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/geobounds/GeoBoundsAggregator.java index 53e5c534094c8..464d0a339a8fb 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/geobounds/GeoBoundsAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/geobounds/GeoBoundsAggregator.java @@ -51,10 +51,9 @@ public final class GeoBoundsAggregator extends MetricsAggregator { DoubleArray negLefts; DoubleArray negRights; - protected GeoBoundsAggregator(String name, AggregationContext aggregationContext, - Aggregator parent, - ValuesSource.GeoPoint valuesSource, boolean wrapLongitude, List reducers, Map metaData) - throws IOException { + protected GeoBoundsAggregator(String name, AggregationContext aggregationContext, Aggregator parent, + ValuesSource.GeoPoint valuesSource, boolean wrapLongitude, List reducers, + Map metaData) throws IOException { super(name, aggregationContext, parent, reducers, metaData); this.valuesSource = valuesSource; this.wrapLongitude = wrapLongitude; @@ -184,8 +183,7 @@ protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggre @Override protected Aggregator doCreateInternal(ValuesSource.GeoPoint valuesSource, AggregationContext aggregationContext, - Aggregator parent, - boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException { + Aggregator parent, boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException { return new GeoBoundsAggregator(name, aggregationContext, parent, valuesSource, wrapLongitude, reducers, metaData); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/geobounds/InternalGeoBounds.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/geobounds/InternalGeoBounds.java index d0dbebf7a8ee6..fcf9200975260 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/geobounds/InternalGeoBounds.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/geobounds/InternalGeoBounds.java @@ -57,8 +57,8 @@ public InternalGeoBounds readResult(StreamInput in) throws IOException { } InternalGeoBounds(String name, double top, double bottom, double posLeft, double posRight, - double negLeft, double negRight, - boolean wrapLongitude, List reducers, Map metaData) { + double negLeft, double negRight, boolean wrapLongitude, + List reducers, Map metaData) { super(name, reducers, metaData); this.top = top; this.bottom = bottom; diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/max/MaxAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/max/MaxAggregator.java index 0c97ba38ac3d3..7ade492660eef 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/max/MaxAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/max/MaxAggregator.java @@ -53,8 +53,8 @@ public class MaxAggregator extends NumericMetricsAggregator.SingleValue { DoubleArray maxes; public MaxAggregator(String name, ValuesSource.Numeric valuesSource, @Nullable ValueFormatter formatter, - AggregationContext context, - Aggregator parent, List reducers, Map metaData) throws IOException { + AggregationContext context, Aggregator parent, List reducers, + Map metaData) throws IOException { super(name, context, parent, reducers, metaData); this.valuesSource = valuesSource; this.formatter = formatter; @@ -80,16 +80,16 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final NumericDoubleValues values = MultiValueMode.MAX.select(allValues, Double.NEGATIVE_INFINITY); return new LeafBucketCollectorBase(sub, allValues) { - @Override + @Override public void collect(int doc, long bucket) throws IOException { if (bucket >= maxes.size()) { - long from = maxes.size(); + long from = maxes.size(); maxes = bigArrays.grow(maxes, bucket + 1); - maxes.fill(from, maxes.size(), Double.NEGATIVE_INFINITY); - } - final double value = values.get(doc); + maxes.fill(from, maxes.size(), Double.NEGATIVE_INFINITY); + } + final double value = values.get(doc); double max = maxes.get(bucket); - max = Math.max(max, value); + max = Math.max(max, value); maxes.set(bucket, max); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/min/MinAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/min/MinAggregator.java index c80b7b8f064df..cf832cabe1ff0 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/min/MinAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/min/MinAggregator.java @@ -53,8 +53,8 @@ public class MinAggregator extends NumericMetricsAggregator.SingleValue { DoubleArray mins; public MinAggregator(String name, ValuesSource.Numeric valuesSource, @Nullable ValueFormatter formatter, - AggregationContext context, - Aggregator parent, List reducers, Map metaData) throws IOException { + AggregationContext context, Aggregator parent, List reducers, + Map metaData) throws IOException { super(name, context, parent, reducers, metaData); this.valuesSource = valuesSource; if (valuesSource != null) { @@ -74,22 +74,22 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final LeafBucketCollector sub) throws IOException { if (valuesSource == null) { return LeafBucketCollector.NO_OP_COLLECTOR; - } + } final BigArrays bigArrays = context.bigArrays(); final SortedNumericDoubleValues allValues = valuesSource.doubleValues(ctx); final NumericDoubleValues values = MultiValueMode.MIN.select(allValues, Double.POSITIVE_INFINITY); return new LeafBucketCollectorBase(sub, allValues) { - @Override + @Override public void collect(int doc, long bucket) throws IOException { if (bucket >= mins.size()) { - long from = mins.size(); + long from = mins.size(); mins = bigArrays.grow(mins, bucket + 1); - mins.fill(from, mins.size(), Double.POSITIVE_INFINITY); - } - final double value = values.get(doc); + mins.fill(from, mins.size(), Double.POSITIVE_INFINITY); + } + final double value = values.get(doc); double min = mins.get(bucket); - min = Math.min(min, value); + min = Math.min(min, value); mins.set(bucket, min); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/AbstractPercentilesAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/AbstractPercentilesAggregator.java index 8dd75b5911064..a73639a3d7faf 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/AbstractPercentilesAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/AbstractPercentilesAggregator.java @@ -55,8 +55,8 @@ private static int indexOfKey(double[] keys, double key) { public AbstractPercentilesAggregator(String name, ValuesSource.Numeric valuesSource, AggregationContext context, Aggregator parent, double[] keys, double compression, boolean keyed, - @Nullable ValueFormatter formatter, List reducers, - Map metaData) throws IOException { + @Nullable ValueFormatter formatter, List reducers, + Map metaData) throws IOException { super(name, context, parent, reducers, metaData); this.valuesSource = valuesSource; this.keyed = keyed; diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/PercentileRanksAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/PercentileRanksAggregator.java index 9d14e3b70c375..380482b8ab378 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/PercentileRanksAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/PercentileRanksAggregator.java @@ -96,8 +96,7 @@ protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggre protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException { return new PercentileRanksAggregator(name, valuesSource, aggregationContext, parent, values, compression, - keyed, - config.formatter(), reducers, metaData); + keyed, config.formatter(), reducers, metaData); } } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/PercentilesAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/PercentilesAggregator.java index 1a9a839bb757b..2a42dc94620b2 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/PercentilesAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/PercentilesAggregator.java @@ -40,7 +40,7 @@ public class PercentilesAggregator extends AbstractPercentilesAggregator { public PercentilesAggregator(String name, Numeric valuesSource, AggregationContext context, - Aggregator parent, double[] percents, + Aggregator parent, double[] percents, double compression, boolean keyed, @Nullable ValueFormatter formatter, List reducers, Map metaData) throws IOException { super(name, valuesSource, context, parent, percents, compression, keyed, formatter, reducers, metaData); @@ -97,8 +97,7 @@ protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggre protected Aggregator doCreateInternal(ValuesSource.Numeric valuesSource, AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, List reducers, Map metaData) throws IOException { return new PercentilesAggregator(name, valuesSource, aggregationContext, parent, percents, compression, - keyed, - config.formatter(), reducers, metaData); + keyed, config.formatter(), reducers, metaData); } } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java index edcfb8116609c..c67e8f3853da1 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java @@ -106,7 +106,7 @@ public InternalAggregation doReduce(List aggregations, Redu aggregation = aggregationObjects; } return new InternalScriptedMetric(firstAggregation.getName(), aggregation, firstAggregation.scriptLang, firstAggregation.scriptType, - firstAggregation.reduceScript, firstAggregation.reduceParams, reducers(), getMetaData()); + firstAggregation.reduceScript, firstAggregation.reduceParams, reducers(), getMetaData()); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/StatsAggegator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/StatsAggegator.java index 8a454b6cb734d..7b1f6c84a2d16 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/StatsAggegator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/StatsAggegator.java @@ -57,8 +57,8 @@ public class StatsAggegator extends NumericMetricsAggregator.MultiValue { public StatsAggegator(String name, ValuesSource.Numeric valuesSource, @Nullable ValueFormatter formatter, - AggregationContext context, - Aggregator parent, List reducers, Map metaData) throws IOException { + AggregationContext context, Aggregator parent, List reducers, + Map metaData) throws IOException { super(name, context, parent, reducers, metaData); this.valuesSource = valuesSource; if (valuesSource != null) { @@ -83,35 +83,35 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final LeafBucketCollector sub) throws IOException { if (valuesSource == null) { return LeafBucketCollector.NO_OP_COLLECTOR; - } + } final BigArrays bigArrays = context.bigArrays(); final SortedNumericDoubleValues values = valuesSource.doubleValues(ctx); return new LeafBucketCollectorBase(sub, values) { - @Override + @Override public void collect(int doc, long bucket) throws IOException { if (bucket >= counts.size()) { - final long from = counts.size(); + final long from = counts.size(); final long overSize = BigArrays.overSize(bucket + 1); - counts = bigArrays.resize(counts, overSize); - sums = bigArrays.resize(sums, overSize); - mins = bigArrays.resize(mins, overSize); - maxes = bigArrays.resize(maxes, overSize); - mins.fill(from, overSize, Double.POSITIVE_INFINITY); - maxes.fill(from, overSize, Double.NEGATIVE_INFINITY); - } - - values.setDocument(doc); - final int valuesCount = values.count(); + counts = bigArrays.resize(counts, overSize); + sums = bigArrays.resize(sums, overSize); + mins = bigArrays.resize(mins, overSize); + maxes = bigArrays.resize(maxes, overSize); + mins.fill(from, overSize, Double.POSITIVE_INFINITY); + maxes.fill(from, overSize, Double.NEGATIVE_INFINITY); + } + + values.setDocument(doc); + final int valuesCount = values.count(); counts.increment(bucket, valuesCount); - double sum = 0; + double sum = 0; double min = mins.get(bucket); double max = maxes.get(bucket); - for (int i = 0; i < valuesCount; i++) { - double value = values.valueAt(i); - sum += value; - min = Math.min(min, value); - max = Math.max(max, value); - } + for (int i = 0; i < valuesCount; i++) { + double value = values.valueAt(i); + sum += value; + min = Math.min(min, value); + max = Math.max(max, value); + } sums.increment(bucket, sum); mins.set(bucket, min); maxes.set(bucket, max); diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/extended/InternalExtendedStats.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/extended/InternalExtendedStats.java index 6785d6f35eb65..7fac72d7b05cc 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/extended/InternalExtendedStats.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/extended/InternalExtendedStats.java @@ -69,8 +69,7 @@ public static Metrics resolve(String name) { InternalExtendedStats() {} // for serialization public InternalExtendedStats(String name, long count, double sum, double min, double max, double sumOfSqrs, - double sigma, - @Nullable ValueFormatter formatter, List reducers, Map metaData) { + double sigma, @Nullable ValueFormatter formatter, List reducers, Map metaData) { super(name, count, sum, min, max, formatter, reducers, metaData); this.sumOfSqrs = sumOfSqrs; this.sigma = sigma; diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/sum/SumAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/sum/SumAggregator.java index af834af7f7b5a..4c7981422f362 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/sum/SumAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/sum/SumAggregator.java @@ -51,8 +51,8 @@ public class SumAggregator extends NumericMetricsAggregator.SingleValue { DoubleArray sums; public SumAggregator(String name, ValuesSource.Numeric valuesSource, @Nullable ValueFormatter formatter, - AggregationContext context, - Aggregator parent, List reducers, Map metaData) throws IOException { + AggregationContext context, Aggregator parent, List reducers, + Map metaData) throws IOException { super(name, context, parent, reducers, metaData); this.valuesSource = valuesSource; this.formatter = formatter; @@ -71,19 +71,19 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final LeafBucketCollector sub) throws IOException { if (valuesSource == null) { return LeafBucketCollector.NO_OP_COLLECTOR; - } + } final BigArrays bigArrays = context.bigArrays(); final SortedNumericDoubleValues values = valuesSource.doubleValues(ctx); return new LeafBucketCollectorBase(sub, values) { - @Override + @Override public void collect(int doc, long bucket) throws IOException { sums = bigArrays.grow(sums, bucket + 1); - values.setDocument(doc); - final int valuesCount = values.count(); - double sum = 0; - for (int i = 0; i < valuesCount; i++) { - sum += values.valueAt(i); - } + values.setDocument(doc); + final int valuesCount = values.count(); + double sum = 0; + for (int i = 0; i < valuesCount; i++) { + sum += values.valueAt(i); + } sums.increment(bucket, sum); } }; diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/valuecount/ValueCountAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/valuecount/ValueCountAggregator.java index 2bd7b50513598..fedd7e09a2b1e 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/valuecount/ValueCountAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/valuecount/ValueCountAggregator.java @@ -70,17 +70,17 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final LeafBucketCollector sub) throws IOException { if (valuesSource == null) { return LeafBucketCollector.NO_OP_COLLECTOR; - } + } final BigArrays bigArrays = context.bigArrays(); final SortedBinaryDocValues values = valuesSource.bytesValues(ctx); return new LeafBucketCollectorBase(sub, values) { - @Override + @Override public void collect(int doc, long bucket) throws IOException { counts = bigArrays.grow(counts, bucket + 1); values.setDocument(doc); counts.increment(bucket, values.count()); - } + } }; } From ccca0386ef3bc446ea1f63dcfdd7811f0567e0df Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Wed, 29 Apr 2015 15:14:23 +0200 Subject: [PATCH 66/68] Other indentation fixes --- .../search/aggregations/bucket/nested/NestedAggregator.java | 2 +- .../aggregations/bucket/nested/ReverseNestedAggregator.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregator.java index 3356c08966753..79da93d73015f 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregator.java @@ -66,7 +66,7 @@ public NestedAggregator(String name, AggregatorFactories factories, ObjectMapper public LeafBucketCollector getLeafCollector(final LeafReaderContext ctx, final LeafBucketCollector sub) throws IOException { // Reset parentFilter, so we resolve the parentDocs for each new segment being searched this.parentFilter = null; - // In ES if parent is deleted, then also the children are deleted. Therefore acceptedDocs can also null here. + // In ES if parent is deleted, then also the children are deleted. Therefore acceptedDocs can also null here. DocIdSet childDocIdSet = childFilter.getDocIdSet(ctx, null); if (DocIdSets.isEmpty(childDocIdSet)) { childDocs = null; diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/ReverseNestedAggregator.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/ReverseNestedAggregator.java index 5644c6acf1f52..9869c6d6a0a66 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/ReverseNestedAggregator.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/ReverseNestedAggregator.java @@ -68,8 +68,8 @@ public ReverseNestedAggregator(String name, AggregatorFactories factories, Objec @Override protected LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final LeafBucketCollector sub) throws IOException { - // In ES if parent is deleted, then also the children are deleted, so the child docs this agg receives - // must belong to parent docs that is alive. For this reason acceptedDocs can be null here. + // In ES if parent is deleted, then also the children are deleted, so the child docs this agg receives + // must belong to parent docs that is alive. For this reason acceptedDocs can be null here. BitDocIdSet docIdSet = parentFilter.getDocIdSet(ctx); final BitSet parentDocs; if (DocIdSets.isEmpty(docIdSet)) { From 3bb8ff2a925e69017826a5f71dca2ee1cdafcaac Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Wed, 29 Apr 2015 14:54:05 +0100 Subject: [PATCH 67/68] fixed issue with eggs in percolation request for 1 shard --- .../percolator/PercolatorService.java | 7 +- .../PercolatorFacetsAndAggregationsTests.java | 92 ++++++++++++++++--- 2 files changed, 78 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/elasticsearch/percolator/PercolatorService.java b/src/main/java/org/elasticsearch/percolator/PercolatorService.java index 23732af5d895d..4480de38051ba 100644 --- a/src/main/java/org/elasticsearch/percolator/PercolatorService.java +++ b/src/main/java/org/elasticsearch/percolator/PercolatorService.java @@ -847,16 +847,11 @@ private InternalAggregations reduceAggregations(List sha return null; } - InternalAggregations aggregations; - if (shardResults.size() == 1) { - aggregations = shardResults.get(0).aggregations(); - } else { List aggregationsList = new ArrayList<>(shardResults.size()); for (PercolateShardResponse shardResult : shardResults) { aggregationsList.add(shardResult.aggregations()); } - aggregations = InternalAggregations.reduce(aggregationsList, new ReduceContext(bigArrays, scriptService)); - } + InternalAggregations aggregations = InternalAggregations.reduce(aggregationsList, new ReduceContext(bigArrays, scriptService)); if (aggregations != null) { List reducers = shardResults.get(0).reducers(); if (reducers != null) { diff --git a/src/test/java/org/elasticsearch/percolator/PercolatorFacetsAndAggregationsTests.java b/src/test/java/org/elasticsearch/percolator/PercolatorFacetsAndAggregationsTests.java index 9f04e4a37b0e9..4540cc75a06a2 100644 --- a/src/test/java/org/elasticsearch/percolator/PercolatorFacetsAndAggregationsTests.java +++ b/src/test/java/org/elasticsearch/percolator/PercolatorFacetsAndAggregationsTests.java @@ -20,12 +20,14 @@ import org.elasticsearch.action.percolate.PercolateRequestBuilder; import org.elasticsearch.action.percolate.PercolateResponse; +import org.elasticsearch.common.settings.ImmutableSettings; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.search.aggregations.Aggregation; import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.Aggregator.SubAggCollectionMode; import org.elasticsearch.search.aggregations.bucket.terms.Terms; +import org.elasticsearch.search.aggregations.bucket.terms.Terms.Order; import org.elasticsearch.search.aggregations.reducers.ReducerBuilders; import org.elasticsearch.search.aggregations.reducers.bucketmetrics.InternalBucketMetricValue; import org.elasticsearch.test.ElasticsearchIntegrationTest; @@ -70,20 +72,18 @@ public void testFacetsAndAggregations() throws Exception { expectedCount[i % numUniqueQueries]++; QueryBuilder queryBuilder = matchQuery("field1", value); client().prepareIndex("test", PercolatorService.TYPE_NAME, Integer.toString(i)) - .setSource(jsonBuilder().startObject().field("query", queryBuilder).field("field2", "b").endObject()) - .execute().actionGet(); + .setSource(jsonBuilder().startObject().field("query", queryBuilder).field("field2", "b").endObject()).execute() + .actionGet(); } client().admin().indices().prepareRefresh("test").execute().actionGet(); for (int i = 0; i < numQueries; i++) { String value = values[i % numUniqueQueries]; - PercolateRequestBuilder percolateRequestBuilder = client().preparePercolate() - .setIndices("test").setDocumentType("type") + PercolateRequestBuilder percolateRequestBuilder = client().preparePercolate().setIndices("test").setDocumentType("type") .setPercolateDoc(docBuilder().setDoc(jsonBuilder().startObject().field("field1", value).endObject())); SubAggCollectionMode aggCollectionMode = randomFrom(SubAggCollectionMode.values()); - percolateRequestBuilder.addAggregation(AggregationBuilders.terms("a").field("field2") - .collectMode(aggCollectionMode )); + percolateRequestBuilder.addAggregation(AggregationBuilders.terms("a").field("field2").collectMode(aggCollectionMode)); if (randomBoolean()) { percolateRequestBuilder.setPercolateQuery(matchAllQuery()); @@ -135,20 +135,18 @@ public void testAggregationsAndReducers() throws Exception { expectedCount[i % numUniqueQueries]++; QueryBuilder queryBuilder = matchQuery("field1", value); client().prepareIndex("test", PercolatorService.TYPE_NAME, Integer.toString(i)) - .setSource(jsonBuilder().startObject().field("query", queryBuilder).field("field2", "b").endObject()) - .execute().actionGet(); + .setSource(jsonBuilder().startObject().field("query", queryBuilder).field("field2", "b").endObject()).execute() + .actionGet(); } client().admin().indices().prepareRefresh("test").execute().actionGet(); for (int i = 0; i < numQueries; i++) { String value = values[i % numUniqueQueries]; - PercolateRequestBuilder percolateRequestBuilder = client().preparePercolate() - .setIndices("test").setDocumentType("type") + PercolateRequestBuilder percolateRequestBuilder = client().preparePercolate().setIndices("test").setDocumentType("type") .setPercolateDoc(docBuilder().setDoc(jsonBuilder().startObject().field("field1", value).endObject())); SubAggCollectionMode aggCollectionMode = randomFrom(SubAggCollectionMode.values()); - percolateRequestBuilder.addAggregation(AggregationBuilders.terms("a").field("field2") - .collectMode(aggCollectionMode )); + percolateRequestBuilder.addAggregation(AggregationBuilders.terms("a").field("field2").collectMode(aggCollectionMode)); if (randomBoolean()) { percolateRequestBuilder.setPercolateQuery(matchAllQuery()); @@ -186,7 +184,7 @@ public void testAggregationsAndReducers() throws Exception { assertThat(maxA, notNullValue()); assertThat(maxA.getName(), equalTo("max_a")); assertThat(maxA.value(), equalTo((double) expectedCount[i % values.length])); - assertThat(maxA.keys(), equalTo(new String[] {"b"})); + assertThat(maxA.keys(), equalTo(new String[] { "b" })); } } @@ -194,12 +192,76 @@ public void testAggregationsAndReducers() throws Exception { public void testSignificantAggs() throws Exception { client().admin().indices().prepareCreate("test").execute().actionGet(); ensureGreen(); - PercolateRequestBuilder percolateRequestBuilder = client().preparePercolate() - .setIndices("test").setDocumentType("type") + PercolateRequestBuilder percolateRequestBuilder = client().preparePercolate().setIndices("test").setDocumentType("type") .setPercolateDoc(docBuilder().setDoc(jsonBuilder().startObject().field("field1", "value").endObject())) .addAggregation(AggregationBuilders.significantTerms("a").field("field2")); PercolateResponse response = percolateRequestBuilder.get(); assertNoFailures(response); } + @Test + public void testSingleShardAggregations() throws Exception { + assertAcked(prepareCreate("test").setSettings(ImmutableSettings.builder().put(indexSettings()).put("SETTING_NUMBER_OF_SHARDS", 1)) + .addMapping("type", "field1", "type=string", "field2", "type=string")); + ensureGreen(); + + int numQueries = scaledRandomIntBetween(250, 500); + + logger.info("--> registering {} queries", numQueries); + for (int i = 0; i < numQueries; i++) { + String value = "value0"; + QueryBuilder queryBuilder = matchQuery("field1", value); + client().prepareIndex("test", PercolatorService.TYPE_NAME, Integer.toString(i)) + .setSource(jsonBuilder().startObject().field("query", queryBuilder).field("field2", i % 3 == 0 ? "b" : "a").endObject()) + .execute() + .actionGet(); + } + client().admin().indices().prepareRefresh("test").execute().actionGet(); + + for (int i = 0; i < numQueries; i++) { + String value = "value0"; + PercolateRequestBuilder percolateRequestBuilder = client().preparePercolate().setIndices("test").setDocumentType("type") + .setPercolateDoc(docBuilder().setDoc(jsonBuilder().startObject().field("field1", value).endObject())); + + SubAggCollectionMode aggCollectionMode = randomFrom(SubAggCollectionMode.values()); + percolateRequestBuilder.addAggregation(AggregationBuilders.terms("terms").field("field2").collectMode(aggCollectionMode) + .order(Order.term(true)).shardSize(2).size(1)); + + if (randomBoolean()) { + percolateRequestBuilder.setPercolateQuery(matchAllQuery()); + } + if (randomBoolean()) { + percolateRequestBuilder.setScore(true); + } else { + percolateRequestBuilder.setSortByScore(true).setSize(numQueries); + } + + boolean countOnly = randomBoolean(); + if (countOnly) { + percolateRequestBuilder.setOnlyCount(countOnly); + } + + percolateRequestBuilder.addAggregation(ReducerBuilders.maxBucket("max_terms").setBucketsPaths("terms>_count")); + + PercolateResponse response = percolateRequestBuilder.execute().actionGet(); + assertMatchCount(response, numQueries); + if (!countOnly) { + assertThat(response.getMatches(), arrayWithSize(numQueries)); + } + + Aggregations aggregations = response.getAggregations(); + assertThat(aggregations.asList().size(), equalTo(2)); + Terms terms = aggregations.get("terms"); + assertThat(terms, notNullValue()); + assertThat(terms.getName(), equalTo("terms")); + List buckets = new ArrayList<>(terms.getBuckets()); + assertThat(buckets.size(), equalTo(1)); + assertThat(buckets.get(0).getKeyAsString(), equalTo("a")); + + InternalBucketMetricValue maxA = aggregations.get("max_terms"); + assertThat(maxA, notNullValue()); + assertThat(maxA.getName(), equalTo("max_terms")); + assertThat(maxA.keys(), equalTo(new String[] { "a" })); + } + } } From a33e77ff9604afb1ad5314a445ffa1bb3b3f2b2b Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Wed, 29 Apr 2015 16:04:29 +0100 Subject: [PATCH 68/68] Muted intermittently failing tests To reproduce the failures use `-Dtests.seed=D9EF60095522804F` --- .../aggregations/reducers/moving/avg/MovAvgTests.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java b/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java index ae0f89ae8687a..069f9904a3f4b 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/moving/avg/MovAvgTests.java @@ -43,7 +43,12 @@ import org.hamcrest.Matchers; import org.junit.Test; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.search.aggregations.AggregationBuilders.avg; @@ -303,6 +308,7 @@ private double doubleExp(Collection window) { * test simple moving average on single value field */ @Test + @AwaitsFix(bugUrl = "Fails with certain seeds including -Dtests.seed=D9EF60095522804F") public void simpleSingleValuedField() { SearchResponse response = client() @@ -355,6 +361,7 @@ public void simpleSingleValuedField() { } @Test + @AwaitsFix(bugUrl = "Fails with certain seeds including -Dtests.seed=D9EF60095522804F") public void linearSingleValuedField() { SearchResponse response = client() @@ -407,6 +414,7 @@ public void linearSingleValuedField() { } @Test + @AwaitsFix(bugUrl = "Fails with certain seeds including -Dtests.seed=D9EF60095522804F") public void singleSingleValuedField() { SearchResponse response = client() @@ -459,6 +467,7 @@ public void singleSingleValuedField() { } @Test + @AwaitsFix(bugUrl = "Fails with certain seeds including -Dtests.seed=D9EF60095522804F") public void doubleSingleValuedField() { SearchResponse response = client()

DE?qi7@jtkci%XpNhO6BtS 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 literal 0 HcmV?d00001 diff --git a/docs/reference/search/aggregations/reducers/images/double_prediction_global.png b/docs/reference/search/aggregations/reducers/images/double_prediction_global.png new file mode 100644 index 0000000000000000000000000000000000000000..faee6d22bc2b8db0aee280859181b0abcc2b2673 GIT binary patch literal 71898 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