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/85] 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 6dcee411e92..456b1b391b6 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 abstract class InternalAggregation implements Aggregation, ToXContent, St * 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 6a33c0312af..ec4625e2387 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 class InternalAggregations implements Aggregations, ToXContent, Streamabl 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 31d105d5ead..2bccb4234e3 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 abstract class InternalSingleBucketAggregation extends InternalAggregatio 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 2642c99a2de..505547487a6 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 class InternalFilters extends InternalMultiBucketAggregation implements F } @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 0d09f05694d..c30935c5c3d 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 class InternalGeoHashGrid extends InternalMultiBucketAggregation implemen } @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 c7909442016..544f06998ce 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 @@ public class InternalHistogram extends Inter } @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 d436e139287..0bb00d03122 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 class InternalRange extends InternalMulti } @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 199daecf5da..53949937bbb 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 abstract class InternalSignificantTerms extends InternalMultiBucketAggreg } @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 c457c1331b8..bb812741913 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 class UnmappedSignificantTerms extends InternalSignificantTerms { } @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 b8b45e20ce7..a6ca9d4400c 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 abstract class InternalTerms extends InternalMultiBucketAggregation imple } @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 a515596868e..1fffb9508a8 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 @@ public class UnmappedTerms extends InternalTerms { } @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 dcdecbe3b67..8c795a55332 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 class InternalAvg extends InternalNumericMetricsAggregation.SingleValue i } @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 2fd964e5f1f..c8341135fb4 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 @@ public final class InternalCardinality extends InternalNumericMetricsAggregation } @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 cdda6597c14..eb6a61c960d 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 class InternalGeoBounds extends InternalMetricsAggregation implements Geo } @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 90486f3b620..7cae1444c63 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 class InternalMax extends InternalNumericMetricsAggregation.SingleValue i } @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 554152e486c..0974314826c 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 class InternalMin extends InternalNumericMetricsAggregation.SingleValue i } @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 67f33934bf6..19d056e00cd 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 @@ abstract class AbstractInternalPercentiles extends InternalNumericMetricsAggrega 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 f204f8d5478..c7176e0e1e1 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 class InternalScriptedMetric extends InternalMetricsAggregation implement } @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 86bda11cd8e..7186fee979c 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 class InternalStats extends InternalNumericMetricsAggregation.MultiValue } @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 9a700690530..9f88bf4f429 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 class InternalExtendedStats extends InternalStats implements ExtendedStat } @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 b16663db26a..f653c082c79 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 class InternalSum extends InternalNumericMetricsAggregation.SingleValue i } @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 e4fad4ef692..8c5eafa2961 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 class InternalTopHits extends InternalMetricsAggregation implements TopHi } @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 062e88fce5f..b8b675c2eee 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 class InternalValueCount extends InternalNumericMetricsAggregation.Single } @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/85] 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 | 105 ++++++++------- .../nested/ReverseNestedAggregator.java | 63 +++++---- .../bucket/range/InternalRange.java | 13 +- .../bucket/range/RangeAggregator.java | 123 +++++++++--------- .../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 | 64 +++++---- .../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 | 59 +++++---- .../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, 1181 insertions(+), 640 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 fd9519499a8..bce1f9bc196 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 abstract class Aggregator extends BucketCollector implements Releasable { * 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 256700bada5..f49a328fd16 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.apache.lucene.search.Scorer; 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 abstract class AggregatorFactory { 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 abstract class AggregatorFactory { * @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 abstract class AggregatorFactory { 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 abstract class AggregatorFactory { 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 abstract class AggregatorFactory { 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 456b1b391b6..828a1a7ee0f 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.ToXContent; 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 abstract class InternalAggregation implements Aggregation, ToXContent, St protected Map metaData; + private List reducers; + /** Constructs an un initialized addAggregation (used for serialization) **/ protected InternalAggregation() {} @@ -124,8 +127,9 @@ public abstract class InternalAggregation implements Aggregation, ToXContent, St * * @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 abstract class InternalAggregation implements Aggregation, ToXContent, St * 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 abstract class InternalAggregation implements Aggregation, ToXContent, St 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 bd6d8d2728c..ebd2637ac56 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 @@ package org.elasticsearch.search.aggregations; 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 33c4215e27a..9b64c647b38 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 @@ import java.util.Map; */ 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 e4d0260cf93..b7c8fe7ccfc 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 @@ package org.elasticsearch.search.aggregations.bucket; 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 org.elasticsearch.search.aggregations.support.AggregationContext; 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 abstract class BucketsAggregator extends AggregatorBase { */ 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 2bccb4234e3..95157da9e77 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.io.stream.StreamOutput; 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 @@ public abstract class InternalSingleBucketAggregation extends InternalAggregatio * @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 d8b884a88e4..202f02c4a22 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 @@ package org.elasticsearch.search.aggregations.bucket; 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 @@ import java.util.Map; 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 427637b9da7..cfac7f834bc 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.common.io.stream.StreamInput; 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 class InternalChildren extends InternalSingleBucketAggregation implements 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 class InternalChildren extends InternalSingleBucketAggregation implements @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 b4769f05baf..eefbd853444 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.InternalAggregation; 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 class ParentToChildrenAggregator extends SingleBucketAggregator { @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 class ParentToChildrenAggregator extends SingleBucketAggregator { } @Override - protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, Map 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 d5b15dba1ca..da728f1ee04 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.index.LeafReaderContext; 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.InternalAggregation; 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 class FilterAggregator extends SingleBucketAggregator { 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 class FilterAggregator extends SingleBucketAggregator { // 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 class FilterAggregator extends SingleBucketAggregator { } @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 c3d84b9fe51..0429ea20a59 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.common.io.stream.StreamInput; 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 class InternalFilter extends InternalSingleBucketAggregation implements F 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 class InternalFilter extends InternalSingleBucketAggregation implements F @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 b97a5442ced..931ead734fb 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.index.LeafReaderContext; 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.InternalAggregations; 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 @@ public class FiltersAggregator extends BucketsAggregator { 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 class FiltersAggregator extends BucketsAggregator { 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 class FiltersAggregator extends BucketsAggregator { 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 class FiltersAggregator extends BucketsAggregator { 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 class FiltersAggregator extends BucketsAggregator { } @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 505547487a6..1e4c882ef5f 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.InternalAggregations; 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 class InternalFilters extends InternalMultiBucketAggregation implements F 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 class InternalFilters extends InternalMultiBucketAggregation implements F } } - 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 7e9f4682207..c2c646f5702 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.LeafBucketCollectorBase; 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 class GeoHashGridAggregator extends BucketsAggregator { 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 e1ce0a38c13..24b6d490c9f 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.AggregatorFactory; 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 org.elasticsearch.search.internal.SearchContext; import java.io.IOException; import java.util.Collections; +import java.util.List; import java.util.Map; /** @@ -123,9 +125,11 @@ public class GeoHashGridParser implements Aggregator.Parser { } @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 class GeoHashGridParser implements Aggregator.Parser { } @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 c30935c5c3d..83428f8c209 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.InternalAggregations; 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 class InternalGeoHashGrid extends InternalMultiBucketAggregation implemen 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 class InternalGeoHashGrid extends InternalMultiBucketAggregation implemen 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 7862eade5d6..edecdd749dd 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.InternalAggregation; 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 @@ import java.util.Map; */ 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 class GlobalAggregator extends SingleBucketAggregator { 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 class GlobalAggregator extends SingleBucketAggregator { } @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 class GlobalAggregator extends SingleBucketAggregator { 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 6e317f26952..157d2c5c7f9 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.common.io.stream.StreamInput; 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 class InternalGlobal extends InternalSingleBucketAggregation implements G 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 class InternalGlobal extends InternalSingleBucketAggregation implements G @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 a39a488a615..0a6a8bce732 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.InternalAggregation; 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 class HistogramAggregator extends BucketsAggregator { 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 class HistogramAggregator extends BucketsAggregator { // 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 class HistogramAggregator extends BucketsAggregator { } @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 @@ public class HistogramAggregator extends BucketsAggregator { 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 63cab59ad6b..9f9ad81c953 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.common.Nullable; 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 class InternalDateHistogram { @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 544f06998ce..a33cdb49b3c 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.InternalAggregations; 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 class InternalHistogram extends Inter } 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 @@ public class InternalHistogram extends Inter 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 class InternalHistogram extends Inter 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 d314e44e901..0245f117835 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.common.io.stream.StreamInput; 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 class InternalMissing extends InternalSingleBucketAggregation implements 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 class InternalMissing extends InternalSingleBucketAggregation implements @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 1b65bde9904..eb81c6a5ec1 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.InternalAggregation; 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 class MissingAggregator extends SingleBucketAggregator { @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 class MissingAggregator extends SingleBucketAggregator { } @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 8b434a3fd24..86ad26edab3 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.common.io.stream.StreamInput; 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 class InternalNested extends InternalSingleBucketAggregation implements N 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 class InternalNested extends InternalSingleBucketAggregation implements N @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 eec7345d317..6dfaad42b03 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.common.io.stream.StreamInput; 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 class InternalReverseNested extends InternalSingleBucketAggregation imple 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 class InternalReverseNested extends InternalSingleBucketAggregation imple @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 3fa459525f2..459802f62a3 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.LeafBucketCollector; 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 class NestedAggregator extends SingleBucketAggregator { 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. + // 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 + // 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 class NestedAggregator extends SingleBucketAggregator { } @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 4dbeec5898f..b64abf55b10 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.LeafBucketCollector; 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 class ReverseNestedAggregator extends SingleBucketAggregator { @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 @@ public class ReverseNestedAggregator extends SingleBucketAggregator { @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 class ReverseNestedAggregator extends SingleBucketAggregator { } @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 class ReverseNestedAggregator extends SingleBucketAggregator { 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 class ReverseNestedAggregator extends SingleBucketAggregator { } 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 0bb00d03122..59277b9a42f 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.InternalAggregations; 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 class InternalRange extends InternalMulti 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 class InternalRange extends InternalMulti 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 class InternalRange extends InternalMulti 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 47011b8dc49..14fe9ddd3bc 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.InternalAggregations; 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 class RangeAggregator extends BucketsAggregator { 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 class RangeAggregator extends BucketsAggregator { 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 - 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 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 class RangeAggregator extends BucketsAggregator { 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 class RangeAggregator extends BucketsAggregator { 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 class RangeAggregator extends BucketsAggregator { 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 class RangeAggregator extends BucketsAggregator { 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 class RangeAggregator extends BucketsAggregator { } @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 785df76e824..b679a6bc3d5 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.InternalAggregations; 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 class InternalDateRange extends InternalRange { } @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 class InternalDateRange extends InternalRange { 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 713b94595f5..fdaabb075cd 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.AggregatorFactory; 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 class GeoDistanceParser implements Aggregator.Parser { } @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 da2c41d5233..0fef2e2ba00 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.InternalAggregations; 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 class InternalGeoDistance extends InternalRange 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 class InternalGeoDistance extends InternalRange 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 9b608aa42d4..be2f8e52f8f 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.InternalAggregations; 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 class InternalIPv4Range extends InternalRange { } @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 class InternalIPv4Range extends InternalRange { 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 fc8e5e4b7f7..c7e260faf63 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.LeafBucketCollectorBase; 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 org.elasticsearch.search.internal.ContextIndexSearcher; 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 class GlobalOrdinalsSignificantTermsAggregator extends GlobalOrdinalsStri @Override public void collect(int doc, long bucket) throws IOException { super.collect(doc, bucket); - numCollectedDocs++; - } + numCollectedDocs++; + } }; } @@ -124,7 +127,9 @@ public class GlobalOrdinalsSignificantTermsAggregator extends GlobalOrdinalsStri 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 class GlobalOrdinalsSignificantTermsAggregator extends GlobalOrdinalsStri 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 class GlobalOrdinalsSignificantTermsAggregator extends GlobalOrdinalsStri 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 class GlobalOrdinalsSignificantTermsAggregator extends GlobalOrdinalsStri @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 53949937bbb..91ad85364e7 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.InternalAggregation; 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 abstract class InternalSignificantTerms extends InternalMultiBucketAggreg } } - 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 c4f97942ef2..bfb0b70458b 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.BucketStreamContext; 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 class SignificantLongTerms extends InternalSignificantTerms { } // 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 class SignificantLongTerms extends InternalSignificantTerms { @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 0b8d5813721..f67c533956c 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.LeafBucketCollectorBase; 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 org.elasticsearch.search.internal.ContextIndexSearcher; 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 class SignificantLongTermsAggregator extends LongTermsAggregator { 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 class SignificantLongTermsAggregator extends LongTermsAggregator { 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 ff4d5c94e05..295fadd41b9 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.BucketStreamContext; 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 class SignificantStringTerms extends InternalSignificantTerms { 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 class SignificantStringTerms extends InternalSignificantTerms { @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 fb65fd7d6f8..2638e82c607 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.LeafBucketCollectorBase; 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 org.elasticsearch.search.internal.ContextIndexSearcher; 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 class SignificantStringTermsAggregator extends StringTermsAggregator { 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 class SignificantStringTermsAggregator extends StringTermsAggregator { 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 7536bd05b69..7b85a76b21f 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.NonCollectingAggregator; 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.aggregations.support.ValuesSourceConfig; import org.elasticsearch.search.internal.SearchContext; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -64,8 +66,10 @@ public class SignificantTermsAggregatorFactory extends ValuesSourceAggregatorFac @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 @@ public class SignificantTermsAggregatorFactory extends ValuesSourceAggregatorFac @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 @@ public class SignificantTermsAggregatorFactory extends ValuesSourceAggregatorFac @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 class SignificantTermsAggregatorFactory extends ValuesSourceAggregatorFac 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 class SignificantTermsAggregatorFactory extends ValuesSourceAggregatorFac } @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 class SignificantTermsAggregatorFactory extends ValuesSourceAggregatorFac } @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 @@ public class SignificantTermsAggregatorFactory extends ValuesSourceAggregatorFac } } 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 @@ public class SignificantTermsAggregatorFactory extends ValuesSourceAggregatorFac 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 bb812741913..f382237dacf 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.common.xcontent.XContentBuilder; 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 class UnmappedSignificantTerms extends InternalSignificantTerms { 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 e87821e4e38..363895c5a39 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 @@ package org.elasticsearch.search.aggregations.bucket.terms; 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 c004f6e1e90..0e6ca403407 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.AggregationStreams; 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 class DoubleTerms extends InternalTerms { 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 class DoubleTerms extends InternalTerms { } @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 e71be14dc5b..ea98734b94e 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.index.fielddata.FieldData; 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 org.elasticsearch.search.aggregations.support.format.ValueFormat; import java.io.IOException; import java.util.Arrays; +import java.util.List; import java.util.Map; /** @@ -41,8 +43,11 @@ import java.util.Map; 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 @@ public class DoubleTermsAggregator extends LongTermsAggregator { 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 5b0ad6082b8..bff09e07e4a 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.LeafBucketCollector; 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 class GlobalOrdinalsStringTermsAggregator extends AbstractStringTermsAggr 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 class GlobalOrdinalsStringTermsAggregator extends AbstractStringTermsAggr 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 class GlobalOrdinalsStringTermsAggregator extends AbstractStringTermsAggr 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 @@ public class GlobalOrdinalsStringTermsAggregator extends AbstractStringTermsAggr } 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 a6ca9d4400c..75b82d4778c 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.InternalAggregation; 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 abstract class InternalTerms extends InternalMultiBucketAggregation imple 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 abstract class InternalTerms extends InternalMultiBucketAggregation imple } 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 1a7c2b4d0ee..b8edad21dd9 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.AggregationStreams; 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 class LongTerms extends InternalTerms { 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 class LongTerms extends InternalTerms { } @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 a570b06360f..ef1150f1d7e 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.LeafBucketCollector; 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 org.elasticsearch.search.aggregations.support.format.ValueFormatter; 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 class LongTermsAggregator extends TermsAggregator { 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(); + @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; + 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 class LongTermsAggregator extends TermsAggregator { list[i] = bucket; otherDocCount -= bucket.docCount; } - + runDeferredCollections(survivingBucketOrds); //Now build the aggs @@ -156,14 +160,18 @@ public class LongTermsAggregator extends TermsAggregator { 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 7caec199df3..ef9ec91e80c 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.InternalAggregation; 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 class StringTerms extends InternalTerms { 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 class StringTerms extends InternalTerms { } @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 9d731a25529..4d5310b4c19 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.InternalAggregation; 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 class StringTermsAggregator extends AbstractStringTermsAggregator { 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 ef254bb0594..4c4ad7ee31c 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.AggregatorFactories; 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 abstract class TermsAggregator extends BucketsAggregator { 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 6fbbd306411..a9cb4ea19cb 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 @@ public class TermsAggregatorFactory extends ValuesSourceAggregatorFactory 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 @@ public class TermsAggregatorFactory extends ValuesSourceAggregatorFactory 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 @@ public class TermsAggregatorFactory extends ValuesSourceAggregatorFactory 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 class TermsAggregatorFactory extends ValuesSourceAggregatorFactory metaData) throws IOException; + SubAggCollectionMode subAggCollectMode, boolean showTermDocCountError, List reducers, Map metaData) throws IOException; abstract boolean needsGlobalOrdinals(); @@ -152,9 +157,11 @@ public class TermsAggregatorFactory extends ValuesSourceAggregatorFactory 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 class TermsAggregatorFactory extends ValuesSourceAggregatorFactory 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 @@ public class TermsAggregatorFactory extends ValuesSourceAggregatorFactory 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 class UnmappedTerms extends InternalTerms { } @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 e3a9476e56a..8facf4c1ae5 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 0c301e30bde..e9323615fc3 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 abstract class InternalNumericMetricsAggregation extends InternalMetricsA 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 abstract class InternalNumericMetricsAggregation extends InternalMetricsA 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 abstract class InternalNumericMetricsAggregation extends InternalMetricsA 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 f29e063d61a..f3160cf464c 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 @@ package org.elasticsearch.search.aggregations.metrics; 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 66adf3ed74e..6342df383ed 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 @@ import java.util.Map; */ 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 @@ public abstract class NumericMetricsAggregator extends MetricsAggregator { 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 94a2e26c7b8..3f0035330b8 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.InternalAggregation; 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.ValuesSourceConfig; 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 class AvgAggregator extends NumericMetricsAggregator.SingleValue { 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 class AvgAggregator extends NumericMetricsAggregator.SingleValue { 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 class AvgAggregator extends NumericMetricsAggregator.SingleValue { } @Override - protected Aggregator createUnmapped(AggregationContext aggregationContext, Aggregator parent, Map 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 8c795a55332..f30cee32b31 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.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; /** @@ -56,8 +58,9 @@ public class InternalAvg extends InternalNumericMetricsAggregation.SingleValue i 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 class InternalAvg extends InternalNumericMetricsAggregation.SingleValue i 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 e4c2acce93c..98c911c2025 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.Aggregator; 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 class CardinalityAggregator extends NumericMetricsAggregator.SingleValue // 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 2d063dd5bd9..d2341bb2647 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 @@ package org.elasticsearch.search.aggregations.metrics.cardinality; 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 @@ final class CardinalityAggregatorFactory extends ValuesSourceAggregatorFactory 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 c8341135fb4..434140e74f6 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.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; @@ -53,8 +54,9 @@ public final class InternalCardinality extends InternalNumericMetricsAggregation 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 final class InternalCardinality extends InternalNumericMetricsAggregation 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 44e7fd195c0..53e5c534094 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.InternalAggregation; 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 final class GeoBoundsAggregator extends MetricsAggregator { 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 @@ public final class GeoBoundsAggregator extends MetricsAggregator { } @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 eb6a61c960d..f67734bdd09 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.common.xcontent.XContentBuilder; 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 class InternalGeoBounds extends InternalMetricsAggregation implements Geo } 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 class InternalGeoBounds extends InternalMetricsAggregation implements Geo 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 7cae1444c63..a181db30d98 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.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; /** @@ -55,8 +57,8 @@ public class InternalMax extends InternalNumericMetricsAggregation.SingleValue i 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 class InternalMax extends InternalNumericMetricsAggregation.SingleValue i 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 88edddc286c..0c97ba38ac3 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.InternalAggregation; 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.ValuesSourceConfig; 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 class MaxAggregator extends NumericMetricsAggregator.SingleValue { 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 class MaxAggregator extends NumericMetricsAggregator.SingleValue { 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 class MaxAggregator extends NumericMetricsAggregator.SingleValue { } @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 0974314826c..9917f966403 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.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; /** @@ -56,8 +58,8 @@ public class InternalMin extends InternalNumericMetricsAggregation.SingleValue i 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 class InternalMin extends InternalNumericMetricsAggregation.SingleValue i 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 438272e2bc1..c80b7b8f064 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.InternalAggregation; 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.ValuesSourceConfig; 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 class MinAggregator extends NumericMetricsAggregator.SingleValue { 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 class MinAggregator extends NumericMetricsAggregator.SingleValue { 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 class MinAggregator extends NumericMetricsAggregator.SingleValue { } @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 19d056e00cd..7ae2ad9ec60 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.common.xcontent.XContentBuilder; 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 @@ abstract class AbstractInternalPercentiles extends InternalNumericMetricsAggrega } 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 31a097f0b47..8dd75b59110 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.LeafBucketCollectorBase; 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 @@ public abstract class AbstractPercentilesAggregator extends NumericMetricsAggreg 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 190ca363ed3..687e1822b64 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.inject.internal.Nullable; 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 class InternalPercentileRanks extends AbstractInternalPercentiles impleme 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 class InternalPercentileRanks extends AbstractInternalPercentiles impleme 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 5e7d47803d8..357921aeb91 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.inject.internal.Nullable; 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 class InternalPercentiles extends AbstractInternalPercentiles implements 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 class InternalPercentiles extends AbstractInternalPercentiles implements 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 0383e33e8a7..9d14e3b70c3 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.common.inject.internal.Nullable; 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.ValuesSourceConfig; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -37,10 +39,10 @@ import java.util.Map; */ 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 class PercentileRanksAggregator extends AbstractPercentilesAggregator { 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 class PercentileRanksAggregator extends AbstractPercentilesAggregator { } @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 4dd99b73cd9..1a9a839bb75 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.common.inject.internal.Nullable; 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.ValuesSourceConfig; import org.elasticsearch.search.aggregations.support.format.ValueFormatter; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -38,9 +40,10 @@ import java.util.Map; 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 class PercentilesAggregator extends AbstractPercentilesAggregator { 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 class PercentilesAggregator extends AbstractPercentilesAggregator { @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 class PercentilesAggregator extends AbstractPercentilesAggregator { } @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 c7176e0e1e1..2a3900afc46 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.script.ScriptService.ScriptType; 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 class InternalScriptedMetric extends InternalMetricsAggregation implement 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 class InternalScriptedMetric extends InternalMetricsAggregation implement 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 e9260d852ad..22781a18612 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.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.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 class ScriptedMetricAggregator extends MetricsAggregator { } 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 class ScriptedMetricAggregator extends MetricsAggregator { } @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 class ScriptedMetricAggregator extends MetricsAggregator { 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 7186fee979c..5133012aabd 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.common.xcontent.XContentBuilderString; 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 class InternalStats extends InternalNumericMetricsAggregation.MultiValue 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 class InternalStats extends InternalNumericMetricsAggregation.MultiValue 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 8f431578fef..8a454b6cb73 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.InternalAggregation; 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.ValuesSourceConfig; 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 class StatsAggegator extends NumericMetricsAggregator.MultiValue { 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); - } + 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(); + 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 class StatsAggegator extends NumericMetricsAggregator.MultiValue { 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 class StatsAggegator extends NumericMetricsAggregator.MultiValue { } @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 75dc354f874..ae1bc68965d 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.InternalAggregation; 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.ValuesSourceConfig; 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 @@ public class ExtendedStatsAggregator extends NumericMetricsAggregator.MultiValue @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 class ExtendedStatsAggregator extends NumericMetricsAggregator.MultiValue } @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 9f88bf4f429..f5d9c7d1983 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.common.xcontent.XContentBuilderString; 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 class InternalExtendedStats extends InternalStats implements ExtendedStat 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 class InternalExtendedStats extends InternalStats implements ExtendedStat 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 f653c082c79..7f98d6cc4e8 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.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; /** @@ -55,8 +57,8 @@ public class InternalSum extends InternalNumericMetricsAggregation.SingleValue i 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 class InternalSum extends InternalNumericMetricsAggregation.SingleValue i 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 ab6b565a62b..af834af7f7b 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.InternalAggregation; 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.ValuesSourceConfig; 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 class SumAggregator extends NumericMetricsAggregator.SingleValue { 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 class SumAggregator extends NumericMetricsAggregator.SingleValue { 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 class SumAggregator extends NumericMetricsAggregator.SingleValue { } @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 e841ded7d91..20aeaae2f5a 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.AggregationInitializationException; 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.InternalSearchHits; import org.elasticsearch.search.internal.SubSearchContext; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -68,8 +71,9 @@ public class TopHitsAggregator extends MetricsAggregator { 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 class TopHitsAggregator extends MetricsAggregator { return sort.needsScores() || subSearchContext.trackScores(); } else { // sort by score - return true; - } + return true; + } } @Override @@ -180,8 +184,9 @@ public class TopHitsAggregator extends MetricsAggregator { } @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 b8b675c2eee..935eb5e1933 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.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 java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -54,8 +56,9 @@ public class InternalValueCount extends InternalNumericMetricsAggregation.Single 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 class InternalValueCount extends InternalNumericMetricsAggregation.Single 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 a74ec061b8e..2bd7b505135 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.InternalAggregation; 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.ValuesSourceConfig; 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 class ValueCountAggregator extends NumericMetricsAggregator.SingleValue { 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 class ValueCountAggregator extends NumericMetricsAggregator.SingleValue { 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 class ValueCountAggregator extends NumericMetricsAggregator.SingleValue { } @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 00000000000..c74f6f0b0f2 --- /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 00000000000..64e7d1c7baf --- /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 d88f95642c3..dbefc2e2612 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 @@ public abstract class ValuesSourceAggregatorFactory ext } @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 abstract class ValuesSourceAggregatorFactory ext } } - 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 ec3d17b9294..bdfec315402 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.XContentParser; 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.ByteArrayInputStream; 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 @@ public class SignificanceHeuristicTests extends ElasticsearchTestCase { 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/85] 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 10ea7f74c2c..795d9b5724c 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 @@ public class 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 class AggregatorFactories { 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 class AggregatorFactories { 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 f49a328fd16..3db9e5ddd69 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.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; @@ -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 @@ public abstract class AggregatorFactory { * @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 abstract class AggregatorFactory { } - 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/85] 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 9775b3f04d8..e57dd0e0b4e 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 @@ import java.io.IOException; /** * 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 2feaf112104..3910f096246 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 @@ package org.elasticsearch.search.aggregations; 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.stats.extended.ExtendedStat 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 @@ import java.util.List; */ 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 class AggregationModule extends AbstractModule implements SpawnModules{ * @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 795d9b5724c..5103f9c2b7a 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 @@ package org.elasticsearch.search.aggregations; 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 @@ public class 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 class AggregatorFactories { 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 class AggregatorFactories { 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 class AggregatorFactories { 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 3db9e5ddd69..d22fed75a8c 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 @@ public abstract class AggregatorFactory { * @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 b55f6a4f022..e23cf8ef228 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 @@ import java.util.regex.Pattern; 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 class AggregatorParsers { * @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 @@ public class AggregatorParsers { 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 @@ public class AggregatorParsers { 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 7cdff38d7c8..2f9ffafac53 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 class NestedAggregatorTest extends ElasticsearchSingleNodeLuceneTestCase 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/85] 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 3910f096246..cb4bef6ca34 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 class AggregationModule extends AbstractModule implements SpawnModules{ 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 828a1a7ee0f..fb621ea5103 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.XContentBuilder; 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 abstract class InternalAggregation implements Aggregation, ToXContent, St 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 abstract class InternalAggregation implements Aggregation, ToXContent, St 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 ec4625e2387..c41e8a4ff77 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 class InternalAggregations implements Aggregations, ToXContent, Streamabl 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 a33cdb49b3c..5c945afddf0 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 class InternalHistogram extends Inter out.writeVLong(docCount); aggregations.writeTo(out); } + + public ValueFormatter getFormatter() { + return formatter; + } + + public boolean getKeyed() { + return keyed; + } } static class EmptyBucketInfo { @@ -224,7 +232,7 @@ public class InternalHistogram extends Inter } - static class Factory { + public static class Factory { protected Factory() { } @@ -283,7 +291,7 @@ public class InternalHistogram extends Inter 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 f382237dacf..04099009272 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 class UnmappedSignificantTerms extends InternalSignificantTerms { 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 82c850bcac7..89134a394ec 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 @@ public class UnmappedTerms extends InternalTerms { 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 8c5eafa2961..b3e4c5cf4c9 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.SearchHits; 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 class InternalTopHits extends InternalMetricsAggregation implements TopHi 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 class InternalTopHits extends InternalMetricsAggregation implements TopHi } 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 20aeaae2f5a..6abf1917c17 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 class TopHitsAggregator extends MetricsAggregator { 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 00000000000..7d204c007c6 --- /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 c74f6f0b0f2..d87d9fa72e1 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 abstract class Reducer { } + 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 64e7d1c7baf..4249cde2dc3 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 @@ import java.util.Map; /** * 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 00000000000..7a4319e0a2b --- /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 00000000000..e1c510e1a29 --- /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/85] 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 cb4bef6ca34..d1cb6d96800 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.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 org.elasticsearch.search.aggregations.reducers.derivative.DerivativeParser; import java.util.List; @@ -99,7 +100,7 @@ public class AggregationModule extends AbstractModule implements SpawnModules{ 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 ce09d1e5c69..c99f885462c 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.stats.extended.InternalExte 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 @@ public class TransportAggregationModule extends AbstractModule implements SpawnM SignificantStringTerms.registerStreams(); SignificantLongTerms.registerStreams(); UnmappedSignificantTerms.registerStreams(); - InternalGeoHashGrid.registerStreams(); + InternalGeoHashGrid.registerStreams(); DoubleTerms.registerStreams(); UnmappedTerms.registerStreams(); InternalRange.registerStream(); @@ -102,6 +103,9 @@ public class TransportAggregationModule extends AbstractModule implements SpawnM 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 00000000000..0e9b1f7f41f --- /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 00000000000..d2cfae6784b --- /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/85] 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 e23639352cf..661d975a41f 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 @@ package org.elasticsearch.search.aggregations; 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 abstract class AggregatorBase extends Aggregator { 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 d22fed75a8c..41aee8f931f 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 abstract class AggregatorFactory { * 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 abstract class AggregatorFactory { 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 abstract class AggregatorFactory { 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 b7c8fe7ccfc..93fa360b113 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 @@ package org.elasticsearch.search.aggregations.bucket; 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 abstract class BucketsAggregator extends AggregatorBase { 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 c7e260faf63..0d08c6d5efe 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.lease.Releasables; 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 bff09e07e4a..6f538b384d3 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.AbstractRandomAccessOrds; 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 a9cb4ea19cb..59fa19366d6 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/85] 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 d2cfae6784b..975ad809adf 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 class DerivativeReducer extends Reducer { 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/85] 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 e23cf8ef228..62caa385585 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 @@ public class AggregatorParsers { 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 @@ public class AggregatorParsers { } 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/85] 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 9f9ad81c953..0457ad9e92c 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 class InternalDateHistogram { } @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 5c945afddf0..d5b3a1384f1 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.rounding.Rounding; 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 class InternalHistogram extends Inter 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 4249cde2dc3..c4c6b304ba8 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 abstract class ReducerFactory { /** * 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 975ad809adf..76d3bd9b3ac 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.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; @@ -89,6 +88,7 @@ public class DerivativeReducer extends Reducer { 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 class DerivativeReducer extends Reducer { 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/85] 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 76d3bd9b3ac..9a707687cc2 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 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.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 class DerivativeReducer extends Reducer { 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 class DerivativeReducer extends Reducer { 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/85] 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 0e9b1f7f41f..55259102dfd 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.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; @@ -31,6 +33,7 @@ import java.io.IOException; 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 class DerivativeParser implements Reducer.Parser { 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 class DerivativeParser implements Reducer.Parser { } 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 class DerivativeParser implements Reducer.Parser { + "] 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 9a707687cc2..26f40b2824d 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 @@ package org.elasticsearch.search.aggregations.reducers.derivative; 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.ReducerFactory; 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 class DerivativeReducer extends Reducer { 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 class DerivativeReducer extends Reducer { } }; + 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 class DerivativeReducer extends Reducer { 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 class DerivativeReducer extends Reducer { 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/85] 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 5103f9c2b7a..a4f68b05efb 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 class AggregatorFactories { 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 c4c6b304ba8..4249cde2dc3 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 abstract class ReducerFactory { /** * 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/85] 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 d87d9fa72e1..cfc0f76622b 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 abstract class Reducer implements Streamable { } - 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 abstract class Reducer implements Streamable { return name; } + public String[] bucketsPaths() { + return bucketsPaths; + } + public Map metaData() { return metaData; } @@ -89,6 +95,7 @@ public abstract class Reducer implements Streamable { @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 abstract class Reducer implements Streamable { @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 4249cde2dc3..f904a564dd2 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 55259102dfd..edb416f875a 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.aggregations.support.format.ValueFormatter; 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 class DerivativeParser implements Reducer.Parser { 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 class DerivativeParser implements Reducer.Parser { } } - 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 class DerivativeParser implements Reducer.Parser { 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 26f40b2824d..2bd42164c46 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.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 org.elasticsearch.search.aggregations.support.format.ValueFormatterStreams; import java.io.IOException; import java.util.ArrayList; @@ -72,14 +73,12 @@ public class DerivativeReducer extends Reducer { }; 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 class DerivativeReducer extends Reducer { } 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 @@ public class DerivativeReducer extends Reducer { @Override public void doReadFrom(StreamInput in) throws IOException { - bucketsPath = in.readString(); - + formatter = ValueFormatterStreams.readOptional(in); } @Override public void doWriteTo(StreamOutput out) throws IOException { - out.writeString(bucketsPath); + ValueFormatterStreams.writeOptional(formatter, out); } public static class Factory extends ReducerFactory { - private final String bucketsPath; private final ValueFormatter formatter; - public Factory(String name, String bucketsPath, @Nullable ValueFormatter formatter) { - super(name, TYPE.name()); - this.bucketsPath = bucketsPath; + public Factory(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter) { + super(name, TYPE.name(), bucketsPaths); this.formatter = formatter; } @Override protected Reducer createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, Map 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/85] 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 a4f68b05efb..ad17c533cc0 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 class AggregatorFactories { 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 f904a564dd2..05cb6fbed48 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 abstract class ReducerFactory { 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/85] 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 ebd2637ac56..5efc2180229 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 abstract class InternalMultiBucketAggregation extends InternalAggregation 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 00000000000..e2ab1f65245 --- /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/85] 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 2bd42164c46..a0a3d9cb425 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 @@ 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; @@ -32,6 +33,7 @@ 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; @@ -92,12 +94,16 @@ public class DerivativeReducer extends Reducer { 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 class DerivativeReducer extends Reducer { 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/85] 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 ad17c533cc0..258b90c2653 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 class AggregatorFactories { 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 @@ public class AggregatorFactories { 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/85] 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 cfc0f76622b..ed602b31751 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 00000000000..49bba5a0ecb --- /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 00000000000..21c901af80d --- /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/85] 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 00000000000..87165c32ac0 --- /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 edb416f875a..8a562050dcb 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.ArrayList; 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/85] 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 5b9fab55aa4..cc3033e883f 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 @@ import java.util.Map; public abstract class AggregationBuilder> extends AbstractAggregationBuilder { private List aggregations; + private List> reducers; private BytesReference aggregationsBinary; private Map metaData; @@ -59,6 +62,18 @@ public abstract class AggregationBuilder> extend 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 abstract class AggregationBuilder> extend 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 abstract class AggregationBuilder> extend } } + 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 c99f885462c..fe4542830cc 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.stats.extended.InternalExte 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 @@ public class TransportAggregationModule extends AbstractModule implements SpawnM 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 d5b3a1384f1..4171cc3f514 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 class InternalHistogram extends Inter 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 9d503a8e90b..10902064786 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 @@ import java.util.Comparator; /** * 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 49bba5a0ecb..0f0f9225635 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.ToXContent; 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 abstract class ReducerBuilder> implements ToX /** * 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/85] 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 a0a3d9cb425..730a85a2d41 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 class DerivativeReducer extends Reducer { } 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 00000000000..ec131b3a609 --- /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 00000000000..3b51bbbf6b2 --- /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/85] 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 0457ad9e92c..503d3626b2f 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.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; -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 class InternalDateHistogram { 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 4171cc3f514..ad17e3796fe 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 class InternalHistogram extends Inter 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 class InternalHistogram extends Inter 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 10902064786..9d503a8e90b 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 @@ import java.util.Comparator; /** * 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/85] 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 730a85a2d41..40397d8f46e 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 class DerivativeReducer extends Reducer { 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 class DerivativeReducer extends Reducer { } 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/85] 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 258b90c2653..628fe3144a9 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 class AggregatorFactories { } public AggregatorFactories build() { - if (factories.isEmpty()) { + if (factories.isEmpty() && reducerFactories.isEmpty()) { return EMPTY; } List orderedReducers = resolveReducerOrder(this.reducerFactories, this.factories); @@ -212,7 +212,7 @@ public class AggregatorFactories { String[] bucketsPaths = factory.getBucketsPaths(); for (String bucketsPath : bucketsPaths) { ReducerFactory matchingFactory = reducerFactoriesMap.get(bucketsPath); - if (aggFactoryNames.contains(bucketsPath)) { + if (bucketsPath.equals("_count") || bucketsPath.equals("_key") || aggFactoryNames.contains(bucketsPath)) { continue; } else if (matchingFactory != null) { resolveReducerOrder(aggFactoryNames, reducerFactoriesMap, orderedReducers, unmarkedFactories, temporarilyMarked, From 6c12cfd4657070aeac499eede1372ee9e354a849 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Tue, 17 Feb 2015 11:15:04 +0000 Subject: [PATCH 26/85] 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 ec131b3a609..ad1c131c885 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 class DateDerivativeTests extends ElasticsearchIntegrationTest { 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 class DateDerivativeTests extends ElasticsearchIntegrationTest { 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 class DateDerivativeTests extends ElasticsearchIntegrationTest { 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 class DateDerivativeTests extends ElasticsearchIntegrationTest { 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 class DateDerivativeTests extends ElasticsearchIntegrationTest { 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 class DateDerivativeTests extends ElasticsearchIntegrationTest { 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 class DateDerivativeTests extends ElasticsearchIntegrationTest { 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 3b51bbbf6b2..aadc05cd003 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.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSear 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 class DerivativeTests extends ElasticsearchIntegrationTest { ensureSearchable(); } - @AwaitsFix(bugUrl="waiting for derivative to support _count") // NOCOMMIT @Test public void singleValuedField() { @@ -173,17 +173,21 @@ public class DerivativeTests extends ElasticsearchIntegrationTest { 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 class DerivativeTests extends ElasticsearchIntegrationTest { 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 class DerivativeTests extends ElasticsearchIntegrationTest { 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 class DerivativeTests extends ElasticsearchIntegrationTest { } } - @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 class DerivativeTests extends ElasticsearchIntegrationTest { 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 class DerivativeTests extends ElasticsearchIntegrationTest { 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 class DerivativeTests extends ElasticsearchIntegrationTest { 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 class DerivativeTests extends ElasticsearchIntegrationTest { 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 class DerivativeTests extends ElasticsearchIntegrationTest { 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/85] 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 628fe3144a9..552ff49fe1d 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 @@ public class AggregatorFactories { 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? + 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 @@ public class AggregatorFactories { 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? + throw new ElasticsearchIllegalStateException("No reducer found for path [" + bucketsPath + "]"); } } unmarkedFactories.remove(factory); From 7f844660a834ba3d78c491c9f9e9235930f12b9d Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Tue, 17 Feb 2015 11:31:24 +0000 Subject: [PATCH 28/85] 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 aadc05cd003..5cdf2a8cee8 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 class DerivativeTests extends ElasticsearchIntegrationTest { } } - @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 class DerivativeTests extends ElasticsearchIntegrationTest { .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 class DerivativeTests extends ElasticsearchIntegrationTest { } @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 class DerivativeTests extends ElasticsearchIntegrationTest { } @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/85] 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 5cdf2a8cee8..11bac929081 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 class DerivativeTests extends ElasticsearchIntegrationTest { 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 class DerivativeTests extends ElasticsearchIntegrationTest { } } + @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/85] 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 7d204c007c6..9641f187c6c 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 @@ public class InternalSimpleValue extends InternalNumericMetricsAggregation.Singl @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 87165c32ac0..f868e673b1d 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 @@ package org.elasticsearch.search.aggregations.reducers.derivative; 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 class DerivativeBuilder extends ReducerBuilder { 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 8a562050dcb..6b6b826ec6f 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.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.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.ArrayList; 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 class DerivativeParser implements Reducer.Parser { 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 class DerivativeParser implements Reducer.Parser { 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 class DerivativeParser implements Reducer.Parser { 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 40397d8f46e..c0d96f4056b 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 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; @@ -44,9 +46,11 @@ 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; @@ -75,13 +79,16 @@ public class DerivativeReducer extends Reducer { }; 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 class DerivativeReducer extends Reducer { 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 @@ public class DerivativeReducer extends Reducer { 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 { + 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; @@ -138,27 +159,83 @@ public class DerivativeReducer extends Reducer { @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 GapPolicy gapPolicy; - public Factory(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter) { + public Factory(String name, String[] bucketsPaths, @Nullable ValueFormatter formatter, GapPolicy gapPolicy) { super(name, TYPE.name(), bucketsPaths); this.formatter = formatter; + this.gapPolicy = gapPolicy; } @Override protected Reducer createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, Map 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 11bac929081..7d2d5500cd1 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 class DerivativeTests extends ElasticsearchIntegrationTest { } } - @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 class DerivativeTests extends ElasticsearchIntegrationTest { 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)); - 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)); + assertThat(bucket.getDocCount(), equalTo(1l)); 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 class DerivativeTests extends ElasticsearchIntegrationTest { 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 class DerivativeTests extends ElasticsearchIntegrationTest { 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,95 +475,142 @@ public class DerivativeTests extends ElasticsearchIntegrationTest { 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(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(4); + bucket = buckets.get(8); assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(5l)); + assertThat(((Number) bucket.getKey()).longValue(), equalTo(8l)); assertThat(bucket.getDocCount(), equalTo(0l)); docCountDeriv = bucket.getAggregations().get("deriv"); assertThat(docCountDeriv, notNullValue()); - assertThat(docCountDeriv.value(), equalTo(0.25d)); + assertThat(docCountDeriv.value(), equalTo(0d)); - bucket = buckets.get(5); + 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(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(6); + bucket = buckets.get(11); assertThat(bucket, notNullValue()); - assertThat(((Number) bucket.getKey()).longValue(), equalTo(10l)); - assertThat(bucket.getDocCount(), equalTo(0l)); + 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/85] 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 00000000000..145ff1dea1f --- /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 f868e673b1d..210d56d4a6f 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 @@ package org.elasticsearch.search.aggregations.reducers.derivative; 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 6b6b826ec6f..c4d3aa2a229 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.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.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.io.IOException; 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 c0d96f4056b..1130639a1a2 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 @@ 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.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 class DerivativeReducer extends Reducer { 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 class DerivativeReducer extends Reducer { 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 @@ public class DerivativeReducer extends Reducer { } } - - 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 7d2d5500cd1..24a4c8cff5a 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; 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 class DerivativeTests extends ElasticsearchIntegrationTest { .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/85] 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 145ff1dea1f..f92a2b70d3b 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/85] Add randomiziation to test for derivative aggregation --- .../reducers/DerivativeTests.java | 539 +++++------------- 1 file changed, 154 insertions(+), 385 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 24a4c8cff5a..a5c9506aeac 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 @@ package org.elasticsearch.search.aggregations.reducers; 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.search.aggregations.reducers.ReducerBuilders.der 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 @@ import static org.hamcrest.core.IsNull.nullValue; 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 class DerivativeTests extends ElasticsearchIntegrationTest { 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; - } - 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())); + 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))); + } } + // 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 }; + 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())); + 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++; + } + } - builders.add(client().prepareIndex("empty_bucket_idx", "type", "" + 1).setSource( - jsonBuilder().startObject().field(SINGLE_VALUED_FIELD_NAME, 1).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; - 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())); + 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 class DerivativeTests extends ElasticsearchIntegrationTest { 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 class DerivativeTests extends ElasticsearchIntegrationTest { 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 class DerivativeTests extends ElasticsearchIntegrationTest { 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 class DerivativeTests extends ElasticsearchIntegrationTest { 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 class DerivativeTests extends ElasticsearchIntegrationTest { @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 class DerivativeTests extends ElasticsearchIntegrationTest { 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 class DerivativeTests extends ElasticsearchIntegrationTest { .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)); + List buckets = deriv.getBuckets(); + assertThat(buckets.size(), equalTo(valueCounts_empty.length)); - 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()); + 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])); + } + } + } - 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)); + @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(); - 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)); + assertThat(searchResponse.getHits().getTotalHits(), equalTo(numDocsEmptyIdx_rnd)); - 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)); + 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)); - 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)); + 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 class DerivativeTests extends ElasticsearchIntegrationTest { .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)); + List buckets = deriv.getBuckets(); + assertThat(buckets.size(), equalTo(valueCounts_empty.length)); - 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)); + 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/85] [BUILD] fix snapshot URL --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 954f4d897e3..30fd9468946 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/85] 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 c3fe8b94071..5b669cb9175 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.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.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/85] 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 552ff49fe1d..1a4c157da8e 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 class AggregatorFactories { } void setParent(AggregatorFactory parent) { + this.parent = parent; for (AggregatorFactory factory : factories) { factory.parent = parent; } @@ -111,7 +113,7 @@ public class AggregatorFactories { 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 41aee8f931f..f69e54ee710 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 abstract class AggregatorFactory { 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 0a6a8bce732..63325c12aad 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 class HistogramAggregator extends BucketsAggregator { 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 05cb6fbed48..ccdd2ac0328 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 abstract class ReducerFactory { } /** - * 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 abstract class ReducerFactory { 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 1130639a1a2..5f40ab2906e 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 @@ 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; 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.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 DerivativeReducer extends Reducer { @@ -143,5 +148,23 @@ public class DerivativeReducer extends Reducer { 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 ad1c131c885..ede94abd973 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 class DateDerivativeTests extends ElasticsearchIntegrationTest { 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 class DateDerivativeTests extends ElasticsearchIntegrationTest { 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 class DateDerivativeTests extends ElasticsearchIntegrationTest { 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 class DateDerivativeTests extends ElasticsearchIntegrationTest { 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 class DateDerivativeTests extends ElasticsearchIntegrationTest { 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 a5c9506aeac..6f5641fcffa 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 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; @@ -165,7 +164,7 @@ public class DerivativeTests extends ElasticsearchIntegrationTest { 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 class DerivativeTests extends ElasticsearchIntegrationTest { 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 class DerivativeTests extends ElasticsearchIntegrationTest { 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 class DerivativeTests extends ElasticsearchIntegrationTest { 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/85] 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 e7377414eda..856b96979f2 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 org.elasticsearch.search.aggregations.reducers.Reducer; 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 abstract class InternalMultiBucketAggregation extends InternalAggregation super(name, reducers, metaData); } + /** + * Create a new copy of this {@link Aggregation} with the same settings as + * this {@link Aggregation} and contains the provided buckets. + * + * @param buckets + * the buckets to use in the new {@link Aggregation} + * @return the new {@link Aggregation} + */ + public abstract A create(List 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 abstract class InternalMultiBucketAggregation extends InternalAggregation 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 1e4c882ef5f..0383164ba86 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.Aggregations; 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 @@ import java.util.Map; /** * */ -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 class InternalFilters extends InternalMultiBucketAggregation implements F 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 83428f8c209..6bbf1e2dc7f 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 @@ import java.util.Map; * 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 class InternalGeoHashGrid extends InternalMultiBucketAggregation implemen return builder; } } - private int requiredSize; private Collection buckets; protected Map bucketMap; @@ -183,6 +183,16 @@ public class InternalGeoHashGrid extends InternalMultiBucketAggregation implemen 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 503d3626b2f..a82a089066b 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 class InternalDateHistogram { 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 ad17e3796fe..8c5b219379c 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 @@ import java.util.Map; /** * 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 class InternalHistogram extends Inter } - public static class Factory { + public static class Factory extends InternalMultiBucketAggregation.Factory, B> { protected Factory() { } @@ -248,11 +249,17 @@ public class InternalHistogram extends Inter 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 class InternalHistogram extends Inter 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 00ff8b08030..a3602060fd2 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 @@ import java.util.Map; /** * */ -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 class InternalRange extends InternalMulti 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 class InternalRange extends InternalMulti } } - public static class Factory> { + public static class Factory> extends InternalMultiBucketAggregation.Factory { public String type() { return TYPE.name(); @@ -231,12 +240,25 @@ public class InternalRange extends InternalMulti 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 class InternalRange extends InternalMulti 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 class InternalRange extends InternalMulti 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 b679a6bc3d5..6444f53e527 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 @@ import java.util.Map; /** * */ -public class InternalDateRange extends InternalRange { +public class InternalDateRange extends InternalRange { public final static Type TYPE = new Type("date_range", "drange"); @@ -113,7 +113,7 @@ public class InternalDateRange extends InternalRange { } } - private static class Factory extends InternalRange.Factory { + public static class Factory extends InternalRange.Factory { @Override public String type() { @@ -126,10 +126,22 @@ public class InternalDateRange extends InternalRange { 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 class InternalDateRange extends InternalRange { } @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 0fef2e2ba00..b271c3336e0 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 @@ import java.util.Map; /** * */ -public class InternalGeoDistance extends InternalRange { +public class InternalGeoDistance extends InternalRange { public static final Type TYPE = new Type("geo_distance", "gdist"); @@ -101,7 +101,7 @@ public class InternalGeoDistance extends InternalRange { + public static class Factory extends InternalRange.Factory { @Override public String type() { @@ -114,10 +114,22 @@ public class InternalGeoDistance extends InternalRange 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 class InternalGeoDistance extends InternalRange 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 be2f8e52f8f..96668e67c69 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 @@ import java.util.Map; /** * */ -public class InternalIPv4Range extends InternalRange { +public class InternalIPv4Range extends InternalRange { public static final long MAX_IP = 4294967296l; @@ -110,7 +110,7 @@ public class InternalIPv4Range extends InternalRange { } } - private static class Factory extends InternalRange.Factory { + public static class Factory extends InternalRange.Factory { @Override public String type() { @@ -123,10 +123,21 @@ public class InternalIPv4Range extends InternalRange { 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 class InternalIPv4Range extends InternalRange { } @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 a48fc850b90..a949c916c7d 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 @@ import java.util.Map; /** * */ -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 abstract class InternalSignificantTerms extends InternalMultiBucketAggreg } 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 abstract class InternalSignificantTerms extends InternalMultiBucketAggreg // 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 abstract class InternalSignificantTerms extends InternalMultiBucketAggreg 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 85ae983ef18..a450f9d0933 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 @@ import java.util.Map; /** * */ -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 class SignificantLongTerms extends InternalSignificantTerms { 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 class SignificantLongTerms extends InternalSignificantTerms { } @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 d8fc74c9bc5..9fbaa6cc375 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 @@ import java.util.Map; /** * */ -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 class SignificantStringTerms extends InternalSignificantTerms { 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 class SignificantStringTerms extends InternalSignificantTerms { } @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 04099009272..2d0309c9da1 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.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.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.significant.heuristics.JLHScore; import org.elasticsearch.search.aggregations.reducers.Reducer; @@ -34,7 +35,7 @@ import java.util.Map; /** * */ -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 class UnmappedSignificantTerms extends InternalSignificantTerms { 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 class UnmappedSignificantTerms extends InternalSignificantTerms { 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 0e6ca403407..dbb8061db09 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 @@ import java.util.Map; /** * */ -public class DoubleTerms extends InternalTerms { +public class DoubleTerms extends InternalTerms { public static final Type TYPE = new Type("terms", "dterms"); @@ -85,7 +85,8 @@ public class DoubleTerms extends InternalTerms { 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 class DoubleTerms extends InternalTerms { } } - 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 class DoubleTerms extends InternalTerms { } @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 129698daffa..b753f322796 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 @@ import java.util.Map; /** * */ -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 abstract class InternalTerms extends InternalMultiBucketAggregation imple 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 abstract class InternalTerms extends InternalMultiBucketAggregation imple 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 abstract class InternalTerms extends InternalMultiBucketAggregation imple 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 abstract class InternalTerms extends InternalMultiBucketAggregation imple } 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 b8edad21dd9..eee9e6bfc4b 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 @@ import java.util.Map; /** * */ -public class LongTerms extends InternalTerms { +public class LongTerms extends InternalTerms { public static final Type TYPE = new Type("terms", "lterms"); @@ -157,7 +157,7 @@ public class LongTerms extends InternalTerms { 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 class LongTerms extends InternalTerms { } @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 ef9ec91e80c..ee458acdf13 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 @@ import java.util.Map; /** * */ -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 class StringTerms extends InternalTerms { BucketStreams.registerStream(BUCKET_STREAM, TYPE.stream()); } - public static class Bucket extends InternalTerms.Bucket { BytesRef termBytes; @@ -149,10 +148,11 @@ public class StringTerms extends InternalTerms { } } - 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 class StringTerms extends InternalTerms { } @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 89134a394ec..14f07c57e83 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.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.InternalAggregations; import org.elasticsearch.search.aggregations.reducers.Reducer; import java.io.IOException; @@ -33,7 +34,7 @@ import java.util.Map; /** * */ -public class UnmappedTerms extends InternalTerms { +public class UnmappedTerms extends InternalTerms { public static final Type TYPE = new Type("terms", "umterms"); @@ -65,6 +66,21 @@ public class UnmappedTerms extends InternalTerms { 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 class UnmappedTerms extends InternalTerms { 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 5f40ab2906e..40a5b005560 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 class DerivativeReducer extends Reducer { @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 class DerivativeReducer extends Reducer { } 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/85] 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 d1cb6d96800..a8d3895ec78 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.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 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 class AggregationModule extends AbstractModule implements SpawnModules{ aggParsers.add(ChildrenParser.class); reducerParsers.add(DerivativeParser.class); + reducerParsers.add(MovAvgParser.class); } /** @@ -129,7 +132,7 @@ public class AggregationModule extends AbstractModule implements SpawnModules{ @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 fe4542830cc..c3d89cf4f8f 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.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; +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 @@ public class TransportAggregationModule extends AbstractModule implements SpawnM // 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 21c901af80d..0aa8be4e992 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 @@ public final class 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 00000000000..9790604197d --- /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 00000000000..3f241a67b3a --- /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 00000000000..b339cdf487d --- /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 00000000000..907c23fd213 --- /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 00000000000..6c269590d33 --- /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 00000000000..84f7832f893 --- /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 00000000000..96bc9427de3 --- /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 00000000000..71ccbcb31b0 --- /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 00000000000..d27e447baa4 --- /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 00000000000..459729d8960 --- /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 00000000000..b11a3687021 --- /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 00000000000..243b022af2c --- /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 00000000000..f17ba68f498 --- /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 00000000000..bc085f6241a --- /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 00000000000..d22656b0ad5 --- /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/85] 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 | 55 +++++-- .../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, 1010 insertions(+), 128 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 e1309a5c095..732e08ac36b 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.XContentBuilder; 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 class PercolateRequestBuilder extends BroadcastOperationRequestBuilder reducers; PercolateShardResponse() { hls = new ArrayList<>(); @@ -69,6 +75,7 @@ public class PercolateShardResponse extends BroadcastShardOperationResponse { if (result.aggregations() != null) { this.aggregations = (InternalAggregations) result.aggregations(); } + this.reducers = result.reducers(); } } @@ -112,6 +119,10 @@ public class PercolateShardResponse extends BroadcastShardOperationResponse { return aggregations; } + public List reducers() { + return reducers; + } + public byte percolatorTypeId() { return percolatorTypeId; } @@ -144,6 +155,16 @@ public class PercolateShardResponse extends BroadcastShardOperationResponse { 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 class PercolateShardResponse extends BroadcastShardOperationResponse { } } 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 f09e630f459..68fc57b2a17 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.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.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 class PercolateSourceBuilder implements ToXContent { /** * 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 59d6db804b0..fcead5866b7 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.index.query.QueryBuilder; 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 f19b3b076e7..cd5db78226d 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.IndexService; 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.List; 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 @@ public class PercolatorService extends AbstractComponent { return null; } + InternalAggregations aggregations; if (shardResults.size() == 1) { - return shardResults.get(0).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)); } - - List aggregationsList = new ArrayList<>(shardResults.size()); - for (PercolateShardResponse shardResult : shardResults) { - aggregationsList.add(shardResult.aggregations()); + 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 a8d3895ec78..e9da6e719b9 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.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 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 class AggregationModule extends AbstractModule implements SpawnModules{ 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 9d627310142..dd915b80f87 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.common.lucene.search.Queries; 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 class AggregationPhase implements SearchPhase { 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 class AggregationPhase implements SearchPhase { } } 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 1a4c157da8e..1a4dcd4f177 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 @@ public class AggregatorFactories { 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 @@ public class AggregatorFactories { temporarilyMarked.add(factory); String[] bucketsPaths = factory.getBucketsPaths(); for (String bucketsPath : bucketsPaths) { - ReducerFactory matchingFactory = reducerFactoriesMap.get(bucketsPath); - if (bucketsPath.equals("_count") || bucketsPath.equals("_key") || aggFactoryNames.contains(bucketsPath)) { + int aggSepIndex = bucketsPath.indexOf('>'); + 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 c3d89cf4f8f..d405db6c741 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.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.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 @@ public class TransportAggregationModule extends AbstractModule implements SpawnM 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 f92a2b70d3b..30d6fc0107e 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.StreamInput; 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 class BucketHelpers { * @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 ed602b31751..3c0b4fdbe22 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 abstract class Reducer implements Streamable { } + 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 0f0f9225635..4dee8ea96a2 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 @@ package org.elasticsearch.search.aggregations.reducers; 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 @@ import java.util.Map; /** * 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 0aa8be4e992..3f45964153b 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 final class ReducerBuilders { 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 ccdd2ac0328..46ac844808c 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 @@ 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; @@ -62,8 +61,7 @@ public abstract class ReducerFactory { 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 @@ public abstract class ReducerFactory { * * @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 00000000000..b0be9634ddc --- /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 00000000000..69b23ae91ef --- /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 00000000000..eb04617e548 --- /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 00000000000..2a9dab3b6bd --- /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 00000000000..e209684797c --- /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 40a5b005560..a58d0f0e74e 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.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; @@ -68,13 +64,6 @@ public class DerivativeReducer extends Reducer { 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 class DerivativeReducer extends Reducer { 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 class DerivativeReducer extends Reducer { } @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 b339cdf487d..20baa1706f1 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 @@ 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.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.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 { @@ -155,8 +161,7 @@ public class MovAvgReducer extends Reducer { } @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 6adfb53fd41..05ebaf44e05 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.carrotsearch.hppc.ObjectFloatOpenHashMap; 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.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.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.List; 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 class SearchSourceBuilder implements ToXContent { private List aggregations; private BytesReference aggregationsBinary; - private HighlightBuilder highlightBuilder; private SuggestBuilder suggestBuilder; @@ -123,7 +125,6 @@ public class SearchSourceBuilder implements ToXContent { private String[] stats; - /** * Constructs a new search source builder. */ @@ -132,7 +133,7 @@ public class SearchSourceBuilder implements ToXContent { /** * 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 class SearchSourceBuilder implements ToXContent { } /** - * 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 class SearchSourceBuilder implements ToXContent { } /** - * 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 class SearchSourceBuilder implements ToXContent { } /** - * 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 class SearchSourceBuilder implements ToXContent { } /** - * 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 class SearchSourceBuilder implements ToXContent { /** * 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 class SearchSourceBuilder implements ToXContent { } /** - * 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 class SearchSourceBuilder implements ToXContent { /** * Set the rescore window size for rescores that don't specify their window. + * * @param defaultRescoreWindowSize * @return */ @@ -465,8 +472,9 @@ public class SearchSourceBuilder implements ToXContent { } /** - * 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 class SearchSourceBuilder implements ToXContent { } /** - * 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 class SearchSourceBuilder implements ToXContent { } /** - * 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 class SearchSourceBuilder implements ToXContent { } /** - * 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 class SearchSourceBuilder implements ToXContent { } /** - * 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 class SearchSourceBuilder implements ToXContent { } /** - * 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 class SearchSourceBuilder implements ToXContent { } /** - * 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 class SearchSourceBuilder implements ToXContent { /** * 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 class SearchSourceBuilder implements ToXContent { /** * 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 class SearchSourceBuilder implements ToXContent { /** * Adds a script field. - * - * @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) + * + * @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 class SearchSourceBuilder implements ToXContent { } /** - * 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 class SearchSourceBuilder implements ToXContent { } } - @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); @@ -657,7 +690,7 @@ public class SearchSourceBuilder implements ToXContent { 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 @@ public class SearchSourceBuilder implements ToXContent { 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 91d8948878b..cdfacbaa062 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 @@ package org.elasticsearch.search.controller; 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.settings.Settings; 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.query.QuerySearchResultProvider; 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 class SearchPhaseController extends AbstractComponent { } } + if (aggregations != null) { + List 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 50167676cc7..e45006b2c32 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 class QuerySearchResult extends QuerySearchResultProvider { 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 class QuerySearchResult extends QuerySearchResultProvider { 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 class QuerySearchResult extends QuerySearchResultProvider { 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 263af854883..9f04e4a37b0 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.action.percolate.PercolateResponse; 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.assertMatc 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 class PercolatorFacetsAndAggregationsTests extends ElasticsearchIntegrati } } + @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 00000000000..f1932118601 --- /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/85] 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 210d56d4a6f..6504a26d72c 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 class DerivativeBuilder extends ReducerBuilder { 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 @@ public class DerivativeBuilder extends ReducerBuilder { 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 c4d3aa2a229..fab2bd3c0b6 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.io.IOException; 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 class DerivativeParser implements Reducer.Parser { 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 class DerivativeParser implements Reducer.Parser { 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 class DerivativeParser implements Reducer.Parser { 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 a58d0f0e74e..7f02e66b73e 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.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.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.ReducerFactory; 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 class DerivativeReducer extends Reducer { 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 class DerivativeReducer extends Reducer { 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 ede94abd973..eefbe411940 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.dateHist 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 class DateDerivativeTests extends ElasticsearchIntegrationTest { 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 6f5641fcffa..2e4c50fb8aa 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.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.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 class DerivativeTests extends ElasticsearchIntegrationTest { } } + /** + * 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/85] 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 6504a26d72c..210d56d4a6f 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 class DerivativeBuilder extends ReducerBuilder { 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 @@ public class DerivativeBuilder extends ReducerBuilder { 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 fab2bd3c0b6..c4d3aa2a229 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.io.IOException; 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 class DerivativeParser implements Reducer.Parser { 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 class DerivativeParser implements Reducer.Parser { 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 class DerivativeParser implements Reducer.Parser { 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 7f02e66b73e..a58d0f0e74e 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.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.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.ReducerFactory; 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 class DerivativeReducer extends Reducer { 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 class DerivativeReducer extends Reducer { 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 eefbe411940..ede94abd973 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.dateHist 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 class DateDerivativeTests extends ElasticsearchIntegrationTest { 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 2e4c50fb8aa..6f5641fcffa 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.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.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 class DerivativeTests extends ElasticsearchIntegrationTest { } } - /** - * 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/85] 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 2dedbb44f8a..fef75c4e7fb 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 @@ import java.io.IOException; /** * 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 cc3033e883f..d41daa7363f 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.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; @@ -39,7 +38,6 @@ import java.util.Map; public abstract class AggregationBuilder> extends AbstractAggregationBuilder { private List aggregations; - private List> reducers; private BytesReference aggregationsBinary; private Map metaData; @@ -62,18 +60,6 @@ public abstract class AggregationBuilder> extend 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 abstract class AggregationBuilder> extend 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 abstract class AggregationBuilder> extend } } - 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 bce1f9bc196..fd9519499a8 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 abstract class Aggregator extends BucketCollector implements Releasable { * 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 62caa385585..1e1950a15c7 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 @@ public class AggregatorParsers { 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 30d6fc0107e..b6955a086ab 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 class BucketHelpers { } 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/85] 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 3c0b4fdbe22..5ec45064c7f 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/85] 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 f1932118601..48d93766bfc 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.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.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 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.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 class MaxBucketTests extends ElasticsearchIntegrationTest { 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 class MaxBucketTests extends ElasticsearchIntegrationTest { } @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 class MaxBucketTests extends ElasticsearchIntegrationTest { 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/85] 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 e7803a27e9c..98e3ba4ccea 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/85] 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 98e3ba4ccea..74784c110a9 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 00000000000..5b3bff11c18 --- /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 00000000000..f1fa8b44043 --- /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 00000000000..659f3ff1930 --- /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/85] 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 659f3ff1930..ca6f274d189 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,19 +21,20 @@ The following snippet calculates the derivative of the total monthly `sales`: "sum": { "field": "price" } - }, - "sales_deriv": { - "derivative": { - "buckets_paths": "sales" <1> - } } } + }, + "max_monthly_sales": { + "max_bucket": { + "buckets_paths": "sales_per_month>sales" <1> + } } } } -------------------------------------------------- -<1> `bucket_paths` instructs this derivative aggregation to use the output of the `sales` aggregation for the 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: @@ -40,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", @@ -48,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", @@ -56,9 +58,6 @@ And the following may be the response: "doc_count": 2, "sales": { "value": 60 - }, - "sales_deriv": { - "value": -490 <2> } }, { @@ -67,126 +66,17 @@ And the following may be the response: "doc_count": 2, "sales": { "value": 375 - }, - "sales_deriv": { - "value": 315 } } ] + }, + "max_monthly_sales": { + "keys": ["2015/01/01 00:00:00"], <1> + "value": 550 } } } -------------------------------------------------- -<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`. - +<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/85] 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 286f0c55b92..84318096080 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.ElasticsearchIllegalStateException; 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 class AggregatorFactories { } /** - * 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 class AggregatorFactories { 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 class AggregatorFactories { } /* - * 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 @@ public class AggregatorFactories { temporarilyMarked.add(factory); String[] bucketsPaths = factory.getBucketsPaths(); for (String bucketsPath : bucketsPaths) { - int aggSepIndex = bucketsPath.indexOf('>'); - 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 6f5641fcffa..95c13b6fd2f 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.common.xcontent.XContentBuilder; 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 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.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 @@ public class DerivativeTests extends ElasticsearchIntegrationTest { * 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 class DerivativeTests extends ElasticsearchIntegrationTest { } @Test - public void singleValuedField_WithSubAggregation() throws Exception { + public void singleValueAggDerivative() throws Exception { SearchResponse response = client() .prepareSearch("idx") .addAggregation( @@ -242,6 +244,52 @@ public class DerivativeTests extends ElasticsearchIntegrationTest { } } + @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 class DerivativeTests extends ElasticsearchIntegrationTest { } @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 class DerivativeTests extends ElasticsearchIntegrationTest { } @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 class DerivativeTests extends ElasticsearchIntegrationTest { 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 class DerivativeTests extends ElasticsearchIntegrationTest { } @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/85] 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 95c13b6fd2f..1c579c6cd5f 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.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.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 class DerivativeTests extends ElasticsearchIntegrationTest { } } + @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/85] 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 b6955a086ab..f6cdd8ca1f9 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 class BucketHelpers { 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)) { + // doc count never has missing values so gap policy doesn't apply here + boolean isDocCountProperty = aggPathAsList.size() == 1 && "_count".equals(aggPathAsList.get(0)); + if (Double.isInfinite(value) || Double.isNaN(value) || (bucket.getDocCount() == 0 && !isDocCountProperty)) { switch (gapPolicy) { - case INSERT_ZEROS: - return 0.0; - case IGNORE: - default: - return Double.NaN; + case INSERT_ZEROS: + return 0.0; + case IGNORE: + default: + return Double.NaN; } } else { return value; 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 1c579c6cd5f..0974d297d46 100644 --- a/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java +++ b/src/test/java/org/elasticsearch/search/aggregations/reducers/DerivativeTests.java @@ -373,7 +373,8 @@ public class DerivativeTests extends ElasticsearchIntegrationTest { .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(); + .subAggregation(derivative("deriv").setBucketsPaths("_count").gapPolicy(randomFrom(GapPolicy.values())))) + .execute().actionGet(); assertThat(searchResponse.getHits().getTotalHits(), equalTo(numDocsEmptyIdx_rnd)); @@ -449,11 +450,102 @@ public class DerivativeTests extends ElasticsearchIntegrationTest { 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 (bucket.getDocCount() == 0) { + thisSumValue = Double.NaN; + } + SimpleValue sumDeriv = bucket.getAggregations().get("deriv"); if (i == 0) { - assertThat(docCountDeriv, nullValue()); + 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; + } + } + + @Test + public void singleValueAggDerivativeWithGaps_insertZeros() 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").gapPolicy(GapPolicy.INSERT_ZEROS))).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(); + 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 { + 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/85] 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 d22656b0ad5..4f0e3c0d1cf 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 @@ 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; @@ -37,7 +38,8 @@ 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.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 class MovAvgTests extends ElasticsearchIntegrationTest { 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/85] 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 3f45964153b..ba6d3ebe7c2 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 final class ReducerBuilders { 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 9790604197d..5fba23957e9 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 class MovAvgBuilder extends ReducerBuilder { 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 @@ public class MovAvgBuilder extends ReducerBuilder { 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 3f241a67b3a..c1cdadf91ea 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 class MovAvgParser implements Reducer.Parser { 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 class MovAvgParser implements Reducer.Parser { } 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 class MovAvgParser implements Reducer.Parser { 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 class MovAvgParser implements Reducer.Parser { 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 20baa1706f1..4bd2ff4c50a 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.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.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.MovAvgModel; 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 class MovAvgReducer extends Reducer { 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 class MovAvgReducer extends Reducer { 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 class MovAvgReducer extends Reducer { 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 class MovAvgReducer extends Reducer { formatter = ValueFormatterStreams.readOptional(in); gapPolicy = GapPolicy.readFrom(in); window = in.readVInt(); + predict = in.readVInt(); model = MovAvgModelStreams.read(in); + } @Override @@ -141,7 +181,9 @@ public class MovAvgReducer extends Reducer { 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 class MovAvgReducer extends Reducer { 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 907c23fd213..7d32989cda1 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 class DoubleExpModel extends MovAvgModel { 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 class DoubleExpModel extends MovAvgModel { * @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 class DoubleExpModel extends MovAvgModel { 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 84f7832f893..d798887c836 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 4f0e3c0d1cf..00000000000 --- 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 00000000000..9c3a6f23419 --- /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 00000000000..156f4f873a7 --- /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/85] [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 zcmaHRV|ZlSwszQ2$41BL*tV1I*fuM+?T*oLI<{?gY};1FcD|f@&)sM5KF{;jvwl>q zHRl{ybB=esV^pY|j5s_j4lD==2>kbNA_^cN;GG~Kppnp@KJF}y^`nD;V3C^%3(I{M z7ABOlw=prZGzI}t3rcW<;ZWXKc;~xc9okDhP&Z+kAid_95T;t-nPC-;jrp8l^o=M; zBuq?EC^+6|SS}1rs8}gj$_=sP{QTYP{rsBm+&jbT?4Y83emS%Ke%T|_i5>*6EH5So z1dSHNPf*{;0^CYRoZM7rRTBgh#~0E-Aa)lYC&$>#4EAlHh9wX{vKQ2wSoyAg{;qk* z!y~)~>Pv%svX6X>|6|hwBx`9+cu*fi&mEc5$4G4uLq~vt1e*vN1fw&k-G$)*!+GWR z^a@wQJQwslB#5jiacnzi*80^1?$%l4LpFlo@?ijvX8_Wi2*kP5J&_wo*Lv{b3Bu>k z>)qGT7b=M-S0QmaK%b|M#8{y zMv`lleFDpxpkky|q+JthKRtH{{>$6N`HgN8zO7*A&f9}kVW${Q@KKN~U#Ra35Wfet za*}&qPwTu5)luA5Fs8&)o`bQM-ukU~^?)01^2iz4?rc6)(S@-`Hkm_$p!+@-Wc0dT zBoHLA!7Z4lhQO*G)H#eT3!5lCbBO!krBL$Q~-fv1S@Y0q+QPYpx)==Obx4Bi1a~$>dR-g!nuBwlb|iWn1Lj7 z@DSkqn=8Dk%psmDW>Zk40-x5qP`|w-zEs;TTEhrsuo2>Tip7vazgiTwuN}h>BR%%( z9*rx4xmq=fS!zB6AVMRE)aKsj%uN;yDxwzG0F3K~3AlAq`P&U{t6J2A%lK4o=n-84 zoj??Zy6!ti-l}^&yKeN?4L>pNB&&8onB9u>xAid`o)4b<-fc+Q*g$;h|M}kc3p3fz zu2<&${%{ouxWvx|APTGp)4(l{x)5*5O_@3o0HckL5P>WAi;xD5eIxI==%jf|8S%Mq z>^tWlc+ixg6)V3;3tG{A`TQGxvJ>NXD7;x7+xQ?V5>83{V?Htj#XnF(zw*`cp_@EYog9Q}oJ20PaI z^xHHBLr;b*(01vX(^OS7#6W$^ue}Z)9`xJ2tc6E;-v_@9K9DfCeFzz_$%=xjxQUt!C8-DR zPlzWHh(*Zu0}3Uumq`5^_A=a&U_lJ)E=04?x*QT3!j~YFES5t&N8}c;1mVVPu0!M- zwBrCB!SgKjL+-jTe|aSeF*HmGXtE#$F|%wbxrU-w3Y%FXB^D}Vr%wstR6=V4%DMR2 z8QGuYcZy_-M6tT!8JI_*B)f*I5G+BtdvqAEqkYLx17_?B% zz|KBZz$`?kb-(s1Zo6I7xO%lQ^Fb3tPyE&$VBdbeoOxh{)qoX*RfdIwMGT_|a|}ZX za}UE0qYPsO`y!Z}B~wNAfEP6fC?PSd9Zwan65kPT7vCJ;NRcVmk*k%R zQ;3o~oo$~DTgWBs6j_pTj6R1*97`cGOUi}X^TYJVUa!hF)+ISBcs*%7ay`^7<1x>% z`LXvgJqsF(KZ^zn6d=dU9&k2&GdVh?FbO+#JQ)WtG`j`>0U>6XM_7kOhct&^M>12| zg&q>@!qK^yISaY>S+aTb1vdGadcN2X*p67M*eF;MSj*T2Sk)2T5n>VE5oQtW*sa(H zDTgV$DJLli%zKQyMmVOyDM87R$>7N|Dd!A`jFmOE)qrZ(n!GBdnwwghTKg*V8uyxl z8kK5qt$rM|`TMUo}@>c-j$I|lnAyC#P``_(P1EfKpgI}Hc? zEt>t4ZLQtyeWp#=t(LC2Vb+1M(ek0wf%Lw$LEe#`uG*x5B=YdW&;t^B)JhB+5F0uh z+;biR@=PLKjt%q<>yCQu?$#6MzVni0)?wLA)&2YJiEW#$Ow3JW22>6zIU)w)Z$w2j zha|2#Rl>IF-_!;L5A)kipDCj8Xq$y<5WIJRQq@CYjzHv$# ze{=fw8mAJ25JMO1O)E@qNP|kRNPn%ZA~mngChIH^MNvraB2}Ssme$tPR`^2pm<0P7 z)&!OuT?35`EdgyPEFr8`0!*Sp;!&b!IAS<)xG+AmNKdIwIjWeYsH}*pB)E88{S<3cs}?zvDkhOX>V!7F@-TRT6tRj za$F7Fy8QZ~<@@?1CnYBX$5bbMZSXavHEUjXUhejAcf+UFoAz7kn*(^X&rw(`xCWo4 zKQn(0iEN5kjU0~Lk4%U#j98H>lCqSdOKoFEXC_OVPs<*S7-Je+Plii{N?A|?fg=f* z3YVAE9}!MOEUQvURB=-wC^IgL)^J&5Sj4MqwyrfQwMw&oUY6;lTdnBJ=wI*aSoK~K zT-f^6)07);7_q}l?o6&*sA2l6w%wuldYiA^Yw5W_3r#CdEBFrXoWJe;2=opd6bc_d zkMkRs>jL`D@uvHlQ@W8L6)|lKcJp5ASSzq8)8&0P`zUsg5mtOd*lt{ZQ+F-91pAn?9#8o&q?q`@J-O__v-JEfM=mz z;W(jw#A5tmJ{DOvqNHG<>}O$%9;{f^^a|V93)HQ1;ybZj(U*w2=r)h!Guz3n3X1BY z_@X1g>tU(*r1)xd9o`Fl-xe}{&V9+TQnUwp6s88mcgw6S}J)*KH<*{Q&*z&(>UV?RAF+njmK zL+ikGf3uHW$-H(Hc9b-3F!3q<%kbnN1-G~h-P5W_fx3wkhK-8(ywKh6lS0lb&Y%OB zgPw`#gVgM6tBy!6NzO>HS3qjx($~^iP^(hWcaFR9wu31n>o9us?1`OA zucg%|pd_f4B$rNG`)O`9@~u0!^EwXf%8Y35wIwl0(K=}mPY+n(4^7;Emh&fxc@k10+8<5&l3 zQ<_ta$~$2@h#N;8KzEz#x7)zy-ol~Dq1)nK#9lj%#JXo*b1%!R=N$aEkuO%)&&33m{1vo=^mQ~EbWyaBbmg>#Z${7R4LNPo z)yGv_?HJ8bniFeCYenzISHLlo&3zN-P^o2abl=#pt1Gb0XI79OG9V0uZg_s~M7ds4_Jh@LTM=fp9 zj02BDvdR16WJ`M2V_cS8kJeS=Fa;VZ0reV%zq3`*KwUy)S>mfqgOIfxXg)=5oi3+x zzQ*kueoo7kj%p;@*ouaY<3&rD``MEzygB?p1Q9BzA~-YuwPUzPB7M2{GQ-O7lKLVP z*DvQXNB8{~>y5UNtGD`qt^@%Cho80!S5A%0bkHN3%W%n*|fBV+9_Yz_w-dXGE^Frc91cfFAsi@gR_tL7Bd&V*x<3aW9m-00};#1GtoZ~AN;c@uFewJIGjijX#-L8_Kf zB~c^O@V%bbILSQo5NYpl#|T)~D%&IK(|PST7_d~)_>I;&{p=L~p7**v%9Z=!+q1&s z_~Y%<0#rQIK4d=BKu8Q?a}2Ano6th{ryd(IQqh(#^Wkw(7#!VB@wCB|iFa6ciwdjH zJL#8=s5Pi0vL~`ke8p*&sX`LG8P2z}J2Qr+9eMN_+T&_G_aqK{K!-c6@uX{h`biz7 zA-G|B+QSYOc@7V^@08(&v2J{=m4my+JMB=& zUOrK|aU8VnyPh={vts)&(yK}?ZlL4QK80;ekd9mG;bFgencnO1@{EXD{u5F$gbyI2 zD2+IZHi7?q_&j7de&Dc~6jMobO|&t%LEv+-Jdf4vlTJ$x{??o<*P zdwPorWfcQ+gR%!>xaC;K(E^1Unb1AHU9s&&kw>8dogN>g6f0t3u8w1#O17-(ICiu5 zIVFNoYJGH`=C~}!Qpyr|!i@L&D^e?AwU5WGjnh-Tb^7rq1Tn;SA@}aKZhp}g5x72~ zK6sbw?^&cKWFh$(-xfx+lExCkf4V5k$@fy-Q5aKmmsAzEDbUJYFOrl-DHE({md92c z<>r^t<_VX=Ir&)xM>n^L6Ye|+P zmo^+ZjsYjVyEpUAmbwh~@0BM$u7qb*Wa&1=7};(o0D{yD(m z<^bRHY~NF&Q(Ou?V)7Z29KJLkU5)Bxjra78 ztp->XWM3)Li8}tsS#=O5^BA6MP!Jjj5Smbs#S)O&AqiimB8Zsx(9V0PK++6X(gUMU zJOXHgBq;@ywqi^NJ%o@$6gIwMlZbr2PCxKbf>>f-7|^&PcLPo*Az+0rv+LyXi++Af zT!_{Pdemp;z@ZBWnR2b8;zH7n@D9ozNG8Nqr^e_uH`uA7<@h{~<$`gKG7*BJ|fO19;4u;zt*~I(F$ve_OH&#L-{#faAlgnIOknI z&U<=n<-yI;!YW>8QkUfDhmXN|a1rcE0{ejzVv2msZVnFNgd;SipMaE_Iq7ZfKZPT9 zj0@Qg%rJ*vr=d;|@ttUioLXORkS^*jxwh6eYmWy_WB8hhr1;vX6t*u; z4|&Di4gI6-`r-Rb$x<`axKa{oxa(iz53mbv8nGvs#cSCe+~`Yo({#98IZF=hhXz?g zuSVGOs0VCxn_aSkKbV%j&rer4+19t8IdmoXu^st-5&E&JUN&^?yzd?F0l<@C)2B-I zjsVCbvV^^{gxR^}dcRvJ+`djBwuFWWM8GpUlzSIwpfAlNh)EYDguhx0sD!|mLr_IM zaD(7HLc>8g3E!|R@ktLf9I^-|(O7v*`SPMl269U4V;=TMIv=Ie^|k_4@sssO3>t^^2nB@0hE0XLsNOR(MmV4|QVGD3`6HXi5&uG?@ia9mz%lr!D8&g&l zmf2RaUvFfRq+Gnu0q@;ijoC*@s3gob80$jk(5MlLU^BtBoDLGYiazmlP-TvlWEG9p zG%p%UIzDJM&qpBaF(4j(=oes&gixzs+M6UIprOJhNZ7+csoBvKCh>GspOwRK{EqZU zIFPM_*(c2p0gk-xUvh)K_kA`Ltsbb(ZE9~K=D{b3Ajp!-rXb9M_6`)3&>n_TqEd?f zk=Tnkd&%O-GFC@dC+TQRP~`S_IY( z_eSn&^uFMN^Gy7L9)Sh!6A|m~Cd1jq{NM#eG>NW(-X~wH+ibn2JTGtKCZ?zDd#_Gp zlQMa!xv0@gd*@>2-dKa`SL{J2#x;Xn6=@{t$ zTKi)v&%f?+$(gwtTdIkeSs7b9e8}MAWaZ@fpYi|oXl6U*vl!3vYVnD!zK)Neqx-k9}BhY05Bk*v$EFK=}_lFn| zXlPLAP9ZSOf133#sg-1#JN3*xy?^K|&?O0@v}aX#pGc1Ss3BEl0U|mOnO$vYK-snJ z2t3D_Vn6SY9T+A#x`QfIFj|;DpGv_eeBly?zyH%j0dAC{aoK#EpB>g5{&UnvRvWJF z`ct(mb>*a+SC&t%PACa-(9+3A9o_V!8MtrFo1g#bA`s{%c2HgU%BSf-Zj)(xQ>kqE zSM#~9xuKh+#s+pgALSlJI?mh{_$l~ion_~}nCwTVp&2ofKixKo4?%p??_rahT>>0x zT$9vXZ_3>NimrIOzvp`+NnSN2qXO+lP4OB0j33wFCn${#{Y zxgdOf%q}gadZu>2Yu;1(;y~SgF{%VhA)CAKelI$F!WEqH;Dg9I@~fHZ9HjmJT=i&W z)&M?VceR`a=+c?9l`Vp;JCWN-KaurdXBYo|YtE%s9*oWu^>&}X0R)Qd#kbjmwzoMl z7fT*%@Qiss-Lp6o!Y%C8?X5x98QtzLKUOt7+NGB78&(QjTSOyO1pM)meOFM53XZQ@ zen+N1LPG+3%&Ao4c7D5LDrx9pczT(|qp*3E)*h`iX5{i$NaYSTAuB0pNkMh0OD`Q~ zynNABEZ`ddt|J4boTiM83$TddqC?0+l4n_PntRZB1a&{&I;h(O;-XfvPIDHQ9?>K6 z2#P0AHFBL79kU0pRj9m*^&efTVA>RPzMN7UNT17)b-f2bZp2`zRkIZ`}cqfUBBnlhKC_gJNi0QbFHZI(F*vH&;Q98b?IGX! ztvM^i@)|r<758yFrA>W5evY@91ze$a+x&F3MgCLs)f53v>-U=yqKl_~ zd44!!Kq!;pJQ;VpO4ALQu5M2Dj`eh*0~@(~!E&qJzElgfj9JvrpAEBs0^ppP?MO+d5G#(X-1&#<1SC{lw?|B?5uV&!0Jo+D}N772o-fS|nBFyaKYaHt_ zq5^yzD`A84t%0n;);C_^9doBaa@~`DXt7y&8%jO9ZVy&$QW@Z2@j5BerOb!){GFQE zjMSZmy#7d6s$edo(ldiD2`Js{of1Ab(z#nyPdCd(705#Ot7wvS3aSmS$P6)Gdt;{W z3Z^&S?>?=u(gm`WFdh^wvyWqoBYSLQHE$F(JdsTsG7rerPccFch=fsCwQ4(Iq;7~! zZG!!2SQELQZ_E#k3H6c7M}^L0yPLqP^VjdcvhSo_J^5bkwesepD$3D`b5RNMRO2hl z<>Qa8>#z(}MYW<-!0dVt?dlhV01DtgO*>fRZZE%;2RF!Eyzc$tSNN=Z=JaJOjagB5 zLP|s0V##xvJ^1Y1$ANMk3uB>%(VGuffdA2_awZRpOu^K$z_{M9!}$p>6&+Ke$8=nI zS%)?A7isEqqtCPkx-^%XXyMz2*9@i6dv9B6v z*`<>_B|W`@U`b1y55QM?%%YhobpK8?=A;Bl8%7*y^5N8kj3<`DgWRK-PG)IWXe>r0O8djb3|3upM> z7;PK&f$N1;TVMY~RRAQ|5Aju4xyY|XFAX>vET8|hc8p{bxHHp+E4QNm0rOwPg2Mhw z?G^~I78i2=yGCU~k~#TOJ<;4hhiu~c!C}%VJU(74X)6lP7XNgJ+^qQYk%X`b_@n+g z7zAux48&w&{Z53D;7@fy{QkeQx+@!bNRnvL#Ep_9n=ca#k&oR%R+#o0V?Ha?&yR62zwtMYQ8z=pst1eo7 z-n}IxByv7n(=<2QXuT#eG&H27t-YqFWwB5(C-hvbSP1Cy1!b|_6rhFhd3!v#Su)LJ zE!v1fJuQ1w?mk<=E!q{rEGhEhV@<#;TI}tv)Ro*1^7u2Y%j!WooX!u=_*hh1**P!x zC{~S-&-H>Zo!29=yQjzO;bLpw9TF1qNkc#Y9j;ZDOKQ_+2XS*aqI8m-__3MFr2l>>)3B4V+wD+bltJ4JG@ zCA_A=B)JOr=QjKjFQBZ9-hMMJsidT2^c4vgSF+Y*EN699%l&aLY3#uGpyT;0SHCBi z)qMksz*CP+G0~&r?a9LZZpqYar$4gJWDJM1ys-h|N;m{zuC>AONWuLuI|Q&lnqmUV z!OA*!9kh|Eh*^;W#QJamAML?BRiCJ0Dp8smr--F}{{QVpB&yB+p(A8kI zQn$%%Hci@c)&e}-iRS-0-%{h8IF{G^@^Zjm*wn<~4lHk^U}pa5>FHTi{QUCrS#oXY zC;3d1-Oi!z`x9pM5i8tc*_g5ZJ)+M;fOSZa1x8mnoP~>em-nV0R2jV)=tCs?de+WB zG<(zjNvQMm%l`DcmuHND&kG1Vf=nR}>yrj8?;}O9nQmnP>TH5_nisBU!whsfWZ3hc z$#NDij8=c|gd5=Q}QFTKmDTyaENhFU_pRDJ7^0E23ER=8;cF)O>Y znbuQcjl_KGTs-`%1Gpobdf|72H-j9gLS)HqZ+XU9ZvKD{wdN49wd;Uooc{gZP}JKf zTEtKAI7V(iy#74&hqe$RG4Y|b1hh$%iUmj`QU~Mdxi2rCVPRo8Ue9+&7RJWL%LDA( z+?oUqxYBDbn_ZyVvV052IW*&f^eqt{kaT~-nlY#R|@{?z-z01R*PxYFmfl#RW^^)g4s>n4EU#n<_K zUHZfj7Xw2Km(6l=zgL#;kyP8R4}JONF+o))+j_0VOuA}QpSG&}Im2}urr2OzBG7Ce z-+>#iJw=henqjnHWFCxjgTrJLYq{Dmq1(U%4LOFb4-&_XC zqt)ixyMx!MXm61X&+AA|rt8zj4H>Veo2hrTZzj@o;(jKz#u#zx$;J-Gn)h}PCUmO} zXvo%X&p_SxR2`2~)~C1U^ZH3#9GnsoVM*;J`}dRcR@*HhS`d1*DuIQA_-`~1d=Hxe zvz9_6FUv0AlEcbs*m#yC{})2D1PFXUN1FT!bLqSrg~$E_qxqjpeX#i|$q#_n7Ag8~ z*1mc4ftc=3sJ#z=MtBg26BOSI3ru9HKPfE;v?~`VMBCL3`H##0;wzAT$C9k^D)w>1 z=HoLLF@Cc>0~_1s18YGSD|VT@CJ6rw!keNHbXqOnYzSWCJ4Ph4kHcBrP11`{;o*;E zJ8`|)Z%=S`S1n5sCo}QYg%owXLQzsV!-#E&WUAyhXDM-|K!5?qR;Ek61B5hgA4NVKLhe&lbcyGg%%%)F<5X!?cwL@NOGi zaB&yHO>?lL1N+0h+5DCz_KHX0_WcDs9*IUeHRW?Wgum~*wrBx~DaO{H6104RWkhGw z#b)4}sMIG@9m{f9>4}zlchT05 zvqeWzReM2*u9R@-Lj$dum@+bd*i$7y(BjprHzK+C_m_1+3>FeLb2`5N5+pH9!);ZV z5z0n6W1J~}KV_(h3Q&6yaX-3eFY+j<-*U*bD4eca^2rN@=_z{eQai4!_{AyzZL6n$ zx_9wxm)W+hV}Ip9qs47G_vWG(0*8enG3SZ89axO2WKeiN-}ZUT#$sA@g>do{#1(be zSi9xunlQM>&CI~L^@^-pzVhzIE7+Q=1I(+}%|y{dy2P?0aX4FEdLGaaD*3zz;csu= z*A(@~A-=@Hc*=-HP8?<-R*+_pko@s{D|izP<-2gOwCah~BEdiLT$6E!d7< zk&{%9Pdx_UX{(ySod5A=gS!SNiJT8mP#0BUNJz~SE65@M z3dC}pfXNc}nzq`4UhUDfgGEQ-uefV8JSE}U0qyUer*dY&uq<-fK>JDRe^d(3i5Rl) z0kAjnaR5|f``xCm6~kmcVY1LU*O~`2Vc?Jwdc30Hy84;A^UF8|6N37$l%pFW;`6ix z(lqFr-#S_I$q$wMLd_qv?LXF zPOXW}ot=#-Ag&AWb&7-Vi7f9a^l z%2dnqIr14(o+@dqG3>2K9_dc$zPJ2(+F(j(NwUsAz;_GG@HU2o>rNu>HV0ho2^Xv0XFP2f3r;z1 zqF98k%*+TOViNovp2`IXxv~)3UH3@pS}zz*6{j*sK4L36QGl7_gc{B)NewpdUG|Y| zyVy*rv{A_&nU7ESMl>oxE+lHgCsmkM`*oudmFJSjP{dLTr?s{M1gVmo%9cS|rG>MF zIlu}pbf?t_Zo8kaBusoMJqsQ3nvhdgX}GaqynWG6K6b7*9_d47jq~e)o}RP3NO9eT zf$!q7DHDc&XGC2d(1DMTGt#dW^Y0ow)Ve-3@gBfsL%*i8Z-JXF%uXI}80hNI))Qd0 z2X}eV9fDaw5KQL6bK+2U*~E&FR;$N_ns{7Ialtpof`;8Yqr6q0ups|IxVWf33b(iXfNf7LB16=ojxU3Do@xa08j~!A(PY*1n^yD}|{{b{s0w5o; zQ$P%pa8d)J_rz@j26b$$PTb}CCjrhr=?S={c8;*}4>x6|@NsqBZpOcz^bfFO1$}sN zzuoEEZAVHtVd9aTImzzKif$9s?a%;pxYIJjl>aQEcL8Ap zu7S7FSg1EDU@3cIz$HV#-kdA~1EV{1$2LX2879&zg7}-4Hqrl;3iKKFl5!FIy=ZK)AC%cYDUJRk|sJ?`E{oZz@qCOWXC<#Z~J&??PTRQ<` z0Ml724~G5EcGgSs?IozzL`E0rl7<#|aQX@5r|&D^0F!J^Rn$}z-a*S()7aA}9*7HC z85fmd|A4Oq@z*g1C8~N@#B{tbFg@HEb+U)mM7n?mgLd6Rj)n<<@C$~JNwqI->Xp@v z(Nl%D*!o)`y(tl-P-&M~bD<$>UP~~mW*^Mz`$R!)0jvbxbY?dgPC=KWD1XNMr$Aj< z4+AWEXo`Q~zc~WrV~0B~f!W4}>PK%~XSOv(6xx+c>ObuY0H0q^e#%%Uj7uGrUurm} z+1-^5;HV#4=5B z*4%zY&!)@PS!ddVi9|d#aR*G=LYjoVT94U!mgi7xmeEdqkHQ9;{ANjZ`QlV(wG-D= zRu3-tni8nmgWy%$PY|ma0^Y{e_=a|7#>DPE<5IVR65idIrdCskraSN)8##MILo2JOSgR(b(<| zZVpe(&>Rg=+aC)1jLMJw_7`CB5CpqWp{r7mp;}9nbQSPU%=OfM2=|=j2kQO}M*uLO z&h2sC-8Nc-gq*z%Utrp5t@vp-Jo2WH@u(>ZHau_O%TrnVOup#mH%YJ5#ponw3aLwY zEc05TjXmx3un1B6!yWb{>1N-W1s$)Sv6wH@xV!R9@q4pJPdC>;Mo+@+5agQ{hLpNH zSOBq8m&r5I1z)Z10Pnl8^ePJQkvXRL8Q*v!kSBf>hGggn4w226l?C~8SDkft-o(tGt;B%M-0c|9a({WfY)>CDQJ=6*AcNJd#&KX4epGd%x z24b2iV222%OE6x=jr4XpfMcK;6tvMDk9PINhu~GmOtuGB5wPw6S1h2cbAL4>k##CF zW5O@NPq2}xkT{XsSL_8CzDc8$(nf4O{?xKp!sqyrv6bksN~hg=JiV5sh1N7oPdm*E zs+1OQj$(5u$_YSXypI<-kV#YLsHstu(o%|uzUA!0LFh6?`t-e ztrXuu$@{>8rqByrTZjdILAt;Ld&!}sV}wwHXubM%5FGpOqM>ggYRrw;8kb9cSh#Y$ zx^w?b;V$dWhU=)tpe0BB^e~mg_;7CNryDcd*s%=Ck9{yZ(SK|)@mB4N?{0^hk4`#s zV0JuRcl6PgSi^K7Fec-d)@;a|i3stLs+Q~|o~c)))K8{ISCY*)gUJOLcygq`mL&Aw zQ*${9nT3;U9C}L+-7Fkn#M*CJ$@TL;QArc%V>l%wmqN@h>MI>cYYD4dRZj)`a9}et zP4x%)E!yCo=Q#Z zzx=I3SS3nWyZYk>4|Or=BUvo2DhZsP*qiLaf3;K|Xl5g2V4z8c9kvRMcM6cJuB2!& zm0N=u;U!b<*Tz+KF_CMVKCUR-&E3 zkeR!89jT|O#a$$0&uV=qo3(&JiuLTw=QZ2TIy5vqz}}(q3^XKy)?Cx7`uTQwLu?#; zD!gMlmVE@XrGlRIXaxd6F3`0G^Gz>w7}oHVYW=sKDo-Uee-C?;MI)8dEKGN0E5Bs~ z;=i$UHM#49k=ij`m*FTu-C zQWDr6WhE|X@i;Q}d05+3;4C9XuTc3q6IrSd!SDrqgEpwhS`!o5=@-#u%Rka3_uvA! z1S>wp?-R58Uvh>2V!TPnn@uEIV4f|g7Cy~{8uVn#CLqiA*m#E}YCk zhniJ^T+?XMvfBA!ym5>QJS3o}$wQTB;;Zs3Mw!h8%tx#Yia#P`ayf;`q?-Y@pleNo z5FsIWLc1#bg+D5i56}JLch2UA$Xmgb64Ua}9YiutdOHQtObau+gVk!-4ZlAX{u}qZ z6rq1);eJ^R35G(P?cp;*zd3%JST8iK=mb6Ygu*v>*RSqT=RRS@T%nGi=XYmcV>s_r z0+bY~rbBK(^0$V1$Li_NYgGo@A+c2PTcy7LVshR`5n<=&(hQ2cDioX;&0!vvz3+Qp z%CCGV@@$orCMdW!41t`2(j6=8OdKy*QrrP7T#4QcsyGdiM;PRKQqX>6%PeGa{^}K{ zvWljollRN7hy1Z7(Hq~dJ5$9Gz540{0LuiXNWcQ=0@V171c{)zuf~4bCFAkaIR|ec z)YyFoR8I$;1XYtVJtyjudT?O)g1!M#w(fo}VEnx6`^h|w2N3F-VXla5|F8Ze1-yXQ8EDxuw| zhs8eRa`i)bHw|yc8f)pO_#UAV5W=2x85mN#&)?@5BT^$9j*r=0=)Ip5>-5}v7$2iu zeKBYlG$a|K+mGh6AFWamEBt*mkvY`tg`x`!c;u`s22=D@R|N#Jkhl42i?TU?oOSgP z5SU^CLvSZf^09@j9qfPrg#Sy6v&x~9%|AyHr|~*$;dQo7;1}er^7hV~NzPMu(i{tk z%V5r^^-yUn^oR?Cy)Pz<;6@)z*Xnvp(y!VZ_Q1+ygu)&PBEH+!y&Y(wc9{I0d-B#L zyVZC)($Hm(dVFi4CWcr@w==1{WMnO5eRyFD!8rC8;6{znau=7uHW?Yul8d{cjwwFQ7kNVTSy_r}+zbks6mr%Gta_LU-@ z?XN!CT{_cLY1}?ugsKz!l@|BY>okTNk_871GanSLA^H1O5X4zMF2Y{2#v1#SLAxpCoB*jmhP{YCE#ML-OeKJLUl^ zpU-ch!>~||SeKv&$7X-J{+4>PNckfLwL&igph0NK3cPsL%HnpPN0f@!75yyzWLpG6 ze8+B9V#!gPwogi*vxmLGraZRK+L83?tG4oD^lMAB`>Q;~U#V@zcY)E2*H$d~AoYhm z8nj}{YS`M4#J zZvQU?yh7VXkq%KmOZfQONXCM!#_Lv$}AXEzSoecw(Tt6vSOgvIsyjVSSlv6%{_hG#4TM zHg3H-#XcEu5lvY1~Uy}uB>6l1(8i(=n&T7Pafc=ZRbzQ?$K`ya})KU&k zB&qN+1oeyH97U^PiB(JMT}FXF37y1981xAM21Q%mLxgXd2UKwcg{(G0YiY?qwi@#x z?D(PUkU`lnWhM?wJX#HRXCUt(Co_x!_f?()_?>}LfK4$e;&s`!veF|M(W8MOp$p} zuF$4441r4NG{OyMNcN^H)WMt|xTVwh@xV`0`bRNQCN!@kw7y<9E=4%tP z-{{qhyK7C%c=u`lj{4I*1YDZEKTH{hy2S zfW#p`tBm0I<*mz>U1qfixGO#!wNTG9g#%p&3+lZTpWu>{6F2?@$!x4tCi#?NcZ=Tg z<&n6-nx(U*Xq|EflB7|${U;j&M>{fR=*8&s2bG_`wqT!W+;GXBSok!(o5X%$_8-q7 zlIa!S!r!uEEN~*J02l&w-SXNN-;KxFwh6OD3t-*7cC5W9O$6%4WARY+NQLQ&Ce&(7UQ=^3G9Px<5ILd`Fh|vefZXVmp6W!Q`N9sk1a)P1mx6Aba=4 zi-U2m@)X(e<~B2xZ%m9-edC*)vS;NtSZ z%{Q$k`jIY#@Iu*wlm#upP?CU~+gmCwQrQ1vA-+~%ABE0J%p!he(71xO_ArACAH`Or zrHVCY)0t~2HZLz@2)y=WNs}o~&N-q+l#N~cceUkuaslQaUsMWmxe*Z=a}2J(KU(!$ z8yY;dnQv4h43*WB!*4a#!{mA)*@F6wg+wIGq|p-y%$JsqS!GBkH2uI(eeveJ%E~uW zUVp6RBBxK=rg?aZzvFbSqla?9>7FVd{*+%t_;vUH#5Qo$Fdr$EL#&qkA;$}0v`AvlxKTaQr0 ze3Z&4YcenQ5Nk6E0NXhKtFAbSZ53(yBL7YP8ujS%+(Lm&-Y92hf$G_}S&rV_rt+m9 zOAa!AM2lgG)9!e`)QJhZ9!{s_N%(3}{1QNqjymG)OVprfv|l%6>wRoKYOn?y07{gb zFxmqepQJ7JI3)WujP6RHS`HZ!fDu%N(IQ;Or1N(**;6+HyP- zenwNL>&H2741ULnObv_MemF6-!zA@vI$1bu=^e>lZ16!hB6KHRU}%9rRx^ZJSDVKS z^9_IN{*(`bkJLS_rf6gPtvoMtlBi}x(nhj=aOF3LXWKkJxQ`UQhI8(t4871;bp&6j zf|0?j;=(D!3}q36=Mz~^-A(^oxj6lF<%R4`gobSf@Ftt`FgzXOzxr;NWQ+grbg3;0 z1nS#I;cl4E3ZmunVWvzX-0ulloWg@u{AFE6k8w)I96n7tOHDrLhKu75qu4W+a%1x{ z*}Tt55l$XbN*G5AWp~4s_L8( ze@4vtx3J18KK&hGf9=~QBx|;XIC{I{@&8BEIXKp_eqTRL(x7qDByDV?v28c%iEZ1q zZQHg_8aB3V+y0&2d%y4dAI!{|dG_9Gebzp@Z|!ByZI2L3Va#KWO9!D#HAl$*`>b8K zK0RsLbP+_9m2=4}36oQu#mUhZ)3L~9LWqTZSgV_RKV@qCT9=3p9Gp}MJ;V@1y=pD! zsUa}0sA9{)j6b+W<)bREn3^yL$uX{FzFnfP=r^#9`rmKEWU>d5!s>mf1mK4F@{a|9T0K_BsyUyG(s&g2^+-r|Hd8>rIi~drl$FA_wGY1nwCIJtEIA zEpp?jbD>_P+Rr;R#3h)Uc($iKAD4#DH&Y@e2~@Xec_>vEmF=X(gOpBASj1*uA8MEo z?Fqn&jFOhCFWm*E_=4PzSCgbuY0|6Tk>7vHpcXEnzp>6knS1-cdNdBOp0(&&_0y|p zRd%?EmvmXa5fVqnRLyh(Udji9xPU%9(YJ zx7-h0o2R#r5aM)vmhliB5Ori)5ZK^OOvN1jw`JQBG(@(+{UkxEl&}$*biq-D?R9gG z?Qq_VJ)ki?NQ(ZCt#xgcSQGBTYY z{8HV8jQ@bN0nwolyX0+C2dRTgWHO&UDxso1yTo zqwfRT^#w#3lP}P2%cpP_M^Gfo8-su<>-bqQ)EnZK`r2=J)16m3fQ;jx$#g+Lb5Y$; zHoi7D`yJ!HG&foFRJ-X%(3rfR?5}mj<3Znyg0G56(Vzv*Zh`_5{%L!H(*?R+!S#mQ zKYURB98a)mOYP(bAJKP(ycgbftMo#0IEfdsy4rP-51zkL=;K}iyLnx|u8oA8z8=cd z?x*8-uxcIbkI}X*npkNu?5x|5{()fMF(VFZXhjLfoifly$?s7qXEe6VtQp}0b3*?n zk4mcziP5Z&`M>ictb1Ircek}@)W$z91bA9?+pBfuR;4F90$uTD=uE0b@iA9Ne*`im zaJR2Kt_5)|>>ZQz?<%p!*ezRVI10v2&0n*`$H(if2F#bMRsuCmjWDG4Fl@4KU@Sr_UDns;` z>PWNLZo&XCWO*k<^2F)}?&IUJM?Zoz<%Lg;xot0h+Bf4ANZQ?WB{i>lI@2^!ffE z5l^mAQ=gCZggr$@Gc{>l8dWEfxj_QEm#F%4CX`)?Fey)OSj&Gt_&*-MBh;tmvEZQW z=R}#x7rszW__OMinc#fP;Ey0UK^DHk*l% zkKwfCtj^~bsMX99S7nzYcf97jA2!C`xag`~@MDQ;Exqqe8q%2X2$da^Dim3q1hMD% z99dc&H)&8s^RUU-MJm~+TpIMaXwCi0YOHfZUnBFnYqTtg$mfIjH|8Z(#gpPa6Ay|w9RZdIVdcLQzJ zeXCpO4&F6C(z8wRbrILveQZ&h zDbx}EaCNEYNNA69tDBXl0kYyZs8fsS51&GzHe!bmk?=~&L)()(bM&GbLOJhX8~iTK z^DOpIzq#MgeqG>;8yKt&Dh;D?1hN!I$?>u4f9SR3DfR^yW@C z8GEsw0VY`z?;Nyh^kG1D_=CON?P7lO%QK|PX<4S{eZL;9{^23gC*0C#L`Tnz(NWyL zYVvNpwsGl|%gH0}R9ounIyabl;&3;5%io#FP-Gi#T-BXsKu7bV{wN8i+oJ7m{g|qM z008{s{`hpL5j@hAIeB;WKR<6>)F)8E3;$O((|T@_ns#bAWS3fcqq1yjGffEDhiwps zpDOacH;yxL{h>zZ!(F<%)6CRPQ1$se@tv~b{y^GdEVCF6PJJoS#iOim(Bz6d5pVbg zYyFaA9Y3)ojv3!#Vok&Si)GZj%5ry;DR?Ep>+?)Bewqv)u9IvkFMU4d!Y;B10-;pC zOg4d20-soFkOVjSe!Z(0!ycA^)ll%4!;^j~lZ&NwhY7iALLsraTVWe?kfsNg{@vK{ zsRYL>^KN8l8Z$uqTSwc75per~ima!SMGU2mgVIwmuv$CQChN7`si-ymK4$k|uNz)_ z-(rS(zM^z5c{@Sg@x3Rq(bUw-k`m1RP2vhA;p(;8bjX1iuWvOw;y~_ngqv@*!St)TU!cy@!>OF;vD9^!5!#{a243v3 z1JR2DPi4)MATd`_+Lt(Hu`$?zK0)?m1XFTs(RIfSVze;Og7w(_=W+cnhCbxR1 zL|T5zkHMMC5Dc!JiU%=|qyrT#3zJLBw*hmo`Qw0=He2ipezJ=Xma&n8Q1 z!v!{2H@zo^#Y{yCe1&ssc74^#7 zh<3&5jE6i`JvEyeaj#!Q`-`sMa`BDVM}|UPS{rZ%%sy zq)IvE`wN!h!^kHo6R~7p=6h%eO802S-!PE{;>y*cPeEc)?q z927tRB$s<6X9Ag!0`p!OxS@LS~m18aGFNR}9N*eQ^adQRMbN1r{WAB)^mf5X9p8V*LGMswx zf5iL*83A0k7y2-b0{V##AgLZ34X+rppTjD|a9n$YGowd5z|<-G`C+1J;o_uAVC^Uf zcCSzHNKO=S>Ilx+2!lDTYqCyeX<~#;n$a@VPzv|NQZRy{dY9%x)_GRJTYHPlXyP@Z zRp#pgs!ivml)&u{K2mX-zxJ_`n6)>^+!WmqWGms;ZhdK*nfGiJ=7l&K%y*YxK9Ew% z3~bnv{+z#i((~;;?Xuw+z{qHPlcQh=hEp0sdOY8EaSo+d??6g5H>g zVD4tvKbwVLb=Nt@layTbxWVcOOwq=t1Chz57c&L5U24QDKy3t`J7VHxgK7N-<~iX` z^XtzOMM~U=i1)wW$6xK5X*8l)dNf!w|I~HtI=;L<{C-Me%c#wi9X=t8fc+cZuK^hM02M(!jSXFng%bbzh#*BREWJ zJ^~rG);(unLdBBW0aH#6Y0+4y%eOUz-;O5c-Q$ay?9NfS$FMc2Sz%}*X@ zEO`46VO&0cLez=zTn>p_RC;{L9*~{|&xHx8WO0?SH>gBtn#(SHVu4;IK(BckjF%`D zOT&maIY}a$cyeaY%KA)I=BQ8!o7^}Lz}m$0t)5UVt2(Lxl+yYKNZ?v8UT;7jXq>Mv zMOR%YCWDV)?+0=G3t=*ofX=Bz!}sSr9z|7F&v!cd)WzGU`4ZbZtXIIX0_~;N(hcJ_ zJR;omFVq7lFFG#_p6rfT_P{Ud3ALnvQ6IBm(9|WBa54>Q4d=`UpZOa`p8|spy9F9g z(X~2^eOv1kTj#k!)zD3s!S&=tJu83fLS1(J@#Tx66P&)&YEh#g|FO;q&ff;ikYX)i zbHzY@suEn?5mM$pDx6@F4${fcxWorVv&4NtRz_6VD7U%svo;igX;k0GJYE7Y+G{Zb zU*(vSOBV5HB9Nu)mFOU@Bu{mjin}Xj6MUHdRQh{hq1MZF!lr(Q+QdF+Oh;^L*~Iy$ zq=E3b8s?J^Hx1#-sWc8rq4+q(!A9+7ctAcE#pKpI>SDLQ)<`PL+(54~6TU z^gsC@!Z~qK#q_KsbN0a0l|@yMX#a2uatpQ!+HE~xO@z`mcRan(_FOD2YmALCAT;j} z0n2}7NKf}v-?88{NAOdp@HAWSw_rndq=Er=c_n!(irKAV2p<~ zB7;BwLK!tQzsM@8x{*>FMsn|8fO8-{a2@ds3@FI~DDs5hWB&3(iQa5@sway^g z{};XaDWzkat9w0~re=F$DhY#b^D4VbB%kg4wZKd_bb`lYDbsB$0aNa&bJ%d`ibaih z^g@G|9^78KfpCg$y;gg{{M5g3R76D72*bUnwUU)YyOIqEblvhD$r=^xOyoH6H}d*( zlgOIx%2mcD0AirfY;0g5HDq*#uI3VAXC&BOZl*+^KLH-L#NCtps!Y5@2%L7k0Q0el zx>iiJ(Uz6;+WKfmVBSKHw7k|8Q8%vE)E3}$B3Y3*`v1l$HZ>s)e$P}-!+as-uss`# zlSyl>`I+WL7rc+Jl^b1*C|+UALN=943YJuuh&lKi63P&;A@{(Bcb6)Yr3%B83SUdu zw-IGnj3@Di6pDCOiIVMlsc(KMdHPpm8(*g9dSG#?rmghWWv(57DBO#xrL1I0_pa=DJFhj#s5vwqT$tA* ztE^!K1sY+;d;-e|CQ-rR6o5IMFOgt!Qbik*x$jY8k`@xc`*U<%De0WkIp;e^*aj93 zvx>DM2yle01$AQ4>*G#Clt`aFj?;fpYZvHG@8bu~Kg0}qRSDj^qF{j*ZKFlg8Y*B$ zwC8^{Dp=75pogu?CBJ>qT*r|AT1S?{3#Ip<)%7RO*73Dr|dR0?7qaz z@L!TaWHTr1d?HX0v(NxrP}ly|wEs3W@PK@$hz*Ot*3MvUyACpK*NZf9`wyWgRG8cY z{@%>pmq99&_#;`Cpw@b(9(pz^vNaryk@PR=-1ae&Ud|V+FE7i{L6*_J-1zQyHl3~@ zZHF}))6Ad09`-VPCv$DZf%8+B>s4ob+FDS#HL`C!u%b2Oes1oKeH!oOx~?>dSUz%z zkDX}r&qMlYV9~_(4)sx%F!cqD*zBi&(6dV#@H}}#K8>l6u%Ox4)DRF9n{Si~p|aE# zP20ZcJ-Zy-kj&`5KV2Ct@ls-hk(uAADmqDKI><<{uM^U00PQEir?T!}QhAYqc)PfKEbYhZsR>7d`bT`HCEl2y!oW0rmIiCGzX zAx#c+z}%#6AtIY#_$Dt{FSvTTKV0T-kZR3UxUZDixtn>pMn!eBwBi9dH-!MJN$fTEUpEOOylI|x^}&!!pqg^qUVc~ zPk-cL-1w$&CxiU12Ug?5lWLjFlIC#_5<~`RUv;zA!WmJcDrX}YI-uBV)6(wK&!#H^ zL&Ds`OjX|Oq^7`^6Oi9psqf2!Y{TK3x%B<^t;=lfoQp4e_tLC%1X{0H-~=--^Z3k- zgQSIZ;X;1Jmt)z{hW0cAu+h4^_Y?!!hOBsq!5X>pFLAIM;^WbLlUHRTD8jUi)oUOy zJm0P*ohycTzpz&K1E7Pr$ z^_$LjdIb7GY9l-v;RZScD)kRQLm(?W$bP&^1_Q4Y->jAEniHt13OSOE4{jGTBUYa= z1ggtX;M{8y@fu^eCQrnq*eIvBO(T4~$Nci}{>6vN5O^PO;Lq@{>&Pz3U*WcRT}B(Q zF(69%OVkxz#Nj36cbl5R**4<~31~c>3}u?@^Qss(@f2w>l3_dO+;d_#DTG;!j4Us` zu?QPOsanY=DeV}pa7`jI+QMm=`3|%+Ev~h6SKnu0rIX`D+FFRu#v3@Fhh$INYT6Om zJ+Y5{w_Ewvj9uOlUS3cp29hwy>Hpy@uLm#tTHuAg*Mz?Gpla$H$s%TL~bTLL& z)_$vC)T(PXmdePwM>^xzkl5zQ>MG0Wq;Nrw$nu(|ImbXBl>=g11GE>eh0O_aWL(cx zTKww5W&YCoNF<2%=2UgZC-eRlxEUPRy2icHocWXP(%B+#Hq3*QP!%6z6BIO$aqnBP z?AG5ar6q0x!~*_%RX_>E+!;|XXMj08F0NoecIiAs!lYSx*z@WAT36v)syut_h0`G;xV%z_lQ zp{715fL_ESn^Ag#)XRGvS=x6i&e+20R<)ZGN(R;2U7lTtp6Xz33iz2f z*oeo$l?1ot%LyDV<_tG&r;KMPsFjGSL)rSPKAkV0-h(!(>x~r8`?Lpo_yVq#6O8Gu zuRH1nMy@pqAi@7WByRRDx+?r#?{LVJNX6yUF7xUVo$15Gp!I`Kh z>kx04J&=0d+;1W2;W^OvG=(eVCCHRil}pN{c*i%wA^6x>;QLP)6GWKSg{)-t(5Ii( zU5=1}T(fH~i9E^`$>?2pP200AcaB4Jb4iXvW&41QOfwG+GK%oF%+JuFGZ01;e5*~U zUXBN>ylNvG3!(t_J#g^LC&y31%u}|orxfCa?tqE>#-S&yo=(0mMX*BoA)lGw2kE~$ z({-3p7WJKT%bwKUom=icqcahooMbuC2r!3zF%R5W_W547JzhR$v7ULm$is==4?^^r z8L5QDP!C$&6gTPR+?_ydIA{=KPWFK`+unesTylNBzXz56@k*I&k>24Qb5oQpRS&OL z9%Fy4P@uKo;vR{74DE{Lk;>UxwzjT;Au^v)>4w^S*EDYvh};;fHh0ZyL=^TBTAkpk ziR)UzD=}IN#B2A0?BumofF_#cRYbQm;a?b6(sGoC{$@TtZ2J+m)DWm$C8()+`WzkL{i!c?!&SFp9>5`f$J$^(jO)mJ|I9n{Qf*R9)@9V8Bpm z4G~>R%>jVs>(%+oO@R*6=8%at`#ZuUK;_m2C{}!;28u+qC6TM$@Pk`;Ie<{L=nMb` z$k*jy4sV2JqCk%z)fhE93l>}Vy63Y6IgyTaZCSB#7r9~bkyibG z?xZ8u#{`)Ra6@#iY9<$Nyk&0sEKL>j6%zmhTpd@YdLJ5!Zv{b?G4GN6l!=SOM+R3M zwA`F|cTJ$`9H^R?iYP( zxRG_Wxiw_Tfh!&;UcvLGUQb!%JLiSLdc}u{_84v)ZKS}<4{kGzHO^J>8wV219&3FM z+kedU)kJ=;Qj~^T9)C^Mx?1?@qc7gtrd)&;E$ci6x_LA`M9`gefC`zc!md}m%^ZNG z{3GsY;Ad+}i9y*TP1ZfptH1pX&kl!fLZx^&ql(`irafqnW=w}zIv4b_c3!a+&We(< zhCRcltEao0>M82KhNe+HF{bm}={}8riZpIaiGP+Yv3XrH^D@Q(3=P$}^xRzLokzu6 z@)PMu>DPDHx%}JHKiX5EnVj0!rR`}C8%HUBB}oY`(kTG$Uvt(>lb)U#S$z(z1jxY>1>e@oJ5238`=1nb1Hy;N03vMCFvF1VxG;Xvl z+;p2P`}&Y*9aNDHwRh@vbd3M7%ydZHc{)063_TemCqNT8Yh=sG>WeJ2k zeUWU!ttSD`{T3M&qw=hI*cg!Y?9d|W+qv{+Rm^61+gO4H@q$AqqRqeQ>P@G=IV4K9 zg_=j(VDX709~E)OLSS_R?Nlo1UMyti_6aJ3>am&?2nr*c_{(E8R%!${R;4#XPd-P! zH|*W&j;t&j;Jm+{nTDhIuw86Vlok|LJ>3PylpoNcg%}*HAJ|3c82h1o78hb}v5HY87NJ1)Jl% zH=D;!13vt`H~q@Yun7oTyH%T}vv>bkZ&DS5zc^nRDh=7{VBBbL*1gY&gVEyeC^Eg_#uy*MGfo&-?n$_=-PTjN7EU#hZ`KjmMlZzInCZ;#=Gwf5($HnhV z=I8A2(9dB6eA;Idze5j`Pno3wswdd9mF2Px>lpgN_;;`&stjU)F8u=CXBz67J9AEF zl206u=Q6HCf2krAU_k37%XsTZkxG(?R>Z4acB@0BR%{n2fBbyeReIZ(*-1fEA)4*( zu?Ge6qa##Qym!js*ZPD)d)0R713;BT4*cbxJMMz)E8N#BL}5??=WCq_v2KjdnpmnG zwx)sANo4Q)V-@3YS};b&K|qpB?XFei&s~IxF=ITO7X}A>SyDUh-Own;OnER)L^Tgw zDZ5lrxfunbY+bDUn$?^^x_)g7B38RX1@FjfwwUbTk>TCDOZnX@||J8d$pXS3U~gq6*$@60Jf& zOBRMeslWF0`0_?aFz9X-ZqGVi=b4~u$PIY9Ptpu6s^?Uu!i&5xk z2IhhQNlDKTe=SXXI6ens_HDespQ#BPc!$ZzoMq`+p!fl_WL$~dBKN7I@?EF2uozU!SWT>`Sy0c^9MwLc!px*n~{xOl)^0d|T z)bdv`p{sFpvaGtm{RPFWuLDa|kixy>Q!nh8YwK@o4VxqTWt4Jb%-+B2e%9`;tDBy` z^_G@@2{JK#o~H5_%8x`v9;1g&**@r@uC6FNNK^rQ1w9D*Rm^G+;dHZ538sTZ!+P^5 zcslCGRq~gpzZ!w{L(&Oh%_cRy4o2KlmK93i?U+7a?%)9S5tfH1-LyX&7WKw>RJyxe?wca(2tDp9UiHe3q zjTF?M{n-Z+_?da-1FEvnorVU{Sf3-|BO`HP?>Z9Z@J@sYZ zJ*g<|iittK9Q#wcwtLg(2Nv(2Y9C!~a8t-PpP<_%c}#>Y_mxe}5h_yL-`*yPurL6^ zz)V!pohpVYWlyMPla`DJc9L0SFO=Id5ei$Tzs+m|=O^3Pp;4x;X3}HZa_3VrT=7}s zp$ft(sqGQ!-c?l>@r$q_Cg^^>2PTZ9;)pVQi`(KUw#h@6UKZ1;FmOv;eyP=@0!x6! z*n8LTGkkwNNLsn8I0&3-OYuo5s{{M}smeh4ihKcUnP>hUmuf8MDHxE_=z%)LM%In& zqrpwWd;t0B3cJqBm2)bgOXRgXpEir3`o4}}l$4EHT}JaXoa zhVOB2^u7C)t=|i@c{h9VcNvLwuHoALDJV`XZ#020)#P1!}KKj!{ zTD2h#!+N9Id%<>c7({C)&1EiQ)*FA&1xYk^SXW}UWt!ozHBy2Kr)3y+W-In53Hv^9 zNblOZqcPCv&|q-g+wj=NH?tbBal#KrQ!~n^2h-g#K~@oGJ-)U-i!wlLBu3;hJQ4$; zt5S?+Mu#M&=z`)zyRzwnW$k4j=X81`8~mkyEP4Kddg`U(t`Ys!??X$)f7DyC`LM6H#)G_&F|AtC>BM1z4_O8T%w;SfwQT55TTFrcGc)B!cod*7~)LmH6MR~HCE%WQ9T zI2|DD%h&2Kx-Vw-35LysQGZGaPw7mf)+^S8t*IG`O>7nChc|{MDH@$>EE-EXyM(&{ zjC9QwOBc~KyuN^UX8&?Yn~tIuvr#sRXce@^e%S<;f-*2O;nZ>iGS(Goy$5^NwZMZE}lME`n3QZO|GS7Qz6oR(QB>(_binrZn2$+tVEks;VWA!ZIdMX`t_0{i9_ zI$+*b(=HnA`~qdv!$-Kiqbh%^vXjYB<$k1ke7{3%=TDp~ps8z{{y6)QC)yqs-(lRp zH>`Y?m)S%Jl4mX1^D)^-3A!>jPFelF*3L%(2x;NGuXsUr>2dZVlv9Nqv)Mq@ik! zoL`fL;$RU;X}saAssWOw*`6UF@>&7EE@y4-AkM3_t80%-Ez2$h1aB%jRMR1NcJeW*yS^uGPur&f^@n7X$Zj(?sgB>vn!jaK!B4AcgZk`E7YxTS4D z1%i5nsg1w4$D_swUIcVNz#RPSm6QHsort66BNe#lFvwvWn3& z3Dnd~q|$iNh|qtRO}Fg-D3KGc+;3)m z6HG?{!~J(J>mmt(t;k~@O@-ZROdjgafwNfTS8g)yaf|0sze30wJw7!jP1M{JMPi;V zy-u%(>%4lNkL96Q{jrIy;LdH+dlMlQUb~|6X1`WJCMOb}#8(Ct{u!0(kgW$mDa zvL|$M;5#Om;HQd2U2LZ(D=8cZNat0Dp(K>C$fsqRc*$e3h9XJ+K_Q@SrOl=(L_7Ks z{U$uc-?)a49j+_mJZyv*P4r~;%U&WTMRh~iD@X!K7rVD@QZK$hJF~U2%-V_zlPq58 ziTv+JAqL$}iwMdgyOp?O;?)gTLHo%K2yVEoT!~BJl~OWx^`>8~RfG;QaIW7)GrTPg z+xyM?JA-#pYb($;3i0jd>DT8mL+7a!fdM}UtJqq0iwiHYs(P|CZ=}nVwCAxyarKfJ z-oM`%lJo7cC?w8Zr|ymM{l^|rniyM}XfSpdjWBo94Z54biq!42?}O>;B+QtAba5J- zh{8|t6%W2@PWwyvdlppCbbSN<$5DTz@ren45V3R?1H5Acuh98o*+I-~59eRdw!0Ii zW|I0MfUpy9~kT{qCJUZv|vaPrStU5qG?uhGei~8&)-pm)v>PHl%c~1{tEOt z{-}>2C{(qG<2dKrZ3D=0s;qo`P|>S|wKe+fx?IcUqxH%@*m3y^TE`%pt0y925>opk zBg0bI+^L9gYE#Ss&KFqC1h#gH2LrUa=l_w>L`=Yo^Yk?E#Ujr?bhsa<=}a^%%K~Nd zPgDk{xPn40*dN|*06a4WQ%2}B4Tw*_s{!M<@o#K1*K%w^3>d&{(Na=Ei0l+K^-;!& zxUYk`d|;41pgX}W8%)ngEN*`Mcfo=QANcmc$a}E{jb_i3eyp1PHBld50LPp-&(B4dH(a?iue{Go;K8T2% zOqhg)Z&M$VGRe0vdOe&*!@f>E7t>a%hX(NuxS7J~)3kQdeW4y0G-tr90q$C0=zC-jhc& z^hZ9_UwP{r0Y%o|U0MgcToJ3EA?@rD@FhHATfvI3nQhHy+0USHP;?<%V!FRFGGn+c zvAM|H8*vz}OA?^YZ-xyq7t5JZ{Sj^W;zml!S#w6rBehW%33ENuj_ef(^A9IJ=>;jx zg5G8?D?tvfDJExc@WxE@SqJgU04d?A-HDkje!aVbu*wJsk_MBchTm!?7CUl!7HFA@ z1Sz+Nx;hk=Qa+xx$D`4HD^2#d^Jb8EzxF3Odm=H{noW=Ho`-1Ir1Jn7(s{S`9CuZf zL(6&59E*u5(o(lWZs@d#?BFdaT+PM%0OrY}@jy2R5Q%}2-$(aPUFCO2Mm8tHxD?J2 zO3M$D>rnvP|0-4$k^P&2LD0-!ixih@sm309JY?x6wO;g+26lX}yRu|E2ohc$?8G-Q zyI^9$BA;0GfWpl$tj38XeLy-@cuG_n{bZRP>Qgi+!M+$S%T&i8ZwndJIQSljq&H>_ocp4=#-4epYHT}zn#Su>U@-@N-LLOg?j91fBb z+xIB`>CCQ4rEPYBYReYqN%r6ES~uNgt)-}Do!S0PbAa-|kCvzOw=RCK9&2VA=ZxPl zd@jPW4yU(n6?&}2O9&f^47kW&hkZ!m{qmOtc6lw1}gyj4m))M;PRZL3rDnQ^ngs&^B!*#tKS1Ad<_rN0v_HV-FlR2O7E_UA~rwb9&q zWwgo`i|gGO-hbC>yh!OXn%G^+bO{9;FU{)n_OwpA;Xqh3wfYA4_ib0B=hm!i%9Z_!fG&ytv%r4);e+v&zbVp13cLxum#W=8$p?$>0kKt6|@eLtz`+h?Y zp(?B1dT2NMkeBhx=Ad_}F|CIj*xmuHQaU=xpbE8r`D@JleV55GVEg1P7`3Le*r6l7 z|CNuqGkuQyP);TDu5LWy>LW!Y3VvC%7mq9((4pQ+Vi06}_buSS!ZrLOGTz|pzMKtu zm~4z>(vf9hzq&qP4XLPodpymLKIU=c588gN>g)o|%h&WO$Mu19gBHXl?q?WK(av1-S88qa!!d*RoS_sYZ=@DC{|&FyhZVecw&h-%savLD%qfhHN)9 zdEaBuX_>8QerrU$T+b(ybp?j7Ua$JOQ$eAcO4na(8o@TUa*A&m($|Twn-`~_G-!xP zOYrO(My*qEynTh5$e&TxDYqb>yp&{C+?~YK*99z|cn}4@J{l#R+Zz$pr>=K|3+>jEJg#T#$g$;`Z<;zOaE$0<+`~IaLTRUGA6SJ@$G+& zlbknDj2RVL0}FW?!d7(o(e}_mjOtJsWXwE&FIXr_w=?(_N1E4`S6+n}?YN$W zV=X~N`}~?CX>ULzw5q(UJwy5Yn2DZnv~K_VkiT0O9On+;rzzn>5Ns_f5>3!8XeCU) zqi*3ZX`%gV9Rxxl<&_w(=RIO#&TOB(G)|82iHm;NGgxwGiJXqyycXEvG4P!TKs|m# zVT8VC8qFfLNp|GH`IiuC;Q7?Vza7Wk;JxC!{@XpFe`_QkoF#N$Mbt1UbN;~qeb&rt ztNQH9TLxh?E1W5s@^>l@*F+|{+FK#yX*p5UBKwU$J!Q*^8;|RF_z0!n-_;7at6k^f z(%`P4-l);BEX%p?0~@VeQQ!5!|3Wp^q%qpRmlQ(`bf?i~4qaF3zfVs@J7sQ`tVHcu z?a`oQ2*%V4XH+v!lM39NJb9xdFat@AmnNFwM36A>wY{-;Q*K_Vv;#c~IHw6Nx27PS zFmMX;^mgR3$D)6BB!j)E-G%*V0Cw?WDxdno8_*0X}`thgdp&jLRh&xj2+b zszeHQ@V*?pU)1=;7F+Ezq0%>E! zL--cKo>H+3gMw<8;piPjs;G>8kqA9(n{XDWTx6`{WLKP_xLn#9r(O~``0d(GZ0mjFd+b-AWPdpQfWonV1cPKP zX8*j6Aujmfi#SR98}!EvNE16x7x&D(V59J>tr=e?-LEhkQf3v zVuamv6DlcRt#KaKI1%Ce9+G*o8Yh01p9K31JSGUn{4N33nOE!uPmRBX%- zH3ldpnZ9cO91td56I_rgBR1ZmyK4b0e9~=IDGP-oxgU^W%S%rSyvD}g5Z4!F`uy4ZG6At2@d|@|U@Jbq>sHk?^-qwY}^pb<*2o3YLH-zb7 zN~psda_#f|>k2KgI4w^uKLrI0V-ui+yr?0z9n^>$8De4(TPK%v!%(qo$G^U@j5zL# zWd7=183g6DX(4l9(m$;A6Wmx>WK#%gvG6x;jl{GSDOT z9)1!Or$MA|^Kxj%-F9dHq4H4$BVg8UbC(j2vBySii11eL@{NbNJZs*P+x)Pb6Piji zF5TX@%i>t-2ER336vkOHy0vwBY92-h9qFpeFu7X1&3FFNWiPgN3zljumSZ6gfH?c@ z>o?lm*8wqo+hB@%B^Ru($k$UlD4P-#P~ZUJ`s;R1b81gjhm@jwe5r{@?d6?8Du_$a zW|0b54n>vH7E+S_SoaHRdPJmfSY_O!kgM0>ETH{uhr;nLY3WDzyIG#j6N@dCPi!^w|szjyi(|JuNF*lC-b`+UWw zFukWsOE#ieQYEIbTwNVyGDL+oVjbP0J3WMmQewtTv63FRbNR?sOvI49WnYI5xfXd*uDedH_JRiH}ev`4dxT>!Jm@3 z2tQDR=^cy1`;9_Y&npuisI-s?b4o1%7~4BAFJG)n-D$6PH64+G84bY`d%XGV>xdly zxcl@hlc{V(V-N-duI1yibUiXm7v$KQ8-xt4p%MZsm4q3g6tsx|v)O8(>OFf|#Ju|s zoBtSNu@>Uv%^xpmnUbU}xFTz;sD4D}8jEl-$7co1am(OVNNHPhImwuB2)v|XCyE*% z5mh|Ts=R$V*K}xLNE0PNYn?tzfKBDnhX~_Y;5F9GS9Qf zyEbwg&~C=jYl?2Z7IH6Z*@W2LG}iYr4Jo#36e|(HxL|?1NC@eWN<~bQ1fF%S@>G6I z;|p|SfQNMY6BtE@o}|2eN1I(;1v4`_vEj|Gg{EQxgZq~rm# z?At9VCirO;V=hP}bjNf|`F7p!m4X?JkHaD(P}7O9w;=6=ZlgR16Abi?Wo$U#6uEPYH+;kj6GW^1U^6dupO~|&Gfz0(Zu>Zk$ZHw}YBCKbV$7kHOZ&FC~DE-Z!9j^NcAveJvB%dVZoGKj3Piy{UGktxb(6^RG75#@Bk)1xi<;)V9m({do^H=Lcb z?-?8nTCzgG`05cFIr(S~v>y7po+YzK!+TR8{5;$9nz9i08k{_h>ZogmZ@3&3G9HSh zZWE1j6Ze`YQrZuLLq)-4^jXl8m3~&n9yoV*QxM<8;@{c!y_%)Cn3gk%`JX9m@}|)W;7OkCJ-p+n(MUW5XvO^0&PVwNsAUMRt*b zHp1T|x)bl55`q8p(B`}@szdEort7rJk=G}Kol?g1O&sCD_%v>-rdPnF*L*Q6bMG$2I~o>mY}WTvGtq*Qkxj!gKvw4FzKd)ghZ5_ylL;?91VK(0Bi*@@hCOp z3#YrsYihWB^|J+u8-I{%U)%12zC{vmFhUl@q~QB`zO2peB-oLqlMOa2SzNt<|F)Jj z@*Phjn0Nmi3h-#CdyUJrFtSjUtM2SfTYq_EdOqBdJDl)V6)=xZT4dW?wovOdi~rsF zO@_&;D>R8lUE04i!zq)obzM2}XA=q(gY}W8!{BZD;Yu`!IUFglR~AJJn#TE>EiUAL zd=%dZt=`Dt@n}F3v?DQ4ilB@n*-V)BZ*oRU>ahnZ|F_A{Z52i-Tt zhG(f~*h#ff@9Sx)P^{MrJNR7W^Hbnn(9Me;czg3^L++SzUDc zVB5#-{pgYn%(!QL)0wC?)gPBFjBxR)#=X!riZ$}YD9_zl$BT2F)}pt(?aDVvO?843jD4Rez$&CPODcAq7}H??I921i4&yvS{cpf+YHL>>r0@_Fe|zHNqHObf;5N9f-< zaQEVgFlU^%l80hNTmLeRSbwI8hmz|GxB9*-7ij8k>YSE)2^F1ZJuXvVugcYV`uZ$)#ng<>KJk8PjnKPeMJqZ9t?{5SDsiLPa38(pmNF|H|S9Gl3 zW;rWXh=AHfw`F)0C?(Xhxr2Cfc?(VL$dERqAoIkMl)w5>Jnwgld&fSDO`Y-;C zzRI3lPdeHSr^PP=UoelOhE0Sgbn>C!=ElRXbL}a)9`V1+St$lLJR^uz8FEHmeU!{! zU-dTez`}G9+G7k_N~Xlz zM8;sKR!Vgd%XUxpaUngSvSzCX?wFn}TQlO8A3}@~ntt}}fxoqjE0$AYv+Ec}diX{e zItxpC=Id-t@9QEzSs0k>)eqm zT;(H5StmVR)9>>uY3TWQyt_-&;6_x;GNnIec`GpF2RpP?`%e>Uu94UCu5kVC+nJab zoX`crbgJa1`FiEG~>&;WDrQf=d!{n}S8H)+oN z%<1Y5RxQWyW&-0VUAUP>EQ0iJQHi7gJ$ zG#;>Y8S=2JYw5WSR*smrU{kE2j63441Kf~sJK^J7;Ha*;cDB3!ImSoq_kdeY2DLpF zM~4%8K|NRg`ydyfM#zh=Ey5qB2@xYmfXIyB=xb*dCEaOSwrm=iR)A#OJUu|@h0VfG z@6_Y7hUQPZ#8rK^y1Iuwu|-n>jt#hz>EVh|G7Dw|n;5uMl)&Yb)re93kyOtBs?{6q z+~TJF3_b104SRmM35mb4!a))$8T@6A*T7A_1_P-?s2y=rFZ>`uJ5&8vDI`$VLB)S~ z=k~q~zG6RRni9?l7YY|SSRN-0oPwRH4>`DHk!Uw434Le+Me*QR8b^*yhmDCNa<`28 ztMS`66_a&Cp0Y~Cw9k8(ztb{c@W6|Z6BvJ#*Z!Iy3Ox?2E#=*6rFb%(+^9(#5C}WsACBX(Z7H4ae(Gg8iDZ=3RJiky?ak;TLA(xRni;8(eU8HmC=UykpW2HmP#ryCdZYmm@+ankV_APdNqqIqbDwL!49)O%r)7eR9p!qcu9w&T!>Gq(TWaHeA zYh%-f1)Yx20V_{7TnW1jeZap2SCz_SQ|s*p(LG?M(MJbfFnlVWs?jxkUt%#hxUE|H z74tJ3>cTL#*iww_2y}QSxEuIZQFq^G|Lb7^0@#SHUB|~3W^50GdKchyYc-5f|?{)q)53k9f$nx2Zg+k~5b!DGyY z-mhwau8BuCYEw$olruDZ+gnZRDLMdT1C;t%j2wgxZ8%A*Tnn z?UXyxNO&Chzbq|y1e{P8RpA2=M|Mgy?JBbPUXtK@YB_~Ef*@May8WfFGTkrW&odZn zcN({4YcFwlvR3|DFP|kTOL&mxDV=ohB*cn;?6WwuV)g&V%57PY9=ULL@$jK)Vm zFr>KF=QYV4z;#nby4%<=ETSdv5Hf3VksEUO^>KSk0$QLmBBl47-?$&omXZ&%%H^>A z)cU3WkWz%ih2`TU{VdZ~t_K7%!CHRY8ip9C0SlyPK7|B>?SsKVbZ-n9>2`q%J)e$p z>L0twk{#CM^*A_oSEJ*Df%PDJMOF926x&qKBHlF8z2hmb&TzuITgmS-gDHE;F^)in zNi|9bHlS7$kJ4-_I#v)zgY7F-FVpt57};4-$=$GuMvG4#ejE{XGCZs_{iig z{F(v)EW0bO-@}sxc&7WjV0!J@9uuNKsJn_S;Brtq-%^oP%geOHF>51pcMPNb$_QlswV zx6>CAx^;(#Y$!s<4g)7S`NP6cfTWM!=sWy^!|Dx*y^=VEFY|3$a{tP>m}`kta-MbA zrjSrH9bbmswL0s3@RPIBsce=NMwY_K{rDGCS1CLwxT)zC!;tm7qGzebhO1p@v?yqg zxa!*m=1lWq6mE;73p(kVUGl1n4HVu(foTOUm8jx*Sp*@fm< z?YjS@u$q5g`S;A~erfi##6E&5z%1k-rAnoAShywe_R-| zcR!6gmq51nbFx;Rx=@3U7d@eu6C2zl9v@ksarb9yNCm4wVPqf{9S~I`OL9Mj;<_xn zaMDcFB(Fh&kBsm!@^PJ6SlUV@yM9M=_|E1}nnz{p;tlp6r6>RkQGNgBlwnS-UCH!B z)T^G?A=%2wwCD4pBsO&V3j(}8e-3?#T%H@1y+`q*A-G={`Cyz*M9qS?E9W`qq$zsK zXeh^@*}Yplg@Y{NQ-fp9y0j=3Q}U@uhR6q6$w`a-V&(%|IIHK=)mj%QlQ#1qm!YJqV(172$UENlYU+7o4MZ9%q23?a9k6MB( zhj$WmjNm_iL`3oHikontp4I~njNg2B8&O_5Dh3gt5wx(AEDRn#i;wyq-wb(oDIGBm zWHqNX>lbLz;BzNAg_gKTkn4GlP+woCEZn9qNEMGX!_oXUGv3&7F0fULUgU)L#vHCZ zRG!xkDHG4_bZ<)Lemg`tc{L$#@4Pg3*&8vFI&8e|8$szzoZ zxF_^^KeuRqZNXMP`(n6X&cyXB&U+;R?ca!{@2AQ@;x22GW3IoVA2taDp}jKz@L0S@J%3&X_ed}^GgEiIkl=PG((P+PTXja?!udRq?j{qjhVxSI zG&0(fyJx^Ck??#?ixTPCVmaVleJzip6Z&xG@s-oaZh(5cbQRd28tl24Fp?A0KjYBl zSb(zt@nW;3WS5;6!7q@qU+5^Vw6`OGf&vW<$(OZ9+%+tsvw*=8-1pt_0V(r=hqL-o=Nbu0Ky71%vO`skKOo5e^Nvi(GS@hZDR_NYT zj~ITz*&6%&Per}F$YNU!sXPO|EWIzw)ui2v8k?5K^lQ$sbJF)ja&42ktUp9K*4|cmu}{`N;nMbMs0k<$){u zfxlP{VNoJsU9)YS*?{LEVne^Q!hY+yA`pWBk$~`2fKE}VsoK^;QyBt`t|)tXrZ{2A z!@*azUXa~jt=Nx1iLQT!c-1mhUe_MCs;mWoA z6KpoBajfl>D9iT7;82Gqm~175@P_B_Yy5$r-#}M_D#BNxuOP2?~lEq0)m%D?n@*@$b_4S>Wq5Wxu)| zc4(IVm@_M0b=HNewsxFmO<=wBq+9a_QPsq^LbhSX?l?j`T9b4Bk8yGth-LGM7P-5# zfs2=%T^Dmw?$eu4io%~-bx(iUT2Z5=gJaCO`N!_7#6y#p z3kl}w+-5=dxPMG3(z)!y--;xq6! zagqpY>SNT~IPacTn~F+m(9x!{d3!iD78LAlsY?P|TuID8;*v&u;e|o`ogeKZA3Vy1 z2lQgfwd34iH2%jBdXE9H;p0^2s~!oG`-5Zew%_fwZ+uXS=t+IlavOssxc>MyY<`5T zXKRKuW_JXCTU{$)3OAj$rR?igR9J)Rk3w@X{QPhxIQtqyMm@b#NsL<^4M&p$Yj&CB z-_KzioNb?MY@UnnWqoVzi3g}{@<>j`_M>k~LO`r%brmHujuaC&Elnw%byS3~oS2YA zEtm6ZTh|fwiXP!)Ld$LhFULc|M#a&$``NIdDdfyYI$A8W)9e0x{&x=52|k@W7hY_B zY^Vjdl{=S4Qk(RKhyj` z?CG=L#6g(0pvZl>Lh6a4*TR=XIX8vyd=kWL=dQFJZb|jx^{`-vUxXk1_>YdiEY3_?uKc zuJB^&MDAhS4XEND9_{*iGcEO$;#x$%f$=Yg_g790wW|2J1Rpw3j(=17D`Rig3udTg z<0`tA#V5?rtv6Pzg?OSXTn=8B^n2i9^HYFEt&qn@3+s_F_)8Mhu5ot-H zw14xc;7veEmK+61(*VftNUia^7Lds!}wMCc+z%+)_MRUae5!t#gE>N%Lp zI!{#689~NI|G>0!`9<^Y33vj0UyMv$iaZU85@~IPmWy) zty)j!qGuU(?IAyH#Iu(0Q>N}>1MzAt$bd_=am>1V$j`{WQ+VOQGiNB)KsqFU zp;nhgKhaHfiG?dB%Q;SfYw#KaMKQ3)NA!a=nC;CKW84)(%O&G@O-?m}*wcTnVA6Qm zdeJhub2C+(QvY_FVdiyW|0wg;ef&t(aA3$YX`ecB&W(O}x%=~GTZSDs6g;E|ST)WM zkTO7)7`M|OdTkkv^k%`A<`UnaGnSEvV>vuk_h!z4JCFJ27`-nv-1P4&LzrU8pv(`& zU$;Yil@q1uyk+aXb~GndQT-`=!ITy@t?9#v(r~hwi_Vm!i@_MC3yXf|qX$J{l;)a? zV*T}1D|yj1^YZbsw_F^Q^&HaJadQA7^YGFaic@KaLpsyRo`_9;^O?V=H6BDPhu6_< z#A755c_Shmvp^_`<}k)3215$0x`fDMQ-3N>8eFBDj&wg%X@S8tQ^x6S|c2;l+bRw z&eF%2!Xi*qPnUp1cGMq?$m#J*>@SzGJ&-SmhXq>H*m6gVh#|xror9>o9*TBQ5g8mS@x%j>BJ`78)w-t<2j_r$AE;a0k z2N|)%m6i`rbyB~7#m_o$m_yzaDI$E`iU(`FdbUP>?%QR8*tk3WOe4ziMF@21iY1=O4octck#OSSHPjHYMTVJeRMOKg7$FqQMZ?I5g1xgg zKFXEQRn<~VOJ$vY&fR~gC*Q-rpZJ?En2CHwfb!vOB>X4(FvgXy72xsmajNNFWHQZO z+4V$BfOO2VaT#68*iT7hfx{qz3U%5TaiBJ zI#eweclnfGz5Y-b*df6Ba!VlU%=fBTE9+9U|ITuX_LhcPR#)^_oE^hM`%{0HZ_N&w z^VRs*To$#7&tL*BWw^xp8iR$A-GJwQ&d;hIchPpm&xt94G7|0|Za8N_;zWCmP@a`e z{zM^0cPRLLQh=V*vo$Wq`%gi1VQIyil zr%*c06zFW`QE86s_HlSkl%mkbF)vIf&Tk_Wwlpm3FmmLz;+bg^bwp1rLdTHtC_fXq zb2pT#@yjB*=2OWxNun*dz4igMpw_lBysBabo$$CN&Em63fU&;G&#B+IPNmNw<&#?^ ziAO%4=;cGKJtcZho+mNn-v>Zn=fo8fC?ZgE627w9lW3R})ilrdxNkLo#DQ9ONPlis zh)dAYwO)H;#%M$ExX#_iKAG*aAAYw_%f0~QbX@PHL~n3sGJhPgVgEuP@pt&zwD*mnB-^$YI&|rIzc@fsUqOUtnoILp=ZkX(>Qh78xNhLN&#Bx%z`!3lEuIT-WCF z7plR$4R;CyCvJbGZxiEa7;zdNzbQYK;qn$gd)}>x)S{uOb_J{*?g*VujXAff;WoZP z!Q57(BGQP`ZBkwY-U%t>k}1}91ayM&eyUJ*rrJ165bSapDj$tgjjU9G)(DlIQ@asp z9~75F?Fa1Z8+ZDo^Ee(|mem!|XW64o;dn1xX?MJ8bn#ygYD~%1s{#e#M4X#5er;mg z{VEXDD$Ky4$TZji*=u|MQFqgF(LEWcJ2S(kO$P~BE?c-Q_WM7JFQA|9z ztSLLxk~V-4?k#gpbL04zGEkOf=_~(wM+qQr z#S$|;j|SAgABwZv(h^Rs{q2FM>}k231vrV4nVuvd0Q;;c#7n)t1)PU^+Yq2!=Loit z?|Wl7VjIy9y5n--Ou7tlrM&G`0U^v@f>~y`}4na=bb4Z#^V8&u_~N5v#Rv) zm*N8H63n(ya!fn7&0n zz;M-VRfb$((xqFWtr}6}9jCa=t*f9PR%%4bP>z%Al{lkLeXVMs>DaY#3MBOSg8AoK zXT=e7;>A;=6n!1w;>G`Zx5v%g0bxU0_3SbG`C3bl$X@~b)_*jnF@;?#?h3vu$IV+R zi(izjTRvULmJCJ~$T_PL^(kxMA&-HLPxRgCzvSAz>bqJSOj3SM+&p41?6O6)^id{Z ze#YNAI|$a=nz~-M|2Uy~DfGxp8uj(7>r_}V%k|hr3S=hYMzcSxfNM9HD`P3p;dxp1 zI@M7ktJ6F}5mOtt%`jlR(1MbXIHV9*WMPvQJ;>hk7CM}K% z09AUdUJG}R^Y78sgb0&CuZa?GVM`KG%-U=Py52ckt#NPHBYF+ zznAHNKqF8B9B%y$BPc)_<<6KoBNjZ;ut(UnLCMG$UH4Mv;R)`v_||T_kwvNwbNBUh z6juom@vH#Xe8pc0`Bi!S_f^v04sfhH_vz@#T|YN3ePMff#_y`=+M`1tUI1^M6s3eD zRa=434>fY;1sPsKrM&MZqxoCQxOrd?4EwJGAp2)fbmaU-&B4o%r zcl?6b?sa(3l=LknAnvvA2TNGJlF3*?7 zgWEi!>v{Kf<126W7G3520vziW4Gc*~=tMOX=f5vcZpO_kejys>YiJb%Ox>Y4Yq;E* zuVAGh$jU5hJyT;vS(F!>Ne$*#9wmxKCnen;3ouY9n-<^L2N#1glTb{6ipGi9O4F30 zUAf9w%sbtpNqnotBU;JG#V#gyOm$ezHJK0I^I|YHP9;%-=~It&eD=Jphw+GD4GA0^ z6#hF&HOKSm8)|Z$>&}r~o7NxO3@t3Lr^#pG(*o4Fwk%E{JWsSod2oLD_W9H&L5{czEgf9QYx!qus0VNvz^y4gcATyCyxM##ls z*-}omokU0o{L+UP7S75EfovJ~Y@}H(Frc3*AGUMAKrCtD5U~%mH@e4RG-+H&Al{^a zYQ)Fr3l%oM6H|cDVl7Jea`|Y7`40T0}RnuXzRu78G(KI4LS^y z>vg2)VXD_xe$5Lf%TxH8`A2X?+ zWUy>_)n8oSw0LsTwFD<;PqOQ-$7t)W#EV6#&ud*zY_lyYY1znuMZzQL8kVSBH&M8^ zI;~%Oh^S2Xk`V$iAVXDCYLIBi@I&CsCx5JN{b|rvkJ-)6o)6f55A!U5e1J16#m^80 z9h>421Tg7#l7<@`{jlQxC`Geq03`p6w4kLfq|bm zjaJ#pn_2#d4W{fo9ln)e;3a2mpQnE!dR@i$GV*fVrJ+e;8VL-eoVk$@en$NMm@U8o z3Y%MO#z_2@0TEsc`+|uq%SeHwprAPth{T*U+!qLOT=a5H*(7!S0ZiE-J|U=X+^Sy< zI~AkVT!g|8JN=bL?a_!*BP^^YQIg9{x*>CVlpoNc0W!NvTEay^)3@qFbVdZk+bsXk z)BQXVOWG58gshsf@5Z6AqDOgo$02iKINdP54+g~Z^f7Mr$9 z(V3NK5(}cf@L%C*{uJRHG6Qngc~IRnr;?nU5QlPecU6n>;;RxlH?j?*RIYRvj8cGv zD&4E>{F!wdy_8&D5p*UrOxcvN#$#V4>!G#e(RDCv8C`u3(JWsK>RMpY!m~T~)yQNd z6W*LE6xP`LzLOZZE-8g@>T3oAuu6K+s?A)iKQwYm3S3C6It%)=G*==^e zn^V!E0;2YOUH))g`OM$>sXIIJAevBQ8x_O-g_;lqBzKG*Z?|P0@4gPqx1)t14HKW> zGI5g3&c*h=tDXlG`Dg}hOwvi5J93|EMy`^oqJ^4bbfy`4*&V;f{T5Mv8}aKyiz=Xf zdJ?i9RUxQq6Zn2*f1H*p4&L?9K83i`cSvTHL?F}{ z`wb|*9{v#)^Cn-!+V%F4{A295JYOTD{qlLnh5F=`@Qm5(FZ&QgeJJ;K4Rt4=DQo-9M(8~(h$-?gcL6tH#Ct&h``&j5#R9a zq3At9DHk)9aD%z4>E`EoB$lYWrqgQiqE}M;76VJ^SE~4^#!RO5PCLRty*wI^gwm^Y zszR&_YW++Ab&hb1?-WI=P-8r&A|W!C=}U!a{sMH>hk_J#oama@d?k1^{rnB-w;q$! zwIf#r*WxUv`b>6MfQ0H9(&97R>Plz@frMJ;n|2dVPEV~1S7uPX8E?qLnO28z zCGKmNO<8;TVZW-_;V+8yV~3Sh7V~spDC(=ghJVpp@Ae!Ss$9>5>F(c}T+L9Vo(B?W zzk72QliQUl@jDl?_jJsbym}5n=o7ioCOSS$VK^37ZzeZ~P@S%pOR2n|Rk9=xb{?Jc z>o|^TwC+Zqzk(Vna7=(;6>kVSR?$@WeaKCzE9&X$tn$Mc{j}-)u9Ma&ye*4e9BllE z>lS8$MBW0VX0Qw@;2-fPgX&wGSR)b#dg;K{ukZndv-tV{_?zo7Zx5+hFh#PaiQn!v zGU=FTI!)sP=i;U?qmT$@@Jx3R%I6621*M|8 z9_|cRF6=Zn6_I)dLUkSXY(6fhp*bnbf6M25VerO9grYvrX`GdL^L*ptYQ+qxL?yw^ zdHBH1rO4a(eDOi_b%iRmlTwgnaiy&{3B+5*H9XDt@V|&2&N~$RNIss^dX^-7N8dDd z-|COKnjnr$rz-xochG9A&FUbYa_9lwfPtG8yJz1TN*-Z6;|(3_ir(2ZS^*A%)Ge(P z%}1e{UN7g3`{3i1iYgK|yN$%}va^db(YLE3;VZ9jH0PxIcCN_MU9xu+8h-p(K7}Vp z5eDdj>br+iKU;DB+)PI{NG!>&H?-Cdn@@!Rb+I`2P5RqiC`nCNo=Pd=68_|NlI9xA zP!V)}Glzcx=k^#+2eBiZck(YW*}xgcl;t;7sOL+TS?=dAk7p}~0x$_Mu#(WK6rI?` zsr$&DYXa{5SbkU=iThXKYQ{Yv%Il3qi8^YzP4^Y&+?L!E9)e|Eib_=-jURgarDQs6 zyvtZgOnc!I*8Bh<*AIq&{P~hs1C%tp&S@y?D5r`CCQA4ZC@chXJgi_|eUKoOkD5pb zaVHNKmpVErTF+C=79aUYOn?bJdJysD$#mx~D3Q7|O%*Qt^XTO$;w+}NI7b@hksqIn zKCQG*CZHyo^nAs78QCngy9#Hyz7A(C7L4C3;O1RRLKCYsJa{0Q{HEd>$D2*JDP6LQ zJ3VVY=m^{+6zCQ-$k0nQD=V({H-&=g5VJhNqSCw>)4Q0Bg-IHgx+Q5Z+lNv!S7s=y z;K!lYwbE-4XOzYR`9UoGO&(TpaG6F%Gj1gcITE5QolFqkAK5U+j_G1_Oo`B=FWE3a zM|R*RR`^iQo~++%WE4_tek?i1i%G16@D}ii!+G2E{82K0F>o{;;H+X)?j$k+l1Fy1 zzb+{06FV7#Wsr}{ZCiSa{VU`HsS^ytnz=%$Cv|mnaC(i73(QX8G1XpK-AOf06^b#{ zqf(Q127rzp=mCr};U(pO_-9+rB(!6Dm-Q+r=yC}P=YbfwKc{?IDi7tk)AH>xxGcIq zNagiqh49$X`L766_^0%q!(NElofPrxq@?l{3W*>vMVdz6Rjz_o-XWfKuS_Ksx4C{r z>zKf-adzej%XYg-5g7T(ATo#MPNjH~N~f?@w5b0xH7{_`tOL(2o#d0gQi&E|T(YU1mz^-7_{N4S8)ILcFb~I1klq4d zS#RYqq(bI?NsgQMxtcH)a=5t7LRU7v5|^X>S;HPPF;8#)JDV=}0~U_HtZOm!`5#aT zuXxK5DMQdR=>hkzs2Q!TUZFI=STK`al(6eJyy6cxGqO z{B=ynyQ(;?ri99Pj>sGapVJ`kP*^qP6@N*pu<{5MAhpBKg)ph%Hbakg|hnff&I zZxiQf;<)z~V=jr1Yk{h`J>m6mrmt~weqKb-g^`J}NN#9PgGshn0k{KLkPXMvu<=C# zxYWaY`b$hGUN>J(iw6@us@0hsrA!PMPq9h%Uo}&>68^O`s(D5)!q@Pqy=gV}L02Fo zao^uTUVTt@)8eTY;xepCc{91ZGg=sIU2&|^h_fwqR)AkEtvjVo z6h-H0tHQXkGoW=1aqmVvX&78G-cyR6{`W8SOW^Fp(H-4s6{_Y<-!>w>2pk;a>(d52 zv$HO3c(_SB)v`{$?E8g(2hViSvIw6`Y;0jA3*wkS(;XC@CjtY1EJYllFJO)=B_kqg z>*L=RFjdQ*iN77@lUSDgqZmcY=zLxJQQkfakYtdvYaC>P#d4L{r{K;h;fev352TGN)#{g|pdO+&r{pSn^j_?c1oHE^e0i5gGsa@=~)?Bg40BTeb zTDI(x$TJpIR0V|@mWj>Z!Pi^?WROO%0!I*TFDfrMX^F%PkHy$V6Z0=1p^geh5|~B6_XuIF&g)78F#Gk$b9&XE$d&qnII;W zi`(lK^_&%9BnX}@0-;7jD%&5jQST73)1)QAYtFupt**nrQUjpEDwIyW@Gmj6#RGL}X+0?d}P)tzHnieRN7UIa+2v z`Qq!5m<^pc%bneV1z2>6AseaM{y5hsX%s)S3SfxpyaO0aXohUk$p@XtZ45`T)XAVL z6cNg4#kEve@f@$x?2vk_`o@9nqqbRB3yEDjVNL{PPt?>D1&82<^l=NO9>~NqO0|f) zF}mr~{vnc+$od_}*4z;Ks$^6yNmN}77K{n-9w};2t>jIyqUbfaaAi_%J=Oz=aCjjT zYF9sBDnI@5W2Nr+xhbVrAdcFX`1fE#&B1vbd|ZgG)g(~fx(3&d$o2JnDi1unU}E=nMvieLcklRlzmd}LN_LWyfuLu%2lzz&kgS8 z<|r@E-?inkCrHMs>Pq*lj_&q_!j(PTXj#VYORV1Pv!v)S{oYv8bG2o;Jq2Xby`M~y zcMrhjS7ja=9iNv*hE)38482xMmfVBWf`6C_krvNQbP38VT*jZKKM=CTz<_>Yc-I`N z7S58#H-Cnt(fmGv5f|3eyy!zu2fo#*XvY_Ern$gKjw^6r&G-ovhk3>fo$M1_EC%D} zP+xh)5(kuGX}M~okIm_Ns@5I>DWD#Tdr0VshNfxTt?$zX0m0psP|wDO&xw0idmrGe zJE|TfBLWfgwz^AOgAjd=nD*XW&3gFBiDt45ch5VTNmv{Z5pyqSXo!=1v}~^n*XLYc-3uXf-}DgU5ca^ zOs+EW9h{VHhW;oJCM_<=y65K|CVMwxK3;`HKoGES1r>BNuz8t=R-o!c=B%2ix~Xuj z6j3gJqs>1XpJt+9{jl(k-zqYyEKtU6(FT3M4q+Fp9|7vjC0x7d<@l-9jp2B z>2bd<%+dZbvJH2#8@%5CHd1?tJt#sCEH^L8Ke(XFqYqM0g#$m`h4zdCHba-&G*BA| zvm*wC=;9L9mAc|gVZ1H)y{^*xg$^=b<7yXHrftCHA3HNM$O3Kb+UU`7QJ+w-w24fZ zmwm!r5$4osmiv+R^Qq=-$fi0%IGfPBqe4PbG%|$Qb}x_1bR;4tDI*iUEpL`l<>UW{ zpwdFTC^{wmrA+Z|11~}_LQyEHPP^`jn($Dwa11IdGpn>J zmgOTuccyS=qbq`Y^g9|H>j7L0_x$rN$top!RQ|`quc^@}1?}foEkB3jrPpIKW~8U0uYL8SBAv$fr(?dXK@jF#NN|FB4+gO=a}n*w=~bm z3+k7}JoZshdGNkFP%LZ;(VYJ0{7ZPMYMF&uatzf@ScDYD&Kz_MUTaWt0E08ue6WrL z0m>Ghb*EO|(a&?82_lWp<~1M2e}ta%3#7tq`vJEW4$9&eZ`_(g*MQ$1L9$kzTz9RM z_1B!bItHqa>8wc{E^bqwDR$c8@U)Nl^55X>5TSY=KP#S0KORie?aXYxF5{o4;oups zEMq^7Sb%XYY`#eqvV8$e%Bkj&E$&qWlQwBUOoCC9z`!rl9Y7x?e)K<1#ss$lh=G!WR2W1{@IKqXo;(S^Z4WN&_!Vwjc4C^n;RfOjMKMo)VSZt z1BQLA4IQNkM?^w%VBz4t>p@6YpVOfE0>~pLi%aj*%$9@+v%$P#-o_ItN$`mL)zyn= znHotYL%^+F`k0lbgj%?9d8;#<@I%yPRi~HAa(qeP={8W> zU4UND=H|HVAY)GEAR1U2I}WI%{kzb%oW(J!*6EXzySR_6JIAsb8Zkvfu0JKG@m}`) zl^G6fTuT11=GRyc+^5fw6>@a@d<=xhvY_a*_C>P!B!H!L)Axb9>fHP3Z8Dm5Nx@5r zfUCrXwn>lRC$q4vszme<<$yOgw#>uv?yXlLc181P7jrYPrA7d z!>3FSlwDAEc44XJn(Wt9rYH1mH2?2{V?*}dZ8~-;=@Gs>=9;lPQpNy^l&R_I#v)5z z3h$ri9q=09L<{8gSxsSa*@StmSgb|4z_W?O)6lZ=C1QREJ4Z4ra=wbG}8nY(b*`92;Qh+I(Gt!NoIs zR7FIc=YN^sGW_jcAmrrg7p@yIGUbXrQ678^i3df7SJ6cqd2BfH+g}_JI<^>F4F5#s z$DYnpS}1y;f|$cA{#`tE!Os1FOJ>5QBN!mYb z6_~G?A*-O$u`H`9>saQ(h;N8bq^sK5poCBzZ0>Qb++aPXH z@^|#Ygwplv?TaH{dpb-mU7`X!!Uq;@Z3@^L) zRF6x|5>&6x{|?wyJ8M<#F)oH<$!Cffui+hr>c-^=qdopxkAM2m>gUud63zXa_xl`Y zIo1L`M&kA~C*vjL=^|Opov#zj`Yv^^ver>Q%;EDM*3zY-^E?NKq(g@0*Y58W?3OKY zU)DENrdJ$-ON1n>`WX2!*ghEVt=fcK@*FS@(=TtJz(PAxz^Px~o}CETBS~EEUw@1F zsRa}2_Wpl(Z)v?RNX9KCTOJaHf980-Y@alnaV5zSh86Y)M;up#*4Au;M31N`oYm2} zxQ}!e9oSPX9=2fXUiVcJH~SG#Hf58KO~%ii5*rReV_O#!rJDiNyuw9v6o_*2mE^LB__nr=QB_?y&X$k%_ z0B}%1{h?;}qYzl^9mKGE%@;ME6*zCmyPn%_Ur|)=dN&|@MRbIos425$cq9O%tLH;P zx#QIE)pz@@{z#Es9u6OsQ3h1hG`&XBMugHYuS?UAG}OFq@9+qeH(y_eWNAn|OBIb2 zCs^`n8_DJqdv`2Qpxsa)=O&=O7Od{y6EU}BqJmRng|Pb_O^e5tMUCx=?7-zcG=*Ud z+U$1=A4O>q^IAJyS^HFq_kg5w5;e~RgtRK(#Ls?m>YN=mxx=McQ)PcWO=k>T3?s<~ ztoUs|E9oe@@eSTs&bg;%ybb(jHxchO{^YxXQ1(5GgO*m5j{%Xg<{nUD387xz* zT>Pbi{{JVFn~Y^8HE>J~GBQN>Ct;`y{Qevwm6y$Tb3LRwrfJl{)DwtIuI9P>U1Np5 zf~jmKK${ecJ{4&55O=g>d-u5zTdvOZ2m1QE>kj&qAo%>UhO`v#-&AVuXx9#mWx6Qe z4xBlMwiyRFH%1S#L`{cq0)USS$dPb|breh5P?5I}1V!Lpo%m{Vt-rTg2;~Jruizr3j57Qku7c1Y z|G96BV}{X>rTx~+$&r#_`+x8o7S*cb(zMB72J_G4A}zaOJ|WP7E=;oT207nH4%XPz z<+m+}nljhR==>L#^STG?4{Uw%HOV!%KQ$ZH4T*~RjfyGFWu*a>x^D4~W89l70K`9*#OC*3_#Pa_+k~ zN6=X4{*$<(K&yL0=;~Et&7t{~8{yr=FWB4K@+-(RJYL!lJ|MziA>3q2dH%B8Z4;|@ z_l+||QP0$%#9+DMci0j2;wn4c8oTH)d+=|>P8tXlx+R4-y{t+lT|osL2z-C9t9UJh zLf6yZIsIFy2Rxon?Cm!yWJnoqpq1}~nFv$2lc&rQi<4k!mK)K}DVPB_*``LS1 zzA}d5iyxM%XPGyP+d3!FoDx|uxOUqTAm)f?VofmFo1Fs;+KNJg=6O>Z0K}wHBego9MZ_% zzH(2?u9@A8qMCl~^`WnR(HB;Ov6ZTEj)ej@vD9T^LcDz1wvArP7oBOu(j_AUt6c>3 z6A|puVD>S$hj%EO{YWw_f@qzB>|jM2f@SEiZcZv~r=hwny&CiEIEaRdF`AUR)9l7w z=hY0})C87;jr1LQ^9sWmp&1D0<8l(P+0eb*FEq=WZjW3c|FC)Zq2>7!Lw?{=LG4r~ zV?g6+x*9MUW5}0~uRoeDc3|ju*?78i8FEw@Xg>5bhMNh?Qtdx)Ro1Ujo{gl@VLmEed-o2!J@~ER-nWn?d_s_cna6;S_dyo@AoU5L> zJ8g5KIeGYvY!Ap~{uae>k)vJ8Z^7Nwyf}e$J%j((($k{CLvMw{<5#Na$H_Hs@4I)Z zb91vaXwntC3~Z>r8G-aWNn z(SNfyM78qXxl>j0kq9}f&&}uzRBi%dWi9G+MY3#=7op~vBl`;39B(5H&qgl>fqXa(SeOkR5D+K zow8q2ZW`tCak5k@-$qdIbwz+}EJMOvV|(4YR*g+Fus**9RUMQf7OA;h3FrYRmzAyU=+I{}K_yz3*Oy&C|pbq*o* zkStw}u`(}_UC;cXk5BVtEv->F-q~!MzYl`lE~w+S{)8Pllim37y$BZ;f9B#7!pp&S zphMvrF9u;t32WF0<?fGLX)v8Q!J;y5ZX}2Z$V&9oThKKa4j;Qn%|OL0TsW`U26)2t zor?!A9aVyLMdw@jg5g5qh?Y{@cxm6xv1b54P`IVQb{w2PGf(auv&4UBlEbQPFSb$XAx}9HnlJNBBc-OxS2by%GoZW7f#c!|Zp;y9xpBM@}9NXNTh% z42*Y8U)HH$O#4g8Ji^|y2B+H^eKWpJaeHWQK>TtaqCL*W>lR0+5N*vIYaTe>n}p}l z5PmAH0;bhs*Rq{-^w*qLh3x6Mk&iQsU5Jy$&ytlf(TxG$WCVbegQTtiZ$b<`Z&gx9w!Uv;eQT!Gzsp+>#F1ct!>zj zxv(-4gqOmam0(XwUr{LhD4?rC%L}fR?c6f&HC!KK1Mb@>3$TDE)=Tt_+@mZb9+pQC z1w+E~lwBoB$J56{wt~K&F*-K4R?R9iEG#UC*X0#7k8`;l@$Bh+B9Q}*GPQI6U>>WB zFx0Nj@HX;j)6CijT`XeAjc|lcY3imYz|dW8un@DiARkSyCC#YKb_5AKwNPZBj&z%30l#*ZX17FNMc9Bfl1_A z8PBgt=gZq%prZ`K0PS?qm{mT&&IOIg0n)bE5 zj;G@ZlI@^4i+cU$E0``@avo%kfc(A8FglUQKaG_DgQlFJ@;411uFv+C3K5}ekX2{ zC7EhUNkq7KA~g8h0O-yMAWB6HcpkXvlh1)D@GhE9%^A}<$27*+m8AU zoY(x`Kso0}ryw%jkjgZz$;hcIuuJxsm#Bu!+6>GSo>0kT9y~bkHCSdzK(Lsz5yZp{ zs#kkiyCUQ`+WLL7XQrT)YiDXae>YC3FwEPqwSB6G{6 zr64z>W(Dme1Icb3;xwzmFm5+J!o5)@O2Y&h+{tfI49xejtpkqA!*OQDULJ=v#d z>J=sG%`Xn=rgpy#E9ZUQ@%S*?}nD(8SY>%dYuyMxPOU%IMg#L2Q1}>d& z-@~;JnV70D)tn7NBd@^?LLGW=tJB|CVN*jjNpjr}E+{8|K5EmTslij%vX6Y{*-|nK#G=IP2qi2k8X$Pa(!~5h@<=Zr- zPi@get=S;}t#Lu*9~bAeT9QUN`x%cIgxw5lm zJAIbn0L!1lNkf%T0vXNd$ahsxZquv4k*rk?SQ^GSNE(_R*G$g8rm?}rXfFY5 zb5oGXnmfA7;)_O>^i+op928usg*5*WByg}At7(>>H_qzjZ5vRgWKA*)a&Ff6WQR#7 zy-!?zPHN#VJdF;x_#CKtddiK=C$ro+vo!=V+N+u_Y&_T1gb82L6F@nc6GZ6ar!Jmn*ydnJ8hJIF>b%!{n zV-IHI2@joLP!3aul2*q+5&zZZZ9ie|iY{DfR~*x97}c&~Lw~xuj6@@;N-us(rF~bL zs`l*_!H@w0h_C8-8K0`XA%#Iz9SPeoN(NuD3+ivM0>3?TNtHruhiRs)>pcx6&<1XQ zD!LY#2IsINjlXkL#{v%5RlfFC493y) zfwz>K74N!>$FW7Sq64k48F_(xoC3>C-tj#6Ffk8hi)!VGnU76!C+OOQsR#~p42FE?nm?^fLV`ED&`Ua*=Ocd zN4r%{NcbROkqDT5gj@g2WAWFM0?5gQ2$re{NKX>ror@{d`cei*{B$`zvS#*V zHs@BC;y0Zj$^&6`cy!q;*o~)yGMCCkRt$<=#H5AolCsbEy;gS@@Z`5#*{;V<3!E2Kg{Ki5TRMh)V9CuFS=)hyrC zKx)2|;&89Yrhj%@22(T4>glL_2?|)1v$$4CbK*EQf4DAhbzFcO0*=m^N`QhuPL)}8 zZ)OM*@!I5orDDFJeUBKA%?0N-ln+;+>?Au*b6@5MLi@jFG%7e_J13vHaT=Mz0L`&l z5>@oBqdbf0G5n=bt;vhgt%=a~Xz0fR_9Z2#B)7D@j9B)aco4`8)?wT5b9|Vi!Jt!U z;BjHWe$fRy+uUm$o6{TTnkN#kRkNNsRCBNif2R;sb|m`n1q9B0IIdCX@E!w7xCXZE zf}Mgb(Mw4hw2Y*D%em=`4+i$!CWFFOP4E#$Pe@|(O)iTDV9aux1#UP1Oef&kCk2WSL}+t-9w&Oh*N@mI?!ym<+}rXfN;EwY~|`juA%LSAqMw;pzU zfVHqDM4Pu4Von}_D^y;#zz%-pOwYEpJ_f}Fwfp0|z-VSQFn$gdj`;zT!Gj>C_@yuc zWnnNd<-N0UnafXI$4-ozSRiW}y zt6)O8)_~=CWe%EC-aT=)Y`fNyAo;MiGI*3w&YM5~!G7MzM0+XaBf|0nqoo`AusEKW zYW?eeh0<;4-Yb%h&J|GjkUQoP);Ba!4=W>fo5W&K`GZ)09r* zRkyAM2e`?C5vXxLcT;3Iozo+jl;XzA(i@GZ@?2tUG0&rK)DqS0+w2Ihn+&xqsFe$t z7Z_NC&F+ssl<)*{cHT9Wl4_!Awo`??SmWzKeIYqsaCdO^^^3m-LZH;?M(lyAr%{P_ zafJ;j2eIyd2-rb89{85|wA7ph;Zl zZo61@K)P(>Mj0=R84G&bbCSffnjz(uI{3&4hA4eHJ`S?yaF{JI9Wz49BrXc7+j{iW zk;HpHXlsO>%C>Esa|9}et)^jGPZwvWMrH29AK$8l#a8K?2v0-}c>+6uI{f(jSoL@% zXx~tLv13|%tEwx-fRRy_)-|*>vO!pGA>Hz3oDssHI5%&{DJwCqgJXZ3P$PXGoTjb) zRM-IAxS#+oYL6%)119ln0!%5VXZ*W zoj^9g`Jq~W?^Si)McH6Irb0Kde?0DHet%3SNIWIIyACQCANnCvNG4(YWnH6FfY&8Y zQ{QtMGewk8MqnXt8v4t*tRv;AgZ)G=+(&G9Y?PV8JxLByld%vY)y(uQK}P{yK^vUr!v(QW3s7=^}@=_Biw)`ZD<&A(=>2 zwJz$f&0da_MANP&u+gJebh95)u8)i%fF!lXG{{xUHd~6;fzTa|o+5xPLGw)XuLZyv z;yFsuiq{K~rF0~&?Ab4#GdUWfK>?Z)4<1;kv(NQUxuwA4Dhxs?Ho%HX>|v+DQSJHzG%MD#k>f6Tukm}6(0pIV$Z*a1-^J=kHMKBZ_#&V3m!pFM zpYuWG^N}6vw(CU&QoUlUR5~t}h|K=NDZS`O)~*NWqcqz;BbFP4wbqJy&7d-h$3?`z zy&X#Th+xVVFW2#s5935L=QxKt*TPYi%eIgYK1ih#NfuB(DJ@c;rYvS%)OBwE>C`YW z`I0R7!{5Q9rk2hqM*o6@q778e@Vkm%=wo#SLWkU)Zq7(p@RaykHmEFP^)a-9@)pl_ z5!Z$ak6HU}()h9Fu-1p|nC5^}SJ&wh!26i z$`*3vCMuG`1K_VY6(FPPBbcoj$!-KsWNyrA4TElWu)f+N)$E^5l{7q*Oxl_qypH4P z?$-0Z^%oF;f|fBMj_y#Sv1qDR*^haY@hcYfdfuaHe}q_mDF`Wz|7T%3W>Esr z{KODo_&rSCPxrbwuUHvjVZ=Xwr{;$#AGldNy*UW%0;H}n@igFV02xHIs}54Gf^$?x zyfvJ*@aG+F#_}FPY(6Mf39^yyZ^P91^}0zrMS$veWZF0i1$u)rq7QJsWGlTrb%6oj znG-S*@tS|Qeh}}fA0Bil5G$4ar2Y?%~Z}|8P-5on5AYuPqk}hpxKnx zAiFCi#A0O`f?E{xP+QzrI`_~kRylw02m;7x9{FgT&_Lb+#ni7q_v~GX((r_DSSjxJ5pm_O zZ_q-9mtxvbtvW0b_og>YaI$d7Mjlz=!s5UN?nkVzt?_rX4iVktrIe&?_^>QsH zyt9800Y&~RsKtd0+{%5$I@DNO3nQZ4qkBdq*l=;*4F2(qxnIae3$s6s;#q8^6v2d_u)PV9(vIeoVq*c12c>k(A*QonJ+F3C(Irl2GB)GD5XDdu z`jHZyl6K>oy(&?-41S&QB1DH$ratt+l?W83{Sapy*dJ`YhKLCmZ1y{qRt2ga?b5N>Q0_WI;hdf(Uyd86JS=<*cbEC#*V-EG&f zeEp=@01iq}wV;^}QKC~SDA}-kC$|Bb>Di77YLjtau8}}`n&#J}DiEYSN$^|{+ril( zn+@t#5LBh>%~ZqzD^&u}gK)t^W;|dn_UE)VqyOyn5-`6XpbIUhC9A(sO6@)@duTOU zR1n`Tn&$`7>l**iK0vzBC2QtPTI9XQ;2be)Vr);4#qRY^yy6AjoVWD3yPDI8ktRUi z_gZ#YB`SwDP+|s33XY;h)gTTg2^~}<$xhiZTN4*1K9v?Xm)6f|+!uSYJPk649(|Q> z#3ghGF{xm#i53%!i~1(Ju(adCOi1wNZDdxo;9A8`pID1pQ2Jx`7W_m^K!|P-bdz6C z;Wy5A=a|sAZcG%0<~rv%&QPeX(4}z2b~-oy_0Zt4l1xe?Iy;pQ)Nv3J1N79xCvt%K z1f=a)*kXo~{LT!@HXz(~ta4$M>yYUAOAQmK3U?+urCbW2u9Hz5Y?h=Ap3j()Or`OQ z% z#^}S)3Ko3YGVdS-jwk1Xg}EO+G1 z1<;Ac>A$}O`?B8}r>=6*NNq;iuET?bKq`pj6WYc(lTv0AaHygq`KylFA~ptJ2d-8* z9FIs53FE`Y`=8waP}R$;mz~1jP-;JjoIrW2{+I+#coo+OkV8sqH!fey`}pl2zNb6b z8RcGV zl@7h4(`vhnzPg0}rO(Eov*&SXzU}8n$UV1w|Ht8>o6KnD`C}@dp^U-Vulb&7t0!nR zPipEfI?o|tmRX(<%F!&zd2Aur8`6oNv}erk8sMW+W=fnHyH{Gsv2-CoyPbmKw<>04 z@$4@h)jAifgYbzCXt`A#wp2l%4DEhs|6M`Q_y$fBk?CxByeGWh$gJhvb`1+`ZFUU{ zJQ%oydKp&mp>igHfrFtCnIDU6`=}qx7$CbzlCWp2U8z^f-k&^@)tEb3$>ca(G<{ey zeOr>qw^Nc5b48L2$TK&TIP@<*ACJ64Fma3M!GI>ilI0gfk>zirYNV=fb(g}=qbfi_ z`AyYAPCavY4bXP!#-LYabX+ewZ$?-0L2KVk*8XYVkoH35>2PR9Qk&@^VRcl}{R{tuaa_ zVBW3omf{PJv31;B(xs>`GxHyk3;i;|Q4QU%7UpVni?j;)?}Vh1CG1~twEt+@;9K3= znd`grh9ZDnPkuk}H95!<)W)!%tnPv&KQN$r97T#Dp|L^G*jIWgdmv;T_~K}P+@v;T zeA*L-iM&|#)H3CBDB}r^4r2iJzBPTRp$PYNv|5nCjik7ak}u*4v^%`ZUf4e=o3n5h z!W7D|uw{dKD>keSI%tn;Wq^h|RlMd^+o3PG-Prn>k5w-+MO$2Q#=%bo=J~tYBFin_ zS|b{Kv%io8|HK;sp{AriTpPi%sYb--t>!oF*DTE)IbhXj%Cj*~S{mm7dc=ma_6}EV zJZgsRDfz+RWYSK-5de=C-7K%aeiyaPBtRH@?E#sQ%Hov^^)y(mF69^C-8&czR!h?Fk$8T4|P~gY?t+GrRb)wb0_SA z6nM!E6)0*}!G{Bp=ogvJ(sWHprR<~XC;R83U6JiH^E?7T+x%%{33nsSTJndN(o+z$^A-lM1S8^bZ&Ar3nj6oY04 zwrj;Zs1mT3R0wAX2M~>>vH_0?jvgYpg<2j<_{oJRNW5%(X zS%tEB28&M&i!ny|`!8*IkrBaaERCy3OuvFimGUl^$4@JgY-I8PqN-N^ zAP9hOI?7HPS2ak}Z=I@xLcz7BM|2)KD^^x&yIyj*v?;A^cX zTiXGyUbQ0MVe`RQfUzu?K6NP~SsG$2h4fFzdx8^Dw?(Xmo2w}WgM;R^3Z;}C7Th*o z*>&tuPm#H!hMHLwlSSOIm2(t_v)*OG-z>5ol8eQ{($vc$C%ne>@lU?3c z)f_C0m2RDa(7b*cQAYZrIa|7dMjjb+LH%Sp8QxQ#6Jz@$8xNm!~@-H~Dy_3YK!ZTC<4 zrj=dzx36Cn)0(trdnwrIR`KcvM1{yA1&%*>JTK@id ziraoJLd`e$4fHOk^R%`;w6*0FVI|kHFRz3ZYW6&5W@J055qJ(0S<|fq%rUnn_*-vT1 zM;!v_N!Sict*#F`7k24)tOsl{(No`g2MvC#5Hx))=R2ZTg28bcgV`fi?AjeA{5GBY zBU1|1dO#jV+#|f{67_O@Z5IpRIr~LNQu{j7*U|Hz!JZ|ou9-5&#=p)b53MiY&+lmT zT%}#K`hCU5<$xSR$1|?a<_fB175@$zLBq5CwCy-P67tO#~Wr44r?Uoc(#D{++87fC8m&7`W_rWz!!@~RXV8_xG zN-0a>zXNO20SP0%gyhaEn|OkMBTfT6vKe@PdMaq-F3e`oPkQ+clzW}Q8&mPZFx(wO zpeg@lvGtEKY;9r^Q@^woBmIW8b^F;W{Ay*EINufs)Y#Y0ZEI|K2|+&DppO|ip(aSk z2x$=f17;xBteK!Vt%v5u1KQO>H=k3kg3ngAfQbE0qxQlL zM^-0uH7Hp|=L98QNz-095c$?)TN-@cAwc|tShX8GvpzJBhoQ*7v@(WA=!- z(!JgtGsr+vg6hh<6y`wCJDJG$eZN|zNTV4%tXEU`JoeiHr<5N&Z8B17_5EK$2{n4c z0iq%`7N&N$>dwYT+8_R%;g~eo>iQgj?6%g0ykrHkB30dgDF9=RGs-IChOUV$kAr^Q z3F7Q-5MDRPfyiJUE=vQV0Z=@i6*wKxoXDCv5gl~e`la%b*&}hw^=-*eF2J#EmP^%I z?h#W65$}5pq2AaVL`Fge$aYKi*p?SxbVW(`6*~D;b;RLh)Rd`p1buabP2M20L;Gg9IDOD0s z>!q%jOjC*BoT&4mi|iiRc+(>qKXBBU_Yb?I6a88f*`TVkJ=$`9b?5cHU<}#hpWeHB zbP4`mzh_Gk4vgqYKJE^}e3a)a5r1oyh&0W6C36;tC%~eo_$pqnLgOR@{>x*yv>8b- zF-SHDa>&VsRjF9h>qpg^lcKs~vIZTyPDhlq^!SQ6Jx#c>?3b6*a^ls${2Tt^Ze=QFj%gRTp^kkiP-0Pp@-IVuqh`2kFJ-~F@x?h} zcM_#`{w7Fyu&p{UXZ1xPW?iNmk&qbnreodS&%V}W&(uKcCXW8@@+Xl8!_?{Q|| z{kwCf45&8K#LqJ2Pd%-Doa!TANL<^|%+;zAwvX)Wz!{m#naJTV4dCpmXO}Aksy?gy zRmyMk`nz6OOkRF1u?BfIwTt)+>a=I|X0!UXQdN#iJ}ksdarWFwr`!1r7%Gx}|AUnO zuJzeR7`(WL5)RK6m|>wzWZGJs}N+X+yJ)k*)|8Uw?t0et)A%@W&KNwz=KjEnpXSQ%`0I9N7f=QCJD$OWv{46`02 zPFfXp*I*;rTU-L5yy@-?((GC#N3Q);gA5m8&8W(b z|NDzytK+jfMU4NOsl8;$L5ANH3kS8te{u7hAn07da$i-`WR@=c=Ov#PAN^rSmXh%cFHxBfHX;MsOPwB(qaruaI8z(p+2MM?tE~ZSfr9TLHh~1_GL4 zG4*YAaStt_FKU5xR>qeYPwP(D10yfkxuZ{*10zr5CBjB}11hV}X}8bhR`uRBURT~> z;Q%E3xU&1(#qz^5%A}G8udICewc0=9u3RC$fRg4M1Y7_Rm6zj&V}wmLTwi?Rfv@;0 zbaU$ET-azI0FUm`I-`Za6}VlQdMM53=Xq>b3K%I~A!%-X8P(>)u1i)JNuL_!DE06{ zu085XiIk@(tq+N|ln|(L`+C~|6aiM;$8nu2j(iEe<&ZJXr|6--naNxm4?cMwb|nv2 z_L_ipY32ImTYy&dc&mpOy&1O@wD9~z0 z3=4x++uM5&1KpBRpjsY09YWo@Zo$VWmacts(^4N;bDS5mz*XuIu)J8i(H!YgHR!S; zP98WzAXHcgYN{;uvm*(>_`ji^bTe%TJsugBX&?IT?WWCKavjy(?g=PTmtX zDb4FN@P5JLM3HWk+xz^!Z*LcG?zz3U&RVV#t}obIMDvdWi&^ZZ?+b2qO~e~d$0?h zeY*<0&P;aU0qp-7?eoY*1-`87PN`Oux!g1R|19X}ktAP7r~>wt%XW`{Q@I@^*08&J z`sDX4{$!$!hM8Ls&Lg#j3^dDKe?FAeR>#Pac@-$qI(4uHM!vkWSOCRA;e_?PGQb2E&G?tOT zMfLvCxq-DRmGX`1tA+$JkZaxiHW9)W!^L6h`CBAxhpRJn^59g}%w`IpJxGdYWSRE_U~UBL_C z$cSpspwPwKYY-*X_%G5z>U!@XENRC7`WESzu#a4?>skqOlQcMVYu%S_EcN@(t z|BLQ*VVG{OZzq_Kg3bc-Dp{YI^c2CO&u!?wC^=s>IQ_@GVi&q~m4^r13*Q8^I(#D> zHPER?W?jo{Mc#>0T45z1K^^%P;&VJ&$884 zOq-q?!c;qTD!J!*sK$)e?BeN&oU2?rC$o_Ob^gMURG;N#@K6f#2sw<#L_P#;Ipu3@uIBHQ%vF7f-Grn!V~?=Ex~l23wD9S@%QF#-&&jJ zWNx@7AfZnEQyfX%3O?nt29}{HN^SPmw_|fJ>DOp z2^8+D^-^=gl;0*sKlhzXjN5&yNgxBViLKGDI>v3=r{6lFJaYVzTJ!_Zo!!c=K5#%9 z@48-lETo1!eaK55LiKvkCNT)-5p!R87$J=>E?7#Y;8h_ZFT6x0>J_n(Nn4qUEqNAh zA%{zeYDeOzm$|Bx)FSUU<%?0ZkN2HTX%4w=beBrQ#fFj~U|%iCy68%%hT$8C{NrLH zI$IMdX3nP`P06+Tu{yT(LJVgV;VucJFZNyrarpJfU?ssSS?~;vNE|GeLNC-WH0hx) z_t6@XVh?WO8fhH_x=-`Vy#td>B10 zd#hcmT>fKvO%rq)V%RjfCo2z+R^>X$d;omaap_ za+@q|+n|9Ra-H{R8Z?Mzy(19*&Sw-U{(;KCSImjY=3+xu*#+sEobOxEklp++V5BhB zR;fSu!?oXmR$yHP$&x0t5`{-$AH}->Qhs?ol5$d{?R$*Id4c^zvp;(Y( zTq54|)<3#@){gIR!DFj2`%(V>X+hMCbF*u}BR=FDQ$dYquWGjMnU^=;%Wb%wlBC8| zJK>mDbvIc{Jneb&8PImM!~VlaIvU$I&idgy{#HKC@@J+L`?y+HcsC)h7r3+2yjHzT z-RHyYyMDjac2uLz1-2dHZuj@H_PmBs=j4<5lEak;5i-azH-S+LQ@BSjv?~rC4ZF`F zZH1$qv46ZUu9tTMeH*l6fo_@MdWnH{w_hX|H}7p?fy`exJDMjO4`iz9!I_n4JA%oL z27&VDjgEmELbJuN%KR@V34^Hww8hmrc=GHSrh-=vi#Nmw0i2n;*Tu zvOQi`fUt(2nZN~D+WaQ<_E7m@J$K(kSXjS&fTJms16u^xJI z6q^}T!MFyv2_59fhxu&Z2I3ed7WS!F-wPfUk29tX+nu%IU7P9;sn3fZKYErJ?JHpC zZkRcw!gFb12v4GLPf@+>s2JDFEy4{wP51L zZ$9pY$C;3}$XWE*b+90sVoHB4x_ZC4Ek3NGhWBzQc_0EQ#@jnTL&L4t5}e8O%qf&( z5gKO-TAH|+>CLm1QHIUyT%DLk>6BQTZt1Y74>&KEEH$`CVI~RZg5~OuxAcA4U?#x* z=*q2icQbOwW0H!qg#$?Us(w=WS>7kCk|!S(i=fC~z&{IiHl6+BgMfT(4{qqqkeJWa z#beCwxC27{FO!04JlI6f8ZKE5lWhwJLPL>Js?!GAV*5RP286H_I()m+qk;pPPhKPx zv5AMp74{o@0fzBRm+m?K$Xa~L?p7aa6h3bb6VP}o3bogWIzBp?aQ8@y=<$)$T6;Vv zH6}ldkN#{lpXFOVKDxal;t@0a?R;a)|ECjpE2|uve(@bdSW)oRFZB{fi%{ofi_`o* zu=E+XBc~V--;VF&IXL`W*te_reYbm zfyuKnWp+d(FFX2ASNMNysKpnq4EnC??=lXN2XI2rH+LP9C~cZWvn9l1+h#{M*jCGJ zgcyO~HAmDPi;oH!jA&QG*S4{YIO1_TeZz@AeizA&a;tj(u4fvavONcE-;17cmzF57 zsavm~#Ue74q$#mQKlB=0%Bs-usa_@Co<|hvin=3+oAb7}2HGu)o9RV5NLIPbGddth z)qXbiDm|}1$4cDg)27BqFSGe@V|GVd)h{#7>Smc@S%z3Ie!!jdyyp4Tyoacd&4=i5 z`y&#s=wWnr&cce%A>(O4Ghcv*^<@c5H4e9e)(NT*+{tC8_VIKo1OLZiDn~fsuU^_s zZoo`8NyO@NFOT}>6AQGB`XZot`a=N=jy^s9q7_Sa^DTZ2>%sqQ>;=!|uSh9Oujky&~Eu&2AE*8*u z#Kdzyd`j1F`?4vkScvc?t834aoom}kz*~DXwdP9Yx+TS=7yF~&99b{@0`cSZfFd&y zbjUN+sryIo0O91eZ`y-Q2PVUCt8t#J8x$m}GhbhF&QeR|h1fNjE@*^-w<!g0xDTD(?neeA^=*|))oR`sq~nk@nCFD`ip#~#4_#u?*P z54Y@-*-19O&1#@qc4`zn=HrA%$b*B;fEgS|L)cNLqqpu#TrdA)wh4XiP+7fl>i}Rx zog+N=R_)jA01zZoH}bzpeVmE3nNKEDamAxR7=8h)ZX3M+O8Xk4NeuJ( z8;JaGFOOFX%$XzLF{8`=UpMN%u4ckKD3Ax%{<8e+KU`82W0)WjaF^ZS{Lcp{Qy?$a zuZDXSr~mHYuXk!M2ozOop|F07Gyl)eU`#-5eO-2=;r`$LEikw%L6DnSJGu_(KOaC= zgW5{7;KfM!Pj~JEaobugy_kmI+k+GJwCo-DBsywg`$GA_!h@*cSBJc;!nnh#^C~lh z5$?!9jkNOA*VP9edB{_^8~Ss2Yxl?5Ze!#XwX&_hgY^1}$$;zp+=Al@t@4q1^$1(U!k#E$mDd_GI=p?>tS>Omf3`$ewLfwV7v;F}5)6^OU z%>F5G%>pO(b>RGaFbeo{fnlU)ii6$mxMe2;+}tFLrIXtNjh#@BK4?#Rr%f8BbV|lIs!T`hSxy;uX1D6hM|pCGngIAC(sQ6}=a?W7;Wo0}JaC0nAdD zIXhAwES&VkO8EOH`fpQTTK(wMg>{>@!9IPlp^k02pzWH$>j;g6H&*P-`c8SYz z;VG$5uG1V-L=cuOEie(FYn~7GJ{`S0G2y{{o4BK}%Nt!~}@rLi*JTRj7v>~_Ne3z>`MyGd|v zohStO?}gQQZp`Ka3irA~&4sJl*oavm<~Jqw>J)!Ts*j5P#xpe2z_qS%h}PZYM9j3nO zfS765l#l;Z*2$Hd&|hhUAp!{<(Nug997wjjBB;Xeui#Zu^1*otUC(ivu6`tN%j;&3 zGSLz6J+wQXxix$$68bUH$p>6RMDN)@WLa$r-88ydqjPN+C{pDh_^HNv&8{_#l*?ts zN&?5hY?NWO@P#HJD$KH#6&jTaUi^IvaN$y`)z7+o_o$j%Vz`iD1y>vi=Pd}t)(w9Ho(h6)3Y#ozMR>EfjG3_M>-exsEfFD zztZAv)5;Ik9}lPwWmFx$x_*k{fHqa4?_|z5(ZrNWeUfw429jfpwmwk-t~5_&SJkk= zl*Qdf1o|BjS2V)Yo0Wjq@Cq?e z3##CK6=0z}Pp*>acZ!<91wA6Jbg_FX$C8z*$msHBlnaN^<@z!%;yIv_KK)htJj?g) zjaDlp@Qss!_tHjbH#&%FrAeK_%2R%15}~5s+94f@oqOqky**u>OAcrTy9HJ|@oJ-^ z#IvM>5+>M`GlP1f!`_NoK*UoQ0Wj3{I>AF|2W&3z>eW zr^%E~-u-m;XLopq&sLf?VyU;O~H;<~D$Jy0OoKclTak-Hi3T^r=7U74EfF(Nai z7Fwu4DK8aDd@wu7PT7HV?qf}ks+iFS<}yvt=F{#wGaVptM&cj0a8mjdvm!b;n@EY0PHx*KLs;-%W~R~})Nmv97RH^J5#cc;1S6ZY z71aViOb^#N72(lNR$SqcdVFkSjXv%TSim53BUUO+K{mbaom0sOOT$S#INzIsFis^l zDb#Ex{CWAKYf3dt=vYYJhMAtn@TO0w*&14rC7GfR(XVp70$^V98BM=)yOI?%GMBzx z917O>XXN-=0&ZET{I`|3DsH)oCrRNN5zWqb^>p>mu{Z%3i>4tiEf)p(vo1oJgya~z zqt$5|lJs$(b9Ewb`aJ&G{?aKx8zs7WjQwj2lYtoOYVADZsY_ON(XFmm>``?Ba%&)Z z?SYyux_>!wn!(_H_km*fse_V&p3_^X-}>UnnL;E6RSgypA_%Xh$#OTHqsPsvML?@a z(mA4zS#}!Fbv7-ObZv7`E5Ryv`?8nE6TEs{;8!hbk(R9#lg248I1h*odaTLVQ$tIj zHm#D@XR6qG*ji*?kO_ORCAaQM2re|(m7$-#&EaAB=-k6*AEu)=%1z1g^?>qc*ztE+ zVGOlF>TBezj%4%Z=?{H0Lq7XS4e&K;k+R|Ag8{HS$}fLI@#rvWU|N^BRg%5luc0Uy zIxuzJbii`)AyNUoqc**14c2&a@IZsGPsnWp-}qsH)~L!0c*UlKRUcx36v&i>otLKV z42~x)(n!iQ-^`F8Ur~}HG4&&|Y?tU#t7IV8Y~88L+-IVJVvZ>C?jON1TeOC}K5?2I zpTVc;)Em!bOIZ^h-40m*Wz@x78D z97w6S!xYzIBZ*Ln$r;vZe{r ziG)U9+zl4%&APZLAU56gnD)hR$=CKtlXXADN|*RqoQ5;0W}ss0Y}FZ2UO6I!sm!;W zZj`2s9BS+NNx+sCH`6&ry-#tzM;w|sZQo8zD*Y!uM?|!C`p7{_)g$&6K;$=uk1t+l z$~bHdo$*frOKW8*o>}>&@A0!~#a0{I%rWnbQOzQ2`#OIXKDVYs{Lx(RzL*@F;HI*# z&+*4l+rU(zU#iPOB9*lkxOTP5L~0wt`xXGsZ%`qSSA&EC)@q9_S}n@{FYE`JnvkTL zmJOdFEGA;XHA%{0VbSWF-q!DU<5rSR&bbV-2y?w;rs{`9G~gEEmvz^v!aJ_Mb56f0?3f4DcY>($4#}o&N|} z%INHYtW%_B#@hdhGkDwGI%zU)WR 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 zMIAS|{%t*hb`0SD{vW|P7s>7g)g=knx~xkg_Mz#yYv9cP=D>ao2#1f7sFXwx`-+&Q z_KH3}ONU2)01aE>eRiP|?s;Lnsj7yo{FdHR1%spWp~LCJyZ>s7sRNyo(e$MMcN_>% z4lY2a)OADqe2N}~(`I!xSIcUKO9~9DOIyq36=*#AxL)jZSPL>1f1cZT27c+u^ins) zpjoYi3W(6}cfOEA%vYUAveMB_JjQl?TBF%8;PM6Fswwe_y3+qMBi$Ha=>(<&dvkW+ zY4aYw5+VH$M+!BkkTSYJ;IQReE7;sfT*e0TDJ&UH@ zzn`(}sYiklNw_f)5Kyd(YMk{avYgXq@G!^!%yF^dCUzO^X}Qy&dWi@TWz$CqWHWi{ z=4#YFZ%gMd+WJr|lJI}5kpJNu5IC;vM+YdsuMT%gF1s$-wOw!F+G{%aK$ZhlS0@mf zCUASUn3{BBIsNc-I6#`KA>Rp3Ue5k%5!0$tYvtp}J{hi1EAcI&deAYep2263(DeRy zRz3vcQveVMb?pZK;zQThTS`UzI6s05cti25XjV}R>z<58pEor{h`6A*hoI8R`~N~A z7qr7NzUuI}AMt~w>$S+lr1Y4;NZUc%|4hn1TPXQu1=8Uq?c~N+cJ%nqP4$0`(nH(_ zxLO8>_GjY%eX>ji*r7V?RwMuKga7xF|CeL`{}hnl~S2aemk0+T{!eRMi&js%5_u`ppi^99yMO{C~Fef8T&W(FL-o2K{~dUt{DSV)+gLHv9k2pCn^5I!aGVCj8&^NUn?h zVbUI`(v$t)l|T6lptm`ZlEf7L-3tEMbpU$b7HqUDVTf%>Pr998le{&A8PAfx|IL8S zz6Hc{1|qk~j^YnsC+B&=OuNJB-C9Nd124sI_!^gsL*7%!Yn1J=EBfc&Bz0uCyaI4x z7EL$~6Ofg43Dx{=R(G2@(`8>y+{2SH;&f17@rf39HTZ?q^A{26%s!QY@$H`%dZLc! zlSUXnlJ2%Mx-5Igi4J0%=}?Jc^K>h{y9zv7i1nhAlnu{T{gklWYn~TL;sgE=R#+FO zHofd*ZDriH)o1`NNw3Be?h9hxZ+U@zdsxTHmloxh-H~ULjg)zRA{f8N{k0V%jg*A8 zTcF*#^vu(B94h z6#uRQf?UxzmyA{$O> ztQO>kF5m+GAP8#~mk=PRov7%gnUV|lxh%T&s`tB3Wx9z{1Zul#lm-7iZgOW2zIqeU zDDdJSx>dl3ka*pFVz(crc+`5{A{%6L(L5fbEMbK-xh*W;)VM{IO>e-s@NS1&LC6$w zQbUwi$erU*tLl`6b2uj0>j=Xtt3I~_BZZ5URdIaZVqfcjvROEiAdh{raS2z(V_93b z=gu$RVP7lM_~umUf0EPafk%#hew#e-G$iP`uFk!iS*f>1V&LW&wcZjcpP0zAQ!&p? zW3eIMI_#Aoc7a`7@M5O%^|`3&gg_}zjeA}dp(QJcd2XgEn5$^T!F(J`#)qc6pOZ8r zl?d`n%~P-E1F0^QGIiQ7O!g8DfWxrZAG~dSCA2&hjH|^0UViwx`e#J#YUn9w+tvEX zH`|)ZR@Zeix~&GB1zno5ct>u@J1Q6J8{5@0UbqP^jxT7gkXQ5TX=c^7PqWO)as68{ zDRK0k$1bo@cZfVmSp#WVMnn$3iCnBqDXp?LA`oh6YV(w@_Xw!8I*NDy{iX8JL})qxJRT;gwTFR&9pso*m0P zTFu}`%a5x|`lEaeJ2z04bjrTNfz0kuXaX~y$XS++&g)h5@(Jpm5Rs(q5;p$;BHM}^nLQ`i4;a>bGDUG%A>#V;4X)#-M61{NU4H;Q zL2@*iy?E;Nim+*;1{RFq_13SmRvXO0#}BRaEs@n&-L@@y*R_s@2pD#xGj zssgVih1qaix7%o|jwLQ=LE581fb8pewy^++ABn+y ze-K$po$f)YR(0TU)7#u`I)6zXowg7v$XDXMAo? zwjj6WV+*QIkiVEA#)TvXeAlY5^_;Q>a?*E}G`nR?;44{9L##ile@yf{Pj55WE$ip6 zoHnazhoz&iSBpsZ)lK_lvb@qc+h<*o98HPHuU z6ghP9#0AIcB;uPa8>q73V>6%}80^LVz&y_(B_F2>+~{6XWh6!yER6E$N>*7hw>b`f z;U3itz#4*1?z8|Vlqz8J?AzIkx-)Q`P89D>weLmHgTd|l0}P$q;K4Ck_+_PY~Ql@g!T7U|QdKoe=DS1bVq@R0YeH zDRi-)7PF*8a}c3{Lgepk@vy9Rs4c^eDr$t77a~%-*%8L(gL^sn;lPJo4{W;MMi(4X zzF62ktgkAT#1(A~8E$IJ{b)NT2+v7(7jmdn^ zY3Av2EiT67E^{jzF?mw3LJbT`dwA$3+JZT`eD7Q2CfVd=S2m$h8q8H-&UYO|eKg12 z*JA8AuB84H-js7bRY9aK-bB!5hqHRa9{tv$w9_c;wRh$wdf_2{(LTB+U4=U>vEb=G z{!OO4>1h;eV&dKol?(a(a>wuioLtMTwA%APNtI|IPv!MdqPh;L(YhQAB1zU1x_cHp zj9%VvEnJ1y0y$u9^&TmbO?ZdQUgB!}XA{tvQvcM0F?t3j@XV69Dy6vWp|QwT{~R0n zNh)?va?7@wWQ66{Uny}IJ389)t*0GL@8PEbQhWXO=zP!xis5sDczbP@0Q0a!;2He#M}ZeBcw&9IOhbLoE@Hqzi4LYw0s&TJIWM_X zQ_n6f{)P5oN#a`V^C)d|s{C=3Hu1H4k}Q`@;@jM5x(t+PT#cpQKlPj!ywA6-sgibG z(M0$a;Qs0~!2?_8e%_zSx-RS#0_t4$!tmTjFgBJyFiPi5GZIFtd%F>}Y1hL0G$s~* z3$kT?E4z6)a5+N!&5g34yft9L-dB64;H37u2WuIfdU0@kV$JJ?m#@1)ez54{fy;&< z9U57Fkwe~|L{VX}kn*o!4-`Oo%8=q#%9P~Ezl_mEZYKF2%ICw0nbVHvGlN~6wYX+C z_x$AtZsmJVVhR>DxYJ?_)o&b*GR-@M>8Q-SDkWxHOzVxUB2|%-8taPE_t>J9G4A7Z zyBfU@aSJB|T`o}v3atl5Rsn#$Ro>f`L&kn3y$%A^n-3&fyG4hM?6u>w0D8cm6nir$3p6>~Q;{1D#kVkWNHU} zhx7F%nrMlcO6n_ZT*(%#aRGtql|3#Dm2|~0-&JZ11PIxzlu>>Hs7AEyuzS{>KU%!uZu;Os!#d7RRC~3}sOo3JO+4SO zB)mg&xVw6a?2vw!V-9-$HRs_9LH0(mn5Q3gwaTndKXF$p*xPrpB*%3(^w~e?e65Sp zi~n;>ng%L&WrzI_9CK8gu{>#Z5lce@hxfSjVfUq)yRN7syz07IEA@4FF|Ba~5?!Z2 zOJauCC!3}7q(lk2(`{|Z1>&9%1Y?gi@1iPZwfdxqOyp8}hN@pYDsmGW9bz3v{kCtr zygSEUdql^!af51h)F*;hm%ogv+qT^?Fb1oT6j=)c?Ac0x(>w?@=tGHJAi;+t$+AV0bf&JO^4A3?|v>ZrVL(ZU_o+ z-5AACtP?A3w^Hid0LbpfZ{EPz8;#2aEmp)#7L+da=u$13^2TA65-i~HDV^O+Z6j^& zl6&2F&&_y;k=ud!h4pUZ;g+a$ve&n&m8%gl$YFd&ugj#z&(d0#h0Vhonqr*+om5qd+oX+B}VHe|=fscYBL)_6Pm&ciP zIl~ zha9>$`80Pc4;iCCYjaqTc5B2_?pdf`U$^7$EgE;0cdAljDupiWbP*QEdXgi1!X0&+R5SHaBrH zHf}zD`>We#4LEKm_w$k%1gv08?K@2WfKCJ7BL3|}c){%zZE_Qh>Ne;gGeB*gH(0B{ zy;i(e-ThrtWRJ$()R+l+UHjq7EC$3claL)OzeGYSFV^xq=4ZLw!?{=ublqrKl z>?joIc8)bIb9S*U$9K_AOa9sR(LZyNyjKn44%R?K3 zS@r$_9nC>mV~)ouVLA3!O0-wQ{ALUJBZ9SjF~JypcJlRL*=RztS3nkkLmw_WR(SkF z;fk?Ze#7g9&h}PPA%P}@#B=SR@b#T%z2y37O*R`aqh90O^L8-5LOyEjVn+(6r49le zO0sjTZ}53sUL0>%B}h=^a*_THh6&n8y~r!tmEF57E?aHG**10zx#xuIEAfGHjKzzB zde=WV&CTL!lTPx>V9WDYqp)uWH~VkLkax$YL6D7dYNfTdj4ak&!QYz!;599_66ghk z+_=@(h_t6!D!K*1&>h`p-=DV{t*f&gD-Mt?tS5+Q8o`1G`DNa+Uj%X?CAfF(G%2O= z@2{aP#&N=n;{0uFC%}_WkwR{Idb45GZ-ZUuh&!y zKqaS8%#Cl*NR}n65sxK@#coh;8;~XXMDH4D@jHh#>)%j=ZLeRq(p!}PC%XFpF*-z< z14=W5?_>cZ7c5zzEfzD0Nu({^EIMzTWF7X|i*G&m2|C7Z!jEeux0A7k4Y;YiidFTu z2MX7X>#l0`-jrgi~4liDQ7Q+V}6~oRG_gL3acn~W` zK7KXlc?WubYia+67*&KpOoUI5&NNZpCymvodwmCs9ZPn;HI0&9>=LO2+c>{O! zv(&Qdv<)S#!uT)vF!V$UNSTM;TDO@gv$HfP?jvxfR1#z6D`J)e78>FE8^Tse&Um4V zrPQ8)KMQ!SwZ+EM0{Wl9O{RPOC+7OTy#HL8&oi#Xk?RO2W+84fcHC)=M z)N6%X61Y7>_s%;FDhX`{Bs#CcTO_7|r$%Z4aka1JRc0##5jvq)92nxOv8UH+3b$yx zv#FCiPXsRqWx@3?Y#{U(MRc1v1rSkWayNSYrF9BP5<7>?de3_tJL#ytdRPw`QSC5d)|S)Uv;O&angr8O*Px?Z zYrLJrddbFQt*!VStlN6h;$>#~xg^-me^F&#g!{;0qsoi>I|lTUICt(` zupCk9yB3w{(VU>2vq#x&p}TfCE;EIn-6vsYvLv!wr~fRl@Ty zW|ke0<0cCvx^eRllr6oDN@35Vx!SGbBT-uF+5Q?iO=46fj3)8VZkB6rJasF3WzNbi zxZl^lvcvCZ_pd;=VwWl~O^V${|IN5m?P_T8su8ckN}Cn5x>YSwcVV^dc-rQ-&q=Dh zZYfScKb$}2?r;;OTZ0_4R zh3nS6ZFfV9s0*|Tez;2V9|zEX5L*|WY*Sv2i;FDxf5Vmj7iR481K^`kNp3;;KTtR7 zp&7se(((jr_Wr+5E<=Gpb*t#@|34AoIRGNu_5@|~{-2)z)6pgxpfPQ|ed+&$Z_8$v z0er;Hw@90BpYFf^g(P=V0UGQ0J(}^KHvR)I^QnA98ypjLzkUGE4~bBUyE=cS6b_=b zekw?KxUPI{zdrt^n&4P<18%X$M)v$hSq9{o(bZJBCU{2jvWMW^*Y)biIo!%Z2Dq^E zlb&uHkhh=_B&L5H?rZX!^FyACwsQf^KHp)_z#kxkvW=^;er@??Xln-_aqz5oV>)So zOb3%?ixgedoH%VpTRV{qvM#W-(xR*~{o7@v_1`;ILY-Sin&*X6Fc_}B`Rh2FG8Tf%2+FHT zFXX2}RqJHccYtA?+y7I5PZj$o_bn!A5NKrXjcVH-@inEHq0G+!VPf5(nIkqp$<-xs z4(=3~%Yg+}{kNNuDPC_3p7o?)`7kXlRyGdcC^{V<)ZMKPXQv!WQY55=1>2T-_`+(i zQOpgLgFBsWoHky8r+nSVy1GeTtva&sZA{ zzs-R5fjblP{`@PE;fq&n1QJ?zr~mq9J^S(i9z@`HENv41czP2olP-AiFr#`^k0~y6*4YQq><>$RyFvAje*gF@CNsbm!CN_Tq!I%GKlt#2yFEbWZiSW+0p$?* zR>x!X%4_&)pi>>6CKT5!XzH4%p>7KWZ+Tn2)5!87T`^0n-4<66+v@AUUXET*f4 zd^N{^3b^O9?1MY?Fo&@tc^O%>k%jXDMTw36YALX{=25Pe1T7+KKvk!vo+(%+vvqdH zZdSd-N&*c{psa{-u+wXqWN3`O8k61rSG^Og%ZYg-G8E)=#T}_hN}R%?m~yDU5R~=X zwP#s1)ICTq(y2}HMMoWh0*(I(f`$?W0K;01-(<~JsZN#7i9hO4K!(*>3S)ILEPI5$ zz$vUux?LhYrHBm!(^s%86+SPy87#X)fmu-WO9vuC+cyo;zLDSw^Hq?+Rh)a zt6WcvT9lJ$<`H#3h0DexdH!2WV71y4YdDJSd zQR?*waJ6~h%1b&ejSp4z!Sguf^}#8Ls}qdQsH8&SQmWO__RuPs-Qp^D_*7qG z73vjZ)5S^}O0FV}#&MiR3{z8$3C%`{ zwaU!Zq!g|@LvtlEuVr-PrR!~^c1((kyd}#^;*v!O*P?mbLN6xD`^%=-#EnL8GPLf{ zxplhFU(Y$uInVi>=lMO~-}gMv=Me=t8$uG+(osp*t6x3B!Ow}gD_Xhc{KrJXhuOJ% zGD#6_#bxO*75Jr?29=v5cn5!4oWPU4pww7aSs}~8gtJ`?{iK?QonRG@r{@0&pR_|y zMC*Dt)=p#T`cYtwF*z#9b6g<8n?yem+4sI&U=b{z!b1xn^-Jw^nB{8#|6vT8WG6(} zYuqde)G$*iHA@-4zzgKw*|>2xs0nmUaKyF6+N@Z%&!Om7#LBCnJDs_o^`bL}BpxeoU+h=3~JZE0AU@lizr3UBA_@Ou{`pEDe6)MJ-z z;vB7Ms@zvda7}ymXHmiBcDv6H4}5!tB!eZz$UQ}evr1L4tYtLBDxGg^cj{(^W|w#E zPhqw=Z}9 z{?7Jl_1zDFVV zkc{$jIQI?)cR?$*%J*WqPMUE)+_XEkwDmTA;hDim6EmbG_;^A&64bGJ6(ss2UC*F7 zv2#U5=Hm;Lyx#H~(>BLJf%(TIrL-hK&z^?kz$k<_lj4YXe%)aoJh+Dr99J_vs4-eZLs;-`}R^STJJX4#{}N<`yg26q<{9O4D<(RuXAh{qL!2OtaW zO`V+g_v1QnyVbaLTN{FSM_-5#L)+BjC$>xx2&AK($wtYv#S_`Ht`@b%{P$>V;ifdQ zB3YS|$Z*^*&e8izx?x+S`<^hEb<_k9yv+mK-Ih+*&=EhJ%* zm{jw>XL@-Bw#W@@>Y_$~ktQtP9&*c7nhv}DPJA+yz-J6?h9dFVoz#(s<)Ok;OwK(9RH2Ds_d zj3zyJQO6E(i<4?|BEO{0%NvArKlSpgId*oA>e9SV>*6C6o4oNd4)8884|~o7M3HDm zqUt06&p?A*CefsK79Ek!MI+~IWa1BO9CcF(8LTSjgprD2h9Ij$KMGO_HM0oM@t$$6 zEKQg3X@b#UT+Gb@k3qMQ#IMW47#-A-cJtxBD-9&$I2#K06bX}^mt0qc8r-4iNs;Ei zb<$&qipY0}E@v49H9Z^Rll6FxE%f@w0SCxKlaZCqHWbSOa{9v0iidcHXd9yH^a#^I;CdNzpEORyCU zVGJu^u?4sgw3&Co>f|XWp>`Jd?%7PcSc~VQy{a)xGMAEVlmOR5rd?dcwYYz9jbYVNa_lz~02c3m zqykw&Jw`vyj$V)ByV0ehAQJg%my2omwk+SLo0H%b3n}Q&M}HCC(snqpjfpj}ZBK05wtd2xm=oK!olI=owyiJEySvZsx3lNhId^wgcU4!{ zbyfAbf6B>-!9im~0|5cSeHRy200IK-1_A<(fc*0LghASo7YGQG++0XV?z@l>ft-V_ ziMf?A5Rh6>f;$wu^2Wjk@5AcQUdn;G3F8FG4fli)fOKU=d`apUfNF2UKYJ=!H{Pe_Fgpfe!ok8uc^atoJE8Wv8oDK7w zkn`X`vLZw=?Z8><*AqBf=Mj(D@B+(+0o+~zh;zbV7tRla?m%7Z!H1{tu(0brH<0M| zF9>|=A5pT6oxI;kE;`*zsaL=&PjIl2`9q)iWClKTKtOvwZmVI6m3WPWPRbdGZ&dd2 zt!jdb5mym+O>6*q9$5IJ;c0Q!7iP52dhHP(Hx+oKv{ke-xt8T2eopNdf(3K zd<@l*-B-}3#FAfvv6kNZZFcoeHs0ltGO|5beXF7hV~%VyhXjE4eJ@GrbiIkeh+~3V zFwP85s(Ml8(6=mYBlRr69)h1&l3wTW!|taTK5~WN8`rm~{WihrYC7&B;YYDCpG~!@ z^0>@XL8<&%dcCaRT&?uQCgj9s_*S{Oo$p-OyYMmuTsk>=#huJGKN_T=p{icX5p=I% z>8sY0O&?Xv0>|)I-swrYT=s!|F2oocR<{u7f&$f-&+UYA0pBKpTl_EriRa+JK>0RT zcvP7}yjIMoAV~PXtaqV^zy5lywp+A;63k#F!1fZ2CWm~pENovph9W|I>eoFQR|Ijh zZWOiBd<;Nk{_WLutG{meg<&U2wF}JrPPo6VkN)st@U(lkA#q~^;kiHVqc06331I(Q=HuaT z74c+=kJF4WupUGMr#$jftSL8n>XaXZCMsMQrW_D14IJ}M-h0_e{hmDH`_R~T!8h=z zDMKS#ewiAyqWjM2TmOD0a=^Dg2>QhQ)JT>FyAi~-DR!yY1Y7~!W{08$F-6*RF2scq8`u_&ya=5uK5s^I_W_=fgx`Tge7_!;RZePw3kmeDJB98U{$<1z(-s0ba?3Sl@dKKN+!x zZAl5J$?yzMCUP+*yAcnQpWg;vwChU~Fdw2oKM4#f0DBV5qYG0FNOKbmo}l&|6a;BL zW|M*)K9(Pw9j2yRL=G}k0ILb=+z)#a2oEr@iFF7bu*rgqqqvEZ1R<#h=TCqu9EeH4 z`U3(v@He5lIMy=Ekw8H-%Pv^6;JO?lD*V?VCDY=HCHwxQXLM3KOB zi$pNH;^~=2AtbwotKhAGxq5Zzu_A*Mw{V*vD*{hq>V1HtdL1vJvBOOGZ*oYZADM+$ zve%|UCaVt>55o=_omg7n-ay{qd7|Ef2o2a7wlS5%k@}H#B7XiR(RZNFPkKpmF%py3 z|5c?%&7^`^8h9#AUWR`Ud5_uvq#0!0f3Hto6~>Mm2}d5OIH0~M-bAm3d=7H{r2=Xp zDz)eBx8k<@WsRG68xt=ie$+&_?f~2N%hk*y3$zBb0JJhR3^YP0MW|CKa;QfrUg)<_ z7Lcz3Nm(*gq>o6EC=O5wfi68bJqQvK!`ktb@hb5h@%Hh}@r@Ljavix^**S&CxzpJW z+0ccYLe3E-$;W7O2t+Xy!m}itD7`;Sf9(BM*~YvgX92AzsYj}ZxMMixKDIdaIi_Pq zW%g&*V1_WuF?TRKpT3Ns41vXsfMqq zsJW^+s(z|9u9~ZgsL5UGTP$BBUZSgRtevo@cPO%Na?EpB-NM`wwhy(}aKzi9-ap;e z+TGq~+=Sk0>6#m685kQaA37UI?^_$>8R_k+O&mxh4=en6Kum{HiEayIOKXdB!HrLz zNvO-df!1NuQLo+8dg{`5QL@Z3EW4??|FAu=ZM&6;v57>F!cHkiNKYhASVVnD?50yC zWT!5!HYjkICl@gpPG4FTV^?PvbcutM^CNhWFoq}wSiY>Nrl`{lU{;AKAtf*>MlD+= zhD%yOaZaT}u-=GMt7+^yd7iV@i>!pKfsBNVhI~PiOe#&fLuyIVDGnvhA!#h`EdCa& z5)B_s8{m$u*3?$`O8S%t4GV1oO^&94 z%8HtRIux1^S}Oq}Q6cdp(K{SI{A;){KC?(qsZKevn7OE|h_WQOcwPRjge-f%(nZ_a zBSAy0q~+my-5!n?E><+A-&5L08evRf%$!D^hOZn)L$@x! zerWljKG9jp*}y5qSzjA;O=-=B$AgEfJMlNrYVRvMNGHYB1c zd^KV?Vm~4w+%SAasz}O8iZ-Q<4ULI3bv`wFG<=M4Y&{7k1tNJt4G4xfOe#!XQh!9~ z7eZN;$}bgn75p;evM3GLMfyeDx@MbNqf+Zso0nyo-?Xb0eHs1heI2VlO9Bg9X}wLk z@rL0$Oyn-)x`i61X|?T+#W&l$?cPf-1zMH(!!R!|y%U;YF)$|Ixm`jwc3!;0`U6I%Dx~MkKq;tE;tqO|jqWGdCv$w-i zu}QJjs5;y|E|1s4R?PAws%p71O{)=xcf+3p1>3OO!;vVFFZjNAbYFdGeF(b=vw35v z7-;Rc1G|5Qk{pvbOkaMlj|&<)G}_obM{SOW`PQkxqrf$jH{&opFWa1X%uVCS`Ea|B zRmrq=6nd06ZZPpB{p;}LAO)A0EA8{DaDlpsGrFyc#k}Bs_h})=HAm0^)Isk=)IrMr zybfZM-F?rqNM=}o`?9<8Q`*zFrx9>G@V7)}I%RYRv;wpWo%{CJ>(K6mW1{SwGy^Mp z;j0kjm)OYCVbu}ICCM2HwhC}<9J*RsODa`Ly3TQTo^}vrBpn9N-aXL^>9y4Q1mpzO zlBCjU8-V6kBk#IPJCD=AuFQz`UTeYw*u~z4X;Y>KoU)km#KPlj_yXtL{T$Xp{cP#n zhA+X>rUt04(7I4T$WcUK|L^{>g2RHo{pkH>vGpi24fuvO8)vJpX0;Y4Hf-j;*J>a3 zmLV!)GbnfDaI(so^Ujp6^W2JFDr@U!;6eWJtgUB8bq3Uh z7aPc%y}Sul2PcX`6>d0Ct8j@p`)#cF^`~j^iP`=k$srF`C>D8ApIVMu>Y_P2F1uut z&*kZs^seW)EV&+yo5o=>Bw_-}4KiP6tD=Frgz&NinM{MAjT~@3MQ)ughjPBg-5Op_ z%e9Vb1nSs|hON_OOQ^^BvniYf+(0-X3a}z56W@(fnCCCLa-U`TmEk4zMF`F`mog`h z{a2ffwvp@i`hczkegnriyM^njd+x)%BkAkvtF@hh6NINI2xf2{cnQdRsA}#!seQP< z5Fa=kXxng#uu*PY95lo;r{9M~9^ndQ+YAZpNoe!?nH)D8wSUI~(d!OaD%s=l#AYh;Km03{RQrghE z#jnLKd(@3puI*PFKv)h8$6Xcjq(^1eyYP6dPioJ_t>7#d&d8qnUJ==LS<2}{GBI;& z-7{_{>_fWbf&QcmVw=guGPE#6v6YP~jqvqQeO&hd$tX&xOVvsqey(~EnyI_H-5vd@ zUc59EWt{21zq^mlr|ND0A@Okc-nRD|Vk_2J>*f1O>`iDEQ@0!RdurF>C3ts#|Ni+# zs8PSKya`@Sw}9_lud3L*otGg=t(#HzPuj--PqHcc;itJt_YuN0wlwQ#Y_ zQ}eLrTo8|%b7VUswn+-G#?f-4lZP|k$qrl-tZ~F`_?gtIbigQl>VO7`T1J&bjZDM$ zdLH9Mi_AmBy~7=&ld@LXUJ>8U8^B<|QbnUUjZOOb8QufWO?#vp*Q5B0!qfQE-SYxO zJj6bDKEyysG(vMUi;%nELeH08TTv2`map?+v61NPJL?At4AapZ zb}-Acd%7PFujHuqd6UXku>NAzI-!2?O0P8dh{@p5Y4v$B-ZH1b^CxsCKE?~|o-&TvOxH@?xz!CB*(b}VEopQzk8 z4qEqH&zg%~v3nf(tx7ItpySy-g=LJNj#KLC>9Bg0{@e5Q1p%cz4qP#W*GxuH8etT5 z0`KMUWyo;+z;QD%x{~mQaAR<$CM zIvVO0c@N5P%c+jN1p*}^p=W%%V%wV{k3t19Jw8Y&M%dCq9or&>bXnDD>~;?}Ih;Xi zeRQ7sxGcv?%If5l3HL26LMvglkK4VC!%Mw&`Y8^K2<*F{M^9T1pGb=^OrKyMoa@c^ zED{sako*ksg%Pd9v4pTVS7kZ*-<0bm<*FDo=gg2+p%2sn+YG;xG0uItBuU4S(OHED+inUpQxyZLsr6`)+RF z+HuQ~Lw=pb^CA+a+C9jIu0e4yO$<>FqzOC_J^hIp7S1*5{HPyq$?SML7!#Tc#SmFd zR4qB6vBMbUIdDSugR~|iOA6#W3^{phW}#Akh6HhXEMEi{kHfx-$FcecY=GnK0iNmk zNZLr~`26q`Z>ATpF9VBhjo(?URrauP0u{7~LLU*Sm+F$lQ3cX4#jts_xI*Wa$Boh6 zSP4es{Kbz|=ML9%A6+=Zaz$;QH#^Y%Y_nW}IL(`Cu|y4Q&5Z_Y-u;tF_wBb^rGqn5 zA#I$s9G(}CiHC^x=&$TNkH(Ap#%qupC#Ukojk6a-OhPZ(cCw{K-6GB)IWh1OMqsFB>WhQz@k5Xc0ND^l4Q7%92kA!=0_bQ zPA>RnC(3xxO8`DZVe2P4iNNdU`~wd;h&kq!9+fjLX<|( zlRgtWHf=!2lv^DoC!%(^Pf+$i5&@Pv6?%__!A=zoJM1{7EBXWSLnTjI*K+zJMWv&7vo?HQRRLW zGRp#}eLHk&N%&GkOPW-U6>r|Q>A@N?(_7;wnc zA3ws7MGpGD^x>a&kRVYV5#Lw zjN!Lwh*JbSXBtB1*0)>4%epJht+mbC_x{}>g9Zolnl0%1~L6)D_BW!t8 z1Gc)&u35pKnU=n9oEwa6>w8=dZ3$jXM?N52KStHtmbRVeqvIpsfbhxaBAfa|4*O|D?fX9{EBGvxR^JgPBf(svKw2@M#O?Gz)_coeJA=H6*!=omIGN|a@58V5|ok8=TK<8 zl-4-bFjCWbI=TBermnA>HgW^H3el_Ox%#-)&RxTE;pyxt`&{<8`cK? zN!$c#4j~M(6P>{!iFKHo^KEbhGkY<~5M`hp#SDcjt|zwo8~My+VeEumUZcrxQ%;kU zG}YFuUxg}J3KSmT+P6(4Iq^Hoy$+4g1vt40XN?}`)a!}G92yU0fCBl(DSDy>BkDji^?4v{!VkTSkb-@crlyF6mnc!LuM+sd;-*{SxGN($?ibfmiSB)hd zU(}kHBOtbDAWs0=B?ton#43pPCb2N^PazXTtl^-P>?jJ8cv?zWM+4{YIF* zV)kMltD~)xbTU06J2`9>!+$Okt=Q~T2mMx zyeNs#Xsu&%PQIgrON%%Rf03d~fzVu5A6X7xdDvfF~uL%z;{Pd~<6z)^2M9h;aSp#FDL86UfBCW(UZSzVQ0w4r>gpOdcvu zDzwtybJ26}EJ5|F^|mLXCn6`7I1M;#zTKU;zH5tttY@s1w&lJ>iD{?zcfj)Ipng~@ ztFMG$KTQ-K_0CuM~iQxZOBEzginR=@YnFTiG~ry73VG zUk}dD?|(g}BO>^}E>4y_L~7D<1VXkB#sn<1%(V1GywC&$1l$frCY%bwqW>KJ`HP3h z%*n})la9{S)s@zjiPqM^l#YRegM*Hqk&cm(=CcQlqq~ihz8j5=Bk@0k{7a6ov7@1b zxt)`_tqs9na`g>tot=1yi2j=B@6SKhY3yeH@0n~I|Eboef^>hi&@s@`)BP>`b13&; zPdVkx-Hff&gw3suZ5%)6;N@Ut;Qqhza z1}uOm0){q;R_*7vy{sZ|c~NmbTwz>Y-gI1njVNCxUat?D2MHnxfh0%)6^QO32tq=5 z_cqb&YH~fT8=JUG%JFcczdoAI<$C_sacMHnGiFi<@=cH*5)zn@04&fC5fS7cKjikn z#z}RHt{G-;f!$_rp@v`oqw(K8lfZ-_S-%e!{ypyBO|xzwL|_Y#m1<-Z|0(#N9ms%@ z{H2kvzx|IUmJC10ZkBhdUr^A%g#VWJ&lP^>57dhb^!gu7{1M~?f_4!fp~n9ILm@rn zOGejS#Xl4!04sq2lsV#DJ-upIz3fIVoTKVAJ`3*f5I-YOx&-t!fPi)W#R_frlQMC)Kk= zbAOYqjowfau;Q7AQRMI)+%i|l;k0V$q$Q7UKxW#SbwDWbG>mMK6f+;?&&BPbTn@tb z`Ca+)KcwNpcJAq%{%FIneN74G{Lu7U^F0r+Sx-c&^ zxqAGu-P|m1wtT}NmF|XCBTL&2GvOUHtJ1jDS4=F==ZtUi?u>2F{;WTZ6ZK1?H&r_2 zn698R$k>yXl0uqmBb4ubkI>0BIv{T3BPbEyD7Tu^uknm&HNWJ!EoHOhRuNz*Jwpz=i$!8uUhO{#k!* zR7V!rW*Vz1CEQ(cJKy#O!Yg=dEqAoH2YxIqZvQjBZx29aZO)B}wtS!t!{>{oPwx8H zu*3r!K@B;~3Oq#Mm7uR~A4d?EBUlJ&uUA-Y)lMkm%&q7Ah`FTZ0%|Bdt zQ~h%O{ZVL5>ecQW^vce7&e^>Xv#~EJ|HYy&db#VV3Z%vcTagl=A<~-#MvR+V@8lAb z*Y}Le#Tn7-|j=%YS;2V7KR4YEQ97Lz$+0}~| z`tr^gRM*L|XtyiSG9Mf?a=+Q<@vM0zF6}Rtn`-xh617E{SztEhU-w%EeF}^VN+Jd7 zk2AiS?sxDsL*lpGWitI$bC%H2fMoWyYJ~*T%S+VF)!6e))gIW^eh1C9)XKU27v1**(l#c~(&^QGjw8G1`6ehYbt-EF z{>GE#A|~=kQgxaZ-O3ZMm0N{}Q6gZrn@f07{)1>lzJtuD?X4>maiDwU1ve9*G23^T z_sRBe+_b{_<5YB;nYO+^5WaUs(FI*pL3Yop5^(*G-4W@sX$_P@hHuV`!9N%C^;CSC z@>p)oQRcTP$ua6RT0U45ASDR^v9Px)ZvYK0wTzpu6b&zgsBk|N2>-0yho_fTpZSwJ zq`-6(U%cKCJRK(iyiAjlL~o{R4#KB~(opr4gsypOok4IeDlafx zb1iv$#owpkJk}lloZsCzOU&vFWQ%TmUWeM|Aag0 z;YxXaDvxoW#x9jv-Ju3p#fCil!BIyHw}b)m-^wY0g0Ko*)_am{orQ>ThO4DwZ#)mT zo9%9eyi^SJQ{gZ%99(r0_-KI{@Vx+I8I_zm&$v0=3=632_%zWRcza!JmDhDF1U8Bk z@~-){SC>pouE3-Snl8ZcTTXv^bKh3^RPNvkyAaHJ9?N|$&{TOyWWdCBBS6}A`@>{4 zw3M-Nr%*eif$S6@LHT*td4eRbT0*d z>}2ccDymo(W8JRI+}e7Vj&E?9OUsO>Upj3Wpx?PVKbFvx0o7vCNfll>)(L}OcLBS@ zHcNx)v3`=uehL(G!F<0s8_0d8XVXG)dcxI0F5BBQZ!$vwGTAMsT>m?(Qk;w$Ha zN)^)#Pi6i3_U7P-VM%Q2T)GOEuikPMk*r5$y%KY$b{T}{Sjj-QbM%A1&ijyf_51#$ zPN8qNXXPu-hM2Cf-eI|$ec8=awzUgovuixhA1L65SVReO5KE^tSZV~f&?6Y!u;m(Q~OX+ z(7!ne0oXq_DGT_~5`~gjzs5vW<@FAg zRn5RnjVqQBlK-Bh2p$aN^_5_GGxV*1 zY_qDCOu}Y44+{VQB(i8vTbNA8H+zC$AmQQrJG@_J+gn{P;LigjwX|>}@pwY_$5MN7 zQ#rS)p3WLKsx0OxOZPK8&jsY<5P6?=QE=JqfGig(z@v6bN-JuXn+ysif;JzGbqxw- zfw;vl#~j3cjUR3mG`7$j_w_>#(A*5H@E=UNTB+XQ#gXCr?*96GQmIyB^i#c@ z7Y1eY;e0LiV0E?G@#-))Y8wE7R|U4k<9RJaNKC{CZ5>}&P}OGhdbHDW9Ak028Dr}vTcS3rbx90b?|i?jEWt;wQAeE6 zRa+VHzbB63j{S&74lI<-j0}K4KxbE752c&}Td{}MrCyDiM2gBbT*IUej!@e5pv-Xg zQylU6{jC*0SgHlwI^y|SMycXYWSac~;?l}9!K|J-9Q0=Ul&LP0#ts-sq@x1Vd%xW4 zb^nr1QHR{;4Q-8l(GzI=bkSo@bQ2CB6$yz@WVfZjWa-jor?X{c@D2qC_*9R(<8R%` z;0}t4qi=SX)U1srfMw=Nm!`7SR%IeE)6*rtUOc0Z*Iw$jVa2R9r2@-^_obd;XR|M45qgQhGe}(5OS5k8Ev3?X%6f9I=2x&>vM3KXZFZV}*$fo@(zGmnsAsadAkMN}cl2@Jyd)bElJe>12_Q1HKP|bWZ1Zlf~b? z8e2UTp2x+7qh#}iSTQN}L#qg4p8N6A)Tp)J9eujf=uT$}5gM$P4AyEL55^^cGB_Ly zkp0w?T%6^oJ8VBSDMI;yJ_hpiUUg8%pXE*GuThGX4a=)cr!N91#t z(vpLW(Jhw3o2yRP4VW;_SEvi3M>3;$?lISWUaZS>J9VHs;jo$IV)Dis_Q^Lg-vE5C zmqMhF6Dcf`^#@84)Ic9cvc7^Nzi7txX=T`}o4;0*HBr#m??noqll>Fj z{tc*hd?3_X1%-uF(lihq+dcjaV$K~W6|M0{SFcYu33T+9KU0~_bCywE(mCaOf~CvU zsjSvZi}uUfO+I##k}!=0$4sWvdVU67hf)xMse&mV{h9sB!9m(x7JtSb?i7vr_jRwE zu{ph4W1JkwZsQ#?)jzqp1uXD}pRCjs&GCI>L|@M;le6mCJ}fq^0_A$2db$cd`y*fw<2P z8S57BSjqZFd+)>SBMXgn{|n&(3ZJmvgj$tXj94TNq8XXlw`+`Zz3LCX?)e>vb!0Tp z`>_22g%mc``OnhA%_nPc$w;U}1woV890(ZjyTPEgrcdR2Sm%>nn=#bo`tAG*V^W7% z6seB+i|D_0gZ~KX2cnjTi9--#5hBRVca+h-*)JRTOqs~$g}IUjmb+GbAq}-YT|5Gy zl)LNG#s7bZFQWOJyMkt;Zgl1is){TL#yr4QVDa@g&$9^xBS6j8s-tM4*#vAl>^}|3?@ZPOayetK zyVY0Ag$t&a5%X17mpgQ83V+9HK8UC=>3d}oug<$j2HmSLS;M%lUuM)YG@thRa@fgL z<6xy7r57w2k3C-tN!h??;lYl=yN@j0z=lbMuwY^xYXpCzqoxA@+# z|Bs=ai*LRJes$v_EE&wU&dn-SMED5AIvYS;?oSPe+gX_Csl(zzOx%ePOSKr_@Os%# zedO5Z9-8$zq<@xu9Lb`7eYg@oeJFTXcp%>lvV2FNi?roGTc0{Rq%CWH>-1fZx(`~f z-W|@}@P_g3OGw+X{%Es%XOYMj_v|wDa=>$? zIsC0u$L0Tvp%!d_xgiKiu*>xzp<$zzs^E2Am%P^olp|359z~Q3Q6}UPzQ+Npw7hcq0f#I~5_BJu^|O zkQa|1TX6~G*K4#WP=zvEyUmX52bUM9bLoW(dIPZuv4GRA;389{<$#nN01rwl!rmOX zr&A|ddri&NURX@h$5~+#BQZ9I_m`XmLsR&d94Hq*!dAhoVaO!jZaBIkJv9_ttTN_~ zIEXvZm(AxOcq&3o=I$W>7>WM~U9*B9nWTzR$*~eHoCeb}*>S!%AT&%&R)c1)bnosH zbH}SpAM6ogh1Ojac^g+Zz_!ddj&xsK&0#$gA?z()nAB>{>B*$=02P_EBByh%Ikw~o zB*qc_U%frH`l$k9enm%CrDax=vjSso+jZW4b-N3SiZ-A!6I`>rZhcf#)wQ%r&SG#r z2E#8UgTb2jIggC+_tnmM=SIn`@*#~DdAZ+(DX~7UZ$1-&95*KP6wH!U?=q7p zAW<@@!wMD><5zcu@>YOf!Z*2w}8pEe~2vXf$F4;pm?ELTtEkwy2Y z`AS$_Tc^PQKxwup%z|cNvwT5^Ca%CD#X~zZIUM+?9=0#yJi-1^ngIA49cG~Vw zetao}sk)rxU&Tmlc{ zC!~S{=mZ50P`yeNqy1+BS(FWCQ?U^lxcO*c>gveP1m+I@dECwog;wu(x>(?5-J`+ltd0ZRHC~MN5vI!G0a&R>QPM^EBDeH+XDw0bI)U8s^0$e;*4kE{ zQ722Sv=vM1$m-skHY}VKf$4QH7i|zQKkl-nNo75pcv(Mr2~;Op=6}*|Idd?TzBv97 zQVPvIAX@pNyOB42JxBEa+ATfd!Gz0OY8?*7=IpBbV(0hum42oh$JI?!9}0YfUF#? zfIP@j3;DGZQCwTihI4scir~47?e*-*%~&(O<%>Vw7n6xdde~_^Ec2Jsex4g9Qv_}9Z_4H$PhJ+yHB@~ zz5+mk^W3leWk@JT3sUYSRMmJBaM<=>Xk=Q5c6gH7Kx`)|{s3|piRPnUUS++1vnZax z>PcVW=|I%dgP^VJ1$H$t*01TiR+8G-hCZ>nkip!=?{?6l*68&mLpCGOR8&lFtq0-q zw+ZJKmVYD-PnGWjZdvmx%mU7JIYdw?_Ei^XOJ;0CHrv+3f=#z?VrRg>uM>u~`&Cdg z(FNmzKBD{Iz-a|~8tLb0{#OH;eFqoqdHpg53@wpwVi}Ork&QJ=#q26d3vWUp@PyRV zWi9Z!{o0aHmqLuM`AY;a$a+eQK2V)vPoNkfxg%m5gdS``fQ!^nEYKH2gPZO?mPE(D zafP20#m!*pi!iMr1Bm$J0twj5!Ix+Ew?kPq_bqxAC-1gcG1A6T8nAIAvYmyzH#MQD zoQW8E?j{|~3YcR#Gy1ZgWFsOL7L@n*cW@cg`~sqxbA%cN+5#hg>ZGlM%G&fidEI>< z28;x|l0$~QcKVCxoyuz zkN^QNxF)J_^CkZQVm~rxChq!{9O7>q`*|YiVHEUY#L|B|$AQ*OmC-kMo}czECSSA6 zV#Fqt3xoWW6V_psT=Ln`SMPTmSyH*QILa#dWHXRz?@IkF3ddQ{+U^aZ$lq>l#{^>X zEF@zoo_~>2S;cr(SM!`QRhC&2CXoP(03BHUT`C{^^Cm7U_Ak-jv-Vg83HSi6ji*z} zG)H2Y+s5oW2x)}~sfC=}F&RmPEky9Y`fy8s*yPLp3#(XCx@J4HXJ5y0voHzrfZwUT zV4oGDi6(P>agxO(he+uyuIQQ}{@TK%&-*nrShW@m5D}ywt5%TR){G;&2(SoWe{C9x z_E`kzzB+RSd)Hys2x`uI%G=lZ&I3$OoW;^lfnMK2-nt&{ z;!V^t)I>_GX?XJKDqj|+(<+qKP_;+I*%_fB#GvsN&XFl4Svs3cX8ff zROs!uZF`wF7dSf(P{jos2+MR`z7{Zy8tDCuncSH2f094IvUHaQ^L=T+efJ%ppA+Zk zcCHxG{Rp&7PqGU34Vt#Q+7EyHEto6OxBT5KV_o#8UOps1;`7zGz!M3~z*RxWSym8R zn}}&en}l56^gD?C{$s!}7e=jn*sfq~b}7o<>ZMQ`OP9&dS7Q{S6I#WBmGswC1L59` z{clgG{{)v!bTD>sd9QwVDJ$Nl;r`mC!B=|ryo^xDh=w{luYUX?Pe)?W((ah=xBFos zlcgDs3<4!qg0yW6zI^0N4gnrM=}j0^aWbGGX4QVf7J>XhC~EbRg_8WOH-}Qqt6#uH;-50)zujI^6jX84T0av>v5i%RnV+~(8ub(9 z1B*KHWg+b`jAtdBdhfPL8E-A6M@s+A_4yM}^d$3FcF=dO9(7BZ-{b_bzc(roiNmz9 z>qAU3D%KL-9Og$wfs6tr{>-2_!m6Mv!_sUZ+qp0me8oP&hk)so@9l{wbYBjBG2CAW zhZ>n{h& zOOBK&wS00%lUcm`Waod@8FrLKMt*P33g|5QGfV-?KG`Rho@8|CW~%7Fn(V#0Oi5s9p?H2EUYgcY-@ z#>jdY=u1p}Ls!BkyXPrD_FjdYm6vMt3LVQj_>k~tN`SDnEW%q?Y;m5UT1Yf!c`>7M zwsA)0S^NvN3I0kGiwr+&YeEhPBeA=X+}@>@YhEA3Y51@OvrG zlT;HdZ_4Q;T`Z_SOUst0s#GYBsvuLXaEfM(QQKMofR2%k*8H{rp#Jnoq{4+R%>I&( zF>ou^raru`za+4}Yruu(@V(lekNWGf?81i(QuE;S$xLl$!fZm?zlvva*GhJx>E9d?C^U&Ipw2urc zvv4}xoIc;w5OZ`&Z|?~Oc|&km+GgB8wrOfl*D)FhGATKyf=(jMKLwkvt?9mS(v5~$ zUqbE8sXSeqYoZc?tgzQDmKsclQhRa<7##SO0Wh8h;gYJ-_4ARlYIj@AW_5MF>dtM3 zs54#TleJzCh{`ok>MK7xp#V-D`^7o!9S?U+4)>>U}{NiJfrxrOdr$geEjb`RImI?zIEJ&MT?d(I(5;RmpXfiANlgK>>zy zyFEx%OA*k8<{blr(2)w_L2Xyf9^?#$d2P&9DFc5RJ4N z08Zb`NPSe4{f?+kp`8PhuKRNk+;+V@^l!auXB@4U6Cod=ETn@o-{niIUmJfju4X&X zArUq?xR0z^Z~#al;Akx4tfb5rb4NAwBH4#XU;KUi&=bgpyqb~(*hhaP^m)UT!k%44 zb!ZsAQM9Wi86Tsa-P&L$e^*R)^vLD?^>?hKB*+5Gcgk{IYeV{_PQc1iMaiyA8X?_G zc-8LJ?u4hu7&MOmTH{@X1bLhzGp#TaU?|N|BRsQ#pTjqFn=G9bk_^ml4oGLx1$&8e z@$(NUb_iXL&Bw8P&S#hP1K$MJrS6l(aRE-wtD}c8J>o=-n!li>t|hQ~sM^XIgS!MdjJB4afRguaCy)r~q@YSxZjR<%x91|p^j zeBowcVN^-!PX6^&pJje{!_Z~j{vAYjakQnb4H7hb*0p1=Z4Xw$rvx0&R z3j(e_WVb{IIotK@a$;y}w{z}b-#x8K1}=r_t2kALqbae!%I!PlgPGA5H(x&HmhF)@ z+BkxapH_2|Ubjtu2wH2rrzjkbHh~h_WUymKy=v zLkLN)0G40a9qj>Jfjy`?s(qPMzdkSuIvLGNnycKC4XblB@TBEHuFpqz1;<9!l2kzI zclv7R{)%0n|M%$%0*eo3`-N`IWbNj9^d+$E7TCmU&2UY?T<<%I=4|b-h1zIj{t`_= z#7?m=F%PnrqmuK2A6aDKT*n=C1TmAfp%=?s`{4CS#C#Y?Oz$&Q#KmxRFMYCMM6`;V z!iE;1TNBbF6K`-IIloj6+LuB$$A_=qs?Rds#JaF|Vuyz+3%6#Z#is5H__m6F630|= zEm-l{%-^W_5a77-yrO3K?25V#d>~|2a7O_i!bLAOt&G?X0bC)jVrx7XTkUfYQzo~W zm!pb1Q*hO4k4ww3Hrn3~J`IUF>JYcguZIT}{fdYu@s5E%!69X#Fg2;Uv&sboT*K_Lf5x<ldL*8raY5uO#90U zj$@hTuK^Mdyw8Nx;k9fok%=o+1`}QNu71a8!$ZGMhjqC#nbVh1=%pKdr70zSx6d67 zDTPrzNkdNz@UWKsR{iwT^LkW*2C=HyX_{2nDAv3#N%&w(ak@J)$&i#Kdyy7v5nK0l zgT)fCP|gb|vq9J_=JL!IdLT@?zvZ6>GWEBDMe-UmTV>d~Xu2wahqPWn(&um?=_kt) z7y!NOaMPgrm$x9k%AvyRMe&@5n0sF%+R zU4Z4dNktpdX0MwL@9`0T|Mv4x?G;p>jn>DqB7VFuYlJJN4g#5c|N>A8KiIJJK2@^gsq{_RDnpq)oG)5cM(&jeze83a4&87uYn1 zhsQv#l*30v6R#>(?93K4*rIb-mnh(e_H7wskk2*#O!dkPs1EP55v96`o6+Heo~JnR zqo{AiSOLB-JfA{Y(QI`cqn6>JA)r9N&%0BC@>wx9lH8j!ogef$xDAr0urgmZ zx+{XV5t_*TD9+0 zCRZF{49on!$MAE%PfN#&R=RqbjYvOlurn9E36V%;F0f*m?lIN7xI{)OhlZK-oO@oLjsSIqz78q{EIut`X250uFxE^q ziT9H{#(>y;%mquA=L48EWHlV)oeQScM0#LsU43vkEAB~RwJfpf_JPdpF`unyjhcI5 z0V5+3_B*HU$}n#kA|boS6o?fMia(O;MzAGLTG#bNV((RF!`FeUqcj%w=`5W7Re=s* z@`ue@aCwXKrNr7JG0DzARaV&MOhgJQ05=E;jqRAZE5s=NRk3CcL&bMr+#d_R3kAM5 z>K6$gEjo8j!lMZ%vHD?JKsd>Uss4vac0w>x7;rWMCL&6#Nd+3m6t0HmH8`edIL*tr ze0+!G%$))}yBv4G5S>XaF0C>iwoWhmDsP|skoGhE`iE<0y@L6O=iPDyf4iMgb*|?$ z5>PW1CYU*t5A&73GiLbN03z~B0mTX~&_znobb!;5&!3o35z7BQA)?i5om-ihm%}U> z`bWL2X*K4IHcZ2~)VM@~(_vncFc;`(_SkM|3($K?vygMfT5^EL9tZ7d*B4){sY==^ zW6ph1QV?@7hL4RfJy3{#qByfuxI2H%oNQl8`0{5+K2wiwVkT9ekrpRJv59=bu-Ty{ z0NfPwZ~OyoVCN{t;2nkGqMPJ;T z#vrw%xiFlfAr*lrM}bi?LSLpzJ4L6S4;DLSrBsYM%!qMYb;|HdB-yOjsaiZ4*Zz+R zi)3Z1T?@VF%C=dwl7TkhsaEIXT?h@SH zT>}Avy9X=W-GfuOyG!Bja4UQ7^V(~--G8ub&N)W!-{@=jo2a^>MikXh=1AB7^^wkK zMuxLt)3+V6WQMbO-mOl|zXVMO*DLNdrpeh6>peK;sIrTGVw`c;Q%Q5xxHceB!BY=?uY$5 zgd+d5MaTP{va9WJP{q278p?LwefE~i4_bqlLvr>uGj=GIh27*KlA)w#oI(XL(IvKK zlL?J8rmZ<)y3%-e6<6cK4rdnEJ~0fF<*(j6O<7%m<0+q}r>=;DdjH4>1yWE4Roymc zd-8sAk7Li0niZ)@9n!9N+T*j8whwVid{K%6Z@fO3k+D1g*fhmJLKio#@eeoHaG-AS zB!M#O7$3pxU+p5{f{DJfTN2ukVC|=+n%nsH%5!a_{nW6;W9b@biyKJ0MySsdC0F{z zMF5FEF!r;ZmN$$y`1j0$P8ywhYqC1$#}@88smX+1E3h#>p#_G}Z`B+tHC9bJ>0H&x z|35inpEM8ZJHF0VS(s%b_aTEM@j;k2%A92``{<};kA`e9OfnC2ECg_o8)v8mC(Ewa zJDiu3&bT`f{T9$-?#6$lo;6+^pP0X6gJmVCuSad~t^o5_hE)DdIYakzt5^c9tJRZ$ zEI!a_ulP=uoe?yv)2BG%eW<|5% z5E-5=$uS;;f4toAe`8=!wJhA{?N6Ye3=1RFa$-T^+ESv3F(#9Da&sXZ(9l|w$m4<-!Vm@~P z&rBWY=Sg|ZR2hf~7hHz`*(so^V)1k_#Q0t8i`Qzr$qHIvYfhN42Tc`j@NC@HNvo_A zM9kDn8)VCFoJRr2gz4D^R>YI{4xpW+ha|!NT_;y;O%<@fljMMVa z2XCxv?btD90~ryq5N#Fa1KW}Y`=5xXC1RlUd60dEO7oy{ezY#+{y`(cuUK<%pgY~Lho};-ng!i;=!=|(h z{?cXu@%@d&8d&hi`GVsc9JbySkEm`Sp;Qj*p(DIZ$7uN*hJpIj%BN?c{z+ zwxpPuA~Ab2&7N6Bi(bH`&tFmEt)}Z4%LWz3JF%Jm*1O*CR=>+zEG@S}Yr&MrDbhm= z#e^Gt)&NTOqC?rNnCe14Ea2L$i9lu)eRXvIaA(*|g!7qDmJ7IGtGKm}u<>r{O|N|U zJx_{d{6j2Tyqn-ZIA;i<2h_^O2QKwe9FUb?_t5jN_ea_0;e8b zfr~2}#s)g4C8mxH$>G5q>$i1|j&q%!%)ShqZrPD!F_y`h7I z@3OiAN6lvVVbA|a%=78ONhm#R%^&Im6Mf1A9c1TO*!{tcy|GX=pwJxCS*{D^_W1V3 z-$YNag#pwYy|Qv1gOU%IS zy%2q5GVbu_d=*t3c*0_uc6F(qeUTdEpqBmniOVR10+p|wA2;7HtRuD;iixEj{U|t3 z`H?dvznmW}qYZU`B(01O)+NR`4GH*ReO|CID=C4xd*0oc@E>L8jiYtL?n}@`40?r5 zLQ7HzCf{v~5yXM$xvB6)CuzVpD7b&BFbL@xBx!{-EckD=fr$tj^~u<%_Xg zui7G#=Ib=siwwRSm`f{O$cj#zRdUqkwJ&vp$&?CMKmMNQLZtpMUd*x3no-lQT)kQ3 z6-V)1E9$>zt{QPrbHHZ7CzEL%d+94G!p2_3%Juw)-t5j3)A!oKqnC63B1m&GJJ9gS zJTw*aD#ShNL12oljKK0c=9HKX8>8x>Nija2k6teFu!QHg;;FUa>v+oT)mx$}c=Up& zH_>#F+*?(K;`<`Y0W${Bi<#XkjQ3=LnyfNy$=Fh&9u+hK28%5XVh)X>e5ZTJ<*ni8 z?dyHl>#IA;XeWK%n2gY}LI0&YkKHH8;})@Uu`bAW34(xMp56u1P?mhR{`{#g26ppf zv1pF4P_{dKvBYTr_L0FevM|KnSF|QMZ#tu`+Mj>I%yNHt8!6f>!m27b4E||xU}0@Z z^hT@2Sz;gAfgN3H&UFUT{OZvJZG4r|4X&N$L+o=BJ(pZ zJ`1!7lx6;};(mF^sH3aftsAMzqD4U1>P=8E^7m;x?8GTo)8W;W5`G3xM>d_sszE0& zXH>@s;qeP`tJK5TcdMmpj>yoh*{;VQhOR+}S-TKPzvbSC2=g$E(~t0#I;fVeRvd-w zmIu%aaJh3z!(XB?-G6^h`)r>kg1cXTb~?x1hynWrWZal@nS!^Ed|1Mel!-X6bD+wd@3Y?IIRLEho6OCQMlq2^Kr4qfaT5dCYS!C z7Vn2m-IWLgYurIcXU8b8^+}5YjLtx%-U*89gO8V;8*|>MSG!f{&ya3+ljF@p*5T3( z*AARe*;l^V(_uO}@AlYp63hYs098 z9Byq{Qxux~3-!$1tEEBQWt|47Y6ZQYADLV=Td71an zMR9$rGYg&N@(d^?+rnSqJFBvdnGluIx>Fbevra9H=elcNKcRj!+Wua(C}&`>7a-)T zYL#4!w;fGKMZoM!g69w-Sh_1CN>^1qwzTxP8=!Qs5ec<4?~lHWq-avxXsfaq!DP-4 zGvoAMP;Fde?yZ#}r7Fp$Bo(Qt|MMpsjRlVXJJl4u)(c4dINV-_%y28nk*dJiOH)+# z8EV`W(e|02EK)ANOf3l~?M0)v_9&T8!JhZ;qwMP|O9d%O8{efL{G2PjrBJJ|?`oHg>m$1TQ; zQLn2iABonPTqxoqT!Dqbe1w<2DGz|)rly-~Ped=ud<$;YqtPL_5c8N3gVO@zSxrjA zv1l~u5Sb?8Rm)Wb2C2bhw-iFPPg@Dh6@fTp#Z*g2?QxH3C9%6|7FmHVbGLXeRwPww2kDC zraR_5jO;GgU?q(bMDwU{)P^{Y<6yE$`gWR{3tdbxWcr8m8!3AxmCL^@fBAh>HiN=@ zP;SP^QUbNddQW$@I;NpDkpzb^(MrgDkpVaT8mp45jp#4fjT$*qJhi|!gNH`h@YB+& zL-MQVPIHnfWkbrWNDmK~G3OhS)`5opLj1!cPb2CC&b5CBubcA}`L{i_IjcVS0cGQ&KebbQTS$!7dy5m>*Zw6Tq3pU+tRdk-Fk zmqCNPx<4@Y^$anSdlF02{?(F_aa<`?T&RXmmq(fG<;)}l* zAteFZJ2#N=qGU=~kd-hKYC2o++5O4z>dJQX?Hf9jm8S%JPC{eUM-`flT&63&!NK`X zR=>+a0@%UiHq%eHT0S;y7j+^iB}}}-X!u-Jo?%hq#=aZpTB6ZLpFITB6KuZ^P{0iw=pxO0}4tZ+3GFm>xow zcibJLBe^-c*S}??IU58+qBYDW!2pZO%a+11rH|B)zWYHWO;pimOepYaHzx>nymD=> zR(cY~U|}SIa#Ee)SNk4%#~0wJm<3qX%=I-clXJ+-EKpKUs;M>c`hvf90%}&)wC(aN z4W+-y$>~ae4dm&rMHly)uhOb<^fXn25eh)Uyt|MvP%OVH>xo44e0lEjf;|Kjq7jMx z+N1zGvcKT3Z05SbUU54JWk!*a0(}3BFMT$%u3YiQAR&JaavnY&r~9YV`)Loj0IMd} z3&_K_-2lS)gwfI4D4=j;0-d-H%7+DjqHpq9C8wLTd1cV3;ZtadbK{hb)$hjWZ- z#8Unvmre1^HAD}vC+uo0(7pz)+iKcb?z^d%K4>{>gj&+Y##gC`K>Fi3`J(v$R**eW zLug516Yt5@@CVhx>5p~4r6+>|JW)lvU2c(v^A;`fF@-R>=M&hDF%W&Tv;2P|2$||U zvMaqP(dmHg0V#_q4hWlaum^4IP%MiRcqb1wDMVFW}`#k5uWZHxI3$Y@nl{n-_{3(d)?i z&?l$fx?Y19jVAt`vZbNDj>C~HePhj0`VFz01A@fBuUu5I?t}qy%;KqoTIR*k=zZ!@k9N(x1=7Uv8RN%Jo%y+i@sG@_W9IE_(D7OIv16E2_q}8HT$$c4LH8_BSPV& zbPlY~&glKBs){}-f+CI4R{+mkr&1)NtcMA+Q|2D_q2e%gs0`B=fK8N2IrWRR2PfiO zdD&rjn;;8n_rJZ7`ojH)M{5A4CS?4$Pol&wh-{V|>DEohQ#~xEGL6ROb(|#SAlg^3 zzqS3Sn4XBuRyqJ12dckgn+Mgm?l=ghX-;jXpN%vpJ-zNdjk)TQykWldFVCbmVY4Ce zQ2xhqcY{v(Qg%G`wcK_^0~bBhS6wrZqxr{Ezj9hD|7JJpXBZw__DAN<*aq^sr(gqL9`GU#*Pz`98KX zldVWS>lu6TU%&ELFSgmay|A3Y@)2D%flRJiFPm!b=ZFPs2EHQlDZwYsX7^HZIJqZ8 z19Y5A(S-yX_l-x_7M>(v<72C~Td!CACNiU%m?Xp)zi2B6o6wo+ON{Ltw&jD-=U*|F z=_dw_wwoUCFlymCgD;~tyujE|N?@#SjQx;lY6%8=G#~?)bU0weSHt6Wws;XywSj5{ zra7L;8vP%)FrBKOS#13-4B(kXJ=i^IP#P2bdZo$#)1iNejx9f3`X$Ldt?}>li<=Q5 zyA-sf=N_q~8HWQvz9Kul92+tbW8FnN*%mHoLQijyvm;6{p7^Y+F9{uS? zfp`VgvRr;j!oPj6HocLgRsAqXOco(lqRX(DmYr)0H_wX#w)egHD$;A)nDUhgJR5sW zVx!3{^$;Jwd#&zi<5)-WpU-L=nv-yrXE~yg1&5&Au}ii3q^Pl%Jse)2maYyl3eVN% zdO@ItRT$B|*uAmkJR)8k6BlRPv|h3F4L->D%UWm1O0Mh*Xpu$>w|ln6ou?Zn%UW#^ z7<9vHrY`2_mv*pP%;SOA&hS*H1T;t7S3j6p2+H7vBW{>B(uU_9OF1yNHL*&V?kN*KM*_4!L0Bp$F0IBq{&%YfrcYc<$-HMK7!F_0DzO!-{$yb~o zEvW84s!E>#!>^C=p^95Cy$sFj1|UcE)E~bKS)H@WNV3B!+8Y$hZSts4W+v0BvLNU@ z?MxRfoN3^FuM(Vm&6TNzKrvVkD-cwDvU>ZrhE^|Z6|bFX3qy284VeQKn-;{awu`}; z@;S>lf54&)TFY7IUkR*)6@w?OV`8TmL}VCF;03Z`1tT=vKHqlphq(S)|}D7W4NX z={pLR%}d;;X^Ck9zUkS}!dsQgNS|)~N^+C#2d{I*lBK-Zub#?|7NiP+cWDD#AisxNBI=eG%0=r;FNoIBoQ@6Z}0R5hd!c z9*aSfk446yOUUQ7CbTv|==MoHXp|yA+v4Zj-rxz;WI>UZGDx9g?e|m^0p9;Mh1SSH zpQLq$q3~>56O(a-*z~mL3TO|r4-Yju4APkCKJ5G%YmV(rGd|z6$mM5*@Y3a{gS5~N z0(14c_}f+eQoKy};1n(uRPHCFI`;ItMc&#{BP^9k_A7BRBtpOvq2K2&XZ&P;vVwh~9LPSD3J5^?6G%;1 z#P-u!w3_m!o$_vOe;q9JCe>s{6G%)SA|Z3L+ATQ;HO&0a-tV!3qK``)KuM+Yf3E-R zaOhkN$>h`U^w2ez_`GDCX^1#pBIl8rIZU#|z@S#%6Q^d30Q9Zugw-}Zqq;fPB9a=j zHOx??e*_1@$=9EO_9WUxTBAzLp^T>qj6k%1Y0nmp%q&AXL6?E{eb!PoL?~ z?7#+JV-`bTWi2+YN^WYG#Y;4zR-UVRoLr$EV%yV^5c8WA1!`qCG_RS)f~~OU$H9p4 zys}peC)oFe3mNRJC_|{E@H9}n0jQu*ii%71OrrU$RPe^ZBbq3oO2O&WidLC=@3EIqcLEw|rrjipQAc#vFh7*GBrW$?Cu+Ec~*$4)^~+tfv?SCUo| zY@X>)f3HOdbSy>dHiG?9cf$T}uauxfE=PnM3n+~@kS4;K(-xpvntRf}Zryg}%*<_( zEaHwV0HZ#AFHTHR*IN4A>i68h2o9!_UDV#}`7^NSuUBPUnx}5dizDLS0eL*y;idHZ zK7^HzQ&r0s$4_nFcd+&bg!O`ht)b;;9&?!!A<^@GuPRfhN6hwwCi*E>4I`MA%T3#m z7lELB?mtMLHGa^@7-y-}TXBsCjY8&XECL0xIU|jE+gGtC6I~etc@`TZVAh?YlKUs7 zVh84u@6HXjJ1(zl950#t0qF>&R(V5mOnWQYU6#hfR&Utf3)<*>LMAau zhWmbWa&TuR9lGb+NMZEOO+f=&oGWFV=!g|MS~<2hkP`wW$@Ex4ug+l>{GkRUD~P{4I>Ne@UUT0jSX}OzC_S;3(~ogSMf#4 zWv-Oqm>Q=c{~tlK{}e)KPy?7sB8I{5XRoTw!~d0=U$3(Mk^u zHO}1q&7Jjb{(<;6-MczmqVxfJA-1k}i^@UK!A1yuyX{eEdRP>i3vsO(&K#N32|SHk zP9$sJUNp0tN|U%$@SzsIcupf?h7d);9)gp zj1Dlo%_!w_B~SdaWxv|mAOS7nv| zS4Yvk9@6T&&-~a4>S<*3rrQ$HuUtt``j1Qfbk{aHNV-CvdxYe1UMo;NE4gSn0d{>W zRZR@9t^rrMi9K0~^}jyRW(k*WmJDjo1t8N&$bHj4jtYAdME**b0`X$HYDJmhXXo+y zX2AMH-hCFz5Gt2j4ctIz?cCqZzF5;^bFoRc-Vnvi-EwgOpVQw`MK(D+aJ*Z) zn3$d@b-}(-0tmzuOz-AT_W3<=F(&{5)N^8c7`MIBsjQsRdXG01=0`~-9B6e`{tCkW ziHUJ<9N6b_SpY9%v` zqI@IAOr)Zk##Yon2ea$of!eKoQ75dW^)MM_ul4eq*DSpDC^zNBSa^>`)u76E@#B~A z>wWD?82={ik?5WSj)Ox-mv4^^xZ{NV2dxQ$%ObMLq(XP-a(!*>SXYSdsSX9+zeK^& zEmr~~G}&Kkj{oAV*Gc0%L*o#A?Hf7&qfLqSXmf-t%r*mk6D0)GrUF_0>!pgdKuFRN z#bO5QA^VMJ5U2~7yeI3e^P{5TaAz_pW&KG-QeU2_*(H(h$MV$VBuy>PlDvC-IA8L= ziGI@5Ks>j!fRcF=V`@fd*vzK|m&}Ez&hOu|=lLzD&id&^s~YsYmNFtwTGD7?=YtXn z=#5M>Qrr5zLAgom+j4H!gQnRsWxz};rM`;Q;Q%vuXk{^y;3t|MMVCZ+s5EH?#z?sc z{V>tnuO8F`=_glNsMBWPf!;w>H{PX;IJ{Xm1%}>Y3?86R#)aJyaI#R3jOMQfJiJFc z^vmbQ*2q=rXK%%@(h?k|zC-Z#4TtMZjx@9f>lnfuWJVH1=J}dMiG@qVmgq+C0rNlb znFy#d#WD{D%MH5Eu4p2?84QhA{KgO_0x@V4An5_J9- zxoHRY^s}J|2YxVI@BGnNLVk*z(L5eStuIA4tH2kTu8@XOIH>9{lmc8Lgx}%=NS=3I zNwM$a!SvNS)N=ThFxxdaOGNpaJtbfr7;y0f21xSR`>?AtBoFu zWMXMpS)ZWi&Azva#u^PdMH&7(q?u#EGXB7Hyvux4;I%4GMZoX{TmOe+i3&X( z(`Y>XACL0AXZXA)LuOX<4tP>kIqxawa%ctb)9V#9bpE!45XnJPnp74~DE*9QRl?+L zWKKJSAWp)yHo-pu*jOVINZ4)o`=4cqD+ykPJ4eH<)?m|Q<;=SyAFrj=Nvjdx$Bu*{ z(^uX~0@daDyhZNWE94sWz!fcJ8D~BqwH8I_2eG;;4<)teG}wh9rb;y4gmHuKMO7hXDKl!h*IxS8R-=~x2*E=tZjak>Nc_`~H#n&434E|-O#e5 zaYo2SAh}*U6hD~h+QETIdx4^$DXQrkF3B<{7hX6fLg*hRaQ47Wko2h$4v}M&w z#``Fxf!0stB}ttDynOLbUK1MRkJg^xExUX>l}eOJDsc9|@rSXJzfr>_gWE($uz-xb z8Ri!3VvmAEk~B)B_&ylUHi{8`%l?>-XA$tdlzkLXC=vslUZuJU(bI7h{n>IRR}D5z z{wp`3DHw#lS=Bpr9DTXOS~uuO12Vhr;aO{TsT8dQCqylx*Hj=4%Pro*rZAvpFxzlx zZ>`~egRE}s?5B+G0bhJHug!Ng>yfe-oB?9ZPas?XuFL9zQ*NGCY`{+n(8($31l)b> z;)ws*zvArdxy$C%)^Oe0GKjCpG^0PtDkj?zIeRI$-&>X4vJ&}#_Pp$Y&->!#?=@uX z1#kS!_oRMU077dc>bIG%W-kEcQ+dYa+X{pT`sS)4=mJcuZ}FpmFK=}=CM_4uB!O7o z+JadXIK|`MPp*nkr+n5KSnY>!h_Q6jk-UW8uSnzqwhY`T=gAUx#CO1kqVq9S&AzoO zzqTKq5YIL)VW%!^#p#R&^Fh;X(Kw2|R#nxaL<}eZ7o~ms=!ev~NZ$V!xO@cX>_wIB z4TmWBCe`TmmT0yli>`VmHL!DBUqi#mrl`sTE*Q~`>IH5h@Ql-2s-?SkGRAngsPe|! zb@01!_ed(nbERMS3I~Mmpp;V7(9T))Pr`O>0r)euu5UZrOuLCraFF6XaB_cBIEuaK z5LIhMLj5Na%f3roxn;95tg0v zRY$uJOs)4P=RLo5*`Y}i=6*pRzF)*_gxkvzHJ0Z%;yf?ToG=?x;D2c0wvUe|LmNE0 zMVvGJBL5uGz@$D(s2MBz?2H^l?d?8Gq&)id-p}Oh;_pF-{;Ze!g9uNYdVUg?Qz|$D z4bY9!8ejlbJY}=MM7e3iq#DZr8EexEjYqsB4|TmZ>jf+V+U(VX;5+_W13HdX=>Hqd#ZoZR9{-m1J!y*eQyeEIqvQ4;fZ`b&`dd&Nr>`6EZ1kaw*Cqf_ z3H6)Y5YsAyOE;9=9}ze;^q?i^8`=uk9-j7YzUEK#ib`}?oMD`ZMNe%HOV-#m6BP-H z52QVtsd&P_C2yx`vlSNvBs5=X#$W5N1aZ|xXa6)lmhahm!KG;<)-fNsO6xLV5fP5L z#1>lt&vndcCt+4>f|RGrP;7DR|6pU8Wf6Vw3%2vJ>Q;X59F^mknfS zer>Q@*VQhU3q!fvm4=w;uKjmv<l)Jq9dNQIIQ`NZI1(O~HFH+PP3pcb?v z8YOG?*;+od} zVm$5CM6WR1r5^^BPQ{<;h29op85;pVNxLVn9y}z1zs+ zJq6%y3%+OI@lxFx9K2mv?kjTpvz_i3K89Hu9x9cMGkaD>2C$3+|`whw2;pI zA{b4+4RM&j?#+Ah%U6+x12WxaQX`4*gE`uVM&xPJ6-6k= zD0!gDAVK8{dN3h?*~UXXYUHsP6duMnp|FloIpMS-a?`qB zbsR5ey@P9;@O!cV3~9kNy>JR2lyUpk??kp8wjKewPmM*#(1fzqyNSjgzCz`6ZE5zH z*6vw|?H@ILSvvJE71h~fzz8OIXu&fbdI(%hmrv`Igq2c4HdCd5cQh&=2Kr>>WG=gd z(|OWTyA+H+^+&ds4W)A5rdea_3DqoIi>4;d`N-wUkr^y<+~}1t}W6S7|jOScPpeo6F2< zYngXr%|N|yw!y{vkNDGKW^5{rt2@iUwZKBsQ;K_Y(g>0^#iQn8k!_(sR}4>m-E(#< zsswN6)p|n3)GwB}VUZ~-8D!Mjl3~eVS%8=O18{D@*kW7^M7SzTSrwKOdufihghEC; z5}AW9iYfvB{`iClP_Nwn*6YId?F|Mbo>~Q206!sHx)Yo9g3fHOpNG~|yP=io%RLD^ zz;6wrpdrN(>L0&3&mbOgy8OSVGR5ijWHaWaSG`Wk|Cai_Medy_$IUlCU?xzZkjfY^ z?U9(%y!Di$Jx9f-n2D~a*l@n#e)Q(4FyvkX`cYzzt`y9S*r;Yulx$;`p*oI5&Agg7 zA%Sul_Q6*XQs;~A{RygbjZH4#n`*pjV=}~xj!F-iQs{beR|Czz`3^`(dRA9Qfi?Tr zCQi@_hK0uN>G4g=@KY$Kre(4$%;4> zTmJ*vfFb7`CoWs_p5W7lExVo_t~m#_3eDT^papF+&2SoZh))$tI56dY#Z zMuzsC{uuBBrVRC}tb%D%4@g{shu7wBIzzQWd+5Es;F(_^J2g=}WsYYLJ+9*w&d^kP zIw9w7gIvgSo^fwwr7%KgJ!WRt^*kh@39za&XD$%6Y3yb=;FkF8q4F|N)J@AwLLA4M z$!IG1(+aF~wsVdkW4j!&CNQj;rEsRH=Erk-y3H8O#~0GxYRy1bD@18@qrRT26X{L; zWp1$zKyBOyAYcdEAd011EW=&K(Ia*N*k@3?svii|Bf`Y%v*eUnT06M^@DU5uN}nm( z2zE8{^vhDDdH zCU3l`Mwk~AYD@Pk5WAAA0lk>gGRREnH;(&Lap6h?z=v7T6U*-;tn95|X0tRAoV4KC zC-h95BGH`mmLAPwnO0EcdiYvjl%>;XW&iZ~Lj%?@_p5QLjKAv9{^3KJw@a@$QOlFW zYw-FjBA{;@+>?i0+PqY5uA5i}^;^QUB-SGMD%lKOOV;b*h?*M`rUfKcRyv)()JwuQ5%=aoOhsN-N_K^`GtgxdS)@&U3vO3Dzq8a3lm(?SV6(@RX8e*-nVpp~%3mXPoEQUU7uvCCqLyaBFeGQQw4(MQ@ zUT-SR8B+2l(g)q*EmNl&c2J+QjmEszsH>r9sYur?`^g{@;}p>$!hV8#&J4Bkw@>?} zpO)1Sg4C}L=lqyHe=VDQ&1eN@XxyyOp(-WR{ymp^Qoh-@QvE_TT8ni$GTI*#V>rF7{mP{HoW!D@ZlN;$h;!b8S zAMAONa7+vc!ND@n4~zv=(=A_nZSm^?%Y)+>n85MObS|fqk6$CU6F$s02LnG6#(&h| zaeOPBW{7HEUMrXu6OU1Lf<=eMLWg__nCMg&|Afpwg}e^D@E3H|)Ld_KyLjN#cROGH z#j_5AB;(Fr)uhyl`2KbX>%)C%NDJg`8#fEM`Xw9A@ddFrM6^?AONC>}zfKfK@v~ty zZtKRoQl?<0a=yH(<m4V`MjjjmLcXu@==5Z3PXr+k6$6BCCQ4!x_1XF zoE|~t;DtGDe?7h5aMP!pOAwBY*H)?Ulbye#O?aQXbWV#AIw0LGNJXc1FdpcHz6Ff` zZP7eWUZ+=3Z_ey8#m8Q}B2N8qT71vAE&gNHaEaZbQlk}}Pmr~tXD*^!s1w=vKGD5_ zNeKMUBjCzkZye|W05uDj=EqTN=2BGNxb^%m1jhKSBDvVh&O>HJ9TBa(nr#!toK|U{J&=r_i8kBi&R$ITWf+9R zf@aSy!K#9bFA{A$;YjKjg~YAJUGFv}qqNN}D+-Zjysto`QEl2=toqN95_qml2VGnR z2erTF#6BxiS*LbYj7aIh?3ugN&6P}3d`O6y#8jj*KYQY_t*8gEbZ}b55_M*Sqa)hq zjt&H@;HQr!@`KDN!0&E(s(B4@lB==DN%QEPX`L=?nM;!}pm{%U1`V;uWoW`iD|<}J z8H#>3B_$e_;^7lieN4n`-`g{_=Le-& z@ZFaEEcy!mQ)P6N0*$|Tg>g$IP)R3;JDny|Gr83^^YPOjw0mfnT`nby$E^}k!@*zi z=QRkC>L!7BP}tU0PTQX`X#ap-(#uEk70?D@KKTs{CZ8bjEn@-7Ysp8lD$=o!k+ zLgTZv$<`;o_o)qvW_FJ=kHhRF%wYx1i>LDKi*65FYUwnF8X#?_DVa#ZirD{z79!t# z^k)@bXj+x+6yiN5c;@Ry$wsqamza)yf=+t!4QN7==9lb8=}-vZeQ0cpV$Kke9ZGJ8 zcnub~l)pNoB7=Upe{HRNa5lAxs@r~a^0(fB4KU!y8R3$#pp6|wl~d7wdRd^DD@tolb_^PIp!8)EYf^ybF}Lo7uVvu~y-=|siN4NZ5pH_x3SX_29bY>o&AeLy z9^uK4s)dcRu)sEjzAFh#rEjcaH*`f5h_tUphM7~IK8i$<4uXgirhI=&XZ7F*Q`;=v zD2+A2iOX_e4Yv+=xRF9KFP5nsg?Un6YbIuY83Rnfl{2o zm#|HzyfyD4Zb3*g56Wb}P9w$?^3BPM3AlcnJQE7sJ-SoL;qRQ^otC0z(kX&D@q1w`QeslDIYGHMt2_f5Y#Y#OV_Dc#EE{TA`kHm?uxuyEs;C(1e@_pq z`1xKX>}TNPx38un^bJbiKgmg@^_z4NiA5FQp0A{0YP=`)6m{+AOXLm_^m`6_%!ahr z_;cc9m);ic=~(??dY35NJ(6awwK{yauc?pS1zGdM%d%qk%yf3^J`8meI?+J{JJYch zM+vvecTF+wt@-oi@bKp@1KyqpUOZ;fEox{S1-x>}x^iT@NmF`wtX%S%Z@DmAV+@cE zMyWBgtspMhP^?7Eh{Wk__?PhzBw{ zGvMV!6J2#%nmHeRvRuBYR{OAyxF&}{Y;SEsaO2qM2*x>Llq7rw`zI;!nuT21?x~4J z1I3~i*4O5NU(4gvuaj)L**VlA$FHt->%z9iMo%ZsXUo1qn&NsXkMCKJu)99)A>4IFEv!Iy zb9}pWk%!p5XE8S`?D+vgL!@ocC=b#8>l`**Z>%SOtsTrao5|uSJ(Yg%Ij{65Q|`}@ zxYN&@=-;&bm~joqU9F3L|23ZifQ?w7Yq-oOM*P(+VQ&z66As$!3%$F#n~1|%-yg*a zPY(Hlk0w77fmwN7NwpZ9&exjSN8EvEX+0G*yf!G(rZt(KS3j)9m=&nT*1H1B@p5=! zxO>pCRmOz&F|roHsY0UV&4KjVIhT#LY+Yp~{?zM<>Iq?5I-8a{*o)5YQ0n!7M^c#6 zX*fGv)+8RVfIH>O71tF!@Ps*B&Jv(XI8dBF$h&zU)G$TMZNoK{0GwSA8E9NM{Y-J^ zu-P&Ak*K!1xl!=P!Vny{tKB20EUe3wRlxiK)Zj3YA;FPa_V71MZ_B`qFYT!xLYXCL zuKzuOZvbywzoWMwhND`)I8>9y0fYg%gS@#yOp~##F&9$sx8$#VnMXI|6bW_2nur2i z>GT!`X6ASCsI}3OXEeI0!(69xZM>4PxKzGEtStA@vzMrn+d_O1Hw-EiDk(s<9pJ?edhz7YgP-Qyj^c27n zhURkBsn}s46=hcxfJ)C1ib}dg1+}xvV0JiAXm5=Ud}IZmja~dc_DAhH>l`7rjy}j7 zKbu~YehH4q&yK?IhdQL3{i?>_)JtamLFA`dM)WI?*gUU-SE1J!?NS;Fm6{UdZ-M>v z<3rcFbpt-~@Tna3J^ zJ*RS)LC!o^ZsdVh{7#hJr}7hRVF=Eeyvz6UcwWx&EiF8YM&TsofYt(fAGcI-zm$RLYtS>kDk5&9X#@m3@@yn$NDa+p9sE!arffx?oM$lS~O_!0>!;(aCeusNbwNdrMNo;d(->*zP0i* zS?kK2xkmQhX9i)-b#IlY&Py%p@jz8buS(^|p;HF)*j^t=awjBAtvq_Yw zDo@s&zz+K$Dz$O8POb+e|M(#dCiDrylaOk1wzP42A?%s#w@Si(QhL~BGrdO#*SL!e z)_|p{ox)hM_-$pzR1BsHIBF;tvtB-0!ij*LJ?Z0WQE{7P#?5#|;uw0`zKR-pWI&bo z6^L`OE6E0h{nUyQZvP%s4qklb^4>miv1fZk%!awuCt&7);enOr&p6+)y!D_l#_}Jw z-jqT^gsMs1`8oot9%_17Ia9e5Ru(>M?W-eXJuPM9n468ybQm$3g}ojmGb)U!6m;D`KhZPivv$erAVteQF-On5!zcwoduz)ij zKCKzW7czQ$$N%9ZG^f>NsMTTp)BMe5(;MK7RG}RM;i@$NK~v5#h*PC)T0mM19rd0$ z#0cO2uwVxOJ;HhRFdk}_y}MZJuD-~Ge_|1n6Q!}qFNKOoaHi{rIimNuM?ATZAk^8d z?1}}7j0YrzXC186Jgo+WoaW&vjFy5DflHJ2stHC+XnoVt&Iom6M3}kddRp%aXJ zxz;K>bvqdLr$c#)ZwqVanQs1EPgDuk*w*s|ez!13rk zsYEqoW4qN}Iymp=iT<)M0Njw9#5~y_-gW90?)aAG zA?*dRZPYXgfP?49vo?UZ~{JXa|mDg_KZH=}@0%v;%H*=RjN?n#gLB0aicc?w8gQ(%te+O%;=XgE2d-55nh&ro@agFdH{ zEsx)3vGb#$JmW)7nEBhZONy7(1j}*LvnUeCZ02+MjY;UIdf9r8NZwGVOlI7|=Lg{@ zZd%Dp-e24M%NcJJ58kQP69Wm>XCBU`q3;>MUBPdxNuD#ZU-RhaYnd!$4q4lONEDaE z@7FdpB)DDgbw|b@$$aLkH+$?`O)V6T{qs~+G^#Dm8)MV|U}tn=VkZ1Wv05-X7pNxv zz2)`9HWf<&z2t$k#W18fEups?I6{rcc3_p<7W>~S{h!>mOv^zRbzF~Hl|x&lp7#V`+o$}YodY-8++5RI zpobiGwH{C#89tA0Djtc6ZC$>Dl%V$gx#FPd>W~j17Fmp89ln^cDV3)(E4L%BBi3YN z$|%D2r9Y%;uIb9!5bp^Sqi?-}@b^a@mSN}P<_~yon zwiHuyh_2p~;&@9%Pj-L3yG-ZMKpw61Xtx@nfAkqJ-LHY6EjRatj6aQ~=i}M-Yfoq( zK9_X8&*@*7e7J>(TSQZi@7J#8`T(*PbEn=QRqVo+k?1AYs;5)ITOO55l=qE!jdmm2 z?GZuQfA1JU9`q6hluiqk`r$hhz$BG@-_5T@+iEd0T_kfP69^$t++Z`;XP*ZTXS>j6 z-!Dh+mRFvko3UHb6yGR7lwbeCv-N)ez#LkpRMWw!+bxml`wTUPtaeC3k>bgG$x&o1 z@ZVc&jw4*K%f4xe=lA@jp)@ze=SqR9jlLQp%`j z6HZ6aEMgmc#fhHqFB7N)FWI2QH~--Jz@Nt(Q7?^04MhvQ8@g;uZ2jRE4*nBdG*Ua) zC5FDy+Id%vI_4mkN~EAvR#jPXTnh!G$I$Qo zF-DH;IK@6*m|L0Y8DSC!m@{r&Hhpw!SkeVo`zo>EDZ}eyjrFvcfzE0wjw8|s^{Lu6 zFYOA$@i>C+Hi%Xvd*QHtMc^F6712A3_S!~LxM;V>2sp&A@}xHejY`xD7#2OrXE5@P z3E5NtE`pLjHN>!3OHCtM1zAOKJtUVUeV~FvsKIU1qCxUlbdP^MQ}4>^i`X6##KH4U zL_}MIH5<)673Lav!SZ7&J8D}Du65MW!$%1N=Ux#!!v^80+awkO z9_et6^B#hMGi`6qh(6ng%-Z_C;}2M}QIDy(8eBwou1lRyIEs4B226hEs`aR_zVzCE zO#ay5P{sC@2`hjxIB18txZt*ha4!l_vfr>&|Va}AB9OUL1#$Er3o)ryBn zHqQE;B<=i@eEPm@vA6X(c;h`OjEJK%k5z0FpG-4+Pw00CxmTM?^a){m{cz!ZZY^8# zlYYk1<7q41&T7x)36)|uGi&~IkW#5~#IuvhpL}=CeZM!y1HW*l2Y|K6dXUA9esNS6 z4x#mr8i0+z-fJ`9Y+6Hbi-J}qrqvU#n}gZCZ*^``PNYpov5t)*cFolt#YlL%d;4?$~phare!molGVpwpS*K@9GNc^p8Y&oBUK+){_CbO5wn~-ct|# zWot%s!qUCaO@LP>2>yqVO_U)E?$uw))$(4oB+JT*gu1u(U14*Q>$u?i{A33zANUYL z#Xe7KSp3F#MtJwDh7FKTRuI(FmDD4YDL24p`oO}TpS+lPefosGFF$It<55I}U-OXt zC8=0S-tSh2r1cp1MQ8@Te`nVyIn9#f5vrJ|jnn%ruPh*p-aI11qx>2tfTer<3(c)p zQY8GkJ8u=py>NsakwgK9NYP}zjq zA{(6eV#ufRwrZ9R+!w^Nrp3P1Apa;)-u7NtFSsAQ-whnG(XsqFz(}q05CL46Kw-*T zCvqXy+2%-{{Jy!}{ID;WDy7ea{TNU#8rdl3RG+6)xvVH(Q%_F@2an?#9$b{S)`Fq$ z;u&t>G2`pkj7v!!u4P!D52%vKVZmDZ?VbZ0Qo!7tptFp^uWEgy8fm5;`_t=TTPRCf z?EXa-3hteAB%=G%x|~4Ne#)4?jucefz?6ZTDP#1MjTUz8g=LmcY1?HRlGQ1fZLSsR z?#Dw?GDY~#DOj8qwU50G_x$PajDw`BDc}0<<7!5)C$AaU2(y;pF9eec)1ZKC(0JdI z)p4NMw+C4&Y{hQ*5d1}eki_KnPsUG2%4R&NS4N77#_5(5^ntx8e-N5eK9Cc8*mJo* z*;Dx-!Smxpz0rs}{#aDh+fg9Hvnh+PWhnVD&=sXG%f6=G$uJd{ev@0`cj%A2gTw?e zYuql6)y>WpeW+1Rw&rYqi&1xS%)?&cqGctQydN&BeXwXx`{yMxZ#uR$e&b{Bx6nrR zDEd_CPi*g=lnj^86rjL7qZ=Sp5l*D{!3ac8QNKk_bD@CMp-7u7#3xkSsHTb_go!{w z1=nkiEq+taP>I%GFG3@L()Si>zR3lha4Ev@pSItWi}cYCnPLB<%?MriU}WyClk(@P z%VFI9Bg1u6ao2?y)uS3kaX2?aVu!?h|KBg$AvX1!h%AAyf8v2NTi z98(t3Bohz0n1|dLYJj(>9=3vQYoW&8ok3{hM}d`I$zC#WY53KCPjIw71HDh@RM{T& zjJ&mBQQ{{)kXsqieQs1A(vTNizr>5FhuF2bX2SKnAsGyor9Od|?fovPW%?E$1|9fs zF|&(+{m>VRTEU=Tn;>Xvg^kVIGQW8C06rMgv8TC%K!=-W!f1)ds*&drbm7{Z64JaX zrc;Hbpr!vi!Pb!`TAx|DdkGRtTf5Ekm9fT{#0wS%jp<+~NF?!K-|Qe+7q>J$zWzup z!oiT~cc-i#^y_m^a-bJ>U;uDOIA3@D>Omr_xz9uP+SzA=Ns9Di1ca|@wI@NPE!6Hg z;WQlXnAr?)4Jk!kdFTAmm?-65yZ5=f5iiqQq_i_}U5o@asp$)5xk}W9HwACpB&-BT zBSLQ*6ZQ=LC4D4LW1}0AHF2~1eAO#b(>Y(C6Zn>YZl`8MvOyZAU%2zW`TKy68TZ+O zG3d+?Tws64)Jhj{W=tRDN3}r$e~;8T_{Ypq2S3z0p9m{RmeT2?Omu6h#^J2f((NMK zl9B#t?VBeT<3>GT#Sl+jj?K{8!p6lo`IJ^JGZ+LK%i{_e^nx5D&tP>0!=bjtiAzTb zpu00R<4)7r6Re$gT-Nc)od^XtyC35Vm5Vg>P3+1b4Oxc7`M`fEj%)tOf|yOe{heAq zOE}=*`7iP8GUtVfKK^p4f?o|652_NI$LH>NKd-F}vVY~0%X^r?TWN2~kt;F!!~OSD z-=gem_R%NZ779!50FM^IdMd~yO?J#jG}t} z+*48n8I%J_7w}XO;c?1h#VhPpM?%8CGn8|&Ui>k+jx+sM8SwimZ!T{{gXjRq*PuHA zRq6am26%(+W}-cr%M`-X1{I}j!ETEF36&Naw$E!x4TnF znEFfM{D`F*Um73 z1dl@{nN>Wp9!eL_@p1K>z53s?a|tknrT}%8qcH#fEC9X9%DcZfznn~fKVxY_78TqM zw~v?wiE8c}WK-Q@1^~^IGiAbacM&H)8!VycD{RmkFvx28LhcYczP?}5&0Yai19lsF zb2jMwT8$wQ7%q*t137ECIBhHnV8Uv99axv}B{k)BwvqurwA(Z_gBz3p7jDpWXZ5(>fGRLpFFE$+O?fr z#@w6q#dIA-cQmhqu`F%2t8wGj!{ocJF+r&4>YpGO%f_4CbAW^$P00KxDpsSiNqIQm z`$w3CVM_O9hoM)94Hbbk5=OrnO&IHu+c%e1mr>*>eYvxM9?$Y0Sk<}^sQNbDab$UU&I6M1&e(`29~BS;LRV^w@$-Lo12)?-)a?) z#BaWcY0}OruAs-$H~(B5>NbL36#jeY@l6>K6OBOUNQTtab?K3mvBH^@M08k{eW&q? zYws8+>_UdQUAEpZOpUS5ZslCywdl7gR!y4iT?BhW@99G>P8R21XWln)oWY5#p78aO z(IW5Uz%G>tls(#!Kpr@NNhZp2Sd>i@4tZQZWSg2r=Q=D(J(_I5kxe3St+7ZAB{%ZfN0wZf5WAi>(zh+jH;tm_~?cRO?-i^*fh( zNh@7^g3e}};jR7#p6N@%Xz8L*2{Eti;Dh+J*W2vo>&I4aM+oN3>mU& z{y7xA+kT2CL715P>v=Anwn8Gj;G5K`Z}ewV>9(|X)7{?FnB;1Ztkya8kj1Vplx9#$ zK^UXgIUI?g1I|GDj>cyA+)Ohs#_#k|LH26Z`VAjNEf=E$GJ$sy(heZ6O#%-_#Z1Lj z;07-qW#KN{G|KWLyAEofiPXWq8b#69bFA@IUSmpF^?L)iK85JhCaE0B4f- znd1YVzW0rtQ#fdwnGRq70$a%Q;@0u>5ApZNDNQhd!{&(~m)O{lS-v@HA+JjSP=inw zpQ0m9^BW5VFRxvxk|zy+@Yee^MZ~SPxWW(6r{TBO<&L@d?_|PitqU|q!pWQ5O-Mtm4tPYruud%~JEA zx5HII{%iU@m~(dw+(~6}Y`gE{6kV&nMRBB3-mR5MDB^cjZlh7;#?4U*Egly=W&iC; z#~Z?6-dp^+VT>m8bY~=uD|8zz#4v|q!}-7rE!dI&E*ek(naVqyXy{hcbAH!D49=k$DiR$+!?b{=@ZsNlx;lWzEOqoK)2 z;j>Q57yo^X0l$Q5C56PoAho5t?yC)}a;e&vcL=T@z-FhEDDZ7tm0_$n=k^G|v4DIT z_z{-TQk3nfhc_?JS)B*Wks@B7Csfy~m~X&_orv2(^1l-g=?~8n*yCuU_ZfE`@%rQg z1Vl8(uH|Rya~Qt5cD+dvLX&r`$}?M5n@#TOV64_}d$T7<(Cc72V|O}+F+yPI$DjXo z?&TiqIX7v55Vny(%p)bI&`wm&UKSo6=lnAQ9=7<%5YiesV7}M3;6To{+#eqTTh-8o zb4aEmX-~WLL7)Ra9C@jlB2^P-KmfI3E(Esfd*@3`)R8Q(=D6tjeedoy9^=eBUi*!L z_0ZgOx}N00L^iW>HR9q86Z13eNJHe*SA$j!nst8i(L`D~GO-SzQ9zpZ<_uP2$2aZW z0WFlWQrL_>Kv8le5gjC?D@da9QQ(ta|ORc6~ z{n5L(b#|hA%DLSFxi6m~)JwO|3OpUp6n;sFSzXzU%e0kuHhy#t)}?4wjN;|l;v{?N z-fseLjxrLjH!eI2YpT3N3@08!)!Zk1QK}tRRXVC8tkG9nax@DUJ6JpTZN=ov2)&F8 zxrF2UoyoX89qcJRd&lkHwr+wdDEkO zaOT!m$^?>2p!j=StJn-1Iq!B{C)zryb5wA+$SF0`bMpj8WPMbuT%cphsapEI>lib^ z!Rd#k^YHb&t{r@va9EbHJbr{{DBQ{h@nAI7MmIonoPgU`NB8v*Y|WgJ{7W!tOXV^T zYvDO(Bxor`m!*QV3N4MMJ&=Fbwk)SADZ9BOu&XH@kw`(IqnIaJ?RUH$r7fsIw??WB zuT1OJA711^?WKDSPcgPJ-gnn~K{L60tfF32;R`384p)Ri zV7OKue^)uCaUsRgISMVzAEOJhnecou38)1*VZ!#~Js+FbRh2*kKh%x%X zVJ-#s*w}^njYbEvKp`0kkRruXomDSk=z9h`2x@x@dJMIV4;iZ;_{Uia`?)Y<03Sv4ESXt?+Xucc{HR_HkFE9S$ z@kY|CZvPFYaGSizFCI!l=Cts|i1vC&Wv6dxMz*4bQnoMgvW}`Q=O>rmqe)N58d0iw z4YTW83Mu5*i3&V3MGQYxPVDkK9SFqk$yL?9;H$+Phmvbi6_w%lZjfg8U|`+5m>bOW zzuz+k`4(v=i5x+KiDCDH6a3z6SJ`0p1+MT@-*5)G{ecrgLB@AHu)>fRVT*`G<|$Oe z5IMv_lH)C*X8pKqNRIGX+@4v<`fKgW<1e%I;%Xs2`D!bt9E1CoKa2z^^_sblmxuT) zGpfbQodEUIc_X_k7jn;$FFOR})|&!s9SwrD3%2R$=ixtP!C{QlBRqb27(K(yUq%MP z6qyr?P#!8pX_<5BZG1G9F~djbpOD0Y-5$1kM0g!|w8N2m#-3A<%n7@qX~oVqn{>7k z{!y1UXv4F8)EhkiU?m(JZe<9OrCIY*W`1>l29@{G%GU1pjn7PXwJ0Q;TB zjIx;Mrcx!H@#`$|(pmcP;eNbN5`ZoHl}VL{M@IXT>*qG(>p!$hIWpQwGoN7$&Ncq_A-{52u3z4De$snQR7mwXL zyNRHWs$wLSz`*3QY#EQz(tU2j(lR0MdRHexv+C!c_3`wD zsPga=4cdPDDfiH}-s%MKeCkM#lO(N^pT zwE(ZIyEB>?#%S7lON}hSn)a@=bymGvBRvtNBBm@OMzYO^0>1gF_2iPo=Ox=zb2f~C zSazG~1*zC*EW7#4*DkhHWo|p5<;{HQSia0+^{fDY1I|f}B}fjPc^(7h)S(Tjg%$&q z_7m;(u-c&vj@+B_v(N5N`-#qSTKj^z3rTQD02nYphN^vo@kQXPiWt2*bqERKn^Pd^ z^AI*=N!~zNEYDF+g#UaQmWV?f%)a zLx&NNC)me-jtZP%32&Vi*3PeIHx6F6W#NoT!HvkPOwcgbI5r_C`kU9k)E2kIm_? zm2fz`35Kra-N=p*KyCnSb52f~-7TN>8gWk}PHt_mB9T_j<34O~dYKOi_Y zDkumBep4>Xzls(DO^jMGi?E!%QjaHf+yCMpE4BUps0e_NuD@iQE;dFDM-27taY2J!kHj9v*PPc<1q;5}$8pb9Y7^*wVi*wqZ2$jf5TG`otJ#1Ao;0j-w zRaoCS9Kd-n&m6t#o+gMons;AquFipUl{rBR=YfO{u>Q7*2L?sB7;u}apiZoK8RN?L z+-Ueg{}S#rRJZ8+zL3`EPFI+C1NfN6PoRD|Wr z3=bFL!pa(?%wO@bNzj3sj8Yjiz6X)j=vO-f^^V!F$}yQGAofNPuUBBJO^>Pdb7n5kuX6G!n0f+w?KC8%>!xb?fj1zb%6h7mo$*qBbZvz*aibs-=Nkky=)o^t zFNm?}m#0Jg_|eT>OZ$-Ji&y-;oF1f-DHn!syW)L8Fc5wrXm>g_QQLpya9S>VE4&GI zEoVhh9LOWlLQDvhPEno#q>r>KFCrn{Xr9#1S2PGR-B6 zMVla=E!%FvA>E2#o_IrZUyf9o2Tpq4KM>!%%Iu4rPJc-5C(QL?XXj|$%U|r-x>|Q1 zmY{#1uhj!8gs|CRB@%0-c%JIXjrl`sKHH|YF_Siho9L(0>UnSlhOkaB?Dj988}4*(UUp>73}^GVV=YeoN?0Ag3c}5a4X!$wm_4hKc&QXD zeEWU2Anm-&l#%h!3H%tf#yV0_-JvwZa$+^5C2CBdcxj>QbRAfCBR2Te-pYPI5VqYcL*%F^*vN0RKf-LnOu z2Vruyv?*#!N={41Wk^^Kz|Gk;PU>aj&&$a%mAGT_U`XN`Ln zqbQjv19%Yq>lVfH@!)X@oY+}3*xtIEA7)Tr3#)G5TKtHQf7)$B>*%2lj>&A={+xa= zSg*t!eL$}ot(V?sQjW+LSQjAnt0^^(IMw?$s;446k2o+tttDQig`9}kr0dN%#gvlX z_~wAot%r-#;rmgR^k=z9H>*zscR24>3^Dr6j5lo2i!ydE9V{a5%4dRi`(=BK)+^7= zo6lM9U{JAYoYULVFc|K{4TG8=ua0#ojCiTr==j$w64Sl15My*vyL=6750nEbfO=)k zZ**lSS27%n@d|3Jn+hfgMt}|-m+77TV-SM`&hg_?%05YVPXJiZ%rzMM*GD&>b*j`7 zE7e=w*{$W(Y3&%ip`7H)sa6G^=6DZ{hiG)pQv}FnijovdG zS*e$rd3;h4p6wJi9XorAIhSUtRfODGWY7c;d$6H@MD$Ryk+q$l@0x|J`qqbz?cD%$ zbgr>5^7ubC1_pxR^eyEL^R1#xCtvO47iL&Nwq0!^a+P^oqo zx~k0T)P9Sv#~XLnpwBh7I#`1CluLl=BGrqrJDXbG-L(2+wQb9@_`054bL^K73M}ox zmjWta8g0Mdj10XC1}gYu!C;4mhyWw2G< zbcms7nQ`i+IHQD#G+Y|oG~RuMS;6IdBiIF!G=|AC4Y(SRJsAQ#j-3U-PsVl4h%w#C zv!LoHp4=H-!;{?u|Io#q!;nlaiJhiD`Rdv=d|*X{{HVtwg3QR)^U(f4Mxuj}B}rT3 zUy_5wD36mE*HeAdUGb2LcarK-&3!i6X-0~=IJ^u3X)Iq2mxlMbTG`MuV3SDC_`k%? zOYKpmG}wL0%vGwfWh)W8hba=LV#%hesbi<12-2TVW)lYy{SR_xEMwTq+tguIub|QyYd~aab6o$KKjDHw1iDDQf#T!wzwMashaY<~S-h@o+wd2SPT3{?qYSai-r6SkiO zhsunpy^@ta$IyRvwJ^Z6Sx{Yav)5x5j&sRVuRdfl=XRT|pctF~#Pgg?>`}7BB z$+;J=XFWp?7?`G8c_b-suBRjuRoqxvs$eHX z#@pxaQa7OH8T`05dsIjPBRzv~s>Rk$~+$QJ_EHn=}b>i#SLYqpNYK$Hr6M8`f1HJ;021+WNXdX9JHwe<$QJF zo1;r5b3ggwyjnjpi%Fduo;wH)Qv8)hEa(^t5Ps7kBURs$eCJkXGiKDhG80W7Hg!+~7s)m28|dgxeQie>djHE4x*w*~^f%1& zxphkmIzgQ#!+p2BGioGJ_xH`kVrtpArca`;W^uf1x%WS53ml;9tGGOCKZ=@lrK%9} z^s@auq7r%rG-YxA7BXvq$V9?F(tw}4laHLKxBsA%t)Q$48DAfR@{D;^qW}0q&8cdn z{xcpzd$VlMDw%fa8&I3Hp>LDeB&_rJiGq85z;gK8q1wD1{!%?T)yK?$m772*?f0`Y z1vzByE%~2JZ8i9^L9N0iq9v5`XS?+sYp@%aF@INPj42MQSg{U;u!sI--#$9?HMQ}_ z%67`L7pXqHP})L)Aj&-iJ}sKuN4DfIfN)_Ra6=wp3LJ+E#g^SoX{VD&Wy^_qgN0A zyX$wbh$n;dok}f9N)HeotH(?uZDdp6mg+H|1^%ugRst30@_3_{gRhqRsb>0bB_+Na z!TLky(7rE;j@!ZjD|avvl9`UirOC%?joCrDuVBB#^h-o-*D7xxE&E`DVIYjp$l}Y_ zYrd7SQ_Ov>X&@jG>%vYQYCwLV!F3;k5xkdEU)Q*1^ zDAP9$7%j8(yD9Q7S&^lMf~HmGrM0MrYTK3=QaWxZB>59g)Gn50M@r$!P!vwqvuJ!=My}TM)AaORp-u5;{h5p&+5EJ{_9xoSj3&m6=r)g)v5NuxmyzoVB4HI0i zmp2n@Pe%DA18u1j@SsyyjCU@h=vR``cO_xc)uua{|9D@uy&l~wzU1*K4CI$@FKJmN zM>*K}aSz0Po$>SSIsS&J3reU_Iw;I!$gnt^31dzjZMNCvn9%wi)SWvutv1>GuGTB< zZ}vLvkM_)_gfYyD(#2*;bj(QFE-#X>Jicd6vYj+ixY!KFUx6(0YyZ{$-}+Fs)Xn2c z>JgTyVcw%Xi`W|ucncb5=Z}vrm^!LSZ@;pj_KYX|Bc&|R`^IYfUGUT^J-3ctqz=o}MO&3qP zl^TKd#=0xUKs%OwCK!G>Nftlk4xp(?_3>Pj*BQ37rQ2Ta@-Aif?!cv0r*%5!n`XDZ z$8H-}X3Y|4;P{L+mMux^-h1>K_@l!_!_XYiTcwDR2MbGwQhnDLUtsGUCLhX`FMNeGK*)_8S1Sx+0E36&rYe{uNkdbM4( zolTG6seL3h^=5B-6Ljo+$B%S@{LiVS3ug}C_HHm=&I?kfr9ajk+!!$532`kzg=k&6}EH{2* zQQWt{P;vEHQ&|&8uH7ELr}+-)b)*V4fni`YTiU|#mu5teCpX21iXVT+^R}1EW`SUY zY}M1X6_ab25Cn#Oq79Gh6_RCx(;lU@?R2mS-RM5enU*v;oZ(7%XgaYQ%CR{8C1BRl z*893B|0|`_UypF(GqWb61KH5_!AZHBMpMRf@G{Yt-9SeP@0F?eCX4rGnMAS!Z#vTN zwPVjd5JkUZ4V*=d0=e>x$4XH+jUE<6FdLP;k{vs#*@r0RrMN}?^7g>(*SnM&eeEAB z`s}!FxrW{PJ{;vAwh%{0MaX1H-c%!CwECV(jt8$SJ=atPU)e+!O#AD8GbHv|vmP&a zK&?3vF z>glXDuA;fm!qpU2vUuwuI~GN>__c+h(Ku4cyfG1ARq&G~Z8ogEXr$8`nm5FlD&RA> z^#*I&H`An%#t{6uF`cPEKyaJ><~viY1PXNQ>$29{h!E=%~~z+Cc_kNIQDoBps!&tOPMN& z^YlC%PrEJ3qQp$9>riV!k2QkKUDol$7g+pn9uS)`h|-mqELQv(m4NY%h1c@C1LLUR zk!!);4EZ-1LZ&63(kIKAY)*Y1s2;Y-oBOb36EwZfw+s+Unu7uK9_8{PFJ}6bHym-V z4kCF;ax>k*&3AsPAk1hM)=bK}s7*W#ZJ4=T6Z_6xO|2%@O13;c;vQ`&lE3eBL%rk$%K!461i4`ub=PvU~7D}IdVruxfhKe72wQ!X4559#DTVO&px zKiivHx{XUlSeQDYPsp+7>8a3>hR9Uie(bWaC!}Wpq)#ZUG74|Nu*REw=MBMeB=Pss5bxn zB>_tigE+3&7=-h51h3Q^vB(o_kjRAe(K2;hSg|;YI%Wf}lG+I4YVi5|(>z7V9`pmK zKxI#TvOJ&k0=#AwFny!<^CvrS#Dy`hjw;6G<8_BL^>vkPs1l36m+(gFik4uJ3}$xY zD*%igZ1=?Y%HPIc4#B6Q8}1nN^i3fMPvqOS+JbKBAJ+qAZx`>{J*lsPEc~E0j;Dd^ zd%7gc1=s-dbkstaLQ`A{EhDhsy{F@huZcQm&kOKmh)TGmzk1hkMkWM6pi-wquodi^ zQDwENfxei`WtIdb9n@NpyIw^w2IFK)_u5LU*Z85Odgy&2NlX*t(Oeum{%+j3+vY%f z`gELckuzcMkrF)mQcPfycw??O`7X=D3Dm)Kihug`?4^Ze9ELN@wnJ#k2<5enna9~mM%u)Fiie-?IhDi{mq@h%>Xr!%E6+%4 zOuLI-xJaE?@iUa$Odp;tHIPs9#G^4*jUm%4H`M}uN1Y}9g;ADZA;dPHyXH&q+%#k( z)EOrRFacev?r9&!@+X(@CA4&uo8R|)DT{gV5|}R3J@q`BJH11IwWxRa9A}L-Ba&xl zoPPW0Fb36i;zbJD6<^OoRfCAcsKRj+deT)mF=zEpcnSNc$W!I;_S2%vhkjDwVsnJj zE4)am$^B{ueD^S1e*Ak-4HGnaL@)@1dH~pm*9!dvOSSSCzG^>cPTN+lV@1AzJ;+o} z>(a`!+DFWu5{m69_V!$To%kYxajE?d`%L5Pk@iC2VBXmM&l`t{j6Zvt0wsB=7YG?{ zCqj5`UxW|*D5s%*yw;vr|I*B%f;gHR8R}6pN^L1I(x|;&qN*U>c$IKofg$&E%Hg8t zb0sN){21ti5QHyMH|Mb;Db4#r;iQuBAUZIyRiXLSjt8aavZ0U%;+ChG$3}jTn_*wU>`2_|A$)J6*dcco=S~<%N4)pc5EWYk* z2J&wS#W;!4<=;j|e%k+D{d7Qbv$!`D2M;^v;NW@u3^D7EtE)vQ;|tBknPS-o#B*6HJLCu;-ceT;J8aaTjkO%!gS0ei7c|^XGCas2^cFPCKOihc!F3cs6_i9s{pDw*4 z;&X-MO9d(c|9v}H9D?&5C5ZEvqCE&gL(U>3h0(}wB-*OdOv2jJqsa#Jh}h5OlNM%! zq+sUM46?|ou0i@sXMeyTwlP}MJ2r$m#iGAnB2tmCGx%4@r3M$7)wSN@K37nNIAm=U z`Ky%?N_O6XG`eYqb#+TbmyeyxQO1gn_itFHZ)hjp1Qz5*3y35#!c89KEES6? z;Kj{<8x}65%k20O5Wa8a=y!AUh6IPWTp{w}`hCDH zupNwdY*}N1ZLJDPX~#@J!ay&(G`S7dH^9IkD+C2~vSj14ubPN&URB98kry08z z%IY^ySNs1S8v((_l?6>;2U8>c{lFWln00ASnkd50%SCA6rXXJpsjgtcj{iak?AT8K zDY);rohJw@qdKH_-g{4@P*N6DIx|r+u{$b#3UXz~bTsky=1%L9GW&5Nn|w+xyVn{d z?zmglrW-xRp%r}*+<$JA>)(XUJQhWG16he8oD zER-lwpTi_iJl$6xYTwm-M7=&(4d+tLq3tlPc>FDS{udRs-;`5gAMnqn!NGF>2rydx zY!G_V3Whe+uYR5mfU=KDoP zF-e`7h-7vHg8jyI^h~LgKgq`fiVpv297}d?;75+A$#!W+!Y;)tLgzhJg8!1mBA`nK z=E0{6AN4rG?)i8an~P<`WO?KcE$h=dc6=BZ=(*4uQgH-t^uf|eIv z#^rbp-MJ*a;?623YO4}Pg_XJK)-EOdd_U;2H}RdA*$a-za-ly#7-?ihpx24~7i{HX zi=raC{e3IH$M(B@$g|8$>*q3;!me*I&JsK}0ctmbHVf+LPfnQG7Wj+$NY1fp`4o$C zAD+8cdEBA-is>idB013HE(-29jsLx5S@C@yv@XTg)nC9lUqi+z>N^f(C0&Aa z+y~!M-uM3Vt-J2MuCrW+wfD21J+o)e{PxW3ImMLuhg;Q}zyxtjM^mo__%dBJZZeG_ zwG{Gg>pbum<0orfF@H$_;~X}%$^Q&N&?6n9GSrb-Yi2a5J1}`u%pSXS0EZzO|%nAcrbRIoV@B83;Ri1e+ zUaeqg4i$z}ef)?cS3zPwU-5v)JvCQQY`q0j^> zrynW;ZX;-kU2N*k?&^MSHTSzl?zO_F}RtMDHAhDQu@tiUS#`Y9+ zx^4f}`T*tbDuJ&2yL`4rEf;W*`0UivzUrQ|dZ4B@mD*O?aOdMcned0=1^#!)Y=^qK zlRVHDkRo_Z93We}Gt>O0sL(+0Z1Sph=@`RhnXS@F_k+wpvYjTV>SPMAua!v^1(#m) zb*0ZBl9GZ;G6|p!EfIdE|NNXjgi^MekVW) zJzJW=l#BtR0va^*UzqB9RA#7EmglZeTS#D<>w03cBFI>i|=vSub+SejD|Kn zcg~lYI7aAuku{|uh6H`S`{}p}rvW~fOY7j;A37Hd}0=aDHQ*{*?%8uNL(&#k=ru+jpKk*(_+KD!umzuGdw6B}JyheC` zTl7PEP#v>>S+5$)dGbe`T@!X4#d?CTLUU*VrcXpCtT)jraRPj!cpCv9?631(TD>=@ zO&D5=@Tc>+IUBz<0v$&w+2DnLm-MxU~Zu@h&4<8?t zfVc=sM`$KsLL!?%K^y(UHvLVt$iUBxsSIbJHW07C>c5j7B9((pjtr?_x2?gykb$F?txFuhR&d=5vgL%GsQ zKz%5;2pIkc-9&QiKwVuJ&kB{1*mf$S@byI@#HutEp!k4*_-~w!gbuA67dvdiiM=pB z(|M6i23`NrvM=YsaC4%h0yH61Aubwgm zx*d9fsyi}ifII8t-UvhtL9(^n%D5qh_E|)KxaTI%m8A_o8WQbLHlk)eO2P6X$Se2~&YECsIpQnp?SIjP)|qklu*N{y^d{I3s& zg@BAjM{8TU*>Hoe!QdIzDnK9_zuyvXx9O2K)%-pb z@y*2NYcFO~80wKh?wGtI3h9we{Y>@f?*r=z?ptz+U+>*_zY+!kY83@ijTQuz?Qnwj zT*8&sU{$WWbu2}68KSocB;6Xt3KMw}M*C|DF()gzU3X^U(63Gk{lbkjP`nM%vYo@H z7(5ccF@7D=`ST?I&*)18_dJ_FUUPG{Ab4N+Zv;tB#4|&i{SMJK;p_tOvTgWBV1>zk z(E$p=Gm;>Xlg0QBy@j_ZNHJS5Lr3DlP5{z7?~CjmFf--^OPU*^@0pH&b0eD}%r2i( zdh7_e(i44a@7AVk53CS>aGne~F*vqEY;#|GZBJ)yer&9_IO-kV{P-#yb0(DLm#Y`X zIbZVpgq_x8)``vm-_-AX`YX2{b>6Xwmm?7Xd`Oz3^_Y@fQU=Ou#4YKK4YTu|}6zjrIVuje?DG{yKC1ZRtE7-5&~vb=CL6fmacL$8F=)XlP6*Z*Kh!ijp<6 z4RhUo!Z4?{1+jE@a}4lFu{4}((P~+;L+13>stFl~)wT!X@j1?q`_xmg%ptb1&bN6b| zuAg>{CPlQ<_o0x@&=jF)5$}m1y9JJs`}5=fZ|I6^CAIMP8m+<-C_$rl9Juh{;d8uuv6&7LgFmgf5V8Mlk%0_@C;)`CSc{QC zdr$l1aR0UfKxU)Ny}Ds91q^^W(+I}z6haCEH<%3%QnlcLE;ySAzt0*-b-h>M3Jib( zPenR`e!ytud+X{=g$*e1&oEv<0?@Ck|3@+$Q9OWr_`gB6{K|UZ`=RqaxiJU;xkSeS z`VYzFy(hO18mM?0<0-=Yklc)Wa!rB&a{B~waDIC$5ONLxTGRbslRYz_I@ zn)knJ6*+jtWL0p0ZTkMzWA8;4;1!D*InoaX@slIvYQQM0wNie;oC|2fYnOxtK?Z<> zg2J0b4>|ZaWkN_b+M=%~-;axJ5f}Bpfr>JG6ui6&#YG;=xU$rmP8Wc9{sVBF^8{L> zkewHTEP~Qa=l~kvTy-&iUzD64V96Ygl>`APTaPV!0C;Le0Jht;%7&rM`RqEH{w@nZ zX%;|I2Dy*Wg3@CJD;^;G5ix`(VcjKh{xKY~ink~MXme{H9Yr6EkffC3PJ`3|S#GIp z37N^%Z@tBn=5h^;hsn(Z`@4KVfYN^6GF=@&N);Ik0iY8%nT;;u@61+O))HtOrx2^k92(u0tQh*Rve_;rL){0DJPMp!tytl0%#S7i`FgQVdzZsnAkN1RZJU{m1RvEFp(x2nYruSqV{f;HIsyrNSc3)S*6^dE>m= z*1_ohRPI|4Y5|*0&(dC?s2qK>Sw|U%{ivJW=yGq5k4lv*`zc=C|4LDUB+`PQjAQ(M@xtEjTgp0B!yF;@Dil?XOX-t~ z3pD(@ZrcQlUG7;yr!{!qEB4q`qK9np3BRAXTiqNpP($dyu|XW?JNhdwHM4+dC7*M| zn4NB1`T>FG2J$Wna#~>P7-(kMMTN}3msHj zhA&w?l;e?*dms{0b@3FyhPIeW^@>WlIZ=spl4Ap9%2g`-adrpjA|DLwa|fGh!oP;7 zO{WZ9e0>-ix4%wSEG2eGW{JL6^z9dL9BqFS~MC{G@%#?r}^L%0RcIDkL)+_ z(lx8Td`KagyYahlLV&45DAL%so00Wip=K}}`!~F_hQ>CtFM^9L9?$qFda}_?+dAc& zy2|mcK!oxEYnkp@J3l81Z^t+F z@8*en>&2PpxC5M-?0Uj`AJ_ts93yDEt(3P)y1us}UHI+KnH~eG6P@Zl*8IE48Afm5 zVi46^Dh$fcYEeP5ClYf&x&s-WKbY=<=}@`Rzt($vz!GN5yL~!3VQLJ7l>xdAKaxkH}_@ zqa0x#wmXF1M`q-ybDp*U=kN+{wv6A|43LpU0e}_neMH74Qvl6)-?Dl=7=-y}hv)Uz zMf_+%H)eFKXb(F0-$VSXF0E!qWB@HVYK&0--XRcMzWqCrT{W}-zXRY7S&y80h&Xc= z014K$f5DUuz#vog`?SK}=cy+_kJxvfYSQt+V-4ue5_^;L8 zf`4hi5@CMtvOw0X1ug^pK04_V7%(K>%LO^|4_g5KkO6aI*B$*Cb@@#S18~p*sR73a zG5z7@{k72}6e7fM*g>-F-vjE)0Durj39<2CM;Fp1JP;rbU0CH0#Q_4|gA5P>eIDA) z2jT>5i-7+(gdN_^@?(^gRJbhOl_Ao?ikd;DCWu0cs^^XJ#PSeO$VRbd=8k6$ecF5L zw>4Qd?R(JrW9qoo^`Nn`X)keFR>4@|t7o@il&V9o1)+m6I_P5oX%0NRx+uy4e=$u7 z-4J9Hh|!Fn=opyqVad(&q=bq#_0V_&PklXgTH+X0wfxSszCoxLqWxsE;eLK_@lYop_ zkGmu!06Soi8p!gD{pnpbR=KSG>iSDEUnBhK{y*pb>vV=$AgzZ_5CT8W*B~XoX{jdn zA&9~qm%ImhkuMhKRP zFoRO22-lwJO2?4lS93X zJsMZ@dxv2~{CqdZSq`mI(fE_X(T?;)&?(+<5s>ps$1FML{P-KkBY}Aa+$r8|D@nFd z!$1XZA^+enXS^%=ob4OAnkiWy6u7Ft^c}RR?uFOA(UFm5#-cA9YzaUw1a&M7mGlow zXb<_)WW1+obcLjqd_)_I>PhXC%i=*g@&%rVx5Ezs>F6r~f^ zw-v^}cga6YHU#M*2pq{&f%diDvEgSI6Zc*UfM@ayI>{M+Xs&!qJ?5GI76elr?%})3 zXv5!tvI8cRl78}5D^!(*G~Mlowl?B`EuDVsE#9o9m5b6znO@P;NUUR$|9n%dh=@_a z3H${KZJAVlp}EiGtGgZpB94=I8Ks1<@ues2YAUebn%T|*m+fZhvf*^dp=yRV0zW)L zhP<*!6uyIUCr-I(udcUA9S){MS>VF(Scx-q?&-819x8`g-%`V17+kjfj~9FN0u-7D zWjkig`Tj?9l?lj~kLYX6)nL(oh+#XsOaMj4@RIBX6Z^k}H0v2?7h1T?JnL%kUt8D% z7NO)8r_j7=ao8m}deU>Ib%*q+vO)KIooHNQcJ!>yJv3X|Q~UX_n_{LFT~$VqlyrNU z?>rlYRrTFnVICP1swmfhFZViOXQ;S4tC3I1n%q*_>w$ACX*xfi^zd}|Of>dh<-Q<+ROXL4Bj*jGXlLYZ2gT2v3{%JmXMKT z_FNE4?{Ml@+up*7mYM6WSRUVXY(h{T8k<6+@6P4!3uS{~iYv8h_C4trYe}L+Xn~~M z&th+^T3-kG>#7)i!nNOtsa;zhS61q9^kKNBoVti#TUQSxNz9)K>u9gcm${<37 zy5Zw7a}7LIu&s{rvUwlP+vl7}ejHV^--m&p^TO`q8n- zk1;u>_KusL8k?+`0{LWUKV~Tx~+#zS|N=e-VWSHg(R@Ksh%AAuqTMUkp~A$)uy zRL^iusCz2&Xm`ttwD?}gL=}|6*0vK%I+;-WtztiG74{u?#D1kHe(cm| z%hDem_+sxpt$k$^AYg2(v|k)kQMjKIy)Kbx zNL*^={nj!(N_IdTECq+QQG4Yo89G>kmuVD|(EdF>$C+>Vq?0Vq02iv7AzXY#qzc|Dt<@WPZ6TrvB5 z6yA=5ae10jiG%K4FPcZPo>a2ANBP@LLdeS5sFq~|ITLUzKRibZ`p2vJ2tBms&m&zv zTF~@vl&<5x;c_!F6Cy1X4{BYXd)xOa1x6+UvA4mKIl?RDxwbXa)Dc~wIKCLOroFT% z|AGgg-{#OZ0eLGld3X6De`9{@m3oU?6tUA+H8mGKv$tdO?#(z~DasztPtMA!*dad= zuG?m?9@YUw&S{nQgJUWqKQGbivgta3gIVgpV(Z%sUpAk{w@xu#*?t(Ph50Nk#dy-= z13AZX-BqWoQ8m1{`~m?P!5|JxB34FA)|HRTit|;IpB|S9yhqxIMmXPV=ChHjb{u0U z#pM?^Yb7IA(X|h%tHypN2leLEP8@dq8Ez)4^&p=DQq^g%`m+3yJxNN@fvu1)hASPV znRHsw=bru<(=WAxwz1EDFx{50gD-0@iXDP4*^-KIR?YF{_+1xda7BkSoPF7IqIzE^ z2N`m#8FekvsCl#HfznTLj8Et*+k^8$By%Z2$~dZ!G67fg0A9-|(+FGW_$c@VEAEB7Am~T2}%$!qx*NgHP*$v=5kZ?7X zVmBAMei@^=VLa=;;aJuIdFw1rlZ~&Oj#_GxyxPSg7>d|p!_>Y^Zm31BXi@~>fzz@n z6&3>jWvkM!z3YfWtzem@H7TXs7?$=~C9|Fj$K?A2N_ZE_cHc(4QC%r10drq^8M&~P z&8F`Ga&nW_cH>zF*#)RSF1^r5%T4BY;=_&W`JSj%&VNrHC3ZKVM2TpOd6w365p$;G zy~ZK7-@wWHg%xd{I=K$VWwPpS;()IJ`8A<(f6Ral+NZ5oIR;NxInq~}t&5@g+K$|; zNYj6;A(ZIJlGE25t`F6ea9ysh*He!=2q@dgFhiew4t9*NOO21qFkEJIX4A;*($own zytAbX#qpC53wzmFM$a1@E&VC?NpaqwBc{H047obuGMlxW?H(k3VlN&>Uu%Hu@#>P| z(0R4u;*!=Sihyu_$D8Z)O2r&4%a%s5LPtk}kvdY_QFGP*Ob#AHJ)#Ghz9w@FO zE4`z>GK01u2i=7pqXyw(Y?_oITnw-CR!hyvNEr^lj&^lhV*nAK<+zCQ zR@#024$sS>l39QDhFbAHvt__DimVcPwJE>9!Zt3dJYF zaw||v8Xb`hh?zqc%*k&#C?MQV<+mg^aAvi`0-BP~$i)c)g@;i)GkwiOH_K5=(KtDY z-?RuD-SkZ_=hz3vn9-;))$MU(nDL_F;XyTv8RBj-;^F-%+vRSkCLqtj%nlOov zYFBCEah5e< z4wpu?;e||8`RCKLo%+S;)(Ntl>ov&T+&&K7&94=RsnLs{x%EtJU6I*V*|xq!>lshMqyF3pjdfGi659ZQi%vE@*~*`wG*x(RwB?f1ngG<{OsLCZ2(j z0pW~%K)cAC&DC$ z>I+dwVeZ7U$Q(Ohxlj@zw>;F5qenCms!A9BzMh)Z+0X6vX&|L7=27 z&B)s^bp%47*xP-DHaL6MPrPRBQ@!-worSmkNfuuVF)X$73#<}*HW6BP`ebY)KA+g7 zg)7J%ZWTD%w5Zz3qi86}(JraH=VedAN>Oh2W7ZgCCU&d>vdFmtkBd{zVHEQ$UrwvY z*}`fkk(jj?`Yz^~eRpEW;(>g8;@_R*Z+Hi0i^?}6Qa9VOkb;tQASC>JVq&14TYFW;z0rrh=~G13ApqIg(etLcJ{S3_euH#>Qc@;kIWnBR*KpS0Y(+sI z!`MWTojl}QcJD+s-`X2*no|3SUN(y;ArEHHq;ubtz4)NJ^JOYxh3L95vW+yG-ScXc ze)>L>7z5023NAgDikgmvrf*(AFD?vApWZS%<|xFbd|+NK^e`CJm2Cme97foqU({R+ zgnyh5qY#t|SVAZ*wLRT1Oe~D~xP>;!-|f#Pq)iZWJ=Wxad@8*j2-ScuSHp&k%}13l zPK#q_#2`pMW<6iqhw&ZS1>R{IYTc>(%jPRWknDUuClZ6JP1V+`gj0*HZ`Bi8k}*2= zeIjfIWFM?@osR0qh(EBZ=%EQxkh9jkE}ubN++-)Z)BxA#NykZXM3^Y!_&Y8{K7j;m zx|*T6=LUpy|Iw=)@v^QbW~j1t-Eezx?P?rO@1=c|a}{e}c5!djFLPUS`tf&Q;48A@ z_XiVg6Gq60!?Ib}%_Sr*e-v^?kn52d@sfFMInk+kH`1T>O?%_rW&<8E`CYd2|t_A^X0|P)Us!#H;+diqHP=!>u2t{ zH$G}pl9*;0B2&)RkBcihQjuIaeEH|r+GVxP+Ti5X)Ko^$L*Dyq#Te#eKT1!_ecD9o zp+Fi+$RGRp8;!z*DOq&J+(>_?y8OTdv87{7r@j6U3?i;VD9Mp}LeZ{}9xeW_iQRsP zbFpC~Mrl;aGIv`=xF%_X3pCl8N4ow#WtIqJL`UsggYxzQ>M zGS3t4RjH*q`|LTqsJ*XnTli(e;IZV{efb#MByBAdbCJc*GWArza(+`a~bWU2Gv-mj;;_-Um@Rxa!qp zAe-UWG;x7J@L}w~9>|74`_c=a zD%}BL;BRZ` zk-Wm>D4UY@9&XXEHLfTl?Ng(gYPQ$G>h6J7f0QOOY;&TTXT6P5Y}^^P_1LbH!6!BG$W>`lIWJtgA`U{(d3~&|J@VsV?{ig5#yf zdibmn?pakoY9$ab0C;P(%oW@uQJAy z`oe0$J8oyXSXh6;({xN<>kpMf$O(7XxT5L;?{G$-q^>Btu@$2BLujRayiU)PC@pNs zM^swDYg4dpXG6P0UMXzT@`BiPd&OHPTxqiOa;=45i(WC4y_DtIu#a=7H0$P8b8f39+&(He8%Q9F(s1=*)yBJM=s6 zkA#D26ru*i6U;>WB5FJD7e_CtK26d~1R!IEyX`}^WaSlhiao8n@Y1<`JVpNN5 zSFS0(A-X5#8;Vq@i_afiK_fwufee!r@-+QqN{blb_ zxwy?N7v+05fv>JNT;uiXc2EA-#o1HX7S#UeXT(^W@k7kTs;5>5CZ~(~ZPQMCCn@vm zD^L;vnM;tM;X$^4`9@Pj?l7vd)TZzDoFo73fCv1rm(dXX9XFwA1pgJN38ZRQIuyx< zw9EcAxbvsm`v3DM52Mg582Y=w^xUruDsjAhc0RhjU95Vn<3xNa26 z4k}HCB{hg-``bbXWvY$@i`hbO!9uu4)g8EF8;q#BP*RQWkYZZpObB@>8chFkdb=qE zWE%1d@!H_uN6HMV&}ftv!I^6iNLxymR6ZK4B=Y@dw&OdaC6nR?9@zd@x7a??Fk(6y zq1zfzb5h-gX$FF0l?zhoaxjY}spqawF?p-_)(S{Emz(7*P7rw-VT?})23Y%vSTxLy z`Z^Raxvfelv2+E4s&^ATbZ=LlBnRz>?`|Gy?a)fi z2!Y2mIW97>I7JGS9=Dx)V;$Vdh|7)!ymJJ)02*4B_z4EBij@blcxK`^AalAE+59~9Ht>xRP=3FiihU9 z+ulaR8ZXUa8xf8b;nDSgH4DE;cu|J<`N=sEa&%hcA|=>;Vvyi3Ifo^H%t95M8KOB0 zyI)VkFz8U8l2OO$$xV1o9L`!4&XGElqfq$MWa*J4)9nUN$PA-Uc_ z!0-;d_oQ|x`4qs9=N+Dee(Be^PH zbFL_*%uzEe$s9eMilu>ka9$U$06B686JPB_g5gda$VwsdYAt2@S;8wQ8?-$xuB6q8 zet|hP_?D6U4_qWy!|HA`zV&bLA5HE%aN+lyaqabEUm0FVFY3Yt*F8>LgWyMX#@;E- z6S8d;Lv`GewNV(r*>gN|!8l&`X(OS2vn2&tZK+zDvnhuip_9|yHX)#w| zE#Q$_e&e>jG_0vf(F@^vB3jXaGE_cs3MO4b{SxRI!6PY7Dy5;)-NHlF2W$6R-z;6#H&x`+C6}bh~k|DCzWa>w5t)FUj5I z)fvlR*V@J8LS^rV&wSPD4rAnR<+Ebp!H}?%L>a(Ol`5IP85v!Q8C5GXbehv*$V?63 zY)eITcT!<}@#J=WS{{wFJ9r_ov%yl5oWOz&zSWDyZ3F96>Fykf@xjOF+V#h!8q#6r#;ylt7{?8RIZOKwX?@8(IDF4ZUz$ z1d6d;eAin`=iYZ8a?dZccqQZPmu6=>)9Ij5ki^L)-3CijxiaQ`b1Lo-QlG;#_)T5M z{3sLML$v2Bc$%J}X{rLPYhggN39DOH?^VI`gsfM6h8uNMoQb?_k9fZpB0nU+UYeYM z22p6=GhS^^kO^`huW!_&jgYcD=&ZcEYi0a=Z9E1vqBT>F=!+W8k@%I*Qq|9ZrYuOO zAUx5;1#>PoDL*iVZmzjBd!z7bD?7kLlFP1kLF#KNm%8ec=U8m0RMRt!SPLe9aU=!4 z3PT`ei0TY{zYz~H(OCFiiPOhp1__(TxqKV3yJF4%lv+eM9wP=u=(lDW9}h@-jef2B z1%f#icCHmC6b8wdb#C@$XCzLXs(KiIab-T2QG!(ev)h@py?Lyin_%e;{Me;=FXrGIkh=sLB!UTsY=XDF=7A&OV zXa&ZcJ~NG-X4-aP%Vn9g=~Za}KUH_*s?@3Y3v&&4MN&G)g7Z)Qe+BE2&43u?qi;0> zzCW2Vg9RCn@!ZvgzmAik{kW~2tFQyku>&z-Zfdo+v_>TiFV4AI!sW6^kOcf%&s-%} z-LU)QA_ZFkFkeA~vWJx@G#$mWF@|0uSIw5%a$i)=*kiIv^W3pmA|6}BX9ji6%@!~V z2Qoie2=!Kk6y#RWf5!*Sy0#%#_$j}sD&LBS*laPczF!YDQe^FrkFd!bXY%=m<7ZAZ z&Sd+X=I4-YsjMA^r>asDVT%6NSP z9Yh@q7C%+4mBH!B{SlagX+~gKXYumy><46Yklw!9qt||p+<)eV48cn%q`mNB(o~KV zNOTiYE_2#$CfN}~vOth6@yW{gB-=%J@MQ6=I+_WGoJ5COnEen zCuOthCdKELO@~oT606SvBP34F$~~d(E{}4f;jp3}LKm%KVP|9@2P+4rol5wM&xe!9 z#>1OhBv7WX9>sN=yWU!pOzsU?Gv1Xegr=IrP=9}22FjPry3rH3#wGCeM z{>Y#ub|XA}!gZ;@74HToDtVWHu$Z*adg_!c0_=#d?^vd{*o{~P#T5>oCVbeGW1{^B+}h~j#toz;7(ocM17zVklkSxNeFQd>^T6LNRu3nPYo zhu?-@-WTj8sM}9W;Q#z>5z7yxT?VK2yhjUCdAWG5UT_|bUmNQ97}L^fS;pkX#n;WF z<D#)Nm-$TQ<_>X|$H&%frZ`ElPaL zDfZYL$3t#CPo{zX=JP1C?EKNIZF6fyuJ7{JAE}H)-&&>&p&l}K44MAP3fd>_aZAOA z1=an3SaCyYXd0(khgd>Gd-sMj2c74YmhF1l90Swyifm8k4g|?(N#Iq@wM*X0OEfM< zFT?(ob8|uhM9i*YIqQEKt_SH!PznJ!z4%KQ?SubLCIA5QV?*kOJ1uAVzi`A~dl*n% iEbaFO@&Edhx5DIJhBQWs0*Mg7KUqm7i3%~pp#KG?=w4$0 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 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

    @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 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 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 00000000000..d9759629b75 --- /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/85] [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 | 464 ++++++++++-------- 1 file changed, 263 insertions(+), 201 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 9c3a6f23419..d6fd7750346 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 @@ 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; @@ -30,22 +29,21 @@ 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.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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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,24 +117,12 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { } 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); - - double movAvg = 0; - for (double value : window) { - movAvg += value; + if (docCounts[i] == 0 && gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { + continue; } - 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; @@ -146,7 +131,34 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { } movAvg /= window.size(); - simpleMovAvgValueCounts[i] = movAvg; + simpleDocCounts[i] = movAvg; + } + + 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]); + } + + double movAvg = 0; + for (double value : window) { + movAvg += value; + } + movAvg /= window.size(); + + simpleDocValues[i] = movAvg; } @@ -154,14 +166,13 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { 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 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { 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,39 +214,17 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { 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); - - 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; @@ -238,56 +239,53 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { avg = (value * alpha) + (avg * (1 - alpha)); } } - singleExpMovAvgCounts[i] = avg ; + singleDocCounts[i] = avg ; + } + + singleDocValues = 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]); + } + } else { + //if there are docs in this bucket, insert the regular value + window.offer((double) docValues[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)); + } + } + 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); - - 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; @@ -317,7 +315,56 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { last_b = b; } - doubleExpMovAvgValueCounts[i] = s + (0 * b) ; + doubleDocCounts[i] = s + (0 * b) ; + } + + 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]); + } + } else { + //if there are docs in this bucket, insert the regular value + window.offer((double) docValues[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; + } + + doubleDocValues[i] = s + (0 * b) ; } } @@ -332,8 +379,8 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { .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 class MovAvgTests extends ElasticsearchIntegrationTest { .window(windowSize) .modelBuilder(new SimpleModel.SimpleModelBuilder()) .gapPolicy(gapPolicy) - .setBucketsPaths("the_sum")) + .setBucketsPaths("the_metric")) ).execute().actionGet(); assertSearchResponse(response); @@ -356,13 +403,13 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { .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 class MovAvgTests extends ElasticsearchIntegrationTest { .window(windowSize) .modelBuilder(new LinearModel.LinearModelBuilder()) .gapPolicy(gapPolicy) - .setBucketsPaths("the_sum")) + .setBucketsPaths("the_metric")) ).execute().actionGet(); assertSearchResponse(response); @@ -401,13 +448,13 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { .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 class MovAvgTests extends ElasticsearchIntegrationTest { .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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { .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 class MovAvgTests extends ElasticsearchIntegrationTest { .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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { .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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { .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 class MovAvgTests extends ElasticsearchIntegrationTest { .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 class MovAvgTests extends ElasticsearchIntegrationTest { .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 class MovAvgTests extends ElasticsearchIntegrationTest { .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 class MovAvgTests extends ElasticsearchIntegrationTest { .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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { } // 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { } // 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { // 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { // 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 class MovAvgTests extends ElasticsearchIntegrationTest { // 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 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { 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/85] [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 5b3bff11c18..75ac8b9a49a 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/85] 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 d6fd7750346..bab47aadb80 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 @@ package org.elasticsearch.search.aggregations.reducers.moving.avg; 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.bucket.histogram.InternalHistogram. 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.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.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/85] 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 5ec45064c7f..8daa4d6180a 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 abstract class Reducer implements Streamable { 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 eb04617e548..7fbcd54f789 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 class MaxBucketBuilder extends ReducerBuilder { 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 2a9dab3b6bd..7d773747a8d 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 @@ 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.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 class MaxBucketParser implements Reducer.Parser { 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 class MaxBucketParser implements Reducer.Parser { 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 class MaxBucketParser implements Reducer.Parser { 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 e209684797c..b325697568e 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 class MaxBucketReducer extends SiblingReducer { }; private ValueFormatter formatter; + private GapPolicy gapPolicy; public static void registerStreams() { ReducerStreams.registerStream(STREAM, TYPE.stream()); @@ -69,8 +70,10 @@ public class MaxBucketReducer extends SiblingReducer { 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 class MaxBucketReducer extends SiblingReducer { 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 class MaxBucketReducer extends SiblingReducer { @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 c4d3aa2a229..cfca5c60978 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.io.IOException; 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 c1cdadf91ea..5d79b1d1e7a 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.ParseField; 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.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"); 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 48d93766bfc..84e559e4970 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.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.BucketHelpers.GapPolicy; import org.elasticsearch.search.aggregations.reducers.bucketmetrics.InternalBucketMetricValue; import org.elasticsearch.test.ElasticsearchIntegrationTest; import org.junit.Test; @@ -244,6 +245,66 @@ public class MaxBucketTests extends ElasticsearchIntegrationTest { 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/85] [TEST] Restructure MovAvgTests to be more generic, robust --- .../reducers/ReducerTestHelpers.java | 131 +++ .../reducers/moving/avg/MovAvgTests.java | 1045 ++++++++--------- 2 files changed, 646 insertions(+), 530 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 00000000000..8496b93e7ea --- /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 bab47aadb80..c92f0b1cc2e 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; 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.MovAvgModelB 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,32 +60,56 @@ 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 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; + + + enum MovAvgType { + SIMPLE ("simple"), LINEAR("linear"), SINGLE("single"), DOUBLE("double"); + + private final String name; + + MovAvgType(String s) { + name = s; + } + + public String toString(){ + return name; + } + } + + enum MetricTarget { + VALUE ("value"), COUNT("count"); + + private final String name; + + MetricTarget(String s) { + name = s; + } + + public String toString(){ + return name; + } + } - static Double[] simpleDocValues; - static Double[] linearDocValues; - static Double[] singleDocValues; - static Double[] doubleDocValues; @Override public void setupSuiteScopeCluster() throws Exception { @@ -92,297 +117,191 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { createIndex("idx_unmapped"); List builders = new ArrayList<>(); + interval = 5; - numValueBuckets = randomIntBetween(6, 80); - numFilledValueBuckets = numValueBuckets; - windowSize = randomIntBetween(3,10); + numBuckets = randomIntBetween(6, 80); + windowSize = randomIntBetween(3, 10); + alpha = randomDouble(); + beta = randomDouble(); + 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 - } + metric = randomMetric("the_metric", VALUE_FIELD); + mockHisto = ReducerTestHelpers.generateHistogram(interval, numBuckets, randomDouble(), randomDouble()); - // 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())); + testValues = new HashMap<>(8); - 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())); + for (MovAvgType type : MovAvgType.values()) { + for (MetricTarget target : MetricTarget.values()) { + setupExpected(type, target); } } + 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())); + } + } + + // 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())); + + 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 setupSimple() { - simpleDocCounts = new Double[numValueBuckets]; + /** + * 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); - for (int i = 0; i < numValueBuckets; i++) { - if (docCounts[i] == 0 && gapPolicy.equals(BucketHelpers.GapPolicy.IGNORE)) { - continue; - } - window.offer((double)docCounts[i]); - double movAvg = 0; - for (double value : window) { - movAvg += value; - } - movAvg /= window.size(); + for (ReducerTestHelpers.MockBucket mockBucket : mockHisto) { + double metricValue; + double[] docValues = mockBucket.docValues; - simpleDocCounts[i] = movAvg; - } - - window.clear(); - simpleDocValues = new Double[numValueBuckets]; - 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 movAvg = 0; - for (double value : window) { - movAvg += value; + 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; } - movAvg /= window.size(); - - simpleDocValues[i] = movAvg; } - + testValues.put(type.toString() + "_" + target.toString(), values); } - 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]); - - double avg = 0; - long totalWeight = 1; - long current = 1; - - for (double value : window) { - avg += value * current; - totalWeight += current; - current += 1; - } - 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]); - } - - double avg = 0; - long totalWeight = 1; - long current = 1; - - for (double value : window) { - avg += value * current; - totalWeight += current; - current += 1; - } - linearDocValues[i] = avg / totalWeight; + /** + * 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; } - private void setupSingle() { - 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]); + /** + * 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; - 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)); - } - } - singleDocCounts[i] = avg ; + for (double value : window) { + avg += value * current; + totalWeight += current; + current += 1; } - - singleDocValues = 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]); - } - } else { - //if there are docs in this bucket, insert the regular value - window.offer((double) docValues[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)); - } - } - singleDocValues[i] = avg ; - } - + return avg / totalWeight; } - private void setupDouble() { - EvictingQueue window = EvictingQueue.create(windowSize); - doubleDocCounts = new Double[numValueBuckets]; + /** + * 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 (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; - - 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; - } - - doubleDocCounts[i] = s + (0 * b) ; - } - - 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]); - } + for (double value : window) { + if (first) { + avg = value; + first = false; } else { - //if there are docs in this bucket, insert the regular value - window.offer((double) docValues[i]); + avg = (value * alpha) + (avg * (1 - alpha)); } - - 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; - } - - doubleDocValues[i] = s + (0 * b) ; } + return avg; } + /** + * 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 { + 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; + } + + return s + (0 * b) ; + } + + + + /** * test simple moving average on single value field */ @@ -390,11 +309,11 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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(mockHisto.size())); - 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])); + List expectedCounts = testValues.get(MovAvgType.SIMPLE.toString() + "_" + MetricTarget.COUNT.toString()); + List expectedValues = testValues.get(MovAvgType.SIMPLE.toString() + "_" + MetricTarget.VALUE.toString()); - SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); - assertThat(valuesMovAvg, notNullValue()); - assertThat(valuesMovAvg.value(), equalTo(simpleDocValues[i])); + 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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(mockHisto.size())); - 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])); + List expectedCounts = testValues.get(MovAvgType.LINEAR.toString() + "_" + MetricTarget.COUNT.toString()); + List expectedValues = testValues.get(MovAvgType.LINEAR.toString() + "_" + MetricTarget.VALUE.toString()); - SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); - assertThat(valuesMovAvg, notNullValue()); - assertThat(valuesMovAvg.value(), equalTo(linearDocValues[i])); + 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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(mockHisto.size())); - 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])); + List expectedCounts = testValues.get(MovAvgType.SINGLE.toString() + "_" + MetricTarget.COUNT.toString()); + List expectedValues = testValues.get(MovAvgType.SINGLE.toString() + "_" + MetricTarget.VALUE.toString()); - SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); - assertThat(valuesMovAvg, notNullValue()); - assertThat(valuesMovAvg.value(), equalTo(singleDocValues[i])); + 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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(mockHisto.size())); - 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])); + List expectedCounts = testValues.get(MovAvgType.DOUBLE.toString() + "_" + MetricTarget.COUNT.toString()); + List expectedValues = testValues.get(MovAvgType.DOUBLE.toString() + "_" + MetricTarget.VALUE.toString()); - SimpleValue valuesMovAvg = bucket.getAggregations().get("movavg_values"); - assertThat(valuesMovAvg, notNullValue()); - assertThat(valuesMovAvg.value(), equalTo(doubleDocValues[i])); + 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { fail("MovingAvg should not accept non-histogram as parent"); } catch (SearchPhaseExecutionException exception) { - // All good + // All good } } @@ -612,11 +560,11 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { fail("MovingAvg should not accept a prediction size that is zero"); } catch (SearchPhaseExecutionException exception) { - // All Good + // All Good } } @@ -684,11 +658,11 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { /** * 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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(); + 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; + // 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 class MovAvgTests extends ElasticsearchIntegrationTest { */ @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 class MovAvgTests extends ElasticsearchIntegrationTest { 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(); + 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; + // 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 class MovAvgTests extends ElasticsearchIntegrationTest { */ @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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { */ @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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 checkBucketKeyAndDocCount(final String msg, final Histogram.Bucket bucket, final long expectedKey, - long expectedDocCount) { - if (expectedDocCount == -1) { - expectedDocCount = 0; + 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)); + } + + // 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 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { 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/85] 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 4bd2ff4c50a..8b9f73ebf55 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 class MovAvgReducer extends Reducer { 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/85] 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 c92f0b1cc2e..eaedfe4e597 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.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSear 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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/85] 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 8496b93e7ea..0b0f720344f 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 @@ 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 { +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 eaedfe4e597..cd6ac6cf490 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; 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { } } - 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 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { 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 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { // 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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/85] 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 84318096080..4bbc8ba662c 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 class AggregatorFactories { 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 @@ public class AggregatorFactories { 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 856b96979f2..db2ac49bf38 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 abstract class InternalMultiBucketAggregation { - - 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 93fa360b113..041c15a5dc1 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 abstract class BucketsAggregator extends AggregatorBase { */ 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 1fb919558d5..5c10e0d3ad4 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 class InternalHistogram extends Inter } - public static class Factory extends InternalMultiBucketAggregation.Factory, B> { + public static class Factory { protected Factory() { } @@ -249,13 +249,11 @@ public class InternalHistogram extends Inter 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 1bf62b9abb6..db0ccee33e5 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 class InternalRange> extends InternalMultiBucketAggregation.Factory { + public static class Factory> { public String type() { return TYPE.name(); @@ -236,18 +236,16 @@ public class 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 f6cdd8ca1f9..4ac1bff7cfb 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 class BucketHelpers { switch (gapPolicy) { case INSERT_ZEROS: return 0.0; - case IGNORE: + case SKIP: default: return Double.NaN; } 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 7d773747a8d..87afd890e34 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 @@ -47,7 +47,7 @@ public class MaxBucketParser implements Reducer.Parser { String currentFieldName = null; String[] bucketsPaths = null; String format = null; - GapPolicy gapPolicy = GapPolicy.IGNORE; + GapPolicy gapPolicy = GapPolicy.SKIP; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { 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 cfca5c60978..3536377644b 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 @@ -45,7 +45,7 @@ public class DerivativeParser implements Reducer.Parser { String currentFieldName = null; String[] bucketsPaths = null; String format = null; - GapPolicy gapPolicy = GapPolicy.IGNORE; + GapPolicy gapPolicy = GapPolicy.SKIP; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_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 5d79b1d1e7a..0850587de35 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 @@ -64,7 +64,7 @@ public class MovAvgParser implements Reducer.Parser { String[] bucketsPaths = null; String format = null; - GapPolicy gapPolicy = GapPolicy.IGNORE; + GapPolicy gapPolicy = GapPolicy.SKIP; int window = 5; Map 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 ede94abd973..b1ac6756f1e 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.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) { 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 cd6ac6cf490..ae0f89ae868 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { // 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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 class MovAvgTests extends ElasticsearchIntegrationTest { 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/85] 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 d798887c836..b244587c9b2 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 abstract class MovAvgModel { 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 96bc9427de3..a8f40d474ac 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 @@ import java.io.IOException; * 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/85] [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 74784c110a9..7f081616375 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 75ac8b9a49a..d460fd5e450 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 f1fa8b44043..8369d0c1ba0 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 ca6f274d189..a93c7ed8036 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 d9759629b75..a01141f0fec 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/85] 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 | 78 +++++++++---------- .../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 | 47 ++++++----- .../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 | 42 +++++----- .../stats/extended/InternalExtendedStats.java | 3 +- .../metrics/sum/SumAggregator.java | 20 ++--- .../valuecount/ValueCountAggregator.java | 6 +- 32 files changed, 186 insertions(+), 198 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 202f02c4a22..2e032640f98 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 @@ import java.util.Map; 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 da4c2622331..0a8a136d160 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 da728f1ee04..6459ff83215 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 class FilterAggregator extends SingleBucketAggregator { 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 class FilterAggregator extends SingleBucketAggregator { // 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 931ead734fb..913d844cb6a 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 @@ public class FiltersAggregator extends BucketsAggregator { 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 class FiltersAggregator extends BucketsAggregator { 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 c2c646f5702..36448a103c1 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 edecdd749dd..acc1464d349 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 class GlobalAggregator extends SingleBucketAggregator { 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 63325c12aad..44342366b3f 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 5c10e0d3ad4..9e35ddb97b3 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 class InternalHistogram extends Inter 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 eb81c6a5ec1..b60c8510238 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 459802f62a3..3356c089667 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 class NestedAggregator extends SingleBucketAggregator { 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. + // 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 + // 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 b64abf55b10..5644c6acf1f 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 @@ public class ReverseNestedAggregator extends SingleBucketAggregator { // 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 14fe9ddd3bc..d6d961a5998 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 class RangeAggregator extends BucketsAggregator { 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 class RangeAggregator extends BucketsAggregator { 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 class RangeAggregator extends BucketsAggregator { 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 7e16dc29073..492167f1735 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 f67c533956c..329f5f566f5 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 @@ import java.util.Map; 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 2423f228451..a49f18734ee 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 ea98734b94e..9250495524e 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 @@ import java.util.Map; 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 ef1150f1d7e..ea32e388fe6 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 class LongTermsAggregator extends TermsAggregator { 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 class LongTermsAggregator extends TermsAggregator { 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(); + @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; + 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 class LongTermsAggregator extends TermsAggregator { list[i] = bucket; otherDocCount -= bucket.docCount; } - + runDeferredCollections(survivingBucketOrds); //Now build the aggs @@ -160,13 +160,12 @@ public class LongTermsAggregator extends TermsAggregator { 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 f0bbcbef924..6f80142da27 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 664211bc74c..e12e4227fdf 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 @@ public class TermsAggregatorFactory extends ValuesSourceAggregatorFactory 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 class AvgAggregator extends NumericMetricsAggregator.SingleValue { 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 53e5c534094..464d0a339a8 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 @@ public final class GeoBoundsAggregator extends MetricsAggregator { @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 d0dbebf7a8e..fcf92009752 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 class InternalGeoBounds extends InternalMetricsAggregation implements Geo } 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 0c97ba38ac3..7ade492660e 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 class MaxAggregator extends NumericMetricsAggregator.SingleValue { 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 c80b7b8f064..cf832cabe1f 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 class MinAggregator extends NumericMetricsAggregator.SingleValue { 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 8dd75b59110..a73639a3d7f 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 @@ public abstract class AbstractPercentilesAggregator extends NumericMetricsAggreg 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 9d14e3b70c3..380482b8ab3 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 @@ public class PercentileRanksAggregator extends AbstractPercentilesAggregator { 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 1a9a839bb75..2a42dc94620 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 @@ import java.util.Map; 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 @@ public class PercentilesAggregator extends AbstractPercentilesAggregator { 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 edcfb811660..c67e8f3853d 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 class InternalScriptedMetric extends InternalMetricsAggregation implement 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 8a454b6cb73..7b1f6c84a2d 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 class StatsAggegator extends NumericMetricsAggregator.MultiValue { 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); - } + 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(); + 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 6785d6f35eb..7fac72d7b05 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 class InternalExtendedStats extends InternalStats implements ExtendedStat 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 af834af7f7b..4c7981422f3 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 class SumAggregator extends NumericMetricsAggregator.SingleValue { 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 2bd7b505135..fedd7e09a2b 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 class ValueCountAggregator extends NumericMetricsAggregator.SingleValue { 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/85] 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 3356c089667..79da93d7301 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 class NestedAggregator extends SingleBucketAggregator { 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 5644c6acf1f..9869c6d6a0a 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 class ReverseNestedAggregator extends SingleBucketAggregator { @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 4088dd38cbff19462e610db853ba1e54ee9785e4 Mon Sep 17 00:00:00 2001 From: Britta Weber Date: Mon, 2 Mar 2015 10:51:01 +0100 Subject: [PATCH 67/85] Write state also on data nodes if not master eligible When a node was a data node only then the index state was not written. In case this node connected to a master that did not have the index in the cluster state, for example because a master was restarted and the data folder was lost, then the indices were not imported as dangling but instead deleted. This commit makes sure that index state for data nodes is also written if they have at least one shard of this index allocated. closes #8823 closes #9952 --- .../gateway/GatewayMetaState.java | 176 +++++++-- .../gateway/GatewayMetaStateTests.java | 249 ++++++++++++ .../gateway/MetaDataWriteDataNodesTests.java | 354 ++++++++++++++++++ 3 files changed, 743 insertions(+), 36 deletions(-) create mode 100644 src/test/java/org/elasticsearch/gateway/GatewayMetaStateTests.java create mode 100644 src/test/java/org/elasticsearch/gateway/MetaDataWriteDataNodesTests.java diff --git a/src/main/java/org/elasticsearch/gateway/GatewayMetaState.java b/src/main/java/org/elasticsearch/gateway/GatewayMetaState.java index 158a3df5d91..ca8edebc571 100644 --- a/src/main/java/org/elasticsearch/gateway/GatewayMetaState.java +++ b/src/main/java/org/elasticsearch/gateway/GatewayMetaState.java @@ -19,6 +19,7 @@ package org.elasticsearch.gateway; +import com.google.common.collect.ImmutableSet; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.Version; import org.elasticsearch.cluster.ClusterChangedEvent; @@ -27,9 +28,7 @@ import org.elasticsearch.cluster.ClusterStateListener; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.node.DiscoveryNode; -import org.elasticsearch.cluster.routing.DjbHashFunction; -import org.elasticsearch.cluster.routing.HashFunction; -import org.elasticsearch.cluster.routing.SimpleHashFunction; +import org.elasticsearch.cluster.routing.*; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.inject.Inject; @@ -43,6 +42,7 @@ import java.io.IOException; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.*; /** * @@ -57,7 +57,9 @@ public class GatewayMetaState extends AbstractComponent implements ClusterStateL private final DanglingIndicesState danglingIndicesState; @Nullable - private volatile MetaData currentMetaData; + private volatile MetaData previousMetaData; + + private volatile ImmutableSet previouslyWrittenIndices = ImmutableSet.of(); @Inject public GatewayMetaState(Settings settings, NodeEnvironment nodeEnv, MetaStateService metaStateService, @@ -76,7 +78,7 @@ public class GatewayMetaState extends AbstractComponent implements ClusterStateL if (DiscoveryNode.masterNode(settings) || DiscoveryNode.dataNode(settings)) { nodeEnv.ensureAtomicMoveSupported(); } - if (DiscoveryNode.masterNode(settings)) { + if (DiscoveryNode.masterNode(settings) || DiscoveryNode.dataNode(settings)) { try { ensureNoPre019State(); pre20Upgrade(); @@ -96,10 +98,12 @@ public class GatewayMetaState extends AbstractComponent implements ClusterStateL @Override public void clusterChanged(ClusterChangedEvent event) { + Set relevantIndices = new HashSet<>(); final ClusterState state = event.state(); if (state.blocks().disableStatePersistence()) { // reset the current metadata, we need to start fresh... - this.currentMetaData = null; + this.previousMetaData = null; + previouslyWrittenIndices= ImmutableSet.of(); return; } @@ -107,44 +111,47 @@ public class GatewayMetaState extends AbstractComponent implements ClusterStateL // we don't check if metaData changed, since we might be called several times and we need to check dangling... boolean success = true; - // only applied to master node, writing the global and index level states - if (state.nodes().localNode().masterNode()) { + // write the state if this node is a master eligible node or if it is a data node and has shards allocated on it + if (state.nodes().localNode().masterNode() || state.nodes().localNode().dataNode()) { // check if the global state changed? - if (currentMetaData == null || !MetaData.isGlobalStateEquals(currentMetaData, newMetaData)) { + if (previousMetaData == null || !MetaData.isGlobalStateEquals(previousMetaData, newMetaData)) { try { metaStateService.writeGlobalState("changed", newMetaData); + // we determine if or if not we write meta data on data only nodes by looking at the shard routing + // and only write if a shard of this index is allocated on this node + // however, closed indices do not appear in the shard routing. if the meta data for a closed index is + // updated it will therefore not be written in case the list of previouslyWrittenIndices is empty (because state + // persistence was disabled or the node was restarted), see getRelevantIndicesOnDataOnlyNode(). + // we therefore have to check here if we have shards on disk and add their indices to the previouslyWrittenIndices list + if (isDataOnlyNode(state)) { + ImmutableSet.Builder previouslyWrittenIndicesBuilder = ImmutableSet.builder(); + for (IndexMetaData indexMetaData : newMetaData) { + IndexMetaData indexMetaDataOnDisk = null; + if (indexMetaData.state().equals(IndexMetaData.State.CLOSE)) { + try { + indexMetaDataOnDisk = metaStateService.loadIndexState(indexMetaData.index()); + } catch (IOException ex) { + throw new ElasticsearchException("failed to load index state", ex); + } + } + if (indexMetaDataOnDisk != null) { + previouslyWrittenIndicesBuilder.add(indexMetaDataOnDisk.index()); + } + } + previouslyWrittenIndices = previouslyWrittenIndicesBuilder.addAll(previouslyWrittenIndices).build(); + } } catch (Throwable e) { success = false; } } + Iterable writeInfo; + relevantIndices = getRelevantIndices(event.state(), previouslyWrittenIndices); + writeInfo = resolveStatesToBeWritten(previouslyWrittenIndices, relevantIndices, previousMetaData, event.state().metaData()); // check and write changes in indices - for (IndexMetaData indexMetaData : newMetaData) { - String writeReason = null; - IndexMetaData currentIndexMetaData; - if (currentMetaData == null) { - // a new event..., check from the state stored - try { - currentIndexMetaData = metaStateService.loadIndexState(indexMetaData.index()); - } catch (IOException ex) { - throw new ElasticsearchException("failed to load index state", ex); - } - } else { - currentIndexMetaData = currentMetaData.index(indexMetaData.index()); - } - if (currentIndexMetaData == null) { - writeReason = "freshly created"; - } else if (currentIndexMetaData.version() != indexMetaData.version()) { - writeReason = "version changed from [" + currentIndexMetaData.version() + "] to [" + indexMetaData.version() + "]"; - } - - // we update the writeReason only if we really need to write it - if (writeReason == null) { - continue; - } - + for (IndexMetaWriteInfo indexMetaWrite : writeInfo) { try { - metaStateService.writeIndex(writeReason, indexMetaData, currentIndexMetaData); + metaStateService.writeIndex(indexMetaWrite.reason, indexMetaWrite.newMetaData, indexMetaWrite.previousMetaData); } catch (Throwable e) { success = false; } @@ -154,10 +161,29 @@ public class GatewayMetaState extends AbstractComponent implements ClusterStateL danglingIndicesState.processDanglingIndices(newMetaData); if (success) { - currentMetaData = newMetaData; + previousMetaData = newMetaData; + ImmutableSet.Builder builder= ImmutableSet.builder(); + previouslyWrittenIndices = builder.addAll(relevantIndices).build(); } } + public static Set getRelevantIndices(ClusterState state, ImmutableSet previouslyWrittenIndices) { + Set relevantIndices; + if (isDataOnlyNode(state)) { + relevantIndices = getRelevantIndicesOnDataOnlyNode(state, previouslyWrittenIndices); + } else if (state.nodes().localNode().masterNode() == true) { + relevantIndices = getRelevantIndicesForMasterEligibleNode(state); + } else { + relevantIndices = Collections.emptySet(); + } + return relevantIndices; + } + + + protected static boolean isDataOnlyNode(ClusterState state) { + return ((state.nodes().localNode().masterNode() == false) && state.nodes().localNode().dataNode()); + } + /** * Throws an IAE if a pre 0.19 state is detected */ @@ -229,7 +255,7 @@ public class GatewayMetaState extends AbstractComponent implements ClusterStateL } } } - if (hasCustomPre20HashFunction|| pre20UseType != null) { + if (hasCustomPre20HashFunction || pre20UseType != null) { logger.warn("Settings [{}] and [{}] are deprecated. Index settings from your old indices have been updated to record the fact that they " + "used some custom routing logic, you can now remove these settings from your `elasticsearch.yml` file", DEPRECATED_SETTING_ROUTING_HASH_FUNCTION, DEPRECATED_SETTING_ROUTING_USE_TYPE); } @@ -251,4 +277,82 @@ public class GatewayMetaState extends AbstractComponent implements ClusterStateL } } } + + /** + * Loads the current meta state for each index in the new cluster state and checks if it has to be persisted. + * Each index state that should be written to disk will be returned. This is only run for data only nodes. + * It will return only the states for indices that actually have a shard allocated on the current node. + * + * @param previouslyWrittenIndices A list of indices for which the state was already written before + * @param potentiallyUnwrittenIndices The list of indices for which state should potentially be written + * @param previousMetaData The last meta data we know of. meta data for all indices in previouslyWrittenIndices list is persisted now + * @param newMetaData The new metadata + * @return iterable over all indices states that should be written to disk + */ + public static Iterable resolveStatesToBeWritten(ImmutableSet previouslyWrittenIndices, Set potentiallyUnwrittenIndices, MetaData previousMetaData, MetaData newMetaData) { + List indicesToWrite = new ArrayList<>(); + for (String index : potentiallyUnwrittenIndices) { + IndexMetaData newIndexMetaData = newMetaData.index(index); + IndexMetaData previousIndexMetaData = previousMetaData == null ? null : previousMetaData.index(index); + String writeReason = null; + if (previouslyWrittenIndices.contains(index) == false || previousIndexMetaData == null) { + writeReason = "freshly created"; + } else if (previousIndexMetaData.version() != newIndexMetaData.version()) { + writeReason = "version changed from [" + previousIndexMetaData.version() + "] to [" + newIndexMetaData.version() + "]"; + } + if (writeReason != null) { + indicesToWrite.add(new GatewayMetaState.IndexMetaWriteInfo(newIndexMetaData, previousIndexMetaData, writeReason)); + } + } + return indicesToWrite; + } + + public static Set getRelevantIndicesOnDataOnlyNode(ClusterState state, ImmutableSet previouslyWrittenIndices) { + RoutingNode newRoutingNode = state.getRoutingNodes().node(state.nodes().localNodeId()); + if (newRoutingNode == null) { + throw new IllegalStateException("cluster state does not contain this node - cannot write index meta state"); + } + Set indices = new HashSet<>(); + for (MutableShardRouting routing : newRoutingNode) { + indices.add(routing.index()); + } + // we have to check the meta data also: closed indices will not appear in the routing table, but we must still write the state if we have it written on disk previously + for (IndexMetaData indexMetaData : state.metaData()) { + if (previouslyWrittenIndices.contains(indexMetaData.getIndex()) && state.metaData().getIndices().get(indexMetaData.getIndex()).state().equals(IndexMetaData.State.CLOSE)) { + indices.add(indexMetaData.getIndex()); + } + } + return indices; + } + + public static Set getRelevantIndicesForMasterEligibleNode(ClusterState state) { + Set relevantIndices; + relevantIndices = new HashSet<>(); + // we have to iterate over the metadata to make sure we also capture closed indices + for (IndexMetaData indexMetaData : state.metaData()) { + relevantIndices.add(indexMetaData.getIndex()); + } + return relevantIndices; + } + + + public static class IndexMetaWriteInfo { + final IndexMetaData newMetaData; + final String reason; + final IndexMetaData previousMetaData; + + public IndexMetaWriteInfo(IndexMetaData newMetaData, IndexMetaData previousMetaData, String reason) { + this.newMetaData = newMetaData; + this.reason = reason; + this.previousMetaData = previousMetaData; + } + + public IndexMetaData getNewMetaData() { + return newMetaData; + } + + public String getReason() { + return reason; + } + } } diff --git a/src/test/java/org/elasticsearch/gateway/GatewayMetaStateTests.java b/src/test/java/org/elasticsearch/gateway/GatewayMetaStateTests.java new file mode 100644 index 00000000000..06b958d47aa --- /dev/null +++ b/src/test/java/org/elasticsearch/gateway/GatewayMetaStateTests.java @@ -0,0 +1,249 @@ +/* + * 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.gateway; + +import com.google.common.collect.ImmutableSet; +import org.elasticsearch.Version; +import org.elasticsearch.cluster.ClusterChangedEvent; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.cluster.routing.RoutingTable; +import org.elasticsearch.cluster.routing.allocation.AllocationService; +import org.elasticsearch.cluster.routing.allocation.decider.ClusterRebalanceAllocationDecider; +import org.elasticsearch.test.ElasticsearchAllocationTestCase; +import org.junit.Test; + +import java.util.*; + +import static org.elasticsearch.cluster.routing.ShardRoutingState.INITIALIZING; +import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder; +import static org.hamcrest.Matchers.equalTo; + +/** + * Test IndexMetaState for master and data only nodes return correct list of indices to write + * There are many parameters: + * - meta state is not in memory + * - meta state is in memory with old version/ new version + * - meta state is in memory with new version + * - version changed in cluster state event/ no change + * - node is data only node + * - node is master eligible + * for data only nodes: shard initializing on shard + */ +public class GatewayMetaStateTests extends ElasticsearchAllocationTestCase { + + ClusterChangedEvent generateEvent(boolean initializing, boolean versionChanged, boolean masterEligible) { + //ridiculous settings to make sure we don't run into uninitialized because fo default + AllocationService strategy = createAllocationService(settingsBuilder() + .put("cluster.routing.allocation.concurrent_recoveries", 100) + .put(ClusterRebalanceAllocationDecider.CLUSTER_ROUTING_ALLOCATION_ALLOW_REBALANCE, "always") + .put("cluster.routing.allocation.cluster_concurrent_rebalance", 100) + .put("cluster.routing.allocation.node_initial_primaries_recoveries", 100) + .build()); + ClusterState newClusterState, previousClusterState; + MetaData metaDataOldClusterState = MetaData.builder() + .put(IndexMetaData.builder("test").settings(settings(Version.CURRENT)).numberOfShards(5).numberOfReplicas(2)) + .build(); + + RoutingTable routingTableOldClusterState = RoutingTable.builder() + .addAsNew(metaDataOldClusterState.index("test")) + .build(); + + // assign all shards + ClusterState init = ClusterState.builder(org.elasticsearch.cluster.ClusterName.DEFAULT) + .metaData(metaDataOldClusterState) + .routingTable(routingTableOldClusterState) + .nodes(generateDiscoveryNodes(masterEligible)) + .build(); + // new cluster state will have initializing shards on node 1 + RoutingTable routingTableNewClusterState = strategy.reroute(init).routingTable(); + if (initializing == false) { + // pretend all initialized, nothing happened + ClusterState temp = ClusterState.builder(init).routingTable(routingTableNewClusterState).metaData(metaDataOldClusterState).build(); + routingTableNewClusterState = strategy.applyStartedShards(temp, temp.getRoutingNodes().shardsWithState(INITIALIZING)).routingTable(); + routingTableOldClusterState = routingTableNewClusterState; + + } else { + // nothing to do, we have one routing table with unassigned and one with initializing + } + + // create new meta data either with version changed or not + MetaData metaDataNewClusterState = MetaData.builder() + .put(init.metaData().index("test"), versionChanged) + .build(); + + + // create the cluster states with meta data and routing tables as computed before + previousClusterState = ClusterState.builder(init) + .metaData(metaDataOldClusterState) + .routingTable(routingTableOldClusterState) + .nodes(generateDiscoveryNodes(masterEligible)) + .build(); + newClusterState = ClusterState.builder(previousClusterState).routingTable(routingTableNewClusterState).metaData(metaDataNewClusterState).version(previousClusterState.getVersion() + 1).build(); + + ClusterChangedEvent event = new ClusterChangedEvent("test", newClusterState, previousClusterState); + assertThat(event.state().version(), equalTo(event.previousState().version() + 1)); + return event; + } + + ClusterChangedEvent generateCloseEvent(boolean masterEligible) { + //ridiculous settings to make sure we don't run into uninitialized because fo default + AllocationService strategy = createAllocationService(settingsBuilder() + .put("cluster.routing.allocation.concurrent_recoveries", 100) + .put(ClusterRebalanceAllocationDecider.CLUSTER_ROUTING_ALLOCATION_ALLOW_REBALANCE, "always") + .put("cluster.routing.allocation.cluster_concurrent_rebalance", 100) + .put("cluster.routing.allocation.node_initial_primaries_recoveries", 100) + .build()); + ClusterState newClusterState, previousClusterState; + MetaData metaDataIndexCreated = MetaData.builder() + .put(IndexMetaData.builder("test").settings(settings(Version.CURRENT)).numberOfShards(5).numberOfReplicas(2)) + .build(); + + RoutingTable routingTableIndexCreated = RoutingTable.builder() + .addAsNew(metaDataIndexCreated.index("test")) + .build(); + + // assign all shards + ClusterState init = ClusterState.builder(org.elasticsearch.cluster.ClusterName.DEFAULT) + .metaData(metaDataIndexCreated) + .routingTable(routingTableIndexCreated) + .nodes(generateDiscoveryNodes(masterEligible)) + .build(); + RoutingTable routingTableInitializing = strategy.reroute(init).routingTable(); + ClusterState temp = ClusterState.builder(init).routingTable(routingTableInitializing).build(); + RoutingTable routingTableStarted = strategy.applyStartedShards(temp, temp.getRoutingNodes().shardsWithState(INITIALIZING)).routingTable(); + + // create new meta data either with version changed or not + MetaData metaDataStarted = MetaData.builder() + .put(init.metaData().index("test"), true) + .build(); + + // create the cluster states with meta data and routing tables as computed before + MetaData metaDataClosed = MetaData.builder() + .put(IndexMetaData.builder("test").settings(settings(Version.CURRENT)).state(IndexMetaData.State.CLOSE).numberOfShards(5).numberOfReplicas(2)).version(metaDataStarted.version() + 1) + .build(); + previousClusterState = ClusterState.builder(init) + .metaData(metaDataStarted) + .routingTable(routingTableStarted) + .nodes(generateDiscoveryNodes(masterEligible)) + .build(); + newClusterState = ClusterState.builder(previousClusterState) + .routingTable(routingTableIndexCreated) + .metaData(metaDataClosed) + .version(previousClusterState.getVersion() + 1).build(); + + ClusterChangedEvent event = new ClusterChangedEvent("test", newClusterState, previousClusterState); + assertThat(event.state().version(), equalTo(event.previousState().version() + 1)); + return event; + } + + private DiscoveryNodes.Builder generateDiscoveryNodes(boolean masterEligible) { + Map masterNodeAttributes = new HashMap<>(); + masterNodeAttributes.put("master", "true"); + masterNodeAttributes.put("data", "true"); + Map dataNodeAttributes = new HashMap<>(); + dataNodeAttributes.put("master", "false"); + dataNodeAttributes.put("data", "true"); + return DiscoveryNodes.builder().put(newNode("node1", masterEligible ? masterNodeAttributes : dataNodeAttributes)).put(newNode("master_node", masterNodeAttributes)).localNodeId("node1").masterNodeId(masterEligible ? "node1" : "master_node"); + } + + public void assertState(ClusterChangedEvent event, + boolean stateInMemory, + boolean expectMetaData) throws Exception { + MetaData inMemoryMetaData = null; + ImmutableSet oldIndicesList = ImmutableSet.of(); + if (stateInMemory) { + inMemoryMetaData = event.previousState().metaData(); + ImmutableSet.Builder relevantIndices = ImmutableSet.builder(); + oldIndicesList = relevantIndices.addAll(GatewayMetaState.getRelevantIndices(event.previousState(), oldIndicesList)).build(); + } + Set newIndicesList = GatewayMetaState.getRelevantIndices(event.state(), oldIndicesList); + // third, get the actual write info + Iterator indices = GatewayMetaState.resolveStatesToBeWritten(oldIndicesList, newIndicesList, inMemoryMetaData, event.state().metaData()).iterator(); + + if (expectMetaData) { + assertThat(indices.hasNext(), equalTo(true)); + assertThat(indices.next().getNewMetaData().index(), equalTo("test")); + assertThat(indices.hasNext(), equalTo(false)); + } else { + assertThat(indices.hasNext(), equalTo(false)); + } + } + + @Test + public void testVersionChangeIsAlwaysWritten() throws Exception { + // test that version changes are always written + boolean initializing = randomBoolean(); + boolean versionChanged = true; + boolean stateInMemory = randomBoolean(); + boolean masterEligible = randomBoolean(); + boolean expectMetaData = true; + ClusterChangedEvent event = generateEvent(initializing, versionChanged, masterEligible); + assertState(event, stateInMemory, expectMetaData); + } + + @Test + public void testNewShardsAlwaysWritten() throws Exception { + // make sure new shards on data only node always written + boolean initializing = true; + boolean versionChanged = randomBoolean(); + boolean stateInMemory = randomBoolean(); + boolean masterEligible = false; + boolean expectMetaData = true; + ClusterChangedEvent event = generateEvent(initializing, versionChanged, masterEligible); + assertState(event, stateInMemory, expectMetaData); + } + + @Test + public void testAllUpToDateNothingWritten() throws Exception { + // make sure state is not written again if we wrote already + boolean initializing = false; + boolean versionChanged = false; + boolean stateInMemory = true; + boolean masterEligible = randomBoolean(); + boolean expectMetaData = false; + ClusterChangedEvent event = generateEvent(initializing, versionChanged, masterEligible); + assertState(event, stateInMemory, expectMetaData); + } + + @Test + public void testNoWriteIfNothingChanged() throws Exception { + boolean initializing = false; + boolean versionChanged = false; + boolean stateInMemory = true; + boolean masterEligible = randomBoolean(); + boolean expectMetaData = false; + ClusterChangedEvent event = generateEvent(initializing, versionChanged, masterEligible); + ClusterChangedEvent newEventWithNothingChanged = new ClusterChangedEvent("test cluster state", event.state(), event.state()); + assertState(newEventWithNothingChanged, stateInMemory, expectMetaData); + } + + @Test + public void testWriteClosedIndex() throws Exception { + // test that the closing of an index is written also on data only node + boolean masterEligible = randomBoolean(); + boolean expectMetaData = true; + boolean stateInMemory = true; + ClusterChangedEvent event = generateCloseEvent(masterEligible); + assertState(event, stateInMemory, expectMetaData); + } +} diff --git a/src/test/java/org/elasticsearch/gateway/MetaDataWriteDataNodesTests.java b/src/test/java/org/elasticsearch/gateway/MetaDataWriteDataNodesTests.java new file mode 100644 index 00000000000..7947a6698c7 --- /dev/null +++ b/src/test/java/org/elasticsearch/gateway/MetaDataWriteDataNodesTests.java @@ -0,0 +1,354 @@ +/* + * 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.gateway; + +import com.carrotsearch.hppc.cursors.ObjectObjectCursor; +import com.google.common.base.Predicate; +import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse; +import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.routing.allocation.decider.FilterAllocationDecider; +import org.elasticsearch.common.collect.ImmutableOpenMap; +import org.elasticsearch.common.settings.ImmutableSettings; +import org.elasticsearch.test.ElasticsearchIntegrationTest; +import org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope; +import org.elasticsearch.test.InternalTestCluster; +import org.junit.Test; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.elasticsearch.client.Requests.clusterHealthRequest; +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.test.ElasticsearchIntegrationTest.Scope; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.hamcrest.Matchers.equalTo; + +/** + * + */ +@ClusterScope(scope = Scope.TEST, numDataNodes = 0) +public class MetaDataWriteDataNodesTests extends ElasticsearchIntegrationTest { + + @Test + public void testMetaWrittenAlsoOnDataNode() throws Exception { + // this test checks that index state is written on data only nodes + String masterNodeName = startMasterNode(); + String redNode = startDataNode("red"); + assertAcked(prepareCreate("test").setSettings(ImmutableSettings.builder().put("index.number_of_replicas", 0))); + index("test", "doc", "1", jsonBuilder().startObject().field("text", "some text").endObject()); + waitForConcreteMappingsOnAll("test", "doc", "text"); + ensureGreen("test"); + assertIndexInMetaState(redNode, "test"); + assertIndexInMetaState(masterNodeName, "test"); + //stop master node and start again with an empty data folder + ((InternalTestCluster) cluster()).stopCurrentMasterNode(); + String newMasterNode = startMasterNode(); + ensureGreen("test"); + // wait for mapping also on master becasue then we can be sure the state was written + waitForConcreteMappingsOnAll("test", "doc", "text"); + // check for meta data + assertIndexInMetaState(redNode, "test"); + assertIndexInMetaState(newMasterNode, "test"); + // check if index and doc is still there + ensureGreen("test"); + assertTrue(client().prepareGet("test", "doc", "1").get().isExists()); + } + + @Test + public void testMetaWrittenOnlyForIndicesOnNodesThatHaveAShard() throws Exception { + // this test checks that the index state is only written to a data only node if they have a shard of that index allocated on the node + String masterNode = startMasterNode(); + String blueNode = startDataNode("blue"); + String redNode = startDataNode("red"); + + assertAcked(prepareCreate("blue_index").setSettings(ImmutableSettings.builder().put("index.number_of_replicas", 0).put(FilterAllocationDecider.INDEX_ROUTING_INCLUDE_GROUP + "color", "blue"))); + index("blue_index", "doc", "1", jsonBuilder().startObject().field("text", "some text").endObject()); + assertAcked(prepareCreate("red_index").setSettings(ImmutableSettings.builder().put("index.number_of_replicas", 0).put(FilterAllocationDecider.INDEX_ROUTING_INCLUDE_GROUP + "color", "red"))); + index("red_index", "doc", "1", jsonBuilder().startObject().field("text", "some text").endObject()); + ensureGreen(); + waitForConcreteMappingsOnAll("blue_index", "doc", "text"); + waitForConcreteMappingsOnAll("red_index", "doc", "text"); + assertIndexNotInMetaState(blueNode, "red_index"); + assertIndexNotInMetaState(redNode, "blue_index"); + assertIndexInMetaState(blueNode, "blue_index"); + assertIndexInMetaState(redNode, "red_index"); + assertIndexInMetaState(masterNode, "red_index"); + assertIndexInMetaState(masterNode, "blue_index"); + + // not the index state for blue_index should only be written on blue_node and the for red_index only on red_node + // we restart red node and master but with empty data folders + stopNode(redNode); + ((InternalTestCluster) cluster()).stopCurrentMasterNode(); + masterNode = startMasterNode(); + redNode = startDataNode("red"); + + ensureGreen(); + assertIndexNotInMetaState(blueNode, "red_index"); + assertIndexInMetaState(blueNode, "blue_index"); + assertIndexNotInMetaState(redNode, "red_index"); + assertIndexNotInMetaState(redNode, "blue_index"); + assertIndexNotInMetaState(masterNode, "red_index"); + assertIndexInMetaState(masterNode, "blue_index"); + // check that blue index is still there + assertFalse(client().admin().indices().prepareExists("red_index").get().isExists()); + assertTrue(client().prepareGet("blue_index", "doc", "1").get().isExists()); + // red index should be gone + // if the blue node had stored the index state then cluster health would be red and red_index would exist + assertFalse(client().admin().indices().prepareExists("red_index").get().isExists()); + + } + + @Test + public void testMetaIsRemovedIfAllShardsFromIndexRemoved() throws Exception { + // this test checks that the index state is removed from a data only node once all shards have been allocated away from it + String masterNode = startMasterNode(); + String blueNode = startDataNode("blue"); + String redNode = startDataNode("red"); + + // create blue_index on blue_node and same for red + client().admin().cluster().health(clusterHealthRequest().waitForYellowStatus().waitForNodes("3")).get(); + assertAcked(prepareCreate("blue_index").setSettings(ImmutableSettings.builder().put("index.number_of_replicas", 0).put(FilterAllocationDecider.INDEX_ROUTING_INCLUDE_GROUP + "color", "blue"))); + index("blue_index", "doc", "1", jsonBuilder().startObject().field("text", "some text").endObject()); + assertAcked(prepareCreate("red_index").setSettings(ImmutableSettings.builder().put("index.number_of_replicas", 0).put(FilterAllocationDecider.INDEX_ROUTING_INCLUDE_GROUP + "color", "red"))); + index("red_index", "doc", "1", jsonBuilder().startObject().field("text", "some text").endObject()); + + ensureGreen(); + assertIndexNotInMetaState(redNode, "blue_index"); + assertIndexNotInMetaState(blueNode, "red_index"); + assertIndexInMetaState(redNode, "red_index"); + assertIndexInMetaState(blueNode, "blue_index"); + assertIndexInMetaState(masterNode, "red_index"); + assertIndexInMetaState(masterNode, "blue_index"); + + // now relocate blue_index to red_node and red_index to blue_node + logger.debug("relocating indices..."); + client().admin().indices().prepareUpdateSettings("blue_index").setSettings(ImmutableSettings.builder().put(FilterAllocationDecider.INDEX_ROUTING_INCLUDE_GROUP + "color", "red")).get(); + client().admin().indices().prepareUpdateSettings("red_index").setSettings(ImmutableSettings.builder().put(FilterAllocationDecider.INDEX_ROUTING_INCLUDE_GROUP + "color", "blue")).get(); + client().admin().cluster().prepareHealth().setWaitForRelocatingShards(0).get(); + ensureGreen(); + assertIndexNotInMetaState(redNode, "red_index"); + assertIndexNotInMetaState(blueNode, "blue_index"); + assertIndexInMetaState(redNode, "blue_index"); + assertIndexInMetaState(blueNode, "red_index"); + assertIndexInMetaState(masterNode, "red_index"); + assertIndexInMetaState(masterNode, "blue_index"); + waitForConcreteMappingsOnAll("blue_index", "doc", "text"); + waitForConcreteMappingsOnAll("red_index", "doc", "text"); + + //at this point the blue_index is on red node and the red_index on blue node + // now, when we start red and master node again but without data folder, the red index should be gone but the blue index should initialize fine + stopNode(redNode); + ((InternalTestCluster) cluster()).stopCurrentMasterNode(); + masterNode = startMasterNode(); + redNode = startDataNode("red"); + ensureGreen(); + assertIndexNotInMetaState(redNode, "blue_index"); + assertIndexNotInMetaState(blueNode, "blue_index"); + assertIndexNotInMetaState(redNode, "red_index"); + assertIndexInMetaState(blueNode, "red_index"); + assertIndexInMetaState(masterNode, "red_index"); + assertIndexNotInMetaState(masterNode, "blue_index"); + assertTrue(client().prepareGet("red_index", "doc", "1").get().isExists()); + // if the red_node had stored the index state then cluster health would be red and blue_index would exist + assertFalse(client().admin().indices().prepareExists("blue_index").get().isExists()); + } + + @Test + public void testMetaWrittenWhenIndexIsClosed() throws Exception { + String masterNode = startMasterNode(); + String redNodeDataPath = createTempDir().toString(); + String redNode = startDataNode("red", redNodeDataPath); + String blueNode = startDataNode("blue"); + // create red_index on red_node and same for red + client().admin().cluster().health(clusterHealthRequest().waitForYellowStatus().waitForNodes("3")).get(); + assertAcked(prepareCreate("red_index").setSettings(ImmutableSettings.builder().put("index.number_of_replicas", 0).put(FilterAllocationDecider.INDEX_ROUTING_INCLUDE_GROUP + "color", "red"))); + index("red_index", "doc", "1", jsonBuilder().startObject().field("text", "some text").endObject()); + + ensureGreen(); + assertIndexNotInMetaState(blueNode, "red_index"); + assertIndexInMetaState(redNode, "red_index"); + assertIndexInMetaState(masterNode, "red_index"); + + waitForConcreteMappingsOnAll("red_index", "doc", "text"); + client().admin().indices().prepareClose("red_index").get(); + // close the index + ClusterStateResponse clusterStateResponse = client().admin().cluster().prepareState().get(); + assertThat(clusterStateResponse.getState().getMetaData().index("red_index").getState().name(), equalTo(IndexMetaData.State.CLOSE.name())); + + // restart master with empty data folder and maybe red node + boolean restartRedNode = randomBoolean(); + //at this point the red_index on red node + if (restartRedNode) { + stopNode(redNode); + } + ((InternalTestCluster) cluster()).stopCurrentMasterNode(); + masterNode = startMasterNode(); + if (restartRedNode) { + redNode = startDataNode("red", redNodeDataPath); + } + + ensureGreen("red_index"); + assertIndexNotInMetaState(blueNode, "red_index"); + assertIndexInMetaState(redNode, "red_index"); + assertIndexInMetaState(masterNode, "red_index"); + clusterStateResponse = client().admin().cluster().prepareState().get(); + assertThat(clusterStateResponse.getState().getMetaData().index("red_index").getState().name(), equalTo(IndexMetaData.State.CLOSE.name())); + + // open the index again + client().admin().indices().prepareOpen("red_index").get(); + clusterStateResponse = client().admin().cluster().prepareState().get(); + assertThat(clusterStateResponse.getState().getMetaData().index("red_index").getState().name(), equalTo(IndexMetaData.State.OPEN.name())); + // restart again + ensureGreen(); + if (restartRedNode) { + stopNode(redNode); + } + ((InternalTestCluster) cluster()).stopCurrentMasterNode(); + masterNode = startMasterNode(); + if (restartRedNode) { + redNode = startDataNode("red", redNodeDataPath); + } + ensureGreen("red_index"); + assertIndexNotInMetaState(blueNode, "red_index"); + assertIndexInMetaState(redNode, "red_index"); + assertIndexInMetaState(masterNode, "red_index"); + clusterStateResponse = client().admin().cluster().prepareState().get(); + assertThat(clusterStateResponse.getState().getMetaData().index("red_index").getState().name(), equalTo(IndexMetaData.State.OPEN.name())); + assertTrue(client().prepareGet("red_index", "doc", "1").get().isExists()); + } + @Test + public void testMetaWrittenWhenIndexIsClosedAndMetaUpdated() throws Exception { + String masterNode = startMasterNode(); + String redNodeDataPath = createTempDir().toString(); + String redNode = startDataNode("red", redNodeDataPath); + // create red_index on red_node and same for red + client().admin().cluster().health(clusterHealthRequest().waitForYellowStatus().waitForNodes("2")).get(); + assertAcked(prepareCreate("red_index").setSettings(ImmutableSettings.builder().put("index.number_of_replicas", 0).put(FilterAllocationDecider.INDEX_ROUTING_INCLUDE_GROUP + "color", "red"))); + index("red_index", "doc", "1", jsonBuilder().startObject().field("text", "some text").endObject()); + + logger.info("--> wait for green red_index"); + ensureGreen(); + logger.info("--> wait for meta state written for red_index"); + assertIndexInMetaState(redNode, "red_index"); + assertIndexInMetaState(masterNode, "red_index"); + + waitForConcreteMappingsOnAll("red_index", "doc", "text"); + + logger.info("--> close red_index"); + client().admin().indices().prepareClose("red_index").get(); + // close the index + ClusterStateResponse clusterStateResponse = client().admin().cluster().prepareState().get(); + assertThat(clusterStateResponse.getState().getMetaData().index("red_index").getState().name(), equalTo(IndexMetaData.State.CLOSE.name())); + + logger.info("--> restart red node"); + stopNode(redNode); + redNode = startDataNode("red", redNodeDataPath); + client().admin().indices().preparePutMapping("red_index").setType("doc").setSource(jsonBuilder().startObject() + .startObject("properties") + .startObject("integer_field") + .field("type", "integer") + .endObject() + .endObject() + .endObject()).get(); + + GetMappingsResponse getMappingsResponse = client().admin().indices().prepareGetMappings("red_index").addTypes("doc").get(); + assertNotNull(((LinkedHashMap)(getMappingsResponse.getMappings().get("red_index").get("doc").getSourceAsMap().get("properties"))).get("integer_field")); + // restart master with empty data folder and maybe red node + ((InternalTestCluster) cluster()).stopCurrentMasterNode(); + masterNode = startMasterNode(); + + ensureGreen("red_index"); + assertIndexInMetaState(redNode, "red_index"); + assertIndexInMetaState(masterNode, "red_index"); + clusterStateResponse = client().admin().cluster().prepareState().get(); + assertThat(clusterStateResponse.getState().getMetaData().index("red_index").getState().name(), equalTo(IndexMetaData.State.CLOSE.name())); + getMappingsResponse = client().admin().indices().prepareGetMappings("red_index").addTypes("doc").get(); + assertNotNull(((LinkedHashMap)(getMappingsResponse.getMappings().get("red_index").get("doc").getSourceAsMap().get("properties"))).get("integer_field")); + + } + + private String startDataNode(String color) { + return startDataNode(color, createTempDir().toString()); + } + + private String startDataNode(String color, String newDataPath) { + ImmutableSettings.Builder settingsBuilder = ImmutableSettings.builder() + .put("node.data", true) + .put("node.master", false) + .put("node.color", color) + .put("path.data", newDataPath); + return internalCluster().startNode(settingsBuilder.build()); + } + + private String startMasterNode() { + ImmutableSettings.Builder settingsBuilder = ImmutableSettings.builder() + .put("node.data", false) + .put("node.master", true) + .put("path.data", createTempDir().toString()); + return internalCluster().startNode(settingsBuilder.build()); + } + + private void stopNode(String name) throws IOException { + internalCluster().stopRandomNode(InternalTestCluster.nameFilter(name)); + } + + protected void assertIndexNotInMetaState(String nodeName, String indexName) throws Exception { + assertMetaState(nodeName, indexName, false); + } + + protected void assertIndexInMetaState(String nodeName, String indexName) throws Exception { + assertMetaState(nodeName, indexName, true); + } + + private void assertMetaState(final String nodeName, final String indexName, final boolean shouldBe) throws Exception { + awaitBusy(new Predicate() { + @Override + public boolean apply(Object o) { + logger.info("checking if meta state exists..."); + return shouldBe == metaStateExists(nodeName, indexName); + } + }); + boolean inMetaSate = metaStateExists(nodeName, indexName); + if (shouldBe) { + assertTrue("expected " + indexName + " in meta state of node " + nodeName, inMetaSate); + } else { + assertFalse("expected " + indexName + " to not be in meta state of node " + nodeName, inMetaSate); + } + } + + private boolean metaStateExists(String nodeName, String indexName) { + GatewayMetaState redNodeMetaState = ((InternalTestCluster) cluster()).getInstance(GatewayMetaState.class, nodeName); + MetaData redNodeMetaData = null; + try { + redNodeMetaData = redNodeMetaState.loadMetaState(); + } catch (Exception e) { + fail("failed to load meta state"); + } + ImmutableOpenMap indices = redNodeMetaData.getIndices(); + boolean inMetaSate = false; + for (ObjectObjectCursor index : indices) { + inMetaSate = inMetaSate || index.key.equals(indexName); + } + return inMetaSate; + } +} From 1d4df4b6283699bab1dcd4d53cfc06461abcdaf1 Mon Sep 17 00:00:00 2001 From: javanna Date: Wed, 29 Apr 2015 15:26:10 +0200 Subject: [PATCH 68/85] [TEST] remove source parameter validation from REST tests runner source parameter is implicitly supported and doesn't need to be declared in rest spec. It is tested though, as every api that supports get with body can also get requests using POST with body or get with source query_string parameter. --- .../elasticsearch/test/rest/ElasticsearchRestTestCase.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/test/java/org/elasticsearch/test/rest/ElasticsearchRestTestCase.java b/src/test/java/org/elasticsearch/test/rest/ElasticsearchRestTestCase.java index dd6ae14612d..b7b207a6b11 100644 --- a/src/test/java/org/elasticsearch/test/rest/ElasticsearchRestTestCase.java +++ b/src/test/java/org/elasticsearch/test/rest/ElasticsearchRestTestCase.java @@ -20,8 +20,6 @@ package org.elasticsearch.test.rest; import com.carrotsearch.randomizedtesting.RandomizedTest; -import com.carrotsearch.randomizedtesting.annotations.Name; -import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import com.carrotsearch.randomizedtesting.annotations.TestGroup; import com.carrotsearch.randomizedtesting.annotations.TimeoutSuite; import com.google.common.collect.Lists; @@ -221,9 +219,6 @@ public abstract class ElasticsearchRestTestCase extends ElasticsearchIntegration if (!restApi.getMethods().contains("POST")) { errorMessage.append("\n- ").append(restApi.getName()).append(" supports GET with a body but doesn't support POST"); } - if (!restApi.getParams().contains("source")) { - errorMessage.append("\n- ").append(restApi.getName()).append(" supports GET with a body but doesn't support the source query string parameter"); - } } } if (errorMessage.length() > 0) { From 3bb8ff2a925e69017826a5f71dca2ee1cdafcaac Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Wed, 29 Apr 2015 14:54:05 +0100 Subject: [PATCH 69/85] 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 23732af5d89..4480de38051 100644 --- a/src/main/java/org/elasticsearch/percolator/PercolatorService.java +++ b/src/main/java/org/elasticsearch/percolator/PercolatorService.java @@ -847,16 +847,11 @@ public class PercolatorService extends AbstractComponent { 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 9f04e4a37b0..4540cc75a06 100644 --- a/src/test/java/org/elasticsearch/percolator/PercolatorFacetsAndAggregationsTests.java +++ b/src/test/java/org/elasticsearch/percolator/PercolatorFacetsAndAggregationsTests.java @@ -20,12 +20,14 @@ package org.elasticsearch.percolator; 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 class PercolatorFacetsAndAggregationsTests extends ElasticsearchIntegrati 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 class PercolatorFacetsAndAggregationsTests extends ElasticsearchIntegrati 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 class PercolatorFacetsAndAggregationsTests extends ElasticsearchIntegrati 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 class PercolatorFacetsAndAggregationsTests extends ElasticsearchIntegrati 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 6e076efdb9ccd963ba45dfbe1adc2635f903af08 Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Wed, 29 Apr 2015 15:59:24 +0200 Subject: [PATCH 70/85] Docs: Add documentation for the `doc_values` setting on the `boolean` field type. Close #10431 --- docs/reference/mapping/types/core-types.asciidoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/reference/mapping/types/core-types.asciidoc b/docs/reference/mapping/types/core-types.asciidoc index e650ded89ca..1f6dcc01cb5 100644 --- a/docs/reference/mapping/types/core-types.asciidoc +++ b/docs/reference/mapping/types/core-types.asciidoc @@ -426,6 +426,9 @@ and it can be retrieved from it). in `_source`, have `include_in_all` enabled, or `store` be set to `true` for this to be useful. +|`doc_values` |Set to `true` to store field values in a column-stride fashion. +Automatically set to `true` when the fielddata format is `doc_values`. + |`boost` |The boost value. Defaults to `1.0`. |`null_value` |When there is a (JSON) null value for the field, use the From 528f6481eaadd2a0585dc6731a94d7a024b8ce29 Mon Sep 17 00:00:00 2001 From: markharwood Date: Mon, 27 Apr 2015 14:45:32 +0100 Subject: [PATCH 71/85] Query enhancement: return positions of parse errors found in JSON Extend SearchParseException and QueryParsingException to report position information in query JSON where errors were found. All query DSL parser classes that throw these exception types now pass the underlying position information (line and column number) at the point the error was found. Closes #3303 --- .../common/xcontent/XContentLocation.java | 37 +++++++++++ .../common/xcontent/XContentParser.java | 8 +++ .../xcontent/json/JsonXContentParser.java | 12 ++++ .../percolator/PercolatorQueriesRegistry.java | 2 +- .../index/query/AndFilterParser.java | 4 +- .../index/query/BoolFilterParser.java | 8 +-- .../index/query/BoolQueryParser.java | 6 +- .../index/query/BoostingQueryParser.java | 10 +-- .../index/query/CommonTermsQueryParser.java | 19 +++--- .../index/query/ConstantScoreQueryParser.java | 6 +- .../index/query/DisMaxQueryParser.java | 8 +-- .../index/query/ExistsFilterParser.java | 6 +- .../index/query/FQueryFilterParser.java | 6 +- .../query/FieldMaskingSpanQueryParser.java | 11 ++-- .../index/query/FilteredQueryParser.java | 6 +- .../index/query/FuzzyQueryParser.java | 6 +- .../query/GeoBoundingBoxFilterParser.java | 9 +-- .../index/query/GeoDistanceFilterParser.java | 9 +-- .../query/GeoDistanceRangeFilterParser.java | 4 +- .../index/query/GeoPolygonFilterParser.java | 20 +++--- .../index/query/GeoShapeFilterParser.java | 18 +++--- .../index/query/GeoShapeQueryParser.java | 18 +++--- .../index/query/GeohashCellFilter.java | 9 +-- .../index/query/HasChildFilterParser.java | 17 +++--- .../index/query/HasChildQueryParser.java | 20 +++--- .../index/query/HasParentFilterParser.java | 8 +-- .../index/query/HasParentQueryParser.java | 14 ++--- .../index/query/IdsFilterParser.java | 10 +-- .../index/query/IdsQueryParser.java | 14 ++--- .../index/query/IndexQueryParserService.java | 16 ++--- .../index/query/IndicesFilterParser.java | 16 ++--- .../index/query/IndicesQueryParser.java | 16 ++--- .../index/query/LimitFilterParser.java | 4 +- .../index/query/MatchAllQueryParser.java | 2 +- .../index/query/MatchQueryParser.java | 18 +++--- .../index/query/MissingFilterParser.java | 7 +-- .../index/query/MoreLikeThisQueryParser.java | 15 ++--- .../index/query/MultiMatchQueryParser.java | 17 +++--- .../index/query/NestedFilterParser.java | 4 +- .../index/query/NestedQueryParser.java | 8 +-- .../index/query/NotFilterParser.java | 5 +- .../index/query/OrFilterParser.java | 4 +- .../index/query/PrefixFilterParser.java | 2 +- .../index/query/PrefixQueryParser.java | 6 +- .../index/query/QueryParseContext.java | 21 ++++--- .../index/query/QueryParsingException.java | 61 ++++++++++++++++++- .../index/query/QueryStringQueryParser.java | 20 +++--- .../index/query/RangeFilterParser.java | 24 +++++--- .../index/query/RangeQueryParser.java | 15 +++-- .../index/query/RegexpFilterParser.java | 4 +- .../index/query/RegexpQueryParser.java | 6 +- .../index/query/ScriptFilterParser.java | 6 +- .../index/query/SimpleQueryStringParser.java | 14 ++--- .../index/query/SpanFirstQueryParser.java | 10 +-- .../index/query/SpanMultiTermQueryParser.java | 6 +- .../index/query/SpanNearQueryParser.java | 12 ++-- .../index/query/SpanNotQueryParser.java | 14 ++--- .../index/query/SpanOrQueryParser.java | 8 +-- .../index/query/SpanTermQueryParser.java | 4 +- .../index/query/TermFilterParser.java | 6 +- .../index/query/TermQueryParser.java | 6 +- .../index/query/TermsFilterParser.java | 17 +++--- .../index/query/TermsQueryParser.java | 10 +-- .../index/query/TopChildrenQueryParser.java | 12 ++-- .../index/query/TypeFilterParser.java | 6 +- .../index/query/WildcardQueryParser.java | 6 +- .../index/query/WrapperFilterParser.java | 4 +- .../index/query/WrapperQueryParser.java | 4 +- .../functionscore/DecayFunctionParser.java | 4 +- .../FunctionScoreQueryParser.java | 12 ++-- .../ScoreFunctionParserMapper.java | 7 ++- .../FieldValueFactorFunctionParser.java | 6 +- .../random/RandomScoreFunctionParser.java | 9 ++- .../script/ScriptScoreFunctionParser.java | 12 ++-- .../support/InnerHitsQueryParserHelper.java | 2 +- .../support/NestedInnerQueryParseSupport.java | 14 ++--- .../search/SearchParseException.java | 46 +++++++++++++- .../elasticsearch/search/SearchService.java | 24 ++++++-- .../aggregations/AggregatorParsers.java | 29 ++++++--- .../bucket/children/ChildrenParser.java | 11 ++-- .../bucket/filters/FiltersParser.java | 9 ++- .../bucket/histogram/DateHistogramParser.java | 27 +++++--- .../bucket/histogram/ExtendedBounds.java | 2 +- .../bucket/histogram/HistogramParser.java | 18 ++++-- .../bucket/missing/MissingParser.java | 3 +- .../bucket/nested/NestedParser.java | 9 ++- .../nested/ReverseNestedAggregator.java | 3 +- .../bucket/nested/ReverseNestedParser.java | 6 +- .../bucket/range/RangeParser.java | 12 ++-- .../bucket/range/date/DateRangeParser.java | 14 +++-- .../range/geodistance/GeoDistanceParser.java | 18 ++++-- .../bucket/range/ipv4/IpRangeParser.java | 15 +++-- .../bucket/sampler/SamplerParser.java | 10 +-- .../SignificantTermsParametersParser.java | 6 +- .../bucket/terms/TermsParametersParser.java | 21 ++++--- ...icValuesSourceMetricsAggregatorParser.java | 3 +- .../cardinality/CardinalityParser.java | 5 +- .../metrics/geobounds/GeoBoundsParser.java | 6 +- .../AbstractPercentilesParser.java | 12 ++-- .../percentiles/PercentileRanksParser.java | 2 +- .../scripted/ScriptedMetricAggregator.java | 8 ++- .../scripted/ScriptedMetricParser.java | 11 ++-- .../stats/extended/ExtendedStatsParser.java | 8 ++- .../metrics/tophits/TopHitsParser.java | 12 ++-- .../metrics/valuecount/ValueCountParser.java | 3 +- .../aggregations/support/GeoPointParser.java | 5 +- .../support/ValuesSourceParser.java | 3 +- .../highlight/HighlighterParseElement.java | 3 +- .../search/query/FromParseElement.java | 3 +- .../search/query/SizeParseElement.java | 3 +- .../search/sort/ScriptSortParser.java | 8 +-- .../search/sort/SortParseElement.java | 4 +- .../ElasticsearchExceptionTests.java | 60 ++++++++++++------ .../query/TestQueryParsingException.java | 37 +++++++++++ .../rest/BytesRestResponseTests.java | 10 +-- 115 files changed, 834 insertions(+), 482 deletions(-) create mode 100644 src/main/java/org/elasticsearch/common/xcontent/XContentLocation.java create mode 100644 src/test/java/org/elasticsearch/index/query/TestQueryParsingException.java diff --git a/src/main/java/org/elasticsearch/common/xcontent/XContentLocation.java b/src/main/java/org/elasticsearch/common/xcontent/XContentLocation.java new file mode 100644 index 00000000000..ade2a457797 --- /dev/null +++ b/src/main/java/org/elasticsearch/common/xcontent/XContentLocation.java @@ -0,0 +1,37 @@ +/* + * 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.common.xcontent; + +/** + * Simple data structure representing the line and column number of a position + * in some XContent e.g. JSON. Locations are typically used to communicate the + * position of a parsing error to end users and consequently have line and + * column numbers starting from 1. + */ +public class XContentLocation { + public final int lineNumber; + public final int columnNumber; + + public XContentLocation(int lineNumber, int columnNumber) { + super(); + this.lineNumber = lineNumber; + this.columnNumber = columnNumber; + } +} diff --git a/src/main/java/org/elasticsearch/common/xcontent/XContentParser.java b/src/main/java/org/elasticsearch/common/xcontent/XContentParser.java index 0aab32c4ba3..738fd9f6e72 100644 --- a/src/main/java/org/elasticsearch/common/xcontent/XContentParser.java +++ b/src/main/java/org/elasticsearch/common/xcontent/XContentParser.java @@ -241,4 +241,12 @@ public interface XContentParser extends Releasable { * */ byte[] binaryValue() throws IOException; + + /** + * Used for error reporting to highlight where syntax errors occur in + * content being parsed. + * + * @return last token's location or null if cannot be determined + */ + XContentLocation getTokenLocation(); } diff --git a/src/main/java/org/elasticsearch/common/xcontent/json/JsonXContentParser.java b/src/main/java/org/elasticsearch/common/xcontent/json/JsonXContentParser.java index 08174e30a3e..5d3a3f99f4e 100644 --- a/src/main/java/org/elasticsearch/common/xcontent/json/JsonXContentParser.java +++ b/src/main/java/org/elasticsearch/common/xcontent/json/JsonXContentParser.java @@ -19,10 +19,13 @@ package org.elasticsearch.common.xcontent.json; +import com.fasterxml.jackson.core.JsonLocation; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; + import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.IOUtils; +import org.elasticsearch.common.xcontent.XContentLocation; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.support.AbstractXContentParser; @@ -187,6 +190,15 @@ public class JsonXContentParser extends AbstractXContentParser { return parser.getBinaryValue(); } + @Override + public XContentLocation getTokenLocation() { + JsonLocation loc = parser.getTokenLocation(); + if (loc == null) { + return null; + } + return new XContentLocation(loc.getLineNr(), loc.getColumnNr()); + } + @Override public void close() { IOUtils.closeWhileHandlingException(parser); diff --git a/src/main/java/org/elasticsearch/index/percolator/PercolatorQueriesRegistry.java b/src/main/java/org/elasticsearch/index/percolator/PercolatorQueriesRegistry.java index 486101f741f..fd4cce1c763 100644 --- a/src/main/java/org/elasticsearch/index/percolator/PercolatorQueriesRegistry.java +++ b/src/main/java/org/elasticsearch/index/percolator/PercolatorQueriesRegistry.java @@ -223,7 +223,7 @@ public class PercolatorQueriesRegistry extends AbstractIndexShardComponent imple context.setMapUnmappedFieldAsString(mapUnmappedFieldsAsString ? true : false); return queryParserService.parseInnerQuery(context); } catch (IOException e) { - throw new QueryParsingException(queryParserService.index(), "Failed to parse", e); + throw new QueryParsingException(context, "Failed to parse", e); } finally { if (type != null) { QueryParseContext.setTypes(previousTypes); diff --git a/src/main/java/org/elasticsearch/index/query/AndFilterParser.java b/src/main/java/org/elasticsearch/index/query/AndFilterParser.java index 176a8c6dd7b..02322db9a0b 100644 --- a/src/main/java/org/elasticsearch/index/query/AndFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/AndFilterParser.java @@ -100,14 +100,14 @@ public class AndFilterParser implements FilterParser { } else if ("_cache_key".equals(currentFieldName) || "_cacheKey".equals(currentFieldName)) { cacheKey = new HashedBytesRef(parser.text()); } else { - throw new QueryParsingException(parseContext.index(), "[and] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[and] filter does not support [" + currentFieldName + "]"); } } } } if (!filtersFound) { - throw new QueryParsingException(parseContext.index(), "[and] filter requires 'filters' to be set on it'"); + throw new QueryParsingException(parseContext, "[and] filter requires 'filters' to be set on it'"); } if (filters.isEmpty()) { diff --git a/src/main/java/org/elasticsearch/index/query/BoolFilterParser.java b/src/main/java/org/elasticsearch/index/query/BoolFilterParser.java index fcd2e68c8b4..71f8b8248f7 100644 --- a/src/main/java/org/elasticsearch/index/query/BoolFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/BoolFilterParser.java @@ -85,7 +85,7 @@ public class BoolFilterParser implements FilterParser { boolFilter.add(new BooleanClause(filter, BooleanClause.Occur.SHOULD)); } } else { - throw new QueryParsingException(parseContext.index(), "[bool] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[bool] filter does not support [" + currentFieldName + "]"); } } else if (token == XContentParser.Token.START_ARRAY) { if ("must".equals(currentFieldName)) { @@ -114,7 +114,7 @@ public class BoolFilterParser implements FilterParser { } } } else { - throw new QueryParsingException(parseContext.index(), "[bool] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[bool] filter does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("_cache".equals(currentFieldName)) { @@ -124,13 +124,13 @@ public class BoolFilterParser implements FilterParser { } else if ("_cache_key".equals(currentFieldName) || "_cacheKey".equals(currentFieldName)) { cacheKey = new HashedBytesRef(parser.text()); } else { - throw new QueryParsingException(parseContext.index(), "[bool] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[bool] filter does not support [" + currentFieldName + "]"); } } } if (!hasAnyFilter) { - throw new QueryParsingException(parseContext.index(), "[bool] filter has no inner should/must/must_not elements"); + throw new QueryParsingException(parseContext, "[bool] filter has no inner should/must/must_not elements"); } if (boolFilter.clauses().isEmpty()) { diff --git a/src/main/java/org/elasticsearch/index/query/BoolQueryParser.java b/src/main/java/org/elasticsearch/index/query/BoolQueryParser.java index 29d4ba2edd5..b7c31647c94 100644 --- a/src/main/java/org/elasticsearch/index/query/BoolQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/BoolQueryParser.java @@ -85,7 +85,7 @@ public class BoolQueryParser implements QueryParser { clauses.add(new BooleanClause(query, BooleanClause.Occur.SHOULD)); } } else { - throw new QueryParsingException(parseContext.index(), "[bool] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[bool] query does not support [" + currentFieldName + "]"); } } else if (token == XContentParser.Token.START_ARRAY) { if ("must".equals(currentFieldName)) { @@ -110,7 +110,7 @@ public class BoolQueryParser implements QueryParser { } } } else { - throw new QueryParsingException(parseContext.index(), "bool query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "bool query does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("disable_coord".equals(currentFieldName) || "disableCoord".equals(currentFieldName)) { @@ -126,7 +126,7 @@ public class BoolQueryParser implements QueryParser { } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[bool] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[bool] query does not support [" + currentFieldName + "]"); } } } diff --git a/src/main/java/org/elasticsearch/index/query/BoostingQueryParser.java b/src/main/java/org/elasticsearch/index/query/BoostingQueryParser.java index a117256ece1..c160b2f9a4a 100644 --- a/src/main/java/org/elasticsearch/index/query/BoostingQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/BoostingQueryParser.java @@ -66,7 +66,7 @@ public class BoostingQueryParser implements QueryParser { negativeQuery = parseContext.parseInnerQuery(); negativeQueryFound = true; } else { - throw new QueryParsingException(parseContext.index(), "[boosting] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[boosting] query does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("negative_boost".equals(currentFieldName) || "negativeBoost".equals(currentFieldName)) { @@ -74,19 +74,19 @@ public class BoostingQueryParser implements QueryParser { } else if ("boost".equals(currentFieldName)) { boost = parser.floatValue(); } else { - throw new QueryParsingException(parseContext.index(), "[boosting] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[boosting] query does not support [" + currentFieldName + "]"); } } } if (positiveQuery == null && !positiveQueryFound) { - throw new QueryParsingException(parseContext.index(), "[boosting] query requires 'positive' query to be set'"); + throw new QueryParsingException(parseContext, "[boosting] query requires 'positive' query to be set'"); } if (negativeQuery == null && !negativeQueryFound) { - throw new QueryParsingException(parseContext.index(), "[boosting] query requires 'negative' query to be set'"); + throw new QueryParsingException(parseContext, "[boosting] query requires 'negative' query to be set'"); } if (negativeBoost == -1) { - throw new QueryParsingException(parseContext.index(), "[boosting] query requires 'negative_boost' to be set'"); + throw new QueryParsingException(parseContext, "[boosting] query requires 'negative_boost' to be set'"); } // parsers returned null diff --git a/src/main/java/org/elasticsearch/index/query/CommonTermsQueryParser.java b/src/main/java/org/elasticsearch/index/query/CommonTermsQueryParser.java index 683b8dfd9ba..29945de5686 100644 --- a/src/main/java/org/elasticsearch/index/query/CommonTermsQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/CommonTermsQueryParser.java @@ -65,7 +65,7 @@ public class CommonTermsQueryParser implements QueryParser { XContentParser parser = parseContext.parser(); XContentParser.Token token = parser.nextToken(); if (token != XContentParser.Token.FIELD_NAME) { - throw new QueryParsingException(parseContext.index(), "[common] query malformed, no field"); + throw new QueryParsingException(parseContext, "[common] query malformed, no field"); } String fieldName = parser.currentName(); Object value = null; @@ -96,12 +96,13 @@ public class CommonTermsQueryParser implements QueryParser { } else if ("high_freq".equals(innerFieldName) || "highFreq".equals(innerFieldName)) { highFreqMinimumShouldMatch = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[common] query does not support [" + innerFieldName + "] for [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[common] query does not support [" + innerFieldName + + "] for [" + currentFieldName + "]"); } } } } else { - throw new QueryParsingException(parseContext.index(), "[common] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[common] query does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("query".equals(currentFieldName)) { @@ -109,7 +110,7 @@ public class CommonTermsQueryParser implements QueryParser { } else if ("analyzer".equals(currentFieldName)) { String analyzer = parser.text(); if (parseContext.analysisService().analyzer(analyzer) == null) { - throw new QueryParsingException(parseContext.index(), "[common] analyzer [" + parser.text() + "] not found"); + throw new QueryParsingException(parseContext, "[common] analyzer [" + parser.text() + "] not found"); } queryAnalyzer = analyzer; } else if ("disable_coord".equals(currentFieldName) || "disableCoord".equals(currentFieldName)) { @@ -123,7 +124,7 @@ public class CommonTermsQueryParser implements QueryParser { } else if ("and".equalsIgnoreCase(op)) { highFreqOccur = BooleanClause.Occur.MUST; } else { - throw new QueryParsingException(parseContext.index(), + throw new QueryParsingException(parseContext, "[common] query requires operator to be either 'and' or 'or', not [" + op + "]"); } } else if ("low_freq_operator".equals(currentFieldName) || "lowFreqOperator".equals(currentFieldName)) { @@ -133,7 +134,7 @@ public class CommonTermsQueryParser implements QueryParser { } else if ("and".equalsIgnoreCase(op)) { lowFreqOccur = BooleanClause.Occur.MUST; } else { - throw new QueryParsingException(parseContext.index(), + throw new QueryParsingException(parseContext, "[common] query requires operator to be either 'and' or 'or', not [" + op + "]"); } } else if ("minimum_should_match".equals(currentFieldName) || "minimumShouldMatch".equals(currentFieldName)) { @@ -143,7 +144,7 @@ public class CommonTermsQueryParser implements QueryParser { } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[common] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[common] query does not support [" + currentFieldName + "]"); } } } @@ -154,13 +155,13 @@ public class CommonTermsQueryParser implements QueryParser { token = parser.nextToken(); if (token != XContentParser.Token.END_OBJECT) { throw new QueryParsingException( - parseContext.index(), + parseContext, "[common] query parsed in simplified form, with direct field name, but included more options than just the field name, possibly use its 'options' form, with 'query' element?"); } } if (value == null) { - throw new QueryParsingException(parseContext.index(), "No text specified for text query"); + throw new QueryParsingException(parseContext, "No text specified for text query"); } FieldMapper mapper = null; String field; diff --git a/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryParser.java b/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryParser.java index 78c5879b63f..d89ff05b7fa 100644 --- a/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryParser.java @@ -71,7 +71,7 @@ public class ConstantScoreQueryParser implements QueryParser { query = parseContext.parseInnerQuery(); queryFound = true; } else { - throw new QueryParsingException(parseContext.index(), "[constant_score] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[constant_score] query does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("boost".equals(currentFieldName)) { @@ -81,12 +81,12 @@ public class ConstantScoreQueryParser implements QueryParser { } else if ("_cache_key".equals(currentFieldName) || "_cacheKey".equals(currentFieldName)) { cacheKey = new HashedBytesRef(parser.text()); } else { - throw new QueryParsingException(parseContext.index(), "[constant_score] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[constant_score] query does not support [" + currentFieldName + "]"); } } } if (!filterFound && !queryFound) { - throw new QueryParsingException(parseContext.index(), "[constant_score] requires either 'filter' or 'query' element"); + throw new QueryParsingException(parseContext, "[constant_score] requires either 'filter' or 'query' element"); } if (query == null && filter == null) { diff --git a/src/main/java/org/elasticsearch/index/query/DisMaxQueryParser.java b/src/main/java/org/elasticsearch/index/query/DisMaxQueryParser.java index 82feb9854a5..2747387fbd7 100644 --- a/src/main/java/org/elasticsearch/index/query/DisMaxQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/DisMaxQueryParser.java @@ -70,7 +70,7 @@ public class DisMaxQueryParser implements QueryParser { queries.add(query); } } else { - throw new QueryParsingException(parseContext.index(), "[dis_max] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[dis_max] query does not support [" + currentFieldName + "]"); } } else if (token == XContentParser.Token.START_ARRAY) { if ("queries".equals(currentFieldName)) { @@ -83,7 +83,7 @@ public class DisMaxQueryParser implements QueryParser { token = parser.nextToken(); } } else { - throw new QueryParsingException(parseContext.index(), "[dis_max] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[dis_max] query does not support [" + currentFieldName + "]"); } } else { if ("boost".equals(currentFieldName)) { @@ -93,13 +93,13 @@ public class DisMaxQueryParser implements QueryParser { } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[dis_max] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[dis_max] query does not support [" + currentFieldName + "]"); } } } if (!queriesFound) { - throw new QueryParsingException(parseContext.index(), "[dis_max] requires 'queries' field"); + throw new QueryParsingException(parseContext, "[dis_max] requires 'queries' field"); } if (queries.isEmpty()) { diff --git a/src/main/java/org/elasticsearch/index/query/ExistsFilterParser.java b/src/main/java/org/elasticsearch/index/query/ExistsFilterParser.java index eb03586adf2..008f554a57f 100644 --- a/src/main/java/org/elasticsearch/index/query/ExistsFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/ExistsFilterParser.java @@ -23,8 +23,6 @@ import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.Filter; import org.apache.lucene.search.Query; -import org.apache.lucene.search.QueryWrapperFilter; -import org.apache.lucene.search.TermRangeFilter; import org.apache.lucene.search.TermRangeQuery; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.lucene.HashedBytesRef; @@ -71,13 +69,13 @@ public class ExistsFilterParser implements FilterParser { } else if ("_name".equals(currentFieldName)) { filterName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[exists] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[exists] filter does not support [" + currentFieldName + "]"); } } } if (fieldPattern == null) { - throw new QueryParsingException(parseContext.index(), "exists must be provided with a [field]"); + throw new QueryParsingException(parseContext, "exists must be provided with a [field]"); } return newFilter(parseContext, fieldPattern, filterName); diff --git a/src/main/java/org/elasticsearch/index/query/FQueryFilterParser.java b/src/main/java/org/elasticsearch/index/query/FQueryFilterParser.java index cb821912ca9..d31e2f1a943 100644 --- a/src/main/java/org/elasticsearch/index/query/FQueryFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/FQueryFilterParser.java @@ -66,7 +66,7 @@ public class FQueryFilterParser implements FilterParser { queryFound = true; query = parseContext.parseInnerQuery(); } else { - throw new QueryParsingException(parseContext.index(), "[fquery] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[fquery] filter does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("_name".equals(currentFieldName)) { @@ -76,12 +76,12 @@ public class FQueryFilterParser implements FilterParser { } else if ("_cache_key".equals(currentFieldName) || "_cacheKey".equals(currentFieldName)) { cacheKey = new HashedBytesRef(parser.text()); } else { - throw new QueryParsingException(parseContext.index(), "[fquery] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[fquery] filter does not support [" + currentFieldName + "]"); } } } if (!queryFound) { - throw new QueryParsingException(parseContext.index(), "[fquery] requires 'query' element"); + throw new QueryParsingException(parseContext, "[fquery] requires 'query' element"); } if (query == null) { return null; diff --git a/src/main/java/org/elasticsearch/index/query/FieldMaskingSpanQueryParser.java b/src/main/java/org/elasticsearch/index/query/FieldMaskingSpanQueryParser.java index 2b69cf61561..1e8fd7cfa03 100644 --- a/src/main/java/org/elasticsearch/index/query/FieldMaskingSpanQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/FieldMaskingSpanQueryParser.java @@ -64,11 +64,12 @@ public class FieldMaskingSpanQueryParser implements QueryParser { if ("query".equals(currentFieldName)) { Query query = parseContext.parseInnerQuery(); if (!(query instanceof SpanQuery)) { - throw new QueryParsingException(parseContext.index(), "[field_masking_span] query] must be of type span query"); + throw new QueryParsingException(parseContext, "[field_masking_span] query] must be of type span query"); } inner = (SpanQuery) query; } else { - throw new QueryParsingException(parseContext.index(), "[field_masking_span] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[field_masking_span] query does not support [" + + currentFieldName + "]"); } } else { if ("boost".equals(currentFieldName)) { @@ -78,15 +79,15 @@ public class FieldMaskingSpanQueryParser implements QueryParser { } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[field_masking_span] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[field_masking_span] query does not support [" + currentFieldName + "]"); } } } if (inner == null) { - throw new QueryParsingException(parseContext.index(), "field_masking_span must have [query] span query clause"); + throw new QueryParsingException(parseContext, "field_masking_span must have [query] span query clause"); } if (field == null) { - throw new QueryParsingException(parseContext.index(), "field_masking_span must have [field] set for it"); + throw new QueryParsingException(parseContext, "field_masking_span must have [field] set for it"); } FieldMapper mapper = parseContext.fieldMapper(field); diff --git a/src/main/java/org/elasticsearch/index/query/FilteredQueryParser.java b/src/main/java/org/elasticsearch/index/query/FilteredQueryParser.java index e1e27eec64b..f6ec14313b1 100644 --- a/src/main/java/org/elasticsearch/index/query/FilteredQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/FilteredQueryParser.java @@ -73,7 +73,7 @@ public class FilteredQueryParser implements QueryParser { filterFound = true; filter = parseContext.parseInnerFilter(); } else { - throw new QueryParsingException(parseContext.index(), "[filtered] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[filtered] query does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("strategy".equals(currentFieldName)) { @@ -93,7 +93,7 @@ public class FilteredQueryParser implements QueryParser { } else if ("leap_frog_filter_first".equals(value) || "leapFrogFilterFirst".equals(value)) { filterStrategy = FilteredQuery.LEAP_FROG_FILTER_FIRST_STRATEGY; } else { - throw new QueryParsingException(parseContext.index(), "[filtered] strategy value not supported [" + value + "]"); + throw new QueryParsingException(parseContext, "[filtered] strategy value not supported [" + value + "]"); } } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); @@ -104,7 +104,7 @@ public class FilteredQueryParser implements QueryParser { } else if ("_cache_key".equals(currentFieldName) || "_cacheKey".equals(currentFieldName)) { cacheKey = new HashedBytesRef(parser.text()); } else { - throw new QueryParsingException(parseContext.index(), "[filtered] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[filtered] query does not support [" + currentFieldName + "]"); } } } diff --git a/src/main/java/org/elasticsearch/index/query/FuzzyQueryParser.java b/src/main/java/org/elasticsearch/index/query/FuzzyQueryParser.java index 243f86534cd..229fcc95c72 100644 --- a/src/main/java/org/elasticsearch/index/query/FuzzyQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/FuzzyQueryParser.java @@ -57,7 +57,7 @@ public class FuzzyQueryParser implements QueryParser { XContentParser.Token token = parser.nextToken(); if (token != XContentParser.Token.FIELD_NAME) { - throw new QueryParsingException(parseContext.index(), "[fuzzy] query malformed, no field"); + throw new QueryParsingException(parseContext, "[fuzzy] query malformed, no field"); } String fieldName = parser.currentName(); @@ -95,7 +95,7 @@ public class FuzzyQueryParser implements QueryParser { } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[fuzzy] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[fuzzy] query does not support [" + currentFieldName + "]"); } } } @@ -107,7 +107,7 @@ public class FuzzyQueryParser implements QueryParser { } if (value == null) { - throw new QueryParsingException(parseContext.index(), "No value specified for fuzzy query"); + throw new QueryParsingException(parseContext, "No value specified for fuzzy query"); } Query query = null; diff --git a/src/main/java/org/elasticsearch/index/query/GeoBoundingBoxFilterParser.java b/src/main/java/org/elasticsearch/index/query/GeoBoundingBoxFilterParser.java index 8f68dbea074..107e3a507dd 100644 --- a/src/main/java/org/elasticsearch/index/query/GeoBoundingBoxFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/GeoBoundingBoxFilterParser.java @@ -147,7 +147,7 @@ public class GeoBoundingBoxFilterParser implements FilterParser { } else if ("type".equals(currentFieldName)) { type = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[geo_bbox] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[geo_bbox] filter does not support [" + currentFieldName + "]"); } } } @@ -169,11 +169,11 @@ public class GeoBoundingBoxFilterParser implements FilterParser { MapperService.SmartNameFieldMappers smartMappers = parseContext.smartFieldMappers(fieldName); if (smartMappers == null || !smartMappers.hasMapper()) { - throw new QueryParsingException(parseContext.index(), "failed to find geo_point field [" + fieldName + "]"); + throw new QueryParsingException(parseContext, "failed to find geo_point field [" + fieldName + "]"); } FieldMapper mapper = smartMappers.mapper(); if (!(mapper instanceof GeoPointFieldMapper)) { - throw new QueryParsingException(parseContext.index(), "field [" + fieldName + "] is not a geo_point field"); + throw new QueryParsingException(parseContext, "field [" + fieldName + "] is not a geo_point field"); } GeoPointFieldMapper geoMapper = ((GeoPointFieldMapper) mapper); @@ -184,7 +184,8 @@ public class GeoBoundingBoxFilterParser implements FilterParser { IndexGeoPointFieldData indexFieldData = parseContext.getForField(mapper); filter = new InMemoryGeoBoundingBoxFilter(topLeft, bottomRight, indexFieldData); } else { - throw new QueryParsingException(parseContext.index(), "geo bounding box type [" + type + "] not supported, either 'indexed' or 'memory' are allowed"); + throw new QueryParsingException(parseContext, "geo bounding box type [" + type + + "] not supported, either 'indexed' or 'memory' are allowed"); } if (cache != null) { diff --git a/src/main/java/org/elasticsearch/index/query/GeoDistanceFilterParser.java b/src/main/java/org/elasticsearch/index/query/GeoDistanceFilterParser.java index 252afdf25cf..a7859977388 100644 --- a/src/main/java/org/elasticsearch/index/query/GeoDistanceFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/GeoDistanceFilterParser.java @@ -98,7 +98,8 @@ public class GeoDistanceFilterParser implements FilterParser { } else if (currentName.equals(GeoPointFieldMapper.Names.GEOHASH)) { GeoHashUtils.decode(parser.text(), point); } else { - throw new QueryParsingException(parseContext.index(), "[geo_distance] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[geo_distance] filter does not support [" + currentFieldName + + "]"); } } } @@ -141,7 +142,7 @@ public class GeoDistanceFilterParser implements FilterParser { } if (vDistance == null) { - throw new QueryParsingException(parseContext.index(), "geo_distance requires 'distance' to be specified"); + throw new QueryParsingException(parseContext, "geo_distance requires 'distance' to be specified"); } else if (vDistance instanceof Number) { distance = DistanceUnit.DEFAULT.convert(((Number) vDistance).doubleValue(), unit); } else { @@ -155,11 +156,11 @@ public class GeoDistanceFilterParser implements FilterParser { MapperService.SmartNameFieldMappers smartMappers = parseContext.smartFieldMappers(fieldName); if (smartMappers == null || !smartMappers.hasMapper()) { - throw new QueryParsingException(parseContext.index(), "failed to find geo_point field [" + fieldName + "]"); + throw new QueryParsingException(parseContext, "failed to find geo_point field [" + fieldName + "]"); } FieldMapper mapper = smartMappers.mapper(); if (!(mapper instanceof GeoPointFieldMapper)) { - throw new QueryParsingException(parseContext.index(), "field [" + fieldName + "] is not a geo_point field"); + throw new QueryParsingException(parseContext, "field [" + fieldName + "] is not a geo_point field"); } GeoPointFieldMapper geoMapper = ((GeoPointFieldMapper) mapper); diff --git a/src/main/java/org/elasticsearch/index/query/GeoDistanceRangeFilterParser.java b/src/main/java/org/elasticsearch/index/query/GeoDistanceRangeFilterParser.java index b7452bec0f1..113c59d2c83 100644 --- a/src/main/java/org/elasticsearch/index/query/GeoDistanceRangeFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/GeoDistanceRangeFilterParser.java @@ -196,11 +196,11 @@ public class GeoDistanceRangeFilterParser implements FilterParser { MapperService.SmartNameFieldMappers smartMappers = parseContext.smartFieldMappers(fieldName); if (smartMappers == null || !smartMappers.hasMapper()) { - throw new QueryParsingException(parseContext.index(), "failed to find geo_point field [" + fieldName + "]"); + throw new QueryParsingException(parseContext, "failed to find geo_point field [" + fieldName + "]"); } FieldMapper mapper = smartMappers.mapper(); if (!(mapper instanceof GeoPointFieldMapper)) { - throw new QueryParsingException(parseContext.index(), "field [" + fieldName + "] is not a geo_point field"); + throw new QueryParsingException(parseContext, "field [" + fieldName + "] is not a geo_point field"); } GeoPointFieldMapper geoMapper = ((GeoPointFieldMapper) mapper); diff --git a/src/main/java/org/elasticsearch/index/query/GeoPolygonFilterParser.java b/src/main/java/org/elasticsearch/index/query/GeoPolygonFilterParser.java index fefa37c07e3..e63c6012ede 100644 --- a/src/main/java/org/elasticsearch/index/query/GeoPolygonFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/GeoPolygonFilterParser.java @@ -96,10 +96,12 @@ public class GeoPolygonFilterParser implements FilterParser { shell.add(GeoUtils.parseGeoPoint(parser)); } } else { - throw new QueryParsingException(parseContext.index(), "[geo_polygon] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[geo_polygon] filter does not support [" + currentFieldName + + "]"); } } else { - throw new QueryParsingException(parseContext.index(), "[geo_polygon] filter does not support token type [" + token.name() + "] under [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[geo_polygon] filter does not support token type [" + token.name() + + "] under [" + currentFieldName + "]"); } } } else if (token.isValue()) { @@ -113,25 +115,25 @@ public class GeoPolygonFilterParser implements FilterParser { normalizeLat = parser.booleanValue(); normalizeLon = parser.booleanValue(); } else { - throw new QueryParsingException(parseContext.index(), "[geo_polygon] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[geo_polygon] filter does not support [" + currentFieldName + "]"); } } else { - throw new QueryParsingException(parseContext.index(), "[geo_polygon] unexpected token type [" + token.name() + "]"); + throw new QueryParsingException(parseContext, "[geo_polygon] unexpected token type [" + token.name() + "]"); } } if (shell.isEmpty()) { - throw new QueryParsingException(parseContext.index(), "no points defined for geo_polygon filter"); + throw new QueryParsingException(parseContext, "no points defined for geo_polygon filter"); } else { if (shell.size() < 3) { - throw new QueryParsingException(parseContext.index(), "too few points defined for geo_polygon filter"); + throw new QueryParsingException(parseContext, "too few points defined for geo_polygon filter"); } GeoPoint start = shell.get(0); if (!start.equals(shell.get(shell.size() - 1))) { shell.add(start); } if (shell.size() < 4) { - throw new QueryParsingException(parseContext.index(), "too few points defined for geo_polygon filter"); + throw new QueryParsingException(parseContext, "too few points defined for geo_polygon filter"); } } @@ -143,11 +145,11 @@ public class GeoPolygonFilterParser implements FilterParser { MapperService.SmartNameFieldMappers smartMappers = parseContext.smartFieldMappers(fieldName); if (smartMappers == null || !smartMappers.hasMapper()) { - throw new QueryParsingException(parseContext.index(), "failed to find geo_point field [" + fieldName + "]"); + throw new QueryParsingException(parseContext, "failed to find geo_point field [" + fieldName + "]"); } FieldMapper mapper = smartMappers.mapper(); if (!(mapper instanceof GeoPointFieldMapper)) { - throw new QueryParsingException(parseContext.index(), "field [" + fieldName + "] is not a geo_point field"); + throw new QueryParsingException(parseContext, "field [" + fieldName + "] is not a geo_point field"); } IndexGeoPointFieldData indexFieldData = parseContext.getForField(mapper); diff --git a/src/main/java/org/elasticsearch/index/query/GeoShapeFilterParser.java b/src/main/java/org/elasticsearch/index/query/GeoShapeFilterParser.java index 72eba62854e..5a5e45736cd 100644 --- a/src/main/java/org/elasticsearch/index/query/GeoShapeFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/GeoShapeFilterParser.java @@ -113,7 +113,7 @@ public class GeoShapeFilterParser implements FilterParser { } else if ("relation".equals(currentFieldName)) { shapeRelation = ShapeRelation.getRelationByName(parser.text()); if (shapeRelation == null) { - throw new QueryParsingException(parseContext.index(), "Unknown shape operation [" + parser.text() + "]"); + throw new QueryParsingException(parseContext, "Unknown shape operation [" + parser.text() + "]"); } } else if ("strategy".equals(currentFieldName)) { strategyName = parser.text(); @@ -134,13 +134,13 @@ public class GeoShapeFilterParser implements FilterParser { } } if (id == null) { - throw new QueryParsingException(parseContext.index(), "ID for indexed shape not provided"); + throw new QueryParsingException(parseContext, "ID for indexed shape not provided"); } else if (type == null) { - throw new QueryParsingException(parseContext.index(), "Type for indexed shape not provided"); + throw new QueryParsingException(parseContext, "Type for indexed shape not provided"); } shape = fetchService.fetch(id, type, index, shapePath); } else { - throw new QueryParsingException(parseContext.index(), "[geo_shape] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[geo_shape] filter does not support [" + currentFieldName + "]"); } } } @@ -152,26 +152,26 @@ public class GeoShapeFilterParser implements FilterParser { } else if ("_cache_key".equals(currentFieldName)) { cacheKey = new HashedBytesRef(parser.text()); } else { - throw new QueryParsingException(parseContext.index(), "[geo_shape] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[geo_shape] filter does not support [" + currentFieldName + "]"); } } } if (shape == null) { - throw new QueryParsingException(parseContext.index(), "No Shape defined"); + throw new QueryParsingException(parseContext, "No Shape defined"); } else if (shapeRelation == null) { - throw new QueryParsingException(parseContext.index(), "No Shape Relation defined"); + throw new QueryParsingException(parseContext, "No Shape Relation defined"); } MapperService.SmartNameFieldMappers smartNameFieldMappers = parseContext.smartFieldMappers(fieldName); if (smartNameFieldMappers == null || !smartNameFieldMappers.hasMapper()) { - throw new QueryParsingException(parseContext.index(), "Failed to find geo_shape field [" + fieldName + "]"); + throw new QueryParsingException(parseContext, "Failed to find geo_shape field [" + fieldName + "]"); } FieldMapper fieldMapper = smartNameFieldMappers.mapper(); // TODO: This isn't the nicest way to check this if (!(fieldMapper instanceof GeoShapeFieldMapper)) { - throw new QueryParsingException(parseContext.index(), "Field [" + fieldName + "] is not a geo_shape"); + throw new QueryParsingException(parseContext, "Field [" + fieldName + "] is not a geo_shape"); } GeoShapeFieldMapper shapeFieldMapper = (GeoShapeFieldMapper) fieldMapper; diff --git a/src/main/java/org/elasticsearch/index/query/GeoShapeQueryParser.java b/src/main/java/org/elasticsearch/index/query/GeoShapeQueryParser.java index eeed9d270f0..ac3d4f59f92 100644 --- a/src/main/java/org/elasticsearch/index/query/GeoShapeQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/GeoShapeQueryParser.java @@ -93,7 +93,7 @@ public class GeoShapeQueryParser implements QueryParser { } else if ("relation".equals(currentFieldName)) { shapeRelation = ShapeRelation.getRelationByName(parser.text()); if (shapeRelation == null) { - throw new QueryParsingException(parseContext.index(), "Unknown shape operation [" + parser.text() + " ]"); + throw new QueryParsingException(parseContext, "Unknown shape operation [" + parser.text() + " ]"); } } else if ("indexed_shape".equals(currentFieldName) || "indexedShape".equals(currentFieldName)) { while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { @@ -112,13 +112,13 @@ public class GeoShapeQueryParser implements QueryParser { } } if (id == null) { - throw new QueryParsingException(parseContext.index(), "ID for indexed shape not provided"); + throw new QueryParsingException(parseContext, "ID for indexed shape not provided"); } else if (type == null) { - throw new QueryParsingException(parseContext.index(), "Type for indexed shape not provided"); + throw new QueryParsingException(parseContext, "Type for indexed shape not provided"); } shape = fetchService.fetch(id, type, index, shapePath); } else { - throw new QueryParsingException(parseContext.index(), "[geo_shape] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[geo_shape] query does not support [" + currentFieldName + "]"); } } } @@ -128,26 +128,26 @@ public class GeoShapeQueryParser implements QueryParser { } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[geo_shape] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[geo_shape] query does not support [" + currentFieldName + "]"); } } } if (shape == null) { - throw new QueryParsingException(parseContext.index(), "No Shape defined"); + throw new QueryParsingException(parseContext, "No Shape defined"); } else if (shapeRelation == null) { - throw new QueryParsingException(parseContext.index(), "No Shape Relation defined"); + throw new QueryParsingException(parseContext, "No Shape Relation defined"); } MapperService.SmartNameFieldMappers smartNameFieldMappers = parseContext.smartFieldMappers(fieldName); if (smartNameFieldMappers == null || !smartNameFieldMappers.hasMapper()) { - throw new QueryParsingException(parseContext.index(), "Failed to find geo_shape field [" + fieldName + "]"); + throw new QueryParsingException(parseContext, "Failed to find geo_shape field [" + fieldName + "]"); } FieldMapper fieldMapper = smartNameFieldMappers.mapper(); // TODO: This isn't the nicest way to check this if (!(fieldMapper instanceof GeoShapeFieldMapper)) { - throw new QueryParsingException(parseContext.index(), "Field [" + fieldName + "] is not a geo_shape"); + throw new QueryParsingException(parseContext, "Field [" + fieldName + "] is not a geo_shape"); } GeoShapeFieldMapper shapeFieldMapper = (GeoShapeFieldMapper) fieldMapper; diff --git a/src/main/java/org/elasticsearch/index/query/GeohashCellFilter.java b/src/main/java/org/elasticsearch/index/query/GeohashCellFilter.java index f93cc2681b8..63ca22db644 100644 --- a/src/main/java/org/elasticsearch/index/query/GeohashCellFilter.java +++ b/src/main/java/org/elasticsearch/index/query/GeohashCellFilter.java @@ -265,22 +265,23 @@ public class GeohashCellFilter { } if (geohash == null) { - throw new QueryParsingException(parseContext.index(), "no geohash value provided to geohash_cell filter"); + throw new QueryParsingException(parseContext, "no geohash value provided to geohash_cell filter"); } MapperService.SmartNameFieldMappers smartMappers = parseContext.smartFieldMappers(fieldName); if (smartMappers == null || !smartMappers.hasMapper()) { - throw new QueryParsingException(parseContext.index(), "failed to find geo_point field [" + fieldName + "]"); + throw new QueryParsingException(parseContext, "failed to find geo_point field [" + fieldName + "]"); } FieldMapper mapper = smartMappers.mapper(); if (!(mapper instanceof GeoPointFieldMapper)) { - throw new QueryParsingException(parseContext.index(), "field [" + fieldName + "] is not a geo_point field"); + throw new QueryParsingException(parseContext, "field [" + fieldName + "] is not a geo_point field"); } GeoPointFieldMapper geoMapper = ((GeoPointFieldMapper) mapper); if (!geoMapper.isEnableGeohashPrefix()) { - throw new QueryParsingException(parseContext.index(), "can't execute geohash_cell on field [" + fieldName + "], geohash_prefix is not enabled"); + throw new QueryParsingException(parseContext, "can't execute geohash_cell on field [" + fieldName + + "], geohash_prefix is not enabled"); } if(levels > 0) { diff --git a/src/main/java/org/elasticsearch/index/query/HasChildFilterParser.java b/src/main/java/org/elasticsearch/index/query/HasChildFilterParser.java index c04e48b8e1e..d22a05f6a11 100644 --- a/src/main/java/org/elasticsearch/index/query/HasChildFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/HasChildFilterParser.java @@ -94,7 +94,7 @@ public class HasChildFilterParser implements FilterParser { } else if ("inner_hits".equals(currentFieldName)) { innerHits = innerHitsQueryParserHelper.parse(parseContext); } else { - throw new QueryParsingException(parseContext.index(), "[has_child] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[has_child] filter does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("type".equals(currentFieldName) || "child_type".equals(currentFieldName) || "childType".equals(currentFieldName)) { @@ -112,15 +112,15 @@ public class HasChildFilterParser implements FilterParser { } else if ("max_children".equals(currentFieldName) || "maxChildren".equals(currentFieldName)) { maxChildren = parser.intValue(true); } else { - throw new QueryParsingException(parseContext.index(), "[has_child] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[has_child] filter does not support [" + currentFieldName + "]"); } } } if (!queryFound && !filterFound) { - throw new QueryParsingException(parseContext.index(), "[has_child] filter requires 'query' or 'filter' field"); + throw new QueryParsingException(parseContext, "[has_child] filter requires 'query' or 'filter' field"); } if (childType == null) { - throw new QueryParsingException(parseContext.index(), "[has_child] filter requires 'type' field"); + throw new QueryParsingException(parseContext, "[has_child] filter requires 'type' field"); } Query query; @@ -136,7 +136,7 @@ public class HasChildFilterParser implements FilterParser { DocumentMapper childDocMapper = parseContext.mapperService().documentMapper(childType); if (childDocMapper == null) { - throw new QueryParsingException(parseContext.index(), "No mapping for for type [" + childType + "]"); + throw new QueryParsingException(parseContext, "No mapping for for type [" + childType + "]"); } if (innerHits != null) { InnerHitsContext.ParentChildInnerHits parentChildInnerHits = new InnerHitsContext.ParentChildInnerHits(innerHits.v2(), query, null, childDocMapper); @@ -145,7 +145,7 @@ public class HasChildFilterParser implements FilterParser { } ParentFieldMapper parentFieldMapper = childDocMapper.parentFieldMapper(); if (!parentFieldMapper.active()) { - throw new QueryParsingException(parseContext.index(), "Type [" + childType + "] does not have parent mapping"); + throw new QueryParsingException(parseContext, "Type [" + childType + "] does not have parent mapping"); } String parentType = parentFieldMapper.type(); @@ -154,11 +154,12 @@ public class HasChildFilterParser implements FilterParser { DocumentMapper parentDocMapper = parseContext.mapperService().documentMapper(parentType); if (parentDocMapper == null) { - throw new QueryParsingException(parseContext.index(), "[has_child] Type [" + childType + "] points to a non existent parent type [" + parentType + "]"); + throw new QueryParsingException(parseContext, "[has_child] Type [" + childType + "] points to a non existent parent type [" + + parentType + "]"); } if (maxChildren > 0 && maxChildren < minChildren) { - throw new QueryParsingException(parseContext.index(), "[has_child] 'max_children' is less than 'min_children'"); + throw new QueryParsingException(parseContext, "[has_child] 'max_children' is less than 'min_children'"); } BitDocIdSetFilter nonNestedDocsFilter = null; diff --git a/src/main/java/org/elasticsearch/index/query/HasChildQueryParser.java b/src/main/java/org/elasticsearch/index/query/HasChildQueryParser.java index 058f47d5eb7..e088b58a51a 100644 --- a/src/main/java/org/elasticsearch/index/query/HasChildQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/HasChildQueryParser.java @@ -92,7 +92,7 @@ public class HasChildQueryParser implements QueryParser { } else if ("inner_hits".equals(currentFieldName)) { innerHits = innerHitsQueryParserHelper.parse(parseContext); } else { - throw new QueryParsingException(parseContext.index(), "[has_child] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[has_child] query does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("type".equals(currentFieldName) || "child_type".equals(currentFieldName) || "childType".equals(currentFieldName)) { @@ -112,15 +112,15 @@ public class HasChildQueryParser implements QueryParser { } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[has_child] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[has_child] query does not support [" + currentFieldName + "]"); } } } if (!queryFound) { - throw new QueryParsingException(parseContext.index(), "[has_child] requires 'query' field"); + throw new QueryParsingException(parseContext, "[has_child] requires 'query' field"); } if (childType == null) { - throw new QueryParsingException(parseContext.index(), "[has_child] requires 'type' field"); + throw new QueryParsingException(parseContext, "[has_child] requires 'type' field"); } Query innerQuery = iq.asQuery(childType); @@ -132,10 +132,10 @@ public class HasChildQueryParser implements QueryParser { DocumentMapper childDocMapper = parseContext.mapperService().documentMapper(childType); if (childDocMapper == null) { - throw new QueryParsingException(parseContext.index(), "[has_child] No mapping for for type [" + childType + "]"); + throw new QueryParsingException(parseContext, "[has_child] No mapping for for type [" + childType + "]"); } if (!childDocMapper.parentFieldMapper().active()) { - throw new QueryParsingException(parseContext.index(), "[has_child] Type [" + childType + "] does not have parent mapping"); + throw new QueryParsingException(parseContext, "[has_child] Type [" + childType + "] does not have parent mapping"); } if (innerHits != null) { @@ -146,18 +146,18 @@ public class HasChildQueryParser implements QueryParser { ParentFieldMapper parentFieldMapper = childDocMapper.parentFieldMapper(); if (!parentFieldMapper.active()) { - throw new QueryParsingException(parseContext.index(), "[has_child] _parent field not configured"); + throw new QueryParsingException(parseContext, "[has_child] _parent field not configured"); } String parentType = parentFieldMapper.type(); DocumentMapper parentDocMapper = parseContext.mapperService().documentMapper(parentType); if (parentDocMapper == null) { - throw new QueryParsingException(parseContext.index(), "[has_child] Type [" + childType - + "] points to a non existent parent type [" + parentType + "]"); + throw new QueryParsingException(parseContext, "[has_child] Type [" + childType + "] points to a non existent parent type [" + + parentType + "]"); } if (maxChildren > 0 && maxChildren < minChildren) { - throw new QueryParsingException(parseContext.index(), "[has_child] 'max_children' is less than 'min_children'"); + throw new QueryParsingException(parseContext, "[has_child] 'max_children' is less than 'min_children'"); } BitDocIdSetFilter nonNestedDocsFilter = null; diff --git a/src/main/java/org/elasticsearch/index/query/HasParentFilterParser.java b/src/main/java/org/elasticsearch/index/query/HasParentFilterParser.java index fd3335202f3..388f24d4ab0 100644 --- a/src/main/java/org/elasticsearch/index/query/HasParentFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/HasParentFilterParser.java @@ -83,7 +83,7 @@ public class HasParentFilterParser implements FilterParser { } else if ("inner_hits".equals(currentFieldName)) { innerHits = innerHitsQueryParserHelper.parse(parseContext); } else { - throw new QueryParsingException(parseContext.index(), "[has_parent] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[has_parent] filter does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("type".equals(currentFieldName) || "parent_type".equals(currentFieldName) || "parentType".equals(currentFieldName)) { @@ -95,15 +95,15 @@ public class HasParentFilterParser implements FilterParser { } else if ("_cache_key".equals(currentFieldName) || "_cacheKey".equals(currentFieldName)) { // noop to be backwards compatible } else { - throw new QueryParsingException(parseContext.index(), "[has_parent] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[has_parent] filter does not support [" + currentFieldName + "]"); } } } if (!queryFound && !filterFound) { - throw new QueryParsingException(parseContext.index(), "[has_parent] filter requires 'query' or 'filter' field"); + throw new QueryParsingException(parseContext, "[has_parent] filter requires 'query' or 'filter' field"); } if (parentType == null) { - throw new QueryParsingException(parseContext.index(), "[has_parent] filter requires 'parent_type' field"); + throw new QueryParsingException(parseContext, "[has_parent] filter requires 'parent_type' field"); } Query innerQuery; diff --git a/src/main/java/org/elasticsearch/index/query/HasParentQueryParser.java b/src/main/java/org/elasticsearch/index/query/HasParentQueryParser.java index 9525064647b..d7d57b6ddd6 100644 --- a/src/main/java/org/elasticsearch/index/query/HasParentQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/HasParentQueryParser.java @@ -23,7 +23,6 @@ import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.Filter; import org.apache.lucene.search.FilteredQuery; import org.apache.lucene.search.Query; -import org.apache.lucene.search.QueryWrapperFilter; import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.inject.Inject; @@ -88,7 +87,7 @@ public class HasParentQueryParser implements QueryParser { } else if ("inner_hits".equals(currentFieldName)) { innerHits = innerHitsQueryParserHelper.parse(parseContext); } else { - throw new QueryParsingException(parseContext.index(), "[has_parent] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[has_parent] query does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("type".equals(currentFieldName) || "parent_type".equals(currentFieldName) || "parentType".equals(currentFieldName)) { @@ -112,15 +111,15 @@ public class HasParentQueryParser implements QueryParser { } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[has_parent] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[has_parent] query does not support [" + currentFieldName + "]"); } } } if (!queryFound) { - throw new QueryParsingException(parseContext.index(), "[has_parent] query requires 'query' field"); + throw new QueryParsingException(parseContext, "[has_parent] query requires 'query' field"); } if (parentType == null) { - throw new QueryParsingException(parseContext.index(), "[has_parent] query requires 'parent_type' field"); + throw new QueryParsingException(parseContext, "[has_parent] query requires 'parent_type' field"); } Query innerQuery = iq.asQuery(parentType); @@ -145,7 +144,8 @@ public class HasParentQueryParser implements QueryParser { static Query createParentQuery(Query innerQuery, String parentType, boolean score, QueryParseContext parseContext, Tuple innerHits) { DocumentMapper parentDocMapper = parseContext.mapperService().documentMapper(parentType); if (parentDocMapper == null) { - throw new QueryParsingException(parseContext.index(), "[has_parent] query configured 'parent_type' [" + parentType + "] is not a valid type"); + throw new QueryParsingException(parseContext, "[has_parent] query configured 'parent_type' [" + parentType + + "] is not a valid type"); } if (innerHits != null) { @@ -169,7 +169,7 @@ public class HasParentQueryParser implements QueryParser { } } if (parentChildIndexFieldData == null) { - throw new QueryParsingException(parseContext.index(), "[has_parent] no _parent field configured"); + throw new QueryParsingException(parseContext, "[has_parent] no _parent field configured"); } Filter parentFilter = null; diff --git a/src/main/java/org/elasticsearch/index/query/IdsFilterParser.java b/src/main/java/org/elasticsearch/index/query/IdsFilterParser.java index d0402aabf95..138557cd79a 100644 --- a/src/main/java/org/elasticsearch/index/query/IdsFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/IdsFilterParser.java @@ -68,7 +68,7 @@ public class IdsFilterParser implements FilterParser { while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { BytesRef value = parser.utf8BytesOrNull(); if (value == null) { - throw new QueryParsingException(parseContext.index(), "No value specified for term filter"); + throw new QueryParsingException(parseContext, "No value specified for term filter"); } ids.add(value); } @@ -77,12 +77,12 @@ public class IdsFilterParser implements FilterParser { while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { String value = parser.textOrNull(); if (value == null) { - throw new QueryParsingException(parseContext.index(), "No type specified for term filter"); + throw new QueryParsingException(parseContext, "No type specified for term filter"); } types.add(value); } } else { - throw new QueryParsingException(parseContext.index(), "[ids] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[ids] filter does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("type".equals(currentFieldName) || "_type".equals(currentFieldName)) { @@ -90,13 +90,13 @@ public class IdsFilterParser implements FilterParser { } else if ("_name".equals(currentFieldName)) { filterName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[ids] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[ids] filter does not support [" + currentFieldName + "]"); } } } if (!idsProvided) { - throw new QueryParsingException(parseContext.index(), "[ids] filter requires providing a values element"); + throw new QueryParsingException(parseContext, "[ids] filter requires providing a values element"); } if (ids.isEmpty()) { diff --git a/src/main/java/org/elasticsearch/index/query/IdsQueryParser.java b/src/main/java/org/elasticsearch/index/query/IdsQueryParser.java index d0345944c66..3789b3039c0 100644 --- a/src/main/java/org/elasticsearch/index/query/IdsQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/IdsQueryParser.java @@ -74,12 +74,12 @@ public class IdsQueryParser implements QueryParser { (token == XContentParser.Token.VALUE_NUMBER)) { BytesRef value = parser.utf8BytesOrNull(); if (value == null) { - throw new QueryParsingException(parseContext.index(), "No value specified for term filter"); + throw new QueryParsingException(parseContext, "No value specified for term filter"); } ids.add(value); } else { - throw new QueryParsingException(parseContext.index(), - "Illegal value for id, expecting a string or number, got: " + token); + throw new QueryParsingException(parseContext, "Illegal value for id, expecting a string or number, got: " + + token); } } } else if ("types".equals(currentFieldName) || "type".equals(currentFieldName)) { @@ -87,12 +87,12 @@ public class IdsQueryParser implements QueryParser { while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { String value = parser.textOrNull(); if (value == null) { - throw new QueryParsingException(parseContext.index(), "No type specified for term filter"); + throw new QueryParsingException(parseContext, "No type specified for term filter"); } types.add(value); } } else { - throw new QueryParsingException(parseContext.index(), "[ids] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[ids] query does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("type".equals(currentFieldName) || "_type".equals(currentFieldName)) { @@ -102,13 +102,13 @@ public class IdsQueryParser implements QueryParser { } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[ids] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[ids] query does not support [" + currentFieldName + "]"); } } } if (!idsProvided) { - throw new QueryParsingException(parseContext.index(), "[ids] query, no ids values provided"); + throw new QueryParsingException(parseContext, "[ids] query, no ids values provided"); } if (ids.isEmpty()) { diff --git a/src/main/java/org/elasticsearch/index/query/IndexQueryParserService.java b/src/main/java/org/elasticsearch/index/query/IndexQueryParserService.java index 93c94ee98da..e2bcd353e11 100644 --- a/src/main/java/org/elasticsearch/index/query/IndexQueryParserService.java +++ b/src/main/java/org/elasticsearch/index/query/IndexQueryParserService.java @@ -210,7 +210,7 @@ public class IndexQueryParserService extends AbstractIndexComponent { } catch (QueryParsingException e) { throw e; } catch (Exception e) { - throw new QueryParsingException(index, "Failed to parse", e); + throw new QueryParsingException(getParseContext(), "Failed to parse", e); } finally { if (parser != null) { parser.close(); @@ -230,7 +230,7 @@ public class IndexQueryParserService extends AbstractIndexComponent { } catch (QueryParsingException e) { throw e; } catch (Exception e) { - throw new QueryParsingException(index, "Failed to parse", e); + throw new QueryParsingException(getParseContext(), "Failed to parse", e); } finally { if (parser != null) { parser.close(); @@ -250,7 +250,7 @@ public class IndexQueryParserService extends AbstractIndexComponent { } catch (QueryParsingException e) { throw e; } catch (Exception e) { - throw new QueryParsingException(index, "Failed to parse", e); + throw new QueryParsingException(context, "Failed to parse", e); } finally { if (parser != null) { parser.close(); @@ -266,7 +266,7 @@ public class IndexQueryParserService extends AbstractIndexComponent { } catch (QueryParsingException e) { throw e; } catch (Exception e) { - throw new QueryParsingException(index, "Failed to parse [" + source + "]", e); + throw new QueryParsingException(getParseContext(), "Failed to parse [" + source + "]", e); } finally { if (parser != null) { parser.close(); @@ -282,7 +282,7 @@ public class IndexQueryParserService extends AbstractIndexComponent { try { return innerParse(context, parser); } catch (IOException e) { - throw new QueryParsingException(index, "Failed to parse", e); + throw new QueryParsingException(context, "Failed to parse", e); } } @@ -359,7 +359,7 @@ public class IndexQueryParserService extends AbstractIndexComponent { XContentParser qSourceParser = XContentFactory.xContent(querySource).createParser(querySource); parsedQuery = parse(qSourceParser); } else { - throw new QueryParsingException(index(), "request does not support [" + fieldName + "]"); + throw new QueryParsingException(getParseContext(), "request does not support [" + fieldName + "]"); } } } @@ -369,10 +369,10 @@ public class IndexQueryParserService extends AbstractIndexComponent { } catch (QueryParsingException e) { throw e; } catch (Throwable e) { - throw new QueryParsingException(index, "Failed to parse", e); + throw new QueryParsingException(getParseContext(), "Failed to parse", e); } - throw new QueryParsingException(index(), "Required query is missing"); + throw new QueryParsingException(getParseContext(), "Required query is missing"); } private ParsedQuery innerParse(QueryParseContext parseContext, XContentParser parser) throws IOException, QueryParsingException { diff --git a/src/main/java/org/elasticsearch/index/query/IndicesFilterParser.java b/src/main/java/org/elasticsearch/index/query/IndicesFilterParser.java index c1f5b804f94..7bd39dad947 100644 --- a/src/main/java/org/elasticsearch/index/query/IndicesFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/IndicesFilterParser.java @@ -83,30 +83,30 @@ public class IndicesFilterParser implements FilterParser { noMatchFilter = parseContext.parseInnerFilter(); } } else { - throw new QueryParsingException(parseContext.index(), "[indices] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[indices] filter does not support [" + currentFieldName + "]"); } } else if (token == XContentParser.Token.START_ARRAY) { if ("indices".equals(currentFieldName)) { if (indicesFound) { - throw new QueryParsingException(parseContext.index(), "[indices] indices or index already specified"); + throw new QueryParsingException(parseContext, "[indices] indices or index already specified"); } indicesFound = true; Collection indices = new ArrayList<>(); while (parser.nextToken() != XContentParser.Token.END_ARRAY) { String value = parser.textOrNull(); if (value == null) { - throw new QueryParsingException(parseContext.index(), "[indices] no value specified for 'indices' entry"); + throw new QueryParsingException(parseContext, "[indices] no value specified for 'indices' entry"); } indices.add(value); } currentIndexMatchesIndices = matchesIndices(parseContext.index().name(), indices.toArray(new String[indices.size()])); } else { - throw new QueryParsingException(parseContext.index(), "[indices] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[indices] filter does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("index".equals(currentFieldName)) { if (indicesFound) { - throw new QueryParsingException(parseContext.index(), "[indices] indices or index already specified"); + throw new QueryParsingException(parseContext, "[indices] indices or index already specified"); } indicesFound = true; currentIndexMatchesIndices = matchesIndices(parseContext.index().name(), parser.text()); @@ -120,15 +120,15 @@ public class IndicesFilterParser implements FilterParser { } else if ("_name".equals(currentFieldName)) { filterName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[indices] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[indices] filter does not support [" + currentFieldName + "]"); } } } if (!filterFound) { - throw new QueryParsingException(parseContext.index(), "[indices] requires 'filter' element"); + throw new QueryParsingException(parseContext, "[indices] requires 'filter' element"); } if (!indicesFound) { - throw new QueryParsingException(parseContext.index(), "[indices] requires 'indices' or 'index' element"); + throw new QueryParsingException(parseContext, "[indices] requires 'indices' or 'index' element"); } Filter chosenFilter; diff --git a/src/main/java/org/elasticsearch/index/query/IndicesQueryParser.java b/src/main/java/org/elasticsearch/index/query/IndicesQueryParser.java index d5b5cefa149..a45fe9f88f6 100644 --- a/src/main/java/org/elasticsearch/index/query/IndicesQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/IndicesQueryParser.java @@ -76,30 +76,30 @@ public class IndicesQueryParser implements QueryParser { } else if ("no_match_query".equals(currentFieldName)) { innerNoMatchQuery = new XContentStructure.InnerQuery(parseContext, null); } else { - throw new QueryParsingException(parseContext.index(), "[indices] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[indices] query does not support [" + currentFieldName + "]"); } } else if (token == XContentParser.Token.START_ARRAY) { if ("indices".equals(currentFieldName)) { if (indicesFound) { - throw new QueryParsingException(parseContext.index(), "[indices] indices or index already specified"); + throw new QueryParsingException(parseContext, "[indices] indices or index already specified"); } indicesFound = true; Collection indices = new ArrayList<>(); while (parser.nextToken() != XContentParser.Token.END_ARRAY) { String value = parser.textOrNull(); if (value == null) { - throw new QueryParsingException(parseContext.index(), "[indices] no value specified for 'indices' entry"); + throw new QueryParsingException(parseContext, "[indices] no value specified for 'indices' entry"); } indices.add(value); } currentIndexMatchesIndices = matchesIndices(parseContext.index().name(), indices.toArray(new String[indices.size()])); } else { - throw new QueryParsingException(parseContext.index(), "[indices] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[indices] query does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("index".equals(currentFieldName)) { if (indicesFound) { - throw new QueryParsingException(parseContext.index(), "[indices] indices or index already specified"); + throw new QueryParsingException(parseContext, "[indices] indices or index already specified"); } indicesFound = true; currentIndexMatchesIndices = matchesIndices(parseContext.index().name(), parser.text()); @@ -113,15 +113,15 @@ public class IndicesQueryParser implements QueryParser { } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[indices] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[indices] query does not support [" + currentFieldName + "]"); } } } if (!queryFound) { - throw new QueryParsingException(parseContext.index(), "[indices] requires 'query' element"); + throw new QueryParsingException(parseContext, "[indices] requires 'query' element"); } if (!indicesFound) { - throw new QueryParsingException(parseContext.index(), "[indices] requires 'indices' or 'index' element"); + throw new QueryParsingException(parseContext, "[indices] requires 'indices' or 'index' element"); } Query chosenQuery; diff --git a/src/main/java/org/elasticsearch/index/query/LimitFilterParser.java b/src/main/java/org/elasticsearch/index/query/LimitFilterParser.java index 858b23c6693..f4f8fde7427 100644 --- a/src/main/java/org/elasticsearch/index/query/LimitFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/LimitFilterParser.java @@ -53,13 +53,13 @@ public class LimitFilterParser implements FilterParser { if ("value".equals(currentFieldName)) { limit = parser.intValue(); } else { - throw new QueryParsingException(parseContext.index(), "[limit] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[limit] filter does not support [" + currentFieldName + "]"); } } } if (limit == -1) { - throw new QueryParsingException(parseContext.index(), "No value specified for limit filter"); + throw new QueryParsingException(parseContext, "No value specified for limit filter"); } // this filter is deprecated and parses to a filter that matches everything diff --git a/src/main/java/org/elasticsearch/index/query/MatchAllQueryParser.java b/src/main/java/org/elasticsearch/index/query/MatchAllQueryParser.java index 2017b940921..933d3d35631 100644 --- a/src/main/java/org/elasticsearch/index/query/MatchAllQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/MatchAllQueryParser.java @@ -59,7 +59,7 @@ public class MatchAllQueryParser implements QueryParser { if ("boost".equals(currentFieldName)) { boost = parser.floatValue(); } else { - throw new QueryParsingException(parseContext.index(), "[match_all] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[match_all] query does not support [" + currentFieldName + "]"); } } } diff --git a/src/main/java/org/elasticsearch/index/query/MatchQueryParser.java b/src/main/java/org/elasticsearch/index/query/MatchQueryParser.java index a0f595a6626..8dd35c84b4d 100644 --- a/src/main/java/org/elasticsearch/index/query/MatchQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/MatchQueryParser.java @@ -65,7 +65,7 @@ public class MatchQueryParser implements QueryParser { XContentParser.Token token = parser.nextToken(); if (token != XContentParser.Token.FIELD_NAME) { - throw new QueryParsingException(parseContext.index(), "[match] query malformed, no field"); + throw new QueryParsingException(parseContext, "[match] query malformed, no field"); } String fieldName = parser.currentName(); @@ -93,12 +93,12 @@ public class MatchQueryParser implements QueryParser { } else if ("phrase_prefix".equals(tStr) || "phrasePrefix".equals(currentFieldName)) { type = MatchQuery.Type.PHRASE_PREFIX; } else { - throw new QueryParsingException(parseContext.index(), "[match] query does not support type " + tStr); + throw new QueryParsingException(parseContext, "[match] query does not support type " + tStr); } } else if ("analyzer".equals(currentFieldName)) { String analyzer = parser.text(); if (parseContext.analysisService().analyzer(analyzer) == null) { - throw new QueryParsingException(parseContext.index(), "[match] analyzer [" + parser.text() + "] not found"); + throw new QueryParsingException(parseContext, "[match] analyzer [" + parser.text() + "] not found"); } matchQuery.setAnalyzer(analyzer); } else if ("boost".equals(currentFieldName)) { @@ -118,7 +118,8 @@ public class MatchQueryParser implements QueryParser { } else if ("and".equalsIgnoreCase(op)) { matchQuery.setOccur(BooleanClause.Occur.MUST); } else { - throw new QueryParsingException(parseContext.index(), "text query requires operator to be either 'and' or 'or', not [" + op + "]"); + throw new QueryParsingException(parseContext, "text query requires operator to be either 'and' or 'or', not [" + + op + "]"); } } else if ("minimum_should_match".equals(currentFieldName) || "minimumShouldMatch".equals(currentFieldName)) { minimumShouldMatch = parser.textOrNull(); @@ -139,12 +140,12 @@ public class MatchQueryParser implements QueryParser { } else if ("all".equalsIgnoreCase(zeroTermsDocs)) { matchQuery.setZeroTermsQuery(MatchQuery.ZeroTermsQuery.ALL); } else { - throw new QueryParsingException(parseContext.index(), "Unsupported zero_terms_docs value [" + zeroTermsDocs + "]"); + throw new QueryParsingException(parseContext, "Unsupported zero_terms_docs value [" + zeroTermsDocs + "]"); } } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[match] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[match] query does not support [" + currentFieldName + "]"); } } } @@ -154,12 +155,13 @@ public class MatchQueryParser implements QueryParser { // move to the next token token = parser.nextToken(); if (token != XContentParser.Token.END_OBJECT) { - throw new QueryParsingException(parseContext.index(), "[match] query parsed in simplified form, with direct field name, but included more options than just the field name, possibly use its 'options' form, with 'query' element?"); + throw new QueryParsingException(parseContext, + "[match] query parsed in simplified form, with direct field name, but included more options than just the field name, possibly use its 'options' form, with 'query' element?"); } } if (value == null) { - throw new QueryParsingException(parseContext.index(), "No text specified for text query"); + throw new QueryParsingException(parseContext, "No text specified for text query"); } Query query = matchQuery.parse(type, fieldName, value); diff --git a/src/main/java/org/elasticsearch/index/query/MissingFilterParser.java b/src/main/java/org/elasticsearch/index/query/MissingFilterParser.java index 10f0405b832..3f394ff735e 100644 --- a/src/main/java/org/elasticsearch/index/query/MissingFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/MissingFilterParser.java @@ -23,7 +23,6 @@ import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.Filter; import org.apache.lucene.search.Query; -import org.apache.lucene.search.QueryWrapperFilter; import org.apache.lucene.search.TermRangeQuery; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.lucene.HashedBytesRef; @@ -78,13 +77,13 @@ public class MissingFilterParser implements FilterParser { } else if ("_name".equals(currentFieldName)) { filterName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[missing] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[missing] filter does not support [" + currentFieldName + "]"); } } } if (fieldPattern == null) { - throw new QueryParsingException(parseContext.index(), "missing must be provided with a [field]"); + throw new QueryParsingException(parseContext, "missing must be provided with a [field]"); } return newFilter(parseContext, fieldPattern, existence, nullValue, filterName); @@ -92,7 +91,7 @@ public class MissingFilterParser implements FilterParser { public static Filter newFilter(QueryParseContext parseContext, String fieldPattern, boolean existence, boolean nullValue, String filterName) { if (!existence && !nullValue) { - throw new QueryParsingException(parseContext.index(), "missing must have either existence, or null_value, or both set to true"); + throw new QueryParsingException(parseContext, "missing must have either existence, or null_value, or both set to true"); } final FieldMappers fieldNamesMappers = parseContext.mapperService().fullName(FieldNamesFieldMapper.NAME); diff --git a/src/main/java/org/elasticsearch/index/query/MoreLikeThisQueryParser.java b/src/main/java/org/elasticsearch/index/query/MoreLikeThisQueryParser.java index 0050b7199a1..b726d4f0159 100644 --- a/src/main/java/org/elasticsearch/index/query/MoreLikeThisQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/MoreLikeThisQueryParser.java @@ -155,7 +155,7 @@ public class MoreLikeThisQueryParser implements QueryParser { } else if (Fields.INCLUDE.match(currentFieldName, parseContext.parseFlags())) { include = parser.booleanValue(); } else { - throw new QueryParsingException(parseContext.index(), "[mlt] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[mlt] query does not support [" + currentFieldName + "]"); } } else if (token == XContentParser.Token.START_ARRAY) { if (Fields.STOP_WORDS.match(currentFieldName, parseContext.parseFlags())) { @@ -192,7 +192,7 @@ public class MoreLikeThisQueryParser implements QueryParser { parseLikeField(parser, ignoreTexts, ignoreItems); } } else { - throw new QueryParsingException(parseContext.index(), "[mlt] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[mlt] query does not support [" + currentFieldName + "]"); } } else if (token == XContentParser.Token.START_OBJECT) { if (Fields.LIKE.match(currentFieldName, parseContext.parseFlags())) { @@ -201,16 +201,16 @@ public class MoreLikeThisQueryParser implements QueryParser { else if (Fields.IGNORE_LIKE.match(currentFieldName, parseContext.parseFlags())) { parseLikeField(parser, ignoreTexts, ignoreItems); } else { - throw new QueryParsingException(parseContext.index(), "[mlt] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[mlt] query does not support [" + currentFieldName + "]"); } } } if (likeTexts.isEmpty() && likeItems.isEmpty()) { - throw new QueryParsingException(parseContext.index(), "more_like_this requires 'like' to be specified"); + throw new QueryParsingException(parseContext, "more_like_this requires 'like' to be specified"); } if (moreLikeFields != null && moreLikeFields.isEmpty()) { - throw new QueryParsingException(parseContext.index(), "more_like_this requires 'fields' to be non-empty"); + throw new QueryParsingException(parseContext, "more_like_this requires 'fields' to be non-empty"); } // set analyzer @@ -258,8 +258,9 @@ public class MoreLikeThisQueryParser implements QueryParser { } if (item.type() == null) { if (parseContext.queryTypes().size() > 1) { - throw new QueryParsingException(parseContext.index(), - "ambiguous type for item with id: " + item.id() + " and index: " + item.index()); + throw new QueryParsingException(parseContext, + "ambiguous type for item with id: " + item.id() + + " and index: " + item.index()); } else { item.type(parseContext.queryTypes().iterator().next()); } diff --git a/src/main/java/org/elasticsearch/index/query/MultiMatchQueryParser.java b/src/main/java/org/elasticsearch/index/query/MultiMatchQueryParser.java index 3fbd43651de..976dd15dc7b 100644 --- a/src/main/java/org/elasticsearch/index/query/MultiMatchQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/MultiMatchQueryParser.java @@ -20,6 +20,7 @@ package org.elasticsearch.index.query; import com.google.common.collect.Maps; + import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.Query; import org.elasticsearch.common.inject.Inject; @@ -77,8 +78,7 @@ public class MultiMatchQueryParser implements QueryParser { } else if (token.isValue()) { extractFieldAndBoost(parseContext, parser, fieldNameWithBoosts); } else { - throw new QueryParsingException(parseContext.index(), "[" + NAME + "] query does not support [" + currentFieldName - + "]"); + throw new QueryParsingException(parseContext, "[" + NAME + "] query does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("query".equals(currentFieldName)) { @@ -88,7 +88,7 @@ public class MultiMatchQueryParser implements QueryParser { } else if ("analyzer".equals(currentFieldName)) { String analyzer = parser.text(); if (parseContext.analysisService().analyzer(analyzer) == null) { - throw new QueryParsingException(parseContext.index(), "["+ NAME +"] analyzer [" + parser.text() + "] not found"); + throw new QueryParsingException(parseContext, "[" + NAME + "] analyzer [" + parser.text() + "] not found"); } multiMatchQuery.setAnalyzer(analyzer); } else if ("boost".equals(currentFieldName)) { @@ -108,7 +108,8 @@ public class MultiMatchQueryParser implements QueryParser { } else if ("and".equalsIgnoreCase(op)) { multiMatchQuery.setOccur(BooleanClause.Occur.MUST); } else { - throw new QueryParsingException(parseContext.index(), "text query requires operator to be either 'and' or 'or', not [" + op + "]"); + throw new QueryParsingException(parseContext, "text query requires operator to be either 'and' or 'or', not [" + op + + "]"); } } else if ("minimum_should_match".equals(currentFieldName) || "minimumShouldMatch".equals(currentFieldName)) { minimumShouldMatch = parser.textOrNull(); @@ -131,22 +132,22 @@ public class MultiMatchQueryParser implements QueryParser { } else if ("all".equalsIgnoreCase(zeroTermsDocs)) { multiMatchQuery.setZeroTermsQuery(MatchQuery.ZeroTermsQuery.ALL); } else { - throw new QueryParsingException(parseContext.index(), "Unsupported zero_terms_docs value [" + zeroTermsDocs + "]"); + throw new QueryParsingException(parseContext, "Unsupported zero_terms_docs value [" + zeroTermsDocs + "]"); } } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[match] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[match] query does not support [" + currentFieldName + "]"); } } } if (value == null) { - throw new QueryParsingException(parseContext.index(), "No text specified for multi_match query"); + throw new QueryParsingException(parseContext, "No text specified for multi_match query"); } if (fieldNameWithBoosts.isEmpty()) { - throw new QueryParsingException(parseContext.index(), "No fields specified for multi_match query"); + throw new QueryParsingException(parseContext, "No fields specified for multi_match query"); } if (type == null) { type = MultiMatchQueryBuilder.Type.BEST_FIELDS; diff --git a/src/main/java/org/elasticsearch/index/query/NestedFilterParser.java b/src/main/java/org/elasticsearch/index/query/NestedFilterParser.java index f6cad0a57e0..fc2237d6630 100644 --- a/src/main/java/org/elasticsearch/index/query/NestedFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/NestedFilterParser.java @@ -70,7 +70,7 @@ public class NestedFilterParser implements FilterParser { } else if ("inner_hits".equals(currentFieldName)) { builder.setInnerHits(innerHitsQueryParserHelper.parse(parseContext)); } else { - throw new QueryParsingException(parseContext.index(), "[nested] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[nested] filter does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("path".equals(currentFieldName)) { @@ -84,7 +84,7 @@ public class NestedFilterParser implements FilterParser { } else if ("_cache_key".equals(currentFieldName) || "_cacheKey".equals(currentFieldName)) { cacheKey = new HashedBytesRef(parser.text()); } else { - throw new QueryParsingException(parseContext.index(), "[nested] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[nested] filter does not support [" + currentFieldName + "]"); } } } diff --git a/src/main/java/org/elasticsearch/index/query/NestedQueryParser.java b/src/main/java/org/elasticsearch/index/query/NestedQueryParser.java index 989388b79d4..ba9bcf07d46 100644 --- a/src/main/java/org/elasticsearch/index/query/NestedQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/NestedQueryParser.java @@ -75,7 +75,7 @@ public class NestedQueryParser implements QueryParser { } else if ("inner_hits".equals(currentFieldName)) { builder.setInnerHits(innerHitsQueryParserHelper.parse(parseContext)); } else { - throw new QueryParsingException(parseContext.index(), "[nested] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[nested] query does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("path".equals(currentFieldName)) { @@ -93,12 +93,12 @@ public class NestedQueryParser implements QueryParser { } else if ("none".equals(sScoreMode)) { scoreMode = ScoreMode.None; } else { - throw new QueryParsingException(parseContext.index(), "illegal score_mode for nested query [" + sScoreMode + "]"); + throw new QueryParsingException(parseContext, "illegal score_mode for nested query [" + sScoreMode + "]"); } } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[nested] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[nested] query does not support [" + currentFieldName + "]"); } } } @@ -144,7 +144,7 @@ public class NestedQueryParser implements QueryParser { innerQuery = null; } } else { - throw new QueryParsingException(parseContext.index(), "[nested] requires either 'query' or 'filter' field"); + throw new QueryParsingException(parseContext, "[nested] requires either 'query' or 'filter' field"); } if (innerHits != null) { diff --git a/src/main/java/org/elasticsearch/index/query/NotFilterParser.java b/src/main/java/org/elasticsearch/index/query/NotFilterParser.java index db8adccc5dd..38bff1997bb 100644 --- a/src/main/java/org/elasticsearch/index/query/NotFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/NotFilterParser.java @@ -20,7 +20,6 @@ package org.elasticsearch.index.query; import org.apache.lucene.search.Filter; -import org.apache.lucene.search.QueryWrapperFilter; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.lucene.HashedBytesRef; import org.elasticsearch.common.lucene.search.Queries; @@ -80,13 +79,13 @@ public class NotFilterParser implements FilterParser { } else if ("_cache_key".equals(currentFieldName) || "_cacheKey".equals(currentFieldName)) { cacheKey = new HashedBytesRef(parser.text()); } else { - throw new QueryParsingException(parseContext.index(), "[not] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[not] filter does not support [" + currentFieldName + "]"); } } } if (!filterFound) { - throw new QueryParsingException(parseContext.index(), "filter is required when using `not` filter"); + throw new QueryParsingException(parseContext, "filter is required when using `not` filter"); } if (filter == null) { diff --git a/src/main/java/org/elasticsearch/index/query/OrFilterParser.java b/src/main/java/org/elasticsearch/index/query/OrFilterParser.java index 9c3ad615105..22932ac8290 100644 --- a/src/main/java/org/elasticsearch/index/query/OrFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/OrFilterParser.java @@ -100,14 +100,14 @@ public class OrFilterParser implements FilterParser { } else if ("_cache_key".equals(currentFieldName) || "_cacheKey".equals(currentFieldName)) { cacheKey = new HashedBytesRef(parser.text()); } else { - throw new QueryParsingException(parseContext.index(), "[or] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[or] filter does not support [" + currentFieldName + "]"); } } } } if (!filtersFound) { - throw new QueryParsingException(parseContext.index(), "[or] filter requires 'filters' to be set on it'"); + throw new QueryParsingException(parseContext, "[or] filter requires 'filters' to be set on it'"); } if (filters.isEmpty()) { diff --git a/src/main/java/org/elasticsearch/index/query/PrefixFilterParser.java b/src/main/java/org/elasticsearch/index/query/PrefixFilterParser.java index e6bc4e3437f..c6bf3fe0a95 100644 --- a/src/main/java/org/elasticsearch/index/query/PrefixFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/PrefixFilterParser.java @@ -78,7 +78,7 @@ public class PrefixFilterParser implements FilterParser { } if (value == null) { - throw new QueryParsingException(parseContext.index(), "No value specified for prefix filter"); + throw new QueryParsingException(parseContext, "No value specified for prefix filter"); } Filter filter = null; diff --git a/src/main/java/org/elasticsearch/index/query/PrefixQueryParser.java b/src/main/java/org/elasticsearch/index/query/PrefixQueryParser.java index 0cecb0aa651..dc59007c461 100644 --- a/src/main/java/org/elasticsearch/index/query/PrefixQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/PrefixQueryParser.java @@ -53,7 +53,7 @@ public class PrefixQueryParser implements QueryParser { XContentParser.Token token = parser.nextToken(); if (token != XContentParser.Token.FIELD_NAME) { - throw new QueryParsingException(parseContext.index(), "[prefix] query malformed, no field"); + throw new QueryParsingException(parseContext, "[prefix] query malformed, no field"); } String fieldName = parser.currentName(); String rewriteMethod = null; @@ -80,7 +80,7 @@ public class PrefixQueryParser implements QueryParser { queryName = parser.text(); } } else { - throw new QueryParsingException(parseContext.index(), "[prefix] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[prefix] query does not support [" + currentFieldName + "]"); } } parser.nextToken(); @@ -90,7 +90,7 @@ public class PrefixQueryParser implements QueryParser { } if (value == null) { - throw new QueryParsingException(parseContext.index(), "No value specified for prefix query"); + throw new QueryParsingException(parseContext, "No value specified for prefix query"); } MultiTermQuery.RewriteMethod method = QueryParsers.parseRewriteMethod(rewriteMethod, null); diff --git a/src/main/java/org/elasticsearch/index/query/QueryParseContext.java b/src/main/java/org/elasticsearch/index/query/QueryParseContext.java index 2f43985444c..39c0543759b 100644 --- a/src/main/java/org/elasticsearch/index/query/QueryParseContext.java +++ b/src/main/java/org/elasticsearch/index/query/QueryParseContext.java @@ -292,23 +292,23 @@ public class QueryParseContext { if (parser.currentToken() != XContentParser.Token.START_OBJECT) { token = parser.nextToken(); if (token != XContentParser.Token.START_OBJECT) { - throw new QueryParsingException(index, "[_na] query malformed, must start with start_object"); + throw new QueryParsingException(this, "[_na] query malformed, must start with start_object"); } } token = parser.nextToken(); if (token != XContentParser.Token.FIELD_NAME) { - throw new QueryParsingException(index, "[_na] query malformed, no field after start_object"); + throw new QueryParsingException(this, "[_na] query malformed, no field after start_object"); } String queryName = parser.currentName(); // move to the next START_OBJECT token = parser.nextToken(); if (token != XContentParser.Token.START_OBJECT && token != XContentParser.Token.START_ARRAY) { - throw new QueryParsingException(index, "[_na] query malformed, no field after start_object"); + throw new QueryParsingException(this, "[_na] query malformed, no field after start_object"); } QueryParser queryParser = indexQueryParser.queryParser(queryName); if (queryParser == null) { - throw new QueryParsingException(index, "No query registered for [" + queryName + "]"); + throw new QueryParsingException(this, "No query registered for [" + queryName + "]"); } Query result = queryParser.parse(this); if (parser.currentToken() == XContentParser.Token.END_OBJECT || parser.currentToken() == XContentParser.Token.END_ARRAY) { @@ -335,7 +335,7 @@ public class QueryParseContext { if (parser.currentToken() != XContentParser.Token.START_OBJECT) { token = parser.nextToken(); if (token != XContentParser.Token.START_OBJECT) { - throw new QueryParsingException(index, "[_na] filter malformed, must start with start_object"); + throw new QueryParsingException(this, "[_na] filter malformed, must start with start_object"); } } token = parser.nextToken(); @@ -344,18 +344,18 @@ public class QueryParseContext { if (token == XContentParser.Token.END_OBJECT || token == XContentParser.Token.VALUE_NULL) { return null; } - throw new QueryParsingException(index, "[_na] filter malformed, no field after start_object"); + throw new QueryParsingException(this, "[_na] filter malformed, no field after start_object"); } String filterName = parser.currentName(); // move to the next START_OBJECT or START_ARRAY token = parser.nextToken(); if (token != XContentParser.Token.START_OBJECT && token != XContentParser.Token.START_ARRAY) { - throw new QueryParsingException(index, "[_na] filter malformed, no field after start_object"); + throw new QueryParsingException(this, "[_na] filter malformed, no field after start_object"); } FilterParser filterParser = indexQueryParser.filterParser(filterName); if (filterParser == null) { - throw new QueryParsingException(index, "No filter registered for [" + filterName + "]"); + throw new QueryParsingException(this, "No filter registered for [" + filterName + "]"); } Filter result = executeFilterParser(filterParser); if (parser.currentToken() == XContentParser.Token.END_OBJECT || parser.currentToken() == XContentParser.Token.END_ARRAY) { @@ -368,7 +368,7 @@ public class QueryParseContext { public Filter parseInnerFilter(String filterName) throws IOException, QueryParsingException { FilterParser filterParser = indexQueryParser.filterParser(filterName); if (filterParser == null) { - throw new QueryParsingException(index, "No filter registered for [" + filterName + "]"); + throw new QueryParsingException(this, "No filter registered for [" + filterName + "]"); } return executeFilterParser(filterParser); } @@ -432,7 +432,8 @@ public class QueryParseContext { } else { Version indexCreatedVersion = indexQueryParser.getIndexCreatedVersion(); if (fieldMapping == null && indexCreatedVersion.onOrAfter(Version.V_1_4_0_Beta1)) { - throw new QueryParsingException(index, "Strict field resolution and no field mapping can be found for the field with name [" + name + "]"); + throw new QueryParsingException(this, "Strict field resolution and no field mapping can be found for the field with name [" + + name + "]"); } else { return fieldMapping; } diff --git a/src/main/java/org/elasticsearch/index/query/QueryParsingException.java b/src/main/java/org/elasticsearch/index/query/QueryParsingException.java index 5bf1407a107..b9b0381e90e 100644 --- a/src/main/java/org/elasticsearch/index/query/QueryParsingException.java +++ b/src/main/java/org/elasticsearch/index/query/QueryParsingException.java @@ -19,21 +19,67 @@ package org.elasticsearch.index.query; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentLocation; +import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexException; import org.elasticsearch.rest.RestStatus; +import java.io.IOException; + /** * */ public class QueryParsingException extends IndexException { - public QueryParsingException(Index index, String msg) { - super(index, msg); + static final int UNKNOWN_POSITION = -1; + private int lineNumber = UNKNOWN_POSITION; + private int columnNumber = UNKNOWN_POSITION; + + public QueryParsingException(QueryParseContext parseContext, String msg) { + this(parseContext, msg, null); } - public QueryParsingException(Index index, String msg, Throwable cause) { + public QueryParsingException(QueryParseContext parseContext, String msg, Throwable cause) { + super(parseContext.index(), msg, cause); + + XContentParser parser = parseContext.parser(); + if (parser != null) { + XContentLocation location = parser.getTokenLocation(); + if (location != null) { + lineNumber = location.lineNumber; + columnNumber = location.columnNumber; + } + } + } + + /** + * This constructor is provided for use in unit tests where a + * {@link QueryParseContext} may not be available + */ + QueryParsingException(Index index, int line, int col, String msg, Throwable cause) { super(index, msg, cause); + this.lineNumber = line; + this.columnNumber = col; + } + + /** + * Line number of the location of the error + * + * @return the line number or -1 if unknown + */ + public int getLineNumber() { + return lineNumber; + } + + /** + * Column number of the location of the error + * + * @return the column number or -1 if unknown + */ + public int getColumnNumber() { + return columnNumber; } @Override @@ -41,4 +87,13 @@ public class QueryParsingException extends IndexException { return RestStatus.BAD_REQUEST; } + @Override + protected void innerToXContent(XContentBuilder builder, Params params) throws IOException { + if (lineNumber != UNKNOWN_POSITION) { + builder.field("line", lineNumber); + builder.field("col", columnNumber); + } + super.innerToXContent(builder, params); + } + } diff --git a/src/main/java/org/elasticsearch/index/query/QueryStringQueryParser.java b/src/main/java/org/elasticsearch/index/query/QueryStringQueryParser.java index d0b07941888..402080789f4 100644 --- a/src/main/java/org/elasticsearch/index/query/QueryStringQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/QueryStringQueryParser.java @@ -126,7 +126,8 @@ public class QueryStringQueryParser implements QueryParser { } } } else { - throw new QueryParsingException(parseContext.index(), "[query_string] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[query_string] query does not support [" + currentFieldName + + "]"); } } else if (token.isValue()) { if ("query".equals(currentFieldName)) { @@ -140,18 +141,19 @@ public class QueryStringQueryParser implements QueryParser { } else if ("and".equalsIgnoreCase(op)) { qpSettings.defaultOperator(org.apache.lucene.queryparser.classic.QueryParser.Operator.AND); } else { - throw new QueryParsingException(parseContext.index(), "Query default operator [" + op + "] is not allowed"); + throw new QueryParsingException(parseContext, "Query default operator [" + op + "] is not allowed"); } } else if ("analyzer".equals(currentFieldName)) { NamedAnalyzer analyzer = parseContext.analysisService().analyzer(parser.text()); if (analyzer == null) { - throw new QueryParsingException(parseContext.index(), "[query_string] analyzer [" + parser.text() + "] not found"); + throw new QueryParsingException(parseContext, "[query_string] analyzer [" + parser.text() + "] not found"); } qpSettings.forcedAnalyzer(analyzer); } else if ("quote_analyzer".equals(currentFieldName) || "quoteAnalyzer".equals(currentFieldName)) { NamedAnalyzer analyzer = parseContext.analysisService().analyzer(parser.text()); if (analyzer == null) { - throw new QueryParsingException(parseContext.index(), "[query_string] quote_analyzer [" + parser.text() + "] not found"); + throw new QueryParsingException(parseContext, "[query_string] quote_analyzer [" + parser.text() + + "] not found"); } qpSettings.forcedQuoteAnalyzer(analyzer); } else if ("allow_leading_wildcard".equals(currentFieldName) || "allowLeadingWildcard".equals(currentFieldName)) { @@ -199,17 +201,19 @@ public class QueryStringQueryParser implements QueryParser { try { qpSettings.timeZone(DateTimeZone.forID(parser.text())); } catch (IllegalArgumentException e) { - throw new QueryParsingException(parseContext.index(), "[query_string] time_zone [" + parser.text() + "] is unknown"); + throw new QueryParsingException(parseContext, + "[query_string] time_zone [" + parser.text() + "] is unknown"); } } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[query_string] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[query_string] query does not support [" + currentFieldName + + "]"); } } } if (qpSettings.queryString() == null) { - throw new QueryParsingException(parseContext.index(), "query_string must be provided with a [query]"); + throw new QueryParsingException(parseContext, "query_string must be provided with a [query]"); } qpSettings.defaultAnalyzer(parseContext.mapperService().searchAnalyzer()); qpSettings.defaultQuoteAnalyzer(parseContext.mapperService().searchQuoteAnalyzer()); @@ -239,7 +243,7 @@ public class QueryStringQueryParser implements QueryParser { } return query; } catch (org.apache.lucene.queryparser.classic.ParseException e) { - throw new QueryParsingException(parseContext.index(), "Failed to parse query [" + qpSettings.queryString() + "]", e); + throw new QueryParsingException(parseContext, "Failed to parse query [" + qpSettings.queryString() + "]", e); } } } diff --git a/src/main/java/org/elasticsearch/index/query/RangeFilterParser.java b/src/main/java/org/elasticsearch/index/query/RangeFilterParser.java index 300ed66e6d8..8b5f557d0ba 100644 --- a/src/main/java/org/elasticsearch/index/query/RangeFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/RangeFilterParser.java @@ -105,7 +105,7 @@ public class RangeFilterParser implements FilterParser { } else if ("format".equals(currentFieldName)) { forcedDateParser = new DateMathParser(Joda.forPattern(parser.text()), DateFieldMapper.Defaults.TIME_UNIT); } else { - throw new QueryParsingException(parseContext.index(), "[range] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[range] filter does not support [" + currentFieldName + "]"); } } } @@ -119,13 +119,13 @@ public class RangeFilterParser implements FilterParser { } else if ("execution".equals(currentFieldName)) { execution = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[range] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[range] filter does not support [" + currentFieldName + "]"); } } } if (fieldName == null) { - throw new QueryParsingException(parseContext.index(), "[range] filter no field specified for range filter"); + throw new QueryParsingException(parseContext, "[range] filter no field specified for range filter"); } Filter filter = null; @@ -136,33 +136,39 @@ public class RangeFilterParser implements FilterParser { FieldMapper mapper = smartNameFieldMappers.mapper(); if (mapper instanceof DateFieldMapper) { if ((from instanceof Number || to instanceof Number) && timeZone != null) { - throw new QueryParsingException(parseContext.index(), "[range] time_zone when using ms since epoch format as it's UTC based can not be applied to [" + fieldName + "]"); + throw new QueryParsingException(parseContext, + "[range] time_zone when using ms since epoch format as it's UTC based can not be applied to [" + + fieldName + "]"); } filter = ((DateFieldMapper) mapper).rangeFilter(from, to, includeLower, includeUpper, timeZone, forcedDateParser, parseContext); } else { if (timeZone != null) { - throw new QueryParsingException(parseContext.index(), "[range] time_zone can not be applied to non date field [" + fieldName + "]"); + throw new QueryParsingException(parseContext, "[range] time_zone can not be applied to non date field [" + + fieldName + "]"); } filter = mapper.rangeFilter(from, to, includeLower, includeUpper, parseContext); } } else if ("fielddata".equals(execution)) { FieldMapper mapper = smartNameFieldMappers.mapper(); if (!(mapper instanceof NumberFieldMapper)) { - throw new QueryParsingException(parseContext.index(), "[range] filter field [" + fieldName + "] is not a numeric type"); + throw new QueryParsingException(parseContext, "[range] filter field [" + fieldName + "] is not a numeric type"); } if (mapper instanceof DateFieldMapper) { if ((from instanceof Number || to instanceof Number) && timeZone != null) { - throw new QueryParsingException(parseContext.index(), "[range] time_zone when using ms since epoch format as it's UTC based can not be applied to [" + fieldName + "]"); + throw new QueryParsingException(parseContext, + "[range] time_zone when using ms since epoch format as it's UTC based can not be applied to [" + + fieldName + "]"); } filter = ((DateFieldMapper) mapper).rangeFilter(parseContext, from, to, includeLower, includeUpper, timeZone, forcedDateParser, parseContext); } else { if (timeZone != null) { - throw new QueryParsingException(parseContext.index(), "[range] time_zone can not be applied to non date field [" + fieldName + "]"); + throw new QueryParsingException(parseContext, "[range] time_zone can not be applied to non date field [" + + fieldName + "]"); } filter = ((NumberFieldMapper) mapper).rangeFilter(parseContext, from, to, includeLower, includeUpper, parseContext); } } else { - throw new QueryParsingException(parseContext.index(), "[range] filter doesn't support [" + execution + "] execution"); + throw new QueryParsingException(parseContext, "[range] filter doesn't support [" + execution + "] execution"); } } } diff --git a/src/main/java/org/elasticsearch/index/query/RangeQueryParser.java b/src/main/java/org/elasticsearch/index/query/RangeQueryParser.java index cfc600832ec..354da1df704 100644 --- a/src/main/java/org/elasticsearch/index/query/RangeQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/RangeQueryParser.java @@ -55,12 +55,12 @@ public class RangeQueryParser implements QueryParser { XContentParser.Token token = parser.nextToken(); if (token != XContentParser.Token.FIELD_NAME) { - throw new QueryParsingException(parseContext.index(), "[range] query malformed, no field to indicate field name"); + throw new QueryParsingException(parseContext, "[range] query malformed, no field to indicate field name"); } String fieldName = parser.currentName(); token = parser.nextToken(); if (token != XContentParser.Token.START_OBJECT) { - throw new QueryParsingException(parseContext.index(), "[range] query malformed, after field missing start object"); + throw new QueryParsingException(parseContext, "[range] query malformed, after field missing start object"); } Object from = null; @@ -106,7 +106,7 @@ public class RangeQueryParser implements QueryParser { } else if ("format".equals(currentFieldName)) { forcedDateParser = new DateMathParser(Joda.forPattern(parser.text()), DateFieldMapper.Defaults.TIME_UNIT); } else { - throw new QueryParsingException(parseContext.index(), "[range] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[range] query does not support [" + currentFieldName + "]"); } } } @@ -114,7 +114,7 @@ public class RangeQueryParser implements QueryParser { // move to the next end object, to close the field name token = parser.nextToken(); if (token != XContentParser.Token.END_OBJECT) { - throw new QueryParsingException(parseContext.index(), "[range] query malformed, does not end with an object"); + throw new QueryParsingException(parseContext, "[range] query malformed, does not end with an object"); } Query query = null; @@ -124,12 +124,15 @@ public class RangeQueryParser implements QueryParser { FieldMapper mapper = smartNameFieldMappers.mapper(); if (mapper instanceof DateFieldMapper) { if ((from instanceof Number || to instanceof Number) && timeZone != null) { - throw new QueryParsingException(parseContext.index(), "[range] time_zone when using ms since epoch format as it's UTC based can not be applied to [" + fieldName + "]"); + throw new QueryParsingException(parseContext, + "[range] time_zone when using ms since epoch format as it's UTC based can not be applied to [" + fieldName + + "]"); } query = ((DateFieldMapper) mapper).rangeQuery(from, to, includeLower, includeUpper, timeZone, forcedDateParser, parseContext); } else { if (timeZone != null) { - throw new QueryParsingException(parseContext.index(), "[range] time_zone can not be applied to non date field [" + fieldName + "]"); + throw new QueryParsingException(parseContext, "[range] time_zone can not be applied to non date field [" + + fieldName + "]"); } //LUCENE 4 UPGRADE Mapper#rangeQuery should use bytesref as well? query = mapper.rangeQuery(from, to, includeLower, includeUpper, parseContext); diff --git a/src/main/java/org/elasticsearch/index/query/RegexpFilterParser.java b/src/main/java/org/elasticsearch/index/query/RegexpFilterParser.java index 76db069af17..5f1d9174fc7 100644 --- a/src/main/java/org/elasticsearch/index/query/RegexpFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/RegexpFilterParser.java @@ -84,7 +84,7 @@ public class RegexpFilterParser implements FilterParser { } else if ("flags_value".equals(currentFieldName)) { flagsValue = parser.intValue(); } else { - throw new QueryParsingException(parseContext.index(), "[regexp] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[regexp] filter does not support [" + currentFieldName + "]"); } } } @@ -108,7 +108,7 @@ public class RegexpFilterParser implements FilterParser { } if (value == null) { - throw new QueryParsingException(parseContext.index(), "No value specified for regexp filter"); + throw new QueryParsingException(parseContext, "No value specified for regexp filter"); } Filter filter = null; diff --git a/src/main/java/org/elasticsearch/index/query/RegexpQueryParser.java b/src/main/java/org/elasticsearch/index/query/RegexpQueryParser.java index 41d53316a57..a1ec2996332 100644 --- a/src/main/java/org/elasticsearch/index/query/RegexpQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/RegexpQueryParser.java @@ -55,7 +55,7 @@ public class RegexpQueryParser implements QueryParser { XContentParser.Token token = parser.nextToken(); if (token != XContentParser.Token.FIELD_NAME) { - throw new QueryParsingException(parseContext.index(), "[regexp] query malformed, no field"); + throw new QueryParsingException(parseContext, "[regexp] query malformed, no field"); } String fieldName = parser.currentName(); String rewriteMethod = null; @@ -92,7 +92,7 @@ public class RegexpQueryParser implements QueryParser { queryName = parser.text(); } } else { - throw new QueryParsingException(parseContext.index(), "[regexp] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[regexp] query does not support [" + currentFieldName + "]"); } } parser.nextToken(); @@ -102,7 +102,7 @@ public class RegexpQueryParser implements QueryParser { } if (value == null) { - throw new QueryParsingException(parseContext.index(), "No value specified for regexp query"); + throw new QueryParsingException(parseContext, "No value specified for regexp query"); } MultiTermQuery.RewriteMethod method = QueryParsers.parseRewriteMethod(rewriteMethod, null); diff --git a/src/main/java/org/elasticsearch/index/query/ScriptFilterParser.java b/src/main/java/org/elasticsearch/index/query/ScriptFilterParser.java index a619b3d63ba..54dbe6cc1db 100644 --- a/src/main/java/org/elasticsearch/index/query/ScriptFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/ScriptFilterParser.java @@ -85,7 +85,7 @@ public class ScriptFilterParser implements FilterParser { if ("params".equals(currentFieldName)) { params = parser.map(); } else { - throw new QueryParsingException(parseContext.index(), "[script] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[script] filter does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("_name".equals(currentFieldName)) { @@ -95,7 +95,7 @@ public class ScriptFilterParser implements FilterParser { } else if ("_cache_key".equals(currentFieldName) || "_cacheKey".equals(currentFieldName)) { cacheKey = new HashedBytesRef(parser.text()); } else if (!scriptParameterParser.token(currentFieldName, token, parser)){ - throw new QueryParsingException(parseContext.index(), "[script] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[script] filter does not support [" + currentFieldName + "]"); } } } @@ -108,7 +108,7 @@ public class ScriptFilterParser implements FilterParser { scriptLang = scriptParameterParser.lang(); if (script == null) { - throw new QueryParsingException(parseContext.index(), "script must be provided with a [script] filter"); + throw new QueryParsingException(parseContext, "script must be provided with a [script] filter"); } if (params == null) { params = newHashMap(); diff --git a/src/main/java/org/elasticsearch/index/query/SimpleQueryStringParser.java b/src/main/java/org/elasticsearch/index/query/SimpleQueryStringParser.java index 43e64ce0280..446dbc95b57 100644 --- a/src/main/java/org/elasticsearch/index/query/SimpleQueryStringParser.java +++ b/src/main/java/org/elasticsearch/index/query/SimpleQueryStringParser.java @@ -139,8 +139,9 @@ public class SimpleQueryStringParser implements QueryParser { } } } else { - throw new QueryParsingException(parseContext.index(), - "[" + NAME + "] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, + "[" + NAME + "] query does not support [" + currentFieldName + + "]"); } } else if (token.isValue()) { if ("query".equals(currentFieldName)) { @@ -148,7 +149,7 @@ public class SimpleQueryStringParser implements QueryParser { } else if ("analyzer".equals(currentFieldName)) { analyzer = parseContext.analysisService().analyzer(parser.text()); if (analyzer == null) { - throw new QueryParsingException(parseContext.index(), "[" + NAME + "] analyzer [" + parser.text() + "] not found"); + throw new QueryParsingException(parseContext, "[" + NAME + "] analyzer [" + parser.text() + "] not found"); } } else if ("field".equals(currentFieldName)) { field = parser.text(); @@ -159,8 +160,7 @@ public class SimpleQueryStringParser implements QueryParser { } else if ("and".equalsIgnoreCase(op)) { defaultOperator = BooleanClause.Occur.MUST; } else { - throw new QueryParsingException(parseContext.index(), - "[" + NAME + "] default operator [" + op + "] is not allowed"); + throw new QueryParsingException(parseContext, "[" + NAME + "] default operator [" + op + "] is not allowed"); } } else if ("flags".equals(currentFieldName)) { if (parser.currentToken() != XContentParser.Token.VALUE_NUMBER) { @@ -188,14 +188,14 @@ public class SimpleQueryStringParser implements QueryParser { } else if ("minimum_should_match".equals(currentFieldName)) { minimumShouldMatch = parser.textOrNull(); } else { - throw new QueryParsingException(parseContext.index(), "[" + NAME + "] unsupported field [" + parser.currentName() + "]"); + throw new QueryParsingException(parseContext, "[" + NAME + "] unsupported field [" + parser.currentName() + "]"); } } } // Query text is required if (queryBody == null) { - throw new QueryParsingException(parseContext.index(), "[" + NAME + "] query text missing"); + throw new QueryParsingException(parseContext, "[" + NAME + "] query text missing"); } // Support specifying only a field instead of a map diff --git a/src/main/java/org/elasticsearch/index/query/SpanFirstQueryParser.java b/src/main/java/org/elasticsearch/index/query/SpanFirstQueryParser.java index ea8ff3d3923..5a302eb17d7 100644 --- a/src/main/java/org/elasticsearch/index/query/SpanFirstQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/SpanFirstQueryParser.java @@ -63,11 +63,11 @@ public class SpanFirstQueryParser implements QueryParser { if ("match".equals(currentFieldName)) { Query query = parseContext.parseInnerQuery(); if (!(query instanceof SpanQuery)) { - throw new QueryParsingException(parseContext.index(), "spanFirst [match] must be of type span query"); + throw new QueryParsingException(parseContext, "spanFirst [match] must be of type span query"); } match = (SpanQuery) query; } else { - throw new QueryParsingException(parseContext.index(), "[span_first] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[span_first] query does not support [" + currentFieldName + "]"); } } else { if ("boost".equals(currentFieldName)) { @@ -77,15 +77,15 @@ public class SpanFirstQueryParser implements QueryParser { } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[span_first] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[span_first] query does not support [" + currentFieldName + "]"); } } } if (match == null) { - throw new QueryParsingException(parseContext.index(), "spanFirst must have [match] span query clause"); + throw new QueryParsingException(parseContext, "spanFirst must have [match] span query clause"); } if (end == -1) { - throw new QueryParsingException(parseContext.index(), "spanFirst must have [end] set for it"); + throw new QueryParsingException(parseContext, "spanFirst must have [end] set for it"); } SpanFirstQuery query = new SpanFirstQuery(match, end); diff --git a/src/main/java/org/elasticsearch/index/query/SpanMultiTermQueryParser.java b/src/main/java/org/elasticsearch/index/query/SpanMultiTermQueryParser.java index 7c9b2a67277..a44580a5176 100644 --- a/src/main/java/org/elasticsearch/index/query/SpanMultiTermQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/SpanMultiTermQueryParser.java @@ -51,17 +51,17 @@ public class SpanMultiTermQueryParser implements QueryParser { Token token = parser.nextToken(); if (!MATCH_NAME.equals(parser.currentName()) || token != XContentParser.Token.FIELD_NAME) { - throw new QueryParsingException(parseContext.index(), "spanMultiTerm must have [" + MATCH_NAME + "] multi term query clause"); + throw new QueryParsingException(parseContext, "spanMultiTerm must have [" + MATCH_NAME + "] multi term query clause"); } token = parser.nextToken(); if (token != XContentParser.Token.START_OBJECT) { - throw new QueryParsingException(parseContext.index(), "spanMultiTerm must have [" + MATCH_NAME + "] multi term query clause"); + throw new QueryParsingException(parseContext, "spanMultiTerm must have [" + MATCH_NAME + "] multi term query clause"); } Query subQuery = parseContext.parseInnerQuery(); if (!(subQuery instanceof MultiTermQuery)) { - throw new QueryParsingException(parseContext.index(), "spanMultiTerm [" + MATCH_NAME + "] must be of type multi term query"); + throw new QueryParsingException(parseContext, "spanMultiTerm [" + MATCH_NAME + "] must be of type multi term query"); } parser.nextToken(); diff --git a/src/main/java/org/elasticsearch/index/query/SpanNearQueryParser.java b/src/main/java/org/elasticsearch/index/query/SpanNearQueryParser.java index 84283fce1a4..6ecf1b70bea 100644 --- a/src/main/java/org/elasticsearch/index/query/SpanNearQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/SpanNearQueryParser.java @@ -69,12 +69,12 @@ public class SpanNearQueryParser implements QueryParser { while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { Query query = parseContext.parseInnerQuery(); if (!(query instanceof SpanQuery)) { - throw new QueryParsingException(parseContext.index(), "spanNear [clauses] must be of type span query"); + throw new QueryParsingException(parseContext, "spanNear [clauses] must be of type span query"); } clauses.add((SpanQuery) query); } } else { - throw new QueryParsingException(parseContext.index(), "[span_near] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[span_near] query does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("in_order".equals(currentFieldName) || "inOrder".equals(currentFieldName)) { @@ -88,17 +88,17 @@ public class SpanNearQueryParser implements QueryParser { } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[span_near] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[span_near] query does not support [" + currentFieldName + "]"); } } else { - throw new QueryParsingException(parseContext.index(), "[span_near] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[span_near] query does not support [" + currentFieldName + "]"); } } if (clauses.isEmpty()) { - throw new QueryParsingException(parseContext.index(), "span_near must include [clauses]"); + throw new QueryParsingException(parseContext, "span_near must include [clauses]"); } if (slop == null) { - throw new QueryParsingException(parseContext.index(), "span_near must include [slop]"); + throw new QueryParsingException(parseContext, "span_near must include [slop]"); } SpanNearQuery query = new SpanNearQuery(clauses.toArray(new SpanQuery[clauses.size()]), slop.intValue(), inOrder, collectPayloads); diff --git a/src/main/java/org/elasticsearch/index/query/SpanNotQueryParser.java b/src/main/java/org/elasticsearch/index/query/SpanNotQueryParser.java index afadf4c68ef..bcb62e7a224 100644 --- a/src/main/java/org/elasticsearch/index/query/SpanNotQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/SpanNotQueryParser.java @@ -68,17 +68,17 @@ public class SpanNotQueryParser implements QueryParser { if ("include".equals(currentFieldName)) { Query query = parseContext.parseInnerQuery(); if (!(query instanceof SpanQuery)) { - throw new QueryParsingException(parseContext.index(), "spanNot [include] must be of type span query"); + throw new QueryParsingException(parseContext, "spanNot [include] must be of type span query"); } include = (SpanQuery) query; } else if ("exclude".equals(currentFieldName)) { Query query = parseContext.parseInnerQuery(); if (!(query instanceof SpanQuery)) { - throw new QueryParsingException(parseContext.index(), "spanNot [exclude] must be of type span query"); + throw new QueryParsingException(parseContext, "spanNot [exclude] must be of type span query"); } exclude = (SpanQuery) query; } else { - throw new QueryParsingException(parseContext.index(), "[span_not] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[span_not] query does not support [" + currentFieldName + "]"); } } else { if ("dist".equals(currentFieldName)) { @@ -92,18 +92,18 @@ public class SpanNotQueryParser implements QueryParser { } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[span_not] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[span_not] query does not support [" + currentFieldName + "]"); } } } if (include == null) { - throw new QueryParsingException(parseContext.index(), "spanNot must have [include] span query clause"); + throw new QueryParsingException(parseContext, "spanNot must have [include] span query clause"); } if (exclude == null) { - throw new QueryParsingException(parseContext.index(), "spanNot must have [exclude] span query clause"); + throw new QueryParsingException(parseContext, "spanNot must have [exclude] span query clause"); } if (dist != null && (pre != null || post != null)) { - throw new QueryParsingException(parseContext.index(), "spanNot can either use [dist] or [pre] & [post] (or none)"); + throw new QueryParsingException(parseContext, "spanNot can either use [dist] or [pre] & [post] (or none)"); } // set appropriate defaults diff --git a/src/main/java/org/elasticsearch/index/query/SpanOrQueryParser.java b/src/main/java/org/elasticsearch/index/query/SpanOrQueryParser.java index a9d12f6d941..db58d4cca82 100644 --- a/src/main/java/org/elasticsearch/index/query/SpanOrQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/SpanOrQueryParser.java @@ -66,12 +66,12 @@ public class SpanOrQueryParser implements QueryParser { while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { Query query = parseContext.parseInnerQuery(); if (!(query instanceof SpanQuery)) { - throw new QueryParsingException(parseContext.index(), "spanOr [clauses] must be of type span query"); + throw new QueryParsingException(parseContext, "spanOr [clauses] must be of type span query"); } clauses.add((SpanQuery) query); } } else { - throw new QueryParsingException(parseContext.index(), "[span_or] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[span_or] query does not support [" + currentFieldName + "]"); } } else { if ("boost".equals(currentFieldName)) { @@ -79,12 +79,12 @@ public class SpanOrQueryParser implements QueryParser { } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[span_or] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[span_or] query does not support [" + currentFieldName + "]"); } } } if (clauses.isEmpty()) { - throw new QueryParsingException(parseContext.index(), "spanOr must include [clauses]"); + throw new QueryParsingException(parseContext, "spanOr must include [clauses]"); } SpanOrQuery query = new SpanOrQuery(clauses.toArray(new SpanQuery[clauses.size()])); diff --git a/src/main/java/org/elasticsearch/index/query/SpanTermQueryParser.java b/src/main/java/org/elasticsearch/index/query/SpanTermQueryParser.java index 0203bb26051..535b626306a 100644 --- a/src/main/java/org/elasticsearch/index/query/SpanTermQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/SpanTermQueryParser.java @@ -77,7 +77,7 @@ public class SpanTermQueryParser implements QueryParser { } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[span_term] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[span_term] query does not support [" + currentFieldName + "]"); } } } @@ -89,7 +89,7 @@ public class SpanTermQueryParser implements QueryParser { } if (value == null) { - throw new QueryParsingException(parseContext.index(), "No value specified for term query"); + throw new QueryParsingException(parseContext, "No value specified for term query"); } BytesRef valueBytes = null; diff --git a/src/main/java/org/elasticsearch/index/query/TermFilterParser.java b/src/main/java/org/elasticsearch/index/query/TermFilterParser.java index f03a8a43cae..ca077b91ee3 100644 --- a/src/main/java/org/elasticsearch/index/query/TermFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/TermFilterParser.java @@ -81,7 +81,7 @@ public class TermFilterParser implements FilterParser { } else if ("_cache_key".equals(currentFieldName) || "_cacheKey".equals(currentFieldName)) { cacheKey = new HashedBytesRef(parser.text()); } else { - throw new QueryParsingException(parseContext.index(), "[term] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[term] filter does not support [" + currentFieldName + "]"); } } } @@ -100,11 +100,11 @@ public class TermFilterParser implements FilterParser { } if (fieldName == null) { - throw new QueryParsingException(parseContext.index(), "No field specified for term filter"); + throw new QueryParsingException(parseContext, "No field specified for term filter"); } if (value == null) { - throw new QueryParsingException(parseContext.index(), "No value specified for term filter"); + throw new QueryParsingException(parseContext, "No value specified for term filter"); } Filter filter = null; diff --git a/src/main/java/org/elasticsearch/index/query/TermQueryParser.java b/src/main/java/org/elasticsearch/index/query/TermQueryParser.java index 2c016973b6e..3a913fc21ad 100644 --- a/src/main/java/org/elasticsearch/index/query/TermQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/TermQueryParser.java @@ -51,7 +51,7 @@ public class TermQueryParser implements QueryParser { XContentParser.Token token = parser.nextToken(); if (token != XContentParser.Token.FIELD_NAME) { - throw new QueryParsingException(parseContext.index(), "[term] query malformed, no field"); + throw new QueryParsingException(parseContext, "[term] query malformed, no field"); } String fieldName = parser.currentName(); @@ -74,7 +74,7 @@ public class TermQueryParser implements QueryParser { } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[term] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[term] query does not support [" + currentFieldName + "]"); } } } @@ -86,7 +86,7 @@ public class TermQueryParser implements QueryParser { } if (value == null) { - throw new QueryParsingException(parseContext.index(), "No value specified for term query"); + throw new QueryParsingException(parseContext, "No value specified for term query"); } Query query = null; diff --git a/src/main/java/org/elasticsearch/index/query/TermsFilterParser.java b/src/main/java/org/elasticsearch/index/query/TermsFilterParser.java index 3c5ecd15106..46c52b80f64 100644 --- a/src/main/java/org/elasticsearch/index/query/TermsFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/TermsFilterParser.java @@ -90,14 +90,14 @@ public class TermsFilterParser implements FilterParser { currentFieldName = parser.currentName(); } else if (token == XContentParser.Token.START_ARRAY) { if (fieldName != null) { - throw new QueryParsingException(parseContext.index(), "[terms] filter does not support multiple fields"); + throw new QueryParsingException(parseContext, "[terms] filter does not support multiple fields"); } fieldName = currentFieldName; while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { Object value = parser.objectBytes(); if (value == null) { - throw new QueryParsingException(parseContext.index(), "No value specified for terms filter"); + throw new QueryParsingException(parseContext, "No value specified for terms filter"); } terms.add(value); } @@ -118,18 +118,19 @@ public class TermsFilterParser implements FilterParser { } else if ("routing".equals(currentFieldName)) { lookupRouting = parser.textOrNull(); } else { - throw new QueryParsingException(parseContext.index(), "[terms] filter does not support [" + currentFieldName + "] within lookup element"); + throw new QueryParsingException(parseContext, "[terms] filter does not support [" + currentFieldName + + "] within lookup element"); } } } if (lookupType == null) { - throw new QueryParsingException(parseContext.index(), "[terms] filter lookup element requires specifying the type"); + throw new QueryParsingException(parseContext, "[terms] filter lookup element requires specifying the type"); } if (lookupId == null) { - throw new QueryParsingException(parseContext.index(), "[terms] filter lookup element requires specifying the id"); + throw new QueryParsingException(parseContext, "[terms] filter lookup element requires specifying the id"); } if (lookupPath == null) { - throw new QueryParsingException(parseContext.index(), "[terms] filter lookup element requires specifying the path"); + throw new QueryParsingException(parseContext, "[terms] filter lookup element requires specifying the path"); } } else if (token.isValue()) { if (EXECUTION_KEY.equals(currentFieldName)) { @@ -141,13 +142,13 @@ public class TermsFilterParser implements FilterParser { } else if ("_cache_key".equals(currentFieldName) || "_cacheKey".equals(currentFieldName)) { cacheKey = new HashedBytesRef(parser.text()); } else { - throw new QueryParsingException(parseContext.index(), "[terms] filter does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[terms] filter does not support [" + currentFieldName + "]"); } } } if (fieldName == null) { - throw new QueryParsingException(parseContext.index(), "terms filter requires a field name, followed by array of terms"); + throw new QueryParsingException(parseContext, "terms filter requires a field name, followed by array of terms"); } FieldMapper fieldMapper = null; diff --git a/src/main/java/org/elasticsearch/index/query/TermsQueryParser.java b/src/main/java/org/elasticsearch/index/query/TermsQueryParser.java index 15c9f18388e..dcf078d19b1 100644 --- a/src/main/java/org/elasticsearch/index/query/TermsQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/TermsQueryParser.java @@ -75,13 +75,13 @@ public class TermsQueryParser implements QueryParser { currentFieldName = parser.currentName(); } else if (token == XContentParser.Token.START_ARRAY) { if (fieldName != null) { - throw new QueryParsingException(parseContext.index(), "[terms] query does not support multiple fields"); + throw new QueryParsingException(parseContext, "[terms] query does not support multiple fields"); } fieldName = currentFieldName; while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { Object value = parser.objectBytes(); if (value == null) { - throw new QueryParsingException(parseContext.index(), "No value specified for terms query"); + throw new QueryParsingException(parseContext, "No value specified for terms query"); } values.add(value); } @@ -97,15 +97,15 @@ public class TermsQueryParser implements QueryParser { } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[terms] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[terms] query does not support [" + currentFieldName + "]"); } } else { - throw new QueryParsingException(parseContext.index(), "[terms] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[terms] query does not support [" + currentFieldName + "]"); } } if (fieldName == null) { - throw new QueryParsingException(parseContext.index(), "No field specified for terms query"); + throw new QueryParsingException(parseContext, "No field specified for terms query"); } FieldMapper mapper = null; diff --git a/src/main/java/org/elasticsearch/index/query/TopChildrenQueryParser.java b/src/main/java/org/elasticsearch/index/query/TopChildrenQueryParser.java index a44239e863e..095a849b792 100644 --- a/src/main/java/org/elasticsearch/index/query/TopChildrenQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/TopChildrenQueryParser.java @@ -78,7 +78,7 @@ public class TopChildrenQueryParser implements QueryParser { iq = new XContentStructure.InnerQuery(parseContext, childType == null ? null : new String[] {childType}); queryFound = true; } else { - throw new QueryParsingException(parseContext.index(), "[top_children] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[top_children] query does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("type".equals(currentFieldName)) { @@ -96,15 +96,15 @@ public class TopChildrenQueryParser implements QueryParser { } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[top_children] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[top_children] query does not support [" + currentFieldName + "]"); } } } if (!queryFound) { - throw new QueryParsingException(parseContext.index(), "[top_children] requires 'query' field"); + throw new QueryParsingException(parseContext, "[top_children] requires 'query' field"); } if (childType == null) { - throw new QueryParsingException(parseContext.index(), "[top_children] requires 'type' field"); + throw new QueryParsingException(parseContext, "[top_children] requires 'type' field"); } Query innerQuery = iq.asQuery(childType); @@ -115,11 +115,11 @@ public class TopChildrenQueryParser implements QueryParser { DocumentMapper childDocMapper = parseContext.mapperService().documentMapper(childType); if (childDocMapper == null) { - throw new QueryParsingException(parseContext.index(), "No mapping for for type [" + childType + "]"); + throw new QueryParsingException(parseContext, "No mapping for for type [" + childType + "]"); } ParentFieldMapper parentFieldMapper = childDocMapper.parentFieldMapper(); if (!parentFieldMapper.active()) { - throw new QueryParsingException(parseContext.index(), "Type [" + childType + "] does not have parent mapping"); + throw new QueryParsingException(parseContext, "Type [" + childType + "] does not have parent mapping"); } String parentType = childDocMapper.parentFieldMapper().type(); diff --git a/src/main/java/org/elasticsearch/index/query/TypeFilterParser.java b/src/main/java/org/elasticsearch/index/query/TypeFilterParser.java index e4ae0b957e0..a6248a4e228 100644 --- a/src/main/java/org/elasticsearch/index/query/TypeFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/TypeFilterParser.java @@ -50,15 +50,15 @@ public class TypeFilterParser implements FilterParser { XContentParser.Token token = parser.nextToken(); if (token != XContentParser.Token.FIELD_NAME) { - throw new QueryParsingException(parseContext.index(), "[type] filter should have a value field, and the type name"); + throw new QueryParsingException(parseContext, "[type] filter should have a value field, and the type name"); } String fieldName = parser.currentName(); if (!fieldName.equals("value")) { - throw new QueryParsingException(parseContext.index(), "[type] filter should have a value field, and the type name"); + throw new QueryParsingException(parseContext, "[type] filter should have a value field, and the type name"); } token = parser.nextToken(); if (token != XContentParser.Token.VALUE_STRING) { - throw new QueryParsingException(parseContext.index(), "[type] filter should have a value field, and the type name"); + throw new QueryParsingException(parseContext, "[type] filter should have a value field, and the type name"); } BytesRef type = parser.utf8Bytes(); // move to the next token diff --git a/src/main/java/org/elasticsearch/index/query/WildcardQueryParser.java b/src/main/java/org/elasticsearch/index/query/WildcardQueryParser.java index 6a641e96219..a661c185878 100644 --- a/src/main/java/org/elasticsearch/index/query/WildcardQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/WildcardQueryParser.java @@ -52,7 +52,7 @@ public class WildcardQueryParser implements QueryParser { XContentParser.Token token = parser.nextToken(); if (token != XContentParser.Token.FIELD_NAME) { - throw new QueryParsingException(parseContext.index(), "[wildcard] query malformed, no field"); + throw new QueryParsingException(parseContext, "[wildcard] query malformed, no field"); } String fieldName = parser.currentName(); String rewriteMethod = null; @@ -78,7 +78,7 @@ public class WildcardQueryParser implements QueryParser { } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { - throw new QueryParsingException(parseContext.index(), "[wildcard] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, "[wildcard] query does not support [" + currentFieldName + "]"); } } } @@ -89,7 +89,7 @@ public class WildcardQueryParser implements QueryParser { } if (value == null) { - throw new QueryParsingException(parseContext.index(), "No value specified for prefix query"); + throw new QueryParsingException(parseContext, "No value specified for prefix query"); } BytesRef valueBytes; diff --git a/src/main/java/org/elasticsearch/index/query/WrapperFilterParser.java b/src/main/java/org/elasticsearch/index/query/WrapperFilterParser.java index 2346d65943d..35ca8724453 100644 --- a/src/main/java/org/elasticsearch/index/query/WrapperFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/WrapperFilterParser.java @@ -48,11 +48,11 @@ public class WrapperFilterParser implements FilterParser { XContentParser.Token token = parser.nextToken(); if (token != XContentParser.Token.FIELD_NAME) { - throw new QueryParsingException(parseContext.index(), "[wrapper] filter malformed"); + throw new QueryParsingException(parseContext, "[wrapper] filter malformed"); } String fieldName = parser.currentName(); if (!fieldName.equals("filter")) { - throw new QueryParsingException(parseContext.index(), "[wrapper] filter malformed"); + throw new QueryParsingException(parseContext, "[wrapper] filter malformed"); } parser.nextToken(); diff --git a/src/main/java/org/elasticsearch/index/query/WrapperQueryParser.java b/src/main/java/org/elasticsearch/index/query/WrapperQueryParser.java index 3fc16d7af74..f7b98ad3dd5 100644 --- a/src/main/java/org/elasticsearch/index/query/WrapperQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/WrapperQueryParser.java @@ -48,11 +48,11 @@ public class WrapperQueryParser implements QueryParser { XContentParser.Token token = parser.nextToken(); if (token != XContentParser.Token.FIELD_NAME) { - throw new QueryParsingException(parseContext.index(), "[wrapper] query malformed"); + throw new QueryParsingException(parseContext, "[wrapper] query malformed"); } String fieldName = parser.currentName(); if (!fieldName.equals("query")) { - throw new QueryParsingException(parseContext.index(), "[wrapper] query malformed"); + throw new QueryParsingException(parseContext, "[wrapper] query malformed"); } parser.nextToken(); diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/DecayFunctionParser.java b/src/main/java/org/elasticsearch/index/query/functionscore/DecayFunctionParser.java index 6268f4a5c74..001bdf05854 100644 --- a/src/main/java/org/elasticsearch/index/query/functionscore/DecayFunctionParser.java +++ b/src/main/java/org/elasticsearch/index/query/functionscore/DecayFunctionParser.java @@ -154,7 +154,7 @@ public abstract class DecayFunctionParser implements ScoreFunctionParser { // the doc later MapperService.SmartNameFieldMappers smartMappers = parseContext.smartFieldMappers(fieldName); if (smartMappers == null || !smartMappers.hasMapper()) { - throw new QueryParsingException(parseContext.index(), "Unknown field [" + fieldName + "]"); + throw new QueryParsingException(parseContext, "Unknown field [" + fieldName + "]"); } FieldMapper mapper = smartMappers.fieldMappers().mapper(); @@ -167,7 +167,7 @@ public abstract class DecayFunctionParser implements ScoreFunctionParser { } else if (mapper instanceof NumberFieldMapper) { return parseNumberVariable(fieldName, parser, parseContext, (NumberFieldMapper) mapper, mode); } else { - throw new QueryParsingException(parseContext.index(), "Field " + fieldName + " is of type " + mapper.fieldType() + throw new QueryParsingException(parseContext, "Field " + fieldName + " is of type " + mapper.fieldType() + ", but only numeric types are supported."); } } diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryParser.java b/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryParser.java index 10d4c7f3d55..734ab2f7759 100644 --- a/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryParser.java @@ -134,7 +134,7 @@ public class FunctionScoreQueryParser implements QueryParser { // we try to parse a score function. If there is no score // function for the current field name, // functionParserMapper.get() will throw an Exception. - scoreFunction = functionParserMapper.get(parseContext.index(), currentFieldName).parse(parseContext, parser); + scoreFunction = functionParserMapper.get(parseContext, currentFieldName).parse(parseContext, parser); } if (functionArrayFound) { String errorString = "Found \"functions\": [...] already, now encountering \"" + currentFieldName + "\"."; @@ -202,8 +202,8 @@ public class FunctionScoreQueryParser implements QueryParser { ScoreFunction scoreFunction = null; Float functionWeight = null; if (token != XContentParser.Token.START_OBJECT) { - throw new QueryParsingException(parseContext.index(), NAME + ": malformed query, expected a " - + XContentParser.Token.START_OBJECT + " while parsing functions but got a " + token); + throw new QueryParsingException(parseContext, NAME + ": malformed query, expected a " + XContentParser.Token.START_OBJECT + + " while parsing functions but got a " + token); } else { while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { @@ -217,7 +217,7 @@ public class FunctionScoreQueryParser implements QueryParser { // do not need to check null here, // functionParserMapper throws exception if parser // non-existent - ScoreFunctionParser functionParser = functionParserMapper.get(parseContext.index(), currentFieldName); + ScoreFunctionParser functionParser = functionParserMapper.get(parseContext, currentFieldName); scoreFunction = functionParser.parse(parseContext, parser); } } @@ -253,7 +253,7 @@ public class FunctionScoreQueryParser implements QueryParser { } else if ("first".equals(scoreMode)) { return FiltersFunctionScoreQuery.ScoreMode.First; } else { - throw new QueryParsingException(parseContext.index(), NAME + " illegal score_mode [" + scoreMode + "]"); + throw new QueryParsingException(parseContext, NAME + " illegal score_mode [" + scoreMode + "]"); } } @@ -261,7 +261,7 @@ public class FunctionScoreQueryParser implements QueryParser { String boostMode = parser.text(); CombineFunction cf = combineFunctionsMap.get(boostMode); if (cf == null) { - throw new QueryParsingException(parseContext.index(), NAME + " illegal boost_mode [" + boostMode + "]"); + throw new QueryParsingException(parseContext, NAME + " illegal boost_mode [" + boostMode + "]"); } return cf; } diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionParserMapper.java b/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionParserMapper.java index 4f7d9de390b..abe8b5c4e35 100644 --- a/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionParserMapper.java +++ b/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionParserMapper.java @@ -20,9 +20,10 @@ package org.elasticsearch.index.query.functionscore; import com.google.common.collect.ImmutableMap; + import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.index.Index; +import org.elasticsearch.index.query.QueryParseContext; import org.elasticsearch.index.query.QueryParsingException; import java.util.Set; @@ -42,10 +43,10 @@ public class ScoreFunctionParserMapper { this.functionParsers = builder.immutableMap(); } - public ScoreFunctionParser get(Index index, String parserName) { + public ScoreFunctionParser get(QueryParseContext parseContext, String parserName) { ScoreFunctionParser functionParser = get(parserName); if (functionParser == null) { - throw new QueryParsingException(index, "No function with the name [" + parserName + "] is registered."); + throw new QueryParsingException(parseContext, "No function with the name [" + parserName + "] is registered.", null); } return functionParser; } diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/fieldvaluefactor/FieldValueFactorFunctionParser.java b/src/main/java/org/elasticsearch/index/query/functionscore/fieldvaluefactor/FieldValueFactorFunctionParser.java index c5f454ef40a..90c4b953bed 100644 --- a/src/main/java/org/elasticsearch/index/query/functionscore/fieldvaluefactor/FieldValueFactorFunctionParser.java +++ b/src/main/java/org/elasticsearch/index/query/functionscore/fieldvaluefactor/FieldValueFactorFunctionParser.java @@ -72,15 +72,15 @@ public class FieldValueFactorFunctionParser implements ScoreFunctionParser { } else if ("missing".equals(currentFieldName)) { missing = parser.doubleValue(); } else { - throw new QueryParsingException(parseContext.index(), NAMES[0] + " query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, NAMES[0] + " query does not support [" + currentFieldName + "]"); } } else if("factor".equals(currentFieldName) && (token == XContentParser.Token.START_ARRAY || token == XContentParser.Token.START_OBJECT)) { - throw new QueryParsingException(parseContext.index(), "[" + NAMES[0] + "] field 'factor' does not support lists or objects"); + throw new QueryParsingException(parseContext, "[" + NAMES[0] + "] field 'factor' does not support lists or objects"); } } if (field == null) { - throw new QueryParsingException(parseContext.index(), "[" + NAMES[0] + "] required field 'field' missing"); + throw new QueryParsingException(parseContext, "[" + NAMES[0] + "] required field 'field' missing"); } SearchContext searchContext = SearchContext.current(); diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/random/RandomScoreFunctionParser.java b/src/main/java/org/elasticsearch/index/query/functionscore/random/RandomScoreFunctionParser.java index 8bdc3074f3f..e4b26822d66 100644 --- a/src/main/java/org/elasticsearch/index/query/functionscore/random/RandomScoreFunctionParser.java +++ b/src/main/java/org/elasticsearch/index/query/functionscore/random/RandomScoreFunctionParser.java @@ -21,6 +21,7 @@ package org.elasticsearch.index.query.functionscore.random; import com.google.common.primitives.Longs; + import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.lucene.search.function.RandomScoreFunction; import org.elasticsearch.common.lucene.search.function.ScoreFunction; @@ -66,15 +67,17 @@ public class RandomScoreFunctionParser implements ScoreFunctionParser { } else if (parser.numberType() == XContentParser.NumberType.LONG) { seed = Longs.hashCode(parser.longValue()); } else { - throw new QueryParsingException(parseContext.index(), "random_score seed must be an int, long or string, not '" + token.toString() + "'"); + throw new QueryParsingException(parseContext, "random_score seed must be an int, long or string, not '" + + token.toString() + "'"); } } else if (token == XContentParser.Token.VALUE_STRING) { seed = parser.text().hashCode(); } else { - throw new QueryParsingException(parseContext.index(), "random_score seed must be an int/long or string, not '" + token.toString() + "'"); + throw new QueryParsingException(parseContext, "random_score seed must be an int/long or string, not '" + + token.toString() + "'"); } } else { - throw new QueryParsingException(parseContext.index(), NAMES[0] + " query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, NAMES[0] + " query does not support [" + currentFieldName + "]"); } } } diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/script/ScriptScoreFunctionParser.java b/src/main/java/org/elasticsearch/index/query/functionscore/script/ScriptScoreFunctionParser.java index aaa9bec3fac..b01eaee3615 100644 --- a/src/main/java/org/elasticsearch/index/query/functionscore/script/ScriptScoreFunctionParser.java +++ b/src/main/java/org/elasticsearch/index/query/functionscore/script/ScriptScoreFunctionParser.java @@ -28,7 +28,9 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.query.QueryParseContext; import org.elasticsearch.index.query.QueryParsingException; import org.elasticsearch.index.query.functionscore.ScoreFunctionParser; -import org.elasticsearch.script.*; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptParameterParser; import org.elasticsearch.script.ScriptParameterParser.ScriptParameterValue; import org.elasticsearch.script.ScriptService; import org.elasticsearch.script.SearchScript; @@ -67,11 +69,11 @@ public class ScriptScoreFunctionParser implements ScoreFunctionParser { if ("params".equals(currentFieldName)) { vars = parser.map(); } else { - throw new QueryParsingException(parseContext.index(), NAMES[0] + " query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, NAMES[0] + " query does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if (!scriptParameterParser.token(currentFieldName, token, parser)) { - throw new QueryParsingException(parseContext.index(), NAMES[0] + " query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext, NAMES[0] + " query does not support [" + currentFieldName + "]"); } } } @@ -82,7 +84,7 @@ public class ScriptScoreFunctionParser implements ScoreFunctionParser { scriptType = scriptValue.scriptType(); } if (script == null) { - throw new QueryParsingException(parseContext.index(), NAMES[0] + " requires 'script' field"); + throw new QueryParsingException(parseContext, NAMES[0] + " requires 'script' field"); } SearchScript searchScript; @@ -90,7 +92,7 @@ public class ScriptScoreFunctionParser implements ScoreFunctionParser { searchScript = parseContext.scriptService().search(parseContext.lookup(), new Script(scriptParameterParser.lang(), script, scriptType, vars), ScriptContext.Standard.SEARCH); return new ScriptScoreFunction(script, vars, searchScript); } catch (Exception e) { - throw new QueryParsingException(parseContext.index(), NAMES[0] + " the script could not be loaded", e); + throw new QueryParsingException(parseContext, NAMES[0] + " the script could not be loaded", e); } } } \ No newline at end of file diff --git a/src/main/java/org/elasticsearch/index/query/support/InnerHitsQueryParserHelper.java b/src/main/java/org/elasticsearch/index/query/support/InnerHitsQueryParserHelper.java index 149b47eadf9..ae839c41d1c 100644 --- a/src/main/java/org/elasticsearch/index/query/support/InnerHitsQueryParserHelper.java +++ b/src/main/java/org/elasticsearch/index/query/support/InnerHitsQueryParserHelper.java @@ -72,7 +72,7 @@ public class InnerHitsQueryParserHelper { } } } catch (Exception e) { - throw new QueryParsingException(parserContext.index(), "Failed to parse [_inner_hits]", e); + throw new QueryParsingException(parserContext, "Failed to parse [_inner_hits]", e); } return new Tuple<>(innerHitName, subSearchContext); } diff --git a/src/main/java/org/elasticsearch/index/query/support/NestedInnerQueryParseSupport.java b/src/main/java/org/elasticsearch/index/query/support/NestedInnerQueryParseSupport.java index 17eb059e1d0..c96fdb7e103 100644 --- a/src/main/java/org/elasticsearch/index/query/support/NestedInnerQueryParseSupport.java +++ b/src/main/java/org/elasticsearch/index/query/support/NestedInnerQueryParseSupport.java @@ -106,10 +106,10 @@ public class NestedInnerQueryParseSupport { return innerQuery; } else { if (path == null) { - throw new QueryParsingException(parseContext.index(), "[nested] requires 'path' field"); + throw new QueryParsingException(parseContext, "[nested] requires 'path' field"); } if (!queryFound) { - throw new QueryParsingException(parseContext.index(), "[nested] requires either 'query' or 'filter' field"); + throw new QueryParsingException(parseContext, "[nested] requires either 'query' or 'filter' field"); } XContentParser old = parseContext.parser(); @@ -135,10 +135,10 @@ public class NestedInnerQueryParseSupport { return innerFilter; } else { if (path == null) { - throw new QueryParsingException(parseContext.index(), "[nested] requires 'path' field"); + throw new QueryParsingException(parseContext, "[nested] requires 'path' field"); } if (!filterFound) { - throw new QueryParsingException(parseContext.index(), "[nested] requires either 'query' or 'filter' field"); + throw new QueryParsingException(parseContext, "[nested] requires either 'query' or 'filter' field"); } setPathLevel(); @@ -160,15 +160,15 @@ public class NestedInnerQueryParseSupport { this.path = path; MapperService.SmartNameObjectMapper smart = parseContext.smartObjectMapper(path); if (smart == null) { - throw new QueryParsingException(parseContext.index(), "[nested] failed to find nested object under path [" + path + "]"); + throw new QueryParsingException(parseContext, "[nested] failed to find nested object under path [" + path + "]"); } childDocumentMapper = smart.docMapper(); nestedObjectMapper = smart.mapper(); if (nestedObjectMapper == null) { - throw new QueryParsingException(parseContext.index(), "[nested] failed to find nested object under path [" + path + "]"); + throw new QueryParsingException(parseContext, "[nested] failed to find nested object under path [" + path + "]"); } if (!nestedObjectMapper.nested().isNested()) { - throw new QueryParsingException(parseContext.index(), "[nested] nested object under path [" + path + "] is not of nested type"); + throw new QueryParsingException(parseContext, "[nested] nested object under path [" + path + "] is not of nested type"); } } diff --git a/src/main/java/org/elasticsearch/search/SearchParseException.java b/src/main/java/org/elasticsearch/search/SearchParseException.java index 923532373a5..15c6bfd6f90 100644 --- a/src/main/java/org/elasticsearch/search/SearchParseException.java +++ b/src/main/java/org/elasticsearch/search/SearchParseException.java @@ -19,24 +19,64 @@ package org.elasticsearch.search; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentLocation; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.search.internal.SearchContext; +import java.io.IOException; + /** * */ public class SearchParseException extends SearchContextException { - public SearchParseException(SearchContext context, String msg) { - super(context, msg); + public static final int UNKNOWN_POSITION = -1; + private int lineNumber = UNKNOWN_POSITION; + private int columnNumber = UNKNOWN_POSITION; + + public SearchParseException(SearchContext context, String msg, @Nullable XContentLocation location) { + this(context, msg, location, null); } - public SearchParseException(SearchContext context, String msg, Throwable cause) { + public SearchParseException(SearchContext context, String msg, @Nullable XContentLocation location, Throwable cause) { super(context, msg, cause); + if (location != null) { + lineNumber = location.lineNumber; + columnNumber = location.columnNumber; + } } @Override public RestStatus status() { return RestStatus.BAD_REQUEST; } + + @Override + protected void innerToXContent(XContentBuilder builder, Params params) throws IOException { + if (lineNumber != UNKNOWN_POSITION) { + builder.field("line", lineNumber); + builder.field("col", columnNumber); + } + super.innerToXContent(builder, params); + } + + /** + * Line number of the location of the error + * + * @return the line number or -1 if unknown + */ + public int getLineNumber() { + return lineNumber; + } + + /** + * Column number of the location of the error + * + * @return the column number or -1 if unknown + */ + public int getColumnNumber() { + return columnNumber; + } } diff --git a/src/main/java/org/elasticsearch/search/SearchService.java b/src/main/java/org/elasticsearch/search/SearchService.java index 369c2cb499b..38f4e03a0f1 100644 --- a/src/main/java/org/elasticsearch/search/SearchService.java +++ b/src/main/java/org/elasticsearch/search/SearchService.java @@ -24,6 +24,7 @@ import com.carrotsearch.hppc.ObjectSet; import com.carrotsearch.hppc.cursors.ObjectCursor; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableMap; + import org.apache.lucene.index.IndexOptions; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.NumericDocValues; @@ -77,10 +78,23 @@ import org.elasticsearch.script.mustache.MustacheScriptEngineService; import org.elasticsearch.search.dfs.CachedDfSource; import org.elasticsearch.search.dfs.DfsPhase; import org.elasticsearch.search.dfs.DfsSearchResult; -import org.elasticsearch.search.fetch.*; -import org.elasticsearch.search.internal.*; +import org.elasticsearch.search.fetch.FetchPhase; +import org.elasticsearch.search.fetch.FetchSearchResult; +import org.elasticsearch.search.fetch.QueryFetchSearchResult; +import org.elasticsearch.search.fetch.ScrollQueryFetchSearchResult; +import org.elasticsearch.search.fetch.ShardFetchRequest; +import org.elasticsearch.search.internal.DefaultSearchContext; +import org.elasticsearch.search.internal.InternalScrollSearchRequest; +import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.search.internal.SearchContext.Lifetime; -import org.elasticsearch.search.query.*; +import org.elasticsearch.search.internal.ShardSearchLocalRequest; +import org.elasticsearch.search.internal.ShardSearchRequest; +import org.elasticsearch.search.query.QueryPhase; +import org.elasticsearch.search.query.QueryPhaseExecutionException; +import org.elasticsearch.search.query.QuerySearchRequest; +import org.elasticsearch.search.query.QuerySearchResult; +import org.elasticsearch.search.query.QuerySearchResultProvider; +import org.elasticsearch.search.query.ScrollQuerySearchResult; import org.elasticsearch.search.warmer.IndexWarmersMetaData; import org.elasticsearch.threadpool.ThreadPool; @@ -718,7 +732,7 @@ public class SearchService extends AbstractLifecycleComponent { parser.nextToken(); SearchParseElement element = elementParsers.get(fieldName); if (element == null) { - throw new SearchParseException(context, "No parser for element [" + fieldName + "]"); + throw new SearchParseException(context, "No parser for element [" + fieldName + "]", parser.getTokenLocation()); } element.parse(parser, context); } else { @@ -736,7 +750,7 @@ public class SearchService extends AbstractLifecycleComponent { } catch (Throwable e1) { // ignore } - throw new SearchParseException(context, "Failed to parse source [" + sSource + "]", e); + throw new SearchParseException(context, "Failed to parse source [" + sSource + "]", parser.getTokenLocation(), e); } finally { if (parser != null) { parser.close(); diff --git a/src/main/java/org/elasticsearch/search/aggregations/AggregatorParsers.java b/src/main/java/org/elasticsearch/search/aggregations/AggregatorParsers.java index b55f6a4f022..dbf2d948a36 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/AggregatorParsers.java +++ b/src/main/java/org/elasticsearch/search/aggregations/AggregatorParsers.java @@ -19,6 +19,7 @@ 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; @@ -86,16 +87,19 @@ public class AggregatorParsers { XContentParser.Token token = null; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token != XContentParser.Token.FIELD_NAME) { - throw new SearchParseException(context, "Unexpected token " + token + " in [aggs]: aggregations definitions must start with the name of the aggregation."); + throw new SearchParseException(context, "Unexpected token " + token + + " in [aggs]: aggregations definitions must start with the name of the aggregation.", parser.getTokenLocation()); } final String aggregationName = parser.currentName(); if (!validAggMatcher.reset(aggregationName).matches()) { - throw new SearchParseException(context, "Invalid aggregation name [" + aggregationName + "]. Aggregation names must be alpha-numeric and can only contain '_' and '-'"); + throw new SearchParseException(context, "Invalid aggregation name [" + aggregationName + + "]. Aggregation names must be alpha-numeric and can only contain '_' and '-'", parser.getTokenLocation()); } token = parser.nextToken(); if (token != XContentParser.Token.START_OBJECT) { - throw new SearchParseException(context, "Aggregation definition for [" + aggregationName + " starts with a [" + token + "], expected a [" + XContentParser.Token.START_OBJECT + "]."); + throw new SearchParseException(context, "Aggregation definition for [" + aggregationName + " starts with a [" + token + + "], expected a [" + XContentParser.Token.START_OBJECT + "].", parser.getTokenLocation()); } AggregatorFactory factory = null; @@ -105,13 +109,16 @@ public class AggregatorParsers { while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token != XContentParser.Token.FIELD_NAME) { - throw new SearchParseException(context, "Expected [" + XContentParser.Token.FIELD_NAME + "] under a [" + XContentParser.Token.START_OBJECT + "], but got a [" + token + "] in [" + aggregationName + "]"); + throw new SearchParseException(context, "Expected [" + XContentParser.Token.FIELD_NAME + "] under a [" + + XContentParser.Token.START_OBJECT + "], but got a [" + token + "] in [" + aggregationName + "]", + parser.getTokenLocation()); } final String fieldName = parser.currentName(); token = parser.nextToken(); if (token != XContentParser.Token.START_OBJECT) { - throw new SearchParseException(context, "Expected [" + XContentParser.Token.START_OBJECT + "] under [" + fieldName + "], but got a [" + token + "] in [" + aggregationName + "]"); + throw new SearchParseException(context, "Expected [" + XContentParser.Token.START_OBJECT + "] under [" + fieldName + + "], but got a [" + token + "] in [" + aggregationName + "]", parser.getTokenLocation()); } switch (fieldName) { @@ -121,24 +128,28 @@ public class AggregatorParsers { case "aggregations": case "aggs": if (subFactories != null) { - throw new SearchParseException(context, "Found two sub aggregation definitions under [" + aggregationName + "]"); + throw new SearchParseException(context, "Found two sub aggregation definitions under [" + aggregationName + "]", + parser.getTokenLocation()); } 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 + "]"); + throw new SearchParseException(context, "Found two aggregation type definitions in [" + aggregationName + "]: [" + + factory.type + "] and [" + fieldName + "]", parser.getTokenLocation()); } Aggregator.Parser aggregatorParser = parser(fieldName); if (aggregatorParser == null) { - throw new SearchParseException(context, "Could not find aggregator type [" + fieldName + "] in [" + aggregationName + "]"); + throw new SearchParseException(context, "Could not find aggregator type [" + fieldName + "] in [" + aggregationName + + "]", parser.getTokenLocation()); } factory = aggregatorParser.parse(aggregationName, parser, context); } } if (factory == null) { - throw new SearchParseException(context, "Missing definition for aggregation [" + aggregationName + "]"); + throw new SearchParseException(context, "Missing definition for aggregation [" + aggregationName + "]", + parser.getTokenLocation()); } if (metaData != null) { diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/children/ChildrenParser.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/children/ChildrenParser.java index 4834774053b..aacd76b0b5b 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/children/ChildrenParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/children/ChildrenParser.java @@ -56,15 +56,18 @@ public class ChildrenParser implements Aggregator.Parser { if ("type".equals(currentFieldName)) { childType = parser.text(); } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else { - throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "]."); + throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "].", + parser.getTokenLocation()); } } if (childType == null) { - throw new SearchParseException(context, "Missing [child_type] field for children aggregation [" + aggregationName + "]"); + throw new SearchParseException(context, "Missing [child_type] field for children aggregation [" + aggregationName + "]", + parser.getTokenLocation()); } ValuesSourceConfig config = new ValuesSourceConfig<>(ValuesSource.Bytes.WithOrdinals.ParentChild.class); @@ -76,7 +79,7 @@ public class ChildrenParser implements Aggregator.Parser { if (childDocMapper != null) { ParentFieldMapper parentFieldMapper = childDocMapper.parentFieldMapper(); if (!parentFieldMapper.active()) { - throw new SearchParseException(context, "[children] _parent field not configured"); + throw new SearchParseException(context, "[children] _parent field not configured", parser.getTokenLocation()); } parentType = parentFieldMapper.type(); DocumentMapper parentDocMapper = context.mapperService().documentMapper(parentType); diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/FiltersParser.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/FiltersParser.java index 49f43eafc36..e30fcc8a3a4 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/FiltersParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/filters/FiltersParser.java @@ -65,7 +65,8 @@ public class FiltersParser implements Aggregator.Parser { } } } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else if (token == XContentParser.Token.START_ARRAY) { if ("filters".equals(currentFieldName)) { @@ -78,10 +79,12 @@ public class FiltersParser implements Aggregator.Parser { idx++; } } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramParser.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramParser.java index 9d08d2ce81a..6f316d901db 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramParser.java @@ -108,13 +108,15 @@ public class DateHistogramParser implements Aggregator.Parser { } else if (INTERVAL.match(currentFieldName)) { interval = parser.text(); } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else if (token == XContentParser.Token.VALUE_BOOLEAN) { if ("keyed".equals(currentFieldName)) { keyed = parser.booleanValue(); } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else if (token == XContentParser.Token.VALUE_NUMBER) { if ("min_doc_count".equals(currentFieldName) || "minDocCount".equals(currentFieldName)) { @@ -122,7 +124,8 @@ public class DateHistogramParser implements Aggregator.Parser { } else if ("time_zone".equals(currentFieldName) || "timeZone".equals(currentFieldName)) { timeZone = DateTimeZone.forOffsetHours(parser.intValue()); } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else if (token == XContentParser.Token.START_OBJECT) { if ("order".equals(currentFieldName)) { @@ -147,7 +150,8 @@ public class DateHistogramParser implements Aggregator.Parser { } else if ("max".equals(currentFieldName)) { extendedBounds.maxAsStr = parser.text(); } else { - throw new SearchParseException(context, "Unknown extended_bounds key for a " + token + " in aggregation [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown extended_bounds key for a " + token + " in aggregation [" + + aggregationName + "]: [" + currentFieldName + "].", parser.getTokenLocation()); } } else if (token == XContentParser.Token.VALUE_NUMBER) { if ("min".equals(currentFieldName)) { @@ -155,23 +159,28 @@ public class DateHistogramParser implements Aggregator.Parser { } else if ("max".equals(currentFieldName)) { extendedBounds.max = parser.longValue(); } else { - throw new SearchParseException(context, "Unknown extended_bounds key for a " + token + " in aggregation [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown extended_bounds key for a " + token + " in aggregation [" + + aggregationName + "]: [" + currentFieldName + "].", parser.getTokenLocation()); } } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else { - throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "]."); + throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "].", + parser.getTokenLocation()); } } if (interval == null) { - throw new SearchParseException(context, "Missing required field [interval] for histogram aggregation [" + aggregationName + "]"); + throw new SearchParseException(context, + "Missing required field [interval] for histogram aggregation [" + aggregationName + "]", parser.getTokenLocation()); } TimeZoneRounding.Builder tzRoundingBuilder; diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/ExtendedBounds.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/ExtendedBounds.java index b041ef34fdb..c703058b699 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/ExtendedBounds.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/ExtendedBounds.java @@ -56,7 +56,7 @@ public class ExtendedBounds { } if (min != null && max != null && min.compareTo(max) > 0) { throw new SearchParseException(context, "[extended_bounds.min][" + min + "] cannot be greater than " + - "[extended_bounds.max][" + max + "] for histogram aggregation [" + aggName + "]"); + "[extended_bounds.max][" + max + "] for histogram aggregation [" + aggName + "]", null); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/HistogramParser.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/HistogramParser.java index f316237d734..c9c885be3f5 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/HistogramParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/HistogramParser.java @@ -75,7 +75,8 @@ public class HistogramParser implements Aggregator.Parser { } else if ("offset".equals(currentFieldName)) { offset = parser.longValue(); } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in aggregation [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in aggregation [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else if (token == XContentParser.Token.START_OBJECT) { if ("order".equals(currentFieldName)) { @@ -86,7 +87,8 @@ public class HistogramParser implements Aggregator.Parser { String dir = parser.text(); boolean asc = "asc".equals(dir); if (!asc && !"desc".equals(dir)) { - throw new SearchParseException(context, "Unknown order direction [" + dir + "] in aggregation [" + aggregationName + "]. Should be either [asc] or [desc]"); + throw new SearchParseException(context, "Unknown order direction [" + dir + "] in aggregation [" + + aggregationName + "]. Should be either [asc] or [desc]", parser.getTokenLocation()); } order = resolveOrder(currentFieldName, asc); } @@ -102,21 +104,25 @@ public class HistogramParser implements Aggregator.Parser { } else if ("max".equals(currentFieldName)) { extendedBounds.max = parser.longValue(true); } else { - throw new SearchParseException(context, "Unknown extended_bounds key for a " + token + " in aggregation [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown extended_bounds key for a " + token + " in aggregation [" + + aggregationName + "]: [" + currentFieldName + "].", parser.getTokenLocation()); } } } } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in aggregation [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in aggregation [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else { - throw new SearchParseException(context, "Unexpected token " + token + " in aggregation [" + aggregationName + "]."); + throw new SearchParseException(context, "Unexpected token " + token + " in aggregation [" + aggregationName + "].", + parser.getTokenLocation()); } } if (interval < 1) { - throw new SearchParseException(context, "Missing required field [interval] for histogram aggregation [" + aggregationName + "]"); + throw new SearchParseException(context, + "Missing required field [interval] for histogram aggregation [" + aggregationName + "]", parser.getTokenLocation()); } Rounding rounding = new Rounding.Interval(interval); diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/missing/MissingParser.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/missing/MissingParser.java index b37de4c743c..6ecdc129dd0 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/missing/MissingParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/missing/MissingParser.java @@ -52,7 +52,8 @@ public class MissingParser implements Aggregator.Parser { } else if (vsParser.token(currentFieldName, token, parser)) { continue; } else { - throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "]."); + throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "].", + parser.getTokenLocation()); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedParser.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedParser.java index 61044fb4a28..56da7f51b17 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedParser.java @@ -49,16 +49,19 @@ public class NestedParser implements Aggregator.Parser { if ("path".equals(currentFieldName)) { path = parser.text(); } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else { - throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "]."); + throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "].", + parser.getTokenLocation()); } } if (path == null) { // "field" doesn't exist, so we fall back to the context of the ancestors - throw new SearchParseException(context, "Missing [path] field for nested aggregation [" + aggregationName + "]"); + throw new SearchParseException(context, "Missing [path] field for nested aggregation [" + aggregationName + "]", + parser.getTokenLocation()); } return new NestedAggregator.Factory(aggregationName, path, context.queryParserService().autoFilterCachePolicy()); 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 7466bec3b5b..78f997a752b 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 @@ -131,7 +131,8 @@ public class ReverseNestedAggregator extends SingleBucketAggregator { // Early validation NestedAggregator closestNestedAggregator = findClosestNestedAggregator(parent); if (closestNestedAggregator == null) { - throw new SearchParseException(context.searchContext(), "Reverse nested aggregation [" + name + "] can only be used inside a [nested] aggregation"); + throw new SearchParseException(context.searchContext(), "Reverse nested aggregation [" + name + + "] can only be used inside a [nested] aggregation", null); } final ObjectMapper objectMapper; diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/ReverseNestedParser.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/ReverseNestedParser.java index 0ab7cefc9e3..80ab9f5eebd 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/ReverseNestedParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/ReverseNestedParser.java @@ -49,10 +49,12 @@ public class ReverseNestedParser implements Aggregator.Parser { if ("path".equals(currentFieldName)) { path = parser.text(); } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else { - throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "]."); + throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "].", + parser.getTokenLocation()); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/RangeParser.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/RangeParser.java index dbe05df0998..e30b84bf1de 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/RangeParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/RangeParser.java @@ -89,21 +89,25 @@ public class RangeParser implements Aggregator.Parser { ranges.add(new RangeAggregator.Range(key, from, fromAsStr, to, toAsStr)); } } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else if (token == XContentParser.Token.VALUE_BOOLEAN) { if ("keyed".equals(currentFieldName)) { keyed = parser.booleanValue(); } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else { - throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "]."); + throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "].", + parser.getTokenLocation()); } } if (ranges == null) { - throw new SearchParseException(context, "Missing [ranges] in ranges aggregator [" + aggregationName + "]"); + throw new SearchParseException(context, "Missing [ranges] in ranges aggregator [" + aggregationName + "]", + parser.getTokenLocation()); } return new RangeAggregator.Factory(aggregationName, vsParser.config(), InternalRange.FACTORY, ranges, keyed); diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/date/DateRangeParser.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/date/DateRangeParser.java index 06dcba53b95..940e20a79a8 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/date/DateRangeParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/date/DateRangeParser.java @@ -79,7 +79,8 @@ public class DateRangeParser implements Aggregator.Parser { } else if ("to".equals(toOrFromOrKey)) { to = parser.doubleValue(); } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + + "]: [" + currentFieldName + "].", parser.getTokenLocation()); } } else if (token == XContentParser.Token.VALUE_STRING) { if ("from".equals(toOrFromOrKey)) { @@ -89,7 +90,7 @@ public class DateRangeParser implements Aggregator.Parser { } else if ("key".equals(toOrFromOrKey)) { key = parser.text(); } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "].", parser.getTokenLocation()); } } } @@ -100,15 +101,18 @@ public class DateRangeParser implements Aggregator.Parser { if ("keyed".equals(currentFieldName)) { keyed = parser.booleanValue(); } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else { - throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "]."); + throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "].", + parser.getTokenLocation()); } } if (ranges == null) { - throw new SearchParseException(context, "Missing [ranges] in ranges aggregator [" + aggregationName + "]"); + throw new SearchParseException(context, "Missing [ranges] in ranges aggregator [" + aggregationName + "]", + parser.getTokenLocation()); } return new RangeAggregator.Factory(aggregationName, vsParser.config(), InternalDateRange.FACTORY, ranges, keyed); 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 713b94595f5..77d19e3f5ac 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 @@ -98,13 +98,15 @@ public class GeoDistanceParser implements Aggregator.Parser { } else if ("distance_type".equals(currentFieldName) || "distanceType".equals(currentFieldName)) { distanceType = GeoDistance.fromString(parser.text()); } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else if (token == XContentParser.Token.VALUE_BOOLEAN) { if ("keyed".equals(currentFieldName)) { keyed = parser.booleanValue(); } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else if (token == XContentParser.Token.START_ARRAY) { if ("ranges".equals(currentFieldName)) { @@ -138,20 +140,24 @@ public class GeoDistanceParser implements Aggregator.Parser { ranges.add(new RangeAggregator.Range(key(key, from, to), from, fromAsStr, to, toAsStr)); } } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else { - throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "]."); + throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "].", + parser.getTokenLocation()); } } if (ranges == null) { - throw new SearchParseException(context, "Missing [ranges] in geo_distance aggregator [" + aggregationName + "]"); + throw new SearchParseException(context, "Missing [ranges] in geo_distance aggregator [" + aggregationName + "]", + parser.getTokenLocation()); } GeoPoint origin = geoPointParser.geoPoint(); if (origin == null) { - throw new SearchParseException(context, "Missing [origin] in geo_distance aggregator [" + aggregationName + "]"); + throw new SearchParseException(context, "Missing [origin] in geo_distance aggregator [" + aggregationName + "]", + parser.getTokenLocation()); } return new GeoDistanceFactory(aggregationName, vsParser.config(), InternalGeoDistance.FACTORY, origin, unit, distanceType, ranges, keyed); diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ipv4/IpRangeParser.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ipv4/IpRangeParser.java index 49c9c90b16e..37891f6f239 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ipv4/IpRangeParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ipv4/IpRangeParser.java @@ -99,21 +99,25 @@ public class IpRangeParser implements Aggregator.Parser { ranges.add(range); } } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else if (token == XContentParser.Token.VALUE_BOOLEAN) { if ("keyed".equals(currentFieldName)) { keyed = parser.booleanValue(); } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else { - throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "]."); + throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "].", + parser.getTokenLocation()); } } if (ranges == null) { - throw new SearchParseException(context, "Missing [ranges] in ranges aggregator [" + aggregationName + "]"); + throw new SearchParseException(context, "Missing [ranges] in ranges aggregator [" + aggregationName + "]", + parser.getTokenLocation()); } return new RangeAggregator.Factory(aggregationName, vsParser.config(), InternalIPv4Range.FACTORY, ranges, keyed); @@ -122,7 +126,8 @@ public class IpRangeParser implements Aggregator.Parser { private static void parseMaskRange(String cidr, RangeAggregator.Range range, String aggregationName, SearchContext ctx) { long[] fromTo = IPv4RangeBuilder.cidrMaskToMinMax(cidr); if (fromTo == null) { - throw new SearchParseException(ctx, "invalid CIDR mask [" + cidr + "] in aggregation [" + aggregationName + "]"); + throw new SearchParseException(ctx, "invalid CIDR mask [" + cidr + "] in aggregation [" + aggregationName + "]", + null); } range.from = fromTo[0] < 0 ? Double.NEGATIVE_INFINITY : fromTo[0]; range.to = fromTo[1] < 0 ? Double.POSITIVE_INFINITY : fromTo[1]; diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/SamplerParser.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/SamplerParser.java index 35a2963187e..d82dd2c6721 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/SamplerParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/SamplerParser.java @@ -73,17 +73,18 @@ public class SamplerParser implements Aggregator.Parser { maxDocsPerValue = parser.intValue(); } else { throw new SearchParseException(context, "Unsupported property \"" + currentFieldName + "\" for aggregation \"" - + aggregationName); + + aggregationName, parser.getTokenLocation()); } } else if (!vsParser.token(currentFieldName, token, parser)) { if (EXECUTION_HINT_FIELD.match(currentFieldName)) { executionHint = parser.text(); } else { - throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "]."); + throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "].", + parser.getTokenLocation()); } } else { throw new SearchParseException(context, "Unsupported property \"" + currentFieldName + "\" for aggregation \"" - + aggregationName); + + aggregationName, parser.getTokenLocation()); } } @@ -93,7 +94,8 @@ public class SamplerParser implements Aggregator.Parser { } else { if (diversityChoiceMade) { throw new SearchParseException(context, "Sampler aggregation has " + MAX_DOCS_PER_VALUE_FIELD.getPreferredName() - + " setting but no \"field\" or \"script\" setting to provide values for aggregation \"" + aggregationName + "\""); + + " setting but no \"field\" or \"script\" setting to provide values for aggregation \"" + aggregationName + "\"", + parser.getTokenLocation()); } return new SamplerAggregator.Factory(aggregationName, shardSize); diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsParametersParser.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsParametersParser.java index e9288528363..87a60d43967 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsParametersParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsParametersParser.java @@ -68,10 +68,12 @@ public class SignificantTermsParametersParser extends AbstractTermsParametersPar } else if (BACKGROUND_FILTER.match(currentFieldName)) { filter = context.queryParserService().parseInnerFilter(parser).filter(); } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + + "].", parser.getTokenLocation()); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsParametersParser.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsParametersParser.java index 6ae88f63c57..63166bca78c 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsParametersParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsParametersParser.java @@ -56,7 +56,8 @@ public class TermsParametersParser extends AbstractTermsParametersParser { if ("order".equals(currentFieldName)) { this.orderElements = Collections.singletonList(parseOrderParam(aggregationName, parser, context)); } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else if (token == XContentParser.Token.START_ARRAY) { if ("order".equals(currentFieldName)) { @@ -66,18 +67,21 @@ public class TermsParametersParser extends AbstractTermsParametersParser { OrderElement orderParam = parseOrderParam(aggregationName, parser, context); orderElements.add(orderParam); } else { - throw new SearchParseException(context, "Order elements must be of type object in [" + aggregationName + "]."); + throw new SearchParseException(context, "Order elements must be of type object in [" + aggregationName + "].", + parser.getTokenLocation()); } } } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else if (token == XContentParser.Token.VALUE_BOOLEAN) { if (SHOW_TERM_DOC_COUNT_ERROR.match(currentFieldName)) { showTermDocCountError = parser.booleanValue(); } } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + + "].", parser.getTokenLocation()); } } @@ -96,14 +100,17 @@ public class TermsParametersParser extends AbstractTermsParametersParser { } else if ("desc".equalsIgnoreCase(dir)) { orderAsc = false; } else { - throw new SearchParseException(context, "Unknown terms order direction [" + dir + "] in terms aggregation [" + aggregationName + "]"); + throw new SearchParseException(context, "Unknown terms order direction [" + dir + "] in terms aggregation [" + + aggregationName + "]", parser.getTokenLocation()); } } else { - throw new SearchParseException(context, "Unexpected token " + token + " for [order] in [" + aggregationName + "]."); + throw new SearchParseException(context, "Unexpected token " + token + " for [order] in [" + aggregationName + "].", + parser.getTokenLocation()); } } if (orderKey == null) { - throw new SearchParseException(context, "Must specify at least one field for [order] in [" + aggregationName + "]."); + throw new SearchParseException(context, "Must specify at least one field for [order] in [" + aggregationName + "].", + parser.getTokenLocation()); } else { orderParam = new OrderElement(orderKey, orderAsc); } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/NumericValuesSourceMetricsAggregatorParser.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/NumericValuesSourceMetricsAggregatorParser.java index ae9e6844e2f..6847a9a5b3d 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/NumericValuesSourceMetricsAggregatorParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/NumericValuesSourceMetricsAggregatorParser.java @@ -58,7 +58,8 @@ public abstract class NumericValuesSourceMetricsAggregatorParser valuesSourceConfig, double[] keys, double compression, boolean keyed) { if (keys == null) { - throw new SearchParseException(context, "Missing token values in [" + aggregationName + "]."); + throw new SearchParseException(context, "Missing token values in [" + aggregationName + "].", null); } return new PercentileRanksAggregator.Factory(aggregationName, valuesSourceConfig, keys, compression, keyed); } 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 9d52242b7bf..83787a737cf 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 @@ -22,15 +22,17 @@ package org.elasticsearch.search.aggregations.metrics.scripted; import org.apache.lucene.index.LeafReaderContext; import org.elasticsearch.script.ExecutableScript; import org.elasticsearch.script.LeafSearchScript; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptService; -import org.elasticsearch.script.*; import org.elasticsearch.script.ScriptService.ScriptType; +import org.elasticsearch.script.SearchScript; import org.elasticsearch.search.SearchParseException; import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorFactory; -import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.LeafBucketCollector; +import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; import org.elasticsearch.search.aggregations.metrics.MetricsAggregator; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.internal.SearchContext; @@ -190,7 +192,7 @@ public class ScriptedMetricAggregator extends MetricsAggregator { clone = original; } else { throw new SearchParseException(context, "Can only clone primitives, String, ArrayList, and HashMap. Found: " - + original.getClass().getCanonicalName()); + + original.getClass().getCanonicalName(), null); } return clone; } diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricParser.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricParser.java index 1b0b5aa3290..c37d0aaccf8 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricParser.java @@ -72,14 +72,17 @@ public class ScriptedMetricParser implements Aggregator.Parser { } else if (REDUCE_PARAMS_FIELD.match(currentFieldName)) { reduceParams = parser.map(); } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else if (token.isValue()) { if (!scriptParameterParser.token(currentFieldName, token, parser)) { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else { - throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "]."); + throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "].", + parser.getTokenLocation()); } } @@ -114,7 +117,7 @@ public class ScriptedMetricParser implements Aggregator.Parser { scriptLang = scriptParameterParser.lang(); if (mapScript == null) { - throw new SearchParseException(context, "map_script field is required in [" + aggregationName + "]."); + throw new SearchParseException(context, "map_script field is required in [" + aggregationName + "].", parser.getTokenLocation()); } return new ScriptedMetricAggregator.Factory(aggregationName, scriptLang, initScriptType, initScript, mapScriptType, mapScript, combineScriptType, combineScript, reduceScriptType, reduceScript, params, reduceParams); diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/extended/ExtendedStatsParser.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/extended/ExtendedStatsParser.java index 18ca93495c3..ea48e4b11f8 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/extended/ExtendedStatsParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/stats/extended/ExtendedStatsParser.java @@ -65,15 +65,17 @@ public class ExtendedStatsParser implements Aggregator.Parser { if (SIGMA.match(currentFieldName)) { sigma = parser.doubleValue(); } else { - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else { - throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "]."); + throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "].", + parser.getTokenLocation()); } } if (sigma < 0) { - throw new SearchParseException(context, "[sigma] must not be negative. Value provided was" + sigma ); + throw new SearchParseException(context, "[sigma] must not be negative. Value provided was" + sigma, parser.getTokenLocation()); } return createFactory(aggregationName, vsParser.config(), sigma); diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsParser.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsParser.java index 6300374663b..206587ac6a4 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsParser.java @@ -94,7 +94,8 @@ public class TopHitsParser implements Aggregator.Parser { subSearchContext.explain(parser.booleanValue()); break; default: - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else if (token == XContentParser.Token.START_OBJECT) { switch (currentFieldName) { @@ -106,7 +107,8 @@ public class TopHitsParser implements Aggregator.Parser { scriptFieldsParseElement.parse(parser, subSearchContext); break; default: - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else if (token == XContentParser.Token.START_ARRAY) { switch (currentFieldName) { @@ -115,10 +117,12 @@ public class TopHitsParser implements Aggregator.Parser { fieldDataFieldsParseElement.parse(parser, subSearchContext); break; default: - throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + + currentFieldName + "].", parser.getTokenLocation()); } } else { - throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "]."); + throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "].", + parser.getTokenLocation()); } } } catch (Exception e) { diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/valuecount/ValueCountParser.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/valuecount/ValueCountParser.java index fb1d31f41f0..764f6ce9384 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/valuecount/ValueCountParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/valuecount/ValueCountParser.java @@ -49,7 +49,8 @@ public class ValueCountParser implements Aggregator.Parser { if (token == XContentParser.Token.FIELD_NAME) { currentFieldName = parser.currentName(); } else if (!vsParser.token(currentFieldName, token, parser)) { - throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "]."); + throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "].", + parser.getTokenLocation()); } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/support/GeoPointParser.java b/src/main/java/org/elasticsearch/search/aggregations/support/GeoPointParser.java index 35c381ec2a1..b423dd2f755 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/support/GeoPointParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/support/GeoPointParser.java @@ -66,7 +66,8 @@ public class GeoPointParser { lat = parser.doubleValue(); } else { throw new SearchParseException(context, "malformed [" + currentFieldName + "] geo point array in [" + - aggName + "] " + aggType + " aggregation. a geo point array must be of the form [lon, lat]"); + aggName + "] " + aggType + " aggregation. a geo point array must be of the form [lon, lat]", + parser.getTokenLocation()); } } point = new GeoPoint(lat, lon); @@ -88,7 +89,7 @@ public class GeoPointParser { } if (Double.isNaN(lat) || Double.isNaN(lon)) { throw new SearchParseException(context, "malformed [" + currentFieldName + "] geo point object. either [lat] or [lon] (or both) are " + - "missing in [" + aggName + "] " + aggType + " aggregation"); + "missing in [" + aggName + "] " + aggType + " aggregation", parser.getTokenLocation()); } point = new GeoPoint(lat, lon); return true; diff --git a/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSourceParser.java b/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSourceParser.java index 37182685761..88c3f64b089 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSourceParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSourceParser.java @@ -101,7 +101,8 @@ public class ValuesSourceParser { if (targetValueType != null && input.valueType.isNotA(targetValueType)) { throw new SearchParseException(context, aggType.name() + " aggregation [" + aggName + "] was configured with an incompatible value type [" + input.valueType + "]. [" + aggType + - "] aggregation can only work on value of type [" + targetValueType + "]"); + "] aggregation can only work on value of type [" + targetValueType + "]", + parser.getTokenLocation()); } } else if (!scriptParameterParser.token(currentFieldName, token, parser)) { return false; diff --git a/src/main/java/org/elasticsearch/search/highlight/HighlighterParseElement.java b/src/main/java/org/elasticsearch/search/highlight/HighlighterParseElement.java index c4d8aa80ef5..3613327c679 100644 --- a/src/main/java/org/elasticsearch/search/highlight/HighlighterParseElement.java +++ b/src/main/java/org/elasticsearch/search/highlight/HighlighterParseElement.java @@ -21,6 +21,7 @@ package org.elasticsearch.search.highlight; import com.google.common.collect.Lists; import com.google.common.collect.Sets; + import org.apache.lucene.search.vectorhighlight.SimpleBoundaryScanner; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.xcontent.XContentParser; @@ -70,7 +71,7 @@ public class HighlighterParseElement implements SearchParseElement { try { context.highlight(parse(parser, context.queryParserService())); } catch (IllegalArgumentException ex) { - throw new SearchParseException(context, "Error while trying to parse Highlighter element in request"); + throw new SearchParseException(context, "Error while trying to parse Highlighter element in request", parser.getTokenLocation()); } } diff --git a/src/main/java/org/elasticsearch/search/query/FromParseElement.java b/src/main/java/org/elasticsearch/search/query/FromParseElement.java index 13e58caa471..21063a93d35 100644 --- a/src/main/java/org/elasticsearch/search/query/FromParseElement.java +++ b/src/main/java/org/elasticsearch/search/query/FromParseElement.java @@ -35,7 +35,8 @@ public class FromParseElement implements SearchParseElement { if (token.isValue()) { int from = parser.intValue(); if (from < 0) { - throw new SearchParseException(context, "from is set to [" + from + "] and is expected to be higher or equal to 0"); + throw new SearchParseException(context, "from is set to [" + from + "] and is expected to be higher or equal to 0", + parser.getTokenLocation()); } context.from(from); } diff --git a/src/main/java/org/elasticsearch/search/query/SizeParseElement.java b/src/main/java/org/elasticsearch/search/query/SizeParseElement.java index b729ea4cdb2..5560ec939c4 100644 --- a/src/main/java/org/elasticsearch/search/query/SizeParseElement.java +++ b/src/main/java/org/elasticsearch/search/query/SizeParseElement.java @@ -35,7 +35,8 @@ public class SizeParseElement implements SearchParseElement { if (token.isValue()) { int size = parser.intValue(); if (size < 0) { - throw new SearchParseException(context, "size is set to [" + size + "] and is expected to be higher or equal to 0"); + throw new SearchParseException(context, "size is set to [" + size + "] and is expected to be higher or equal to 0", + parser.getTokenLocation()); } context.size(size); } diff --git a/src/main/java/org/elasticsearch/search/sort/ScriptSortParser.java b/src/main/java/org/elasticsearch/search/sort/ScriptSortParser.java index d0bfebe81f4..7caf89e9c08 100644 --- a/src/main/java/org/elasticsearch/search/sort/ScriptSortParser.java +++ b/src/main/java/org/elasticsearch/search/sort/ScriptSortParser.java @@ -118,15 +118,15 @@ public class ScriptSortParser implements SortParser { } if (script == null) { - throw new SearchParseException(context, "_script sorting requires setting the script to sort by"); + throw new SearchParseException(context, "_script sorting requires setting the script to sort by", parser.getTokenLocation()); } if (type == null) { - throw new SearchParseException(context, "_script sorting requires setting the type of the script"); + throw new SearchParseException(context, "_script sorting requires setting the type of the script", parser.getTokenLocation()); } final SearchScript searchScript = context.scriptService().search(context.lookup(), new Script(scriptLang, script, scriptType, params), ScriptContext.Standard.SEARCH); if (STRING_SORT_TYPE.equals(type) && (sortMode == MultiValueMode.SUM || sortMode == MultiValueMode.AVG)) { - throw new SearchParseException(context, "type [string] doesn't support mode [" + sortMode + "]"); + throw new SearchParseException(context, "type [string] doesn't support mode [" + sortMode + "]", parser.getTokenLocation()); } if (sortMode == null) { @@ -196,7 +196,7 @@ public class ScriptSortParser implements SortParser { }; break; default: - throw new SearchParseException(context, "custom script sort type [" + type + "] not supported"); + throw new SearchParseException(context, "custom script sort type [" + type + "] not supported", parser.getTokenLocation()); } return new SortField("_script", fieldComparatorSource, reverse); diff --git a/src/main/java/org/elasticsearch/search/sort/SortParseElement.java b/src/main/java/org/elasticsearch/search/sort/SortParseElement.java index 4723f427dbb..aa2f1315960 100644 --- a/src/main/java/org/elasticsearch/search/sort/SortParseElement.java +++ b/src/main/java/org/elasticsearch/search/sort/SortParseElement.java @@ -212,12 +212,12 @@ public class SortParseElement implements SearchParseElement { if (unmappedType != null) { fieldMapper = context.mapperService().unmappedFieldMapper(unmappedType); } else { - throw new SearchParseException(context, "No mapping found for [" + fieldName + "] in order to sort on"); + throw new SearchParseException(context, "No mapping found for [" + fieldName + "] in order to sort on", null); } } if (!fieldMapper.isSortable()) { - throw new SearchParseException(context, "Sorting not supported for field[" + fieldName + "]"); + throw new SearchParseException(context, "Sorting not supported for field[" + fieldName + "]", null); } // Enable when we also know how to detect fields that do tokenize, but only emit one token diff --git a/src/test/java/org/elasticsearch/ElasticsearchExceptionTests.java b/src/test/java/org/elasticsearch/ElasticsearchExceptionTests.java index e88489c7e98..16914ab6eef 100644 --- a/src/test/java/org/elasticsearch/ElasticsearchExceptionTests.java +++ b/src/test/java/org/elasticsearch/ElasticsearchExceptionTests.java @@ -29,7 +29,7 @@ import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexException; import org.elasticsearch.index.query.QueryParsingException; -import org.elasticsearch.indices.IndexClosedException; +import org.elasticsearch.index.query.TestQueryParsingException; import org.elasticsearch.indices.IndexMissingException; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.search.SearchShardTarget; @@ -73,15 +73,17 @@ public class ElasticsearchExceptionTests extends ElasticsearchTestCase { assertEquals(rootCauses.length, 1); assertEquals(ElasticsearchException.getExceptionName(rootCauses[0]), "index_exception"); assertEquals(rootCauses[0].getMessage(), "index is closed"); - ShardSearchFailure failure = new ShardSearchFailure(new QueryParsingException(new Index("foo"), "foobar"), new SearchShardTarget("node_1", "foo", 1)); - ShardSearchFailure failure1 = new ShardSearchFailure(new QueryParsingException(new Index("foo"), "foobar"), new SearchShardTarget("node_1", "foo", 2)); + ShardSearchFailure failure = new ShardSearchFailure(new TestQueryParsingException(new Index("foo"), "foobar", null), + new SearchShardTarget("node_1", "foo", 1)); + ShardSearchFailure failure1 = new ShardSearchFailure(new TestQueryParsingException(new Index("foo"), "foobar", null), + new SearchShardTarget("node_1", "foo", 2)); SearchPhaseExecutionException ex = new SearchPhaseExecutionException("search", "all shards failed", new ShardSearchFailure[]{failure, failure1}); if (randomBoolean()) { rootCauses = (randomBoolean() ? new RemoteTransportException("remoteboom", ex) : ex).guessRootCauses(); } else { rootCauses = ElasticsearchException.guessRootCauses(randomBoolean() ? new RemoteTransportException("remoteboom", ex) : ex); } - assertEquals(ElasticsearchException.getExceptionName(rootCauses[0]), "query_parsing_exception"); + assertEquals(ElasticsearchException.getExceptionName(rootCauses[0]), "test_query_parsing_exception"); assertEquals(rootCauses[0].getMessage(), "foobar"); ElasticsearchException oneLevel = new ElasticsearchException("foo", new RuntimeException("foobar")); @@ -90,18 +92,23 @@ public class ElasticsearchExceptionTests extends ElasticsearchTestCase { assertEquals(rootCauses[0].getMessage(), "foo"); } { - ShardSearchFailure failure = new ShardSearchFailure(new QueryParsingException(new Index("foo"), "foobar"), new SearchShardTarget("node_1", "foo", 1)); - ShardSearchFailure failure1 = new ShardSearchFailure(new QueryParsingException(new Index("foo1"), "foobar"), new SearchShardTarget("node_1", "foo1", 1)); - ShardSearchFailure failure2 = new ShardSearchFailure(new QueryParsingException(new Index("foo1"), "foobar"), new SearchShardTarget("node_1", "foo1", 2)); + ShardSearchFailure failure = new ShardSearchFailure( + new TestQueryParsingException(new Index("foo"), 1, 2, "foobar", null), + new SearchShardTarget("node_1", "foo", 1)); + ShardSearchFailure failure1 = new ShardSearchFailure(new TestQueryParsingException(new Index("foo1"), 1, 2, "foobar", null), + new SearchShardTarget("node_1", "foo1", 1)); + ShardSearchFailure failure2 = new ShardSearchFailure(new TestQueryParsingException(new Index("foo1"), 1, 2, "foobar", null), + new SearchShardTarget("node_1", "foo1", 2)); SearchPhaseExecutionException ex = new SearchPhaseExecutionException("search", "all shards failed", new ShardSearchFailure[]{failure, failure1, failure2}); final ElasticsearchException[] rootCauses = ex.guessRootCauses(); assertEquals(rootCauses.length, 2); - assertEquals(ElasticsearchException.getExceptionName(rootCauses[0]), "query_parsing_exception"); + assertEquals(ElasticsearchException.getExceptionName(rootCauses[0]), "test_query_parsing_exception"); assertEquals(rootCauses[0].getMessage(), "foobar"); assertEquals(((QueryParsingException)rootCauses[0]).index().name(), "foo"); - assertEquals(ElasticsearchException.getExceptionName(rootCauses[1]), "query_parsing_exception"); + assertEquals(ElasticsearchException.getExceptionName(rootCauses[1]), "test_query_parsing_exception"); assertEquals(rootCauses[1].getMessage(), "foobar"); - assertEquals(((QueryParsingException)rootCauses[1]).index().name(), "foo1"); + assertEquals(((QueryParsingException) rootCauses[1]).getLineNumber(), 1); + assertEquals(((QueryParsingException) rootCauses[1]).getColumnNumber(), 2); } @@ -118,26 +125,31 @@ public class ElasticsearchExceptionTests extends ElasticsearchTestCase { public void testDeduplicate() throws IOException { { - ShardSearchFailure failure = new ShardSearchFailure(new QueryParsingException(new Index("foo"), "foobar"), new SearchShardTarget("node_1", "foo", 1)); - ShardSearchFailure failure1 = new ShardSearchFailure(new QueryParsingException(new Index("foo"), "foobar"), new SearchShardTarget("node_1", "foo", 2)); + ShardSearchFailure failure = new ShardSearchFailure(new TestQueryParsingException(new Index("foo"), "foobar", null), + new SearchShardTarget("node_1", "foo", 1)); + ShardSearchFailure failure1 = new ShardSearchFailure(new TestQueryParsingException(new Index("foo"), "foobar", null), + new SearchShardTarget("node_1", "foo", 2)); SearchPhaseExecutionException ex = new SearchPhaseExecutionException("search", "all shards failed", new ShardSearchFailure[]{failure, failure1}); XContentBuilder builder = XContentFactory.jsonBuilder(); builder.startObject(); ex.toXContent(builder, ToXContent.EMPTY_PARAMS); builder.endObject(); - String expected = "{\"type\":\"search_phase_execution_exception\",\"reason\":\"all shards failed\",\"phase\":\"search\",\"grouped\":true,\"failed_shards\":[{\"shard\":1,\"index\":\"foo\",\"node\":\"node_1\",\"reason\":{\"type\":\"query_parsing_exception\",\"reason\":\"foobar\",\"index\":\"foo\"}}]}"; + String expected = "{\"type\":\"search_phase_execution_exception\",\"reason\":\"all shards failed\",\"phase\":\"search\",\"grouped\":true,\"failed_shards\":[{\"shard\":1,\"index\":\"foo\",\"node\":\"node_1\",\"reason\":{\"type\":\"test_query_parsing_exception\",\"reason\":\"foobar\",\"index\":\"foo\"}}]}"; assertEquals(expected, builder.string()); } { - ShardSearchFailure failure = new ShardSearchFailure(new QueryParsingException(new Index("foo"), "foobar"), new SearchShardTarget("node_1", "foo", 1)); - ShardSearchFailure failure1 = new ShardSearchFailure(new QueryParsingException(new Index("foo1"), "foobar"), new SearchShardTarget("node_1", "foo1", 1)); - ShardSearchFailure failure2 = new ShardSearchFailure(new QueryParsingException(new Index("foo1"), "foobar"), new SearchShardTarget("node_1", "foo1", 2)); + ShardSearchFailure failure = new ShardSearchFailure(new TestQueryParsingException(new Index("foo"), "foobar", null), + new SearchShardTarget("node_1", "foo", 1)); + ShardSearchFailure failure1 = new ShardSearchFailure(new TestQueryParsingException(new Index("foo1"), "foobar", null), + new SearchShardTarget("node_1", "foo1", 1)); + ShardSearchFailure failure2 = new ShardSearchFailure(new TestQueryParsingException(new Index("foo1"), "foobar", null), + new SearchShardTarget("node_1", "foo1", 2)); SearchPhaseExecutionException ex = new SearchPhaseExecutionException("search", "all shards failed", new ShardSearchFailure[]{failure, failure1, failure2}); XContentBuilder builder = XContentFactory.jsonBuilder(); builder.startObject(); ex.toXContent(builder, ToXContent.EMPTY_PARAMS); builder.endObject(); - String expected = "{\"type\":\"search_phase_execution_exception\",\"reason\":\"all shards failed\",\"phase\":\"search\",\"grouped\":true,\"failed_shards\":[{\"shard\":1,\"index\":\"foo\",\"node\":\"node_1\",\"reason\":{\"type\":\"query_parsing_exception\",\"reason\":\"foobar\",\"index\":\"foo\"}},{\"shard\":1,\"index\":\"foo1\",\"node\":\"node_1\",\"reason\":{\"type\":\"query_parsing_exception\",\"reason\":\"foobar\",\"index\":\"foo1\"}}]}"; + String expected = "{\"type\":\"search_phase_execution_exception\",\"reason\":\"all shards failed\",\"phase\":\"search\",\"grouped\":true,\"failed_shards\":[{\"shard\":1,\"index\":\"foo\",\"node\":\"node_1\",\"reason\":{\"type\":\"test_query_parsing_exception\",\"reason\":\"foobar\",\"index\":\"foo\"}},{\"shard\":1,\"index\":\"foo1\",\"node\":\"node_1\",\"reason\":{\"type\":\"test_query_parsing_exception\",\"reason\":\"foobar\",\"index\":\"foo1\"}}]}"; assertEquals(expected, builder.string()); } } @@ -182,6 +194,16 @@ public class ElasticsearchExceptionTests extends ElasticsearchTestCase { assertEquals(expected, builder.string()); } + { + QueryParsingException ex = new TestQueryParsingException(new Index("foo"), 1, 2, "foobar", null); + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.startObject(); + ElasticsearchException.toXContent(builder, ToXContent.EMPTY_PARAMS, ex); + builder.endObject(); + String expected = "{\"type\":\"test_query_parsing_exception\",\"reason\":\"foobar\",\"line\":1,\"col\":2,\"index\":\"foo\"}"; + assertEquals(expected, builder.string()); + } + { // test equivalence ElasticsearchException ex = new RemoteTransportException("foobar", new FileNotFoundException("foo not found")); XContentBuilder builder = XContentFactory.jsonBuilder(); @@ -200,13 +222,15 @@ public class ElasticsearchExceptionTests extends ElasticsearchTestCase { public void testSerializeElasticsearchException() throws IOException { BytesStreamOutput out = new BytesStreamOutput(); - QueryParsingException ex = new QueryParsingException(new Index("foo"), "foobar"); + QueryParsingException ex = new TestQueryParsingException(new Index("foo"), 1, 2, "foobar", null); out.writeThrowable(ex); BytesStreamInput in = new BytesStreamInput(out.bytes()); QueryParsingException e = in.readThrowable(); assertEquals(ex.index(), e.index()); assertEquals(ex.getMessage(), e.getMessage()); + assertEquals(ex.getLineNumber(), e.getLineNumber()); + assertEquals(ex.getColumnNumber(), e.getColumnNumber()); } } \ No newline at end of file diff --git a/src/test/java/org/elasticsearch/index/query/TestQueryParsingException.java b/src/test/java/org/elasticsearch/index/query/TestQueryParsingException.java new file mode 100644 index 00000000000..951b31e59a6 --- /dev/null +++ b/src/test/java/org/elasticsearch/index/query/TestQueryParsingException.java @@ -0,0 +1,37 @@ +/* + * 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.index.query; + +import org.elasticsearch.index.Index; + +/** + * Class used to avoid dragging QueryContext into unit testing framework for + * basic exception handling + */ +public class TestQueryParsingException extends QueryParsingException { + + public TestQueryParsingException(Index index, int line, int col, String msg, Throwable cause) { + super(index, line, col, msg, cause); + } + + public TestQueryParsingException(Index index, String msg, Throwable cause) { + super(index, UNKNOWN_POSITION, UNKNOWN_POSITION, msg, cause); + } +} diff --git a/src/test/java/org/elasticsearch/rest/BytesRestResponseTests.java b/src/test/java/org/elasticsearch/rest/BytesRestResponseTests.java index e110e6ca70d..579408366e9 100644 --- a/src/test/java/org/elasticsearch/rest/BytesRestResponseTests.java +++ b/src/test/java/org/elasticsearch/rest/BytesRestResponseTests.java @@ -23,7 +23,7 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.search.SearchPhaseExecutionException; import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.index.Index; -import org.elasticsearch.index.query.QueryParsingException; +import org.elasticsearch.index.query.TestQueryParsingException; import org.elasticsearch.search.SearchShardTarget; import org.elasticsearch.test.ElasticsearchTestCase; import org.elasticsearch.test.rest.FakeRestRequest; @@ -141,12 +141,14 @@ public class BytesRestResponseTests extends ElasticsearchTestCase { public void testConvert() throws IOException { RestRequest request = new FakeRestRequest(); RestChannel channel = new DetailedExceptionRestChannel(request); - ShardSearchFailure failure = new ShardSearchFailure(new QueryParsingException(new Index("foo"), "foobar"), new SearchShardTarget("node_1", "foo", 1)); - ShardSearchFailure failure1 = new ShardSearchFailure(new QueryParsingException(new Index("foo"), "foobar"), new SearchShardTarget("node_1", "foo", 2)); + ShardSearchFailure failure = new ShardSearchFailure(new TestQueryParsingException(new Index("foo"), "foobar", null), + new SearchShardTarget("node_1", "foo", 1)); + ShardSearchFailure failure1 = new ShardSearchFailure(new TestQueryParsingException(new Index("foo"), "foobar", null), + new SearchShardTarget("node_1", "foo", 2)); SearchPhaseExecutionException ex = new SearchPhaseExecutionException("search", "all shards failed", new ShardSearchFailure[] {failure, failure1}); BytesRestResponse response = new BytesRestResponse(channel, new RemoteTransportException("foo", ex)); String text = response.content().toUtf8(); - String expected = "{\"error\":{\"root_cause\":[{\"type\":\"query_parsing_exception\",\"reason\":\"foobar\",\"index\":\"foo\"}],\"type\":\"search_phase_execution_exception\",\"reason\":\"all shards failed\",\"phase\":\"search\",\"grouped\":true,\"failed_shards\":[{\"shard\":1,\"index\":\"foo\",\"node\":\"node_1\",\"reason\":{\"type\":\"query_parsing_exception\",\"reason\":\"foobar\",\"index\":\"foo\"}}]},\"status\":400}"; + String expected = "{\"error\":{\"root_cause\":[{\"type\":\"test_query_parsing_exception\",\"reason\":\"foobar\",\"index\":\"foo\"}],\"type\":\"search_phase_execution_exception\",\"reason\":\"all shards failed\",\"phase\":\"search\",\"grouped\":true,\"failed_shards\":[{\"shard\":1,\"index\":\"foo\",\"node\":\"node_1\",\"reason\":{\"type\":\"test_query_parsing_exception\",\"reason\":\"foobar\",\"index\":\"foo\"}}]},\"status\":400}"; assertEquals(expected.trim(), text.trim()); } From 9b76be92b3f1f9ea735ee9009661379a24065294 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Wed, 29 Apr 2015 10:53:16 -0400 Subject: [PATCH 72/85] Docs: add notes about using close and awaitClose with bulk processor Closes #10839 --- docs/java-api/bulk.asciidoc | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/java-api/bulk.asciidoc b/docs/java-api/bulk.asciidoc index 9ac61f47f30..96b0b2eb6dc 100644 --- a/docs/java-api/bulk.asciidoc +++ b/docs/java-api/bulk.asciidoc @@ -99,3 +99,22 @@ By default, `BulkProcessor`: * does not set flushInterval * sets concurrentRequests to 1 +When all documents are loaded to the `BulkProcessor` it can be closed by using `awaitClose` or `close` methods: + +[source,java] +-------------------------------------------------- +bulkProcessor.awaitClose(10, TimeUnit.MINUTES); +-------------------------------------------------- + +or + +[source,java] +-------------------------------------------------- +bulkProcessor.close(); +-------------------------------------------------- + +Both methods flush any remaining documents and disable all other scheduled flushes if they were scheduled by setting +`flushInterval`. If concurrent requests were enabled the `awaitClose` method waits for up to the specified timeout for +all bulk requests to complete then returns `true`, if the specified waiting time elapses before all bulk requests complete, +`false` is returned. The `close` method doesn't wait for any remaining bulk requests to complete and exists immediately. + From a33e77ff9604afb1ad5314a445ffa1bb3b3f2b2b Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Wed, 29 Apr 2015 16:04:29 +0100 Subject: [PATCH 73/85] 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 ae0f89ae868..069f9904a3f 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.elasticsearch.test.ElasticsearchIntegrationTest; 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 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { * 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 class MovAvgTests extends ElasticsearchIntegrationTest { } @Test + @AwaitsFix(bugUrl = "Fails with certain seeds including -Dtests.seed=D9EF60095522804F") public void linearSingleValuedField() { SearchResponse response = client() @@ -407,6 +414,7 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { } @Test + @AwaitsFix(bugUrl = "Fails with certain seeds including -Dtests.seed=D9EF60095522804F") public void singleSingleValuedField() { SearchResponse response = client() @@ -459,6 +467,7 @@ public class MovAvgTests extends ElasticsearchIntegrationTest { } @Test + @AwaitsFix(bugUrl = "Fails with certain seeds including -Dtests.seed=D9EF60095522804F") public void doubleSingleValuedField() { SearchResponse response = client() From a202c2a43489aa83c10934e68f1981265ebbb3c6 Mon Sep 17 00:00:00 2001 From: Britta Weber Date: Wed, 29 Apr 2015 17:06:43 +0200 Subject: [PATCH 74/85] Revert "Write state also on data nodes if not master eligible" This reverts commit 4088dd38cbff19462e610db853ba1e54ee9785e4. --- .../gateway/GatewayMetaState.java | 176 ++------- .../gateway/GatewayMetaStateTests.java | 249 ------------ .../gateway/MetaDataWriteDataNodesTests.java | 354 ------------------ 3 files changed, 36 insertions(+), 743 deletions(-) delete mode 100644 src/test/java/org/elasticsearch/gateway/GatewayMetaStateTests.java delete mode 100644 src/test/java/org/elasticsearch/gateway/MetaDataWriteDataNodesTests.java diff --git a/src/main/java/org/elasticsearch/gateway/GatewayMetaState.java b/src/main/java/org/elasticsearch/gateway/GatewayMetaState.java index ca8edebc571..158a3df5d91 100644 --- a/src/main/java/org/elasticsearch/gateway/GatewayMetaState.java +++ b/src/main/java/org/elasticsearch/gateway/GatewayMetaState.java @@ -19,7 +19,6 @@ package org.elasticsearch.gateway; -import com.google.common.collect.ImmutableSet; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.Version; import org.elasticsearch.cluster.ClusterChangedEvent; @@ -28,7 +27,9 @@ import org.elasticsearch.cluster.ClusterStateListener; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.node.DiscoveryNode; -import org.elasticsearch.cluster.routing.*; +import org.elasticsearch.cluster.routing.DjbHashFunction; +import org.elasticsearch.cluster.routing.HashFunction; +import org.elasticsearch.cluster.routing.SimpleHashFunction; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.inject.Inject; @@ -42,7 +43,6 @@ import java.io.IOException; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; /** * @@ -57,9 +57,7 @@ public class GatewayMetaState extends AbstractComponent implements ClusterStateL private final DanglingIndicesState danglingIndicesState; @Nullable - private volatile MetaData previousMetaData; - - private volatile ImmutableSet previouslyWrittenIndices = ImmutableSet.of(); + private volatile MetaData currentMetaData; @Inject public GatewayMetaState(Settings settings, NodeEnvironment nodeEnv, MetaStateService metaStateService, @@ -78,7 +76,7 @@ public class GatewayMetaState extends AbstractComponent implements ClusterStateL if (DiscoveryNode.masterNode(settings) || DiscoveryNode.dataNode(settings)) { nodeEnv.ensureAtomicMoveSupported(); } - if (DiscoveryNode.masterNode(settings) || DiscoveryNode.dataNode(settings)) { + if (DiscoveryNode.masterNode(settings)) { try { ensureNoPre019State(); pre20Upgrade(); @@ -98,12 +96,10 @@ public class GatewayMetaState extends AbstractComponent implements ClusterStateL @Override public void clusterChanged(ClusterChangedEvent event) { - Set relevantIndices = new HashSet<>(); final ClusterState state = event.state(); if (state.blocks().disableStatePersistence()) { // reset the current metadata, we need to start fresh... - this.previousMetaData = null; - previouslyWrittenIndices= ImmutableSet.of(); + this.currentMetaData = null; return; } @@ -111,47 +107,44 @@ public class GatewayMetaState extends AbstractComponent implements ClusterStateL // we don't check if metaData changed, since we might be called several times and we need to check dangling... boolean success = true; - // write the state if this node is a master eligible node or if it is a data node and has shards allocated on it - if (state.nodes().localNode().masterNode() || state.nodes().localNode().dataNode()) { + // only applied to master node, writing the global and index level states + if (state.nodes().localNode().masterNode()) { // check if the global state changed? - if (previousMetaData == null || !MetaData.isGlobalStateEquals(previousMetaData, newMetaData)) { + if (currentMetaData == null || !MetaData.isGlobalStateEquals(currentMetaData, newMetaData)) { try { metaStateService.writeGlobalState("changed", newMetaData); - // we determine if or if not we write meta data on data only nodes by looking at the shard routing - // and only write if a shard of this index is allocated on this node - // however, closed indices do not appear in the shard routing. if the meta data for a closed index is - // updated it will therefore not be written in case the list of previouslyWrittenIndices is empty (because state - // persistence was disabled or the node was restarted), see getRelevantIndicesOnDataOnlyNode(). - // we therefore have to check here if we have shards on disk and add their indices to the previouslyWrittenIndices list - if (isDataOnlyNode(state)) { - ImmutableSet.Builder previouslyWrittenIndicesBuilder = ImmutableSet.builder(); - for (IndexMetaData indexMetaData : newMetaData) { - IndexMetaData indexMetaDataOnDisk = null; - if (indexMetaData.state().equals(IndexMetaData.State.CLOSE)) { - try { - indexMetaDataOnDisk = metaStateService.loadIndexState(indexMetaData.index()); - } catch (IOException ex) { - throw new ElasticsearchException("failed to load index state", ex); - } - } - if (indexMetaDataOnDisk != null) { - previouslyWrittenIndicesBuilder.add(indexMetaDataOnDisk.index()); - } - } - previouslyWrittenIndices = previouslyWrittenIndicesBuilder.addAll(previouslyWrittenIndices).build(); - } } catch (Throwable e) { success = false; } } - Iterable writeInfo; - relevantIndices = getRelevantIndices(event.state(), previouslyWrittenIndices); - writeInfo = resolveStatesToBeWritten(previouslyWrittenIndices, relevantIndices, previousMetaData, event.state().metaData()); // check and write changes in indices - for (IndexMetaWriteInfo indexMetaWrite : writeInfo) { + for (IndexMetaData indexMetaData : newMetaData) { + String writeReason = null; + IndexMetaData currentIndexMetaData; + if (currentMetaData == null) { + // a new event..., check from the state stored + try { + currentIndexMetaData = metaStateService.loadIndexState(indexMetaData.index()); + } catch (IOException ex) { + throw new ElasticsearchException("failed to load index state", ex); + } + } else { + currentIndexMetaData = currentMetaData.index(indexMetaData.index()); + } + if (currentIndexMetaData == null) { + writeReason = "freshly created"; + } else if (currentIndexMetaData.version() != indexMetaData.version()) { + writeReason = "version changed from [" + currentIndexMetaData.version() + "] to [" + indexMetaData.version() + "]"; + } + + // we update the writeReason only if we really need to write it + if (writeReason == null) { + continue; + } + try { - metaStateService.writeIndex(indexMetaWrite.reason, indexMetaWrite.newMetaData, indexMetaWrite.previousMetaData); + metaStateService.writeIndex(writeReason, indexMetaData, currentIndexMetaData); } catch (Throwable e) { success = false; } @@ -161,29 +154,10 @@ public class GatewayMetaState extends AbstractComponent implements ClusterStateL danglingIndicesState.processDanglingIndices(newMetaData); if (success) { - previousMetaData = newMetaData; - ImmutableSet.Builder builder= ImmutableSet.builder(); - previouslyWrittenIndices = builder.addAll(relevantIndices).build(); + currentMetaData = newMetaData; } } - public static Set getRelevantIndices(ClusterState state, ImmutableSet previouslyWrittenIndices) { - Set relevantIndices; - if (isDataOnlyNode(state)) { - relevantIndices = getRelevantIndicesOnDataOnlyNode(state, previouslyWrittenIndices); - } else if (state.nodes().localNode().masterNode() == true) { - relevantIndices = getRelevantIndicesForMasterEligibleNode(state); - } else { - relevantIndices = Collections.emptySet(); - } - return relevantIndices; - } - - - protected static boolean isDataOnlyNode(ClusterState state) { - return ((state.nodes().localNode().masterNode() == false) && state.nodes().localNode().dataNode()); - } - /** * Throws an IAE if a pre 0.19 state is detected */ @@ -255,7 +229,7 @@ public class GatewayMetaState extends AbstractComponent implements ClusterStateL } } } - if (hasCustomPre20HashFunction || pre20UseType != null) { + if (hasCustomPre20HashFunction|| pre20UseType != null) { logger.warn("Settings [{}] and [{}] are deprecated. Index settings from your old indices have been updated to record the fact that they " + "used some custom routing logic, you can now remove these settings from your `elasticsearch.yml` file", DEPRECATED_SETTING_ROUTING_HASH_FUNCTION, DEPRECATED_SETTING_ROUTING_USE_TYPE); } @@ -277,82 +251,4 @@ public class GatewayMetaState extends AbstractComponent implements ClusterStateL } } } - - /** - * Loads the current meta state for each index in the new cluster state and checks if it has to be persisted. - * Each index state that should be written to disk will be returned. This is only run for data only nodes. - * It will return only the states for indices that actually have a shard allocated on the current node. - * - * @param previouslyWrittenIndices A list of indices for which the state was already written before - * @param potentiallyUnwrittenIndices The list of indices for which state should potentially be written - * @param previousMetaData The last meta data we know of. meta data for all indices in previouslyWrittenIndices list is persisted now - * @param newMetaData The new metadata - * @return iterable over all indices states that should be written to disk - */ - public static Iterable resolveStatesToBeWritten(ImmutableSet previouslyWrittenIndices, Set potentiallyUnwrittenIndices, MetaData previousMetaData, MetaData newMetaData) { - List indicesToWrite = new ArrayList<>(); - for (String index : potentiallyUnwrittenIndices) { - IndexMetaData newIndexMetaData = newMetaData.index(index); - IndexMetaData previousIndexMetaData = previousMetaData == null ? null : previousMetaData.index(index); - String writeReason = null; - if (previouslyWrittenIndices.contains(index) == false || previousIndexMetaData == null) { - writeReason = "freshly created"; - } else if (previousIndexMetaData.version() != newIndexMetaData.version()) { - writeReason = "version changed from [" + previousIndexMetaData.version() + "] to [" + newIndexMetaData.version() + "]"; - } - if (writeReason != null) { - indicesToWrite.add(new GatewayMetaState.IndexMetaWriteInfo(newIndexMetaData, previousIndexMetaData, writeReason)); - } - } - return indicesToWrite; - } - - public static Set getRelevantIndicesOnDataOnlyNode(ClusterState state, ImmutableSet previouslyWrittenIndices) { - RoutingNode newRoutingNode = state.getRoutingNodes().node(state.nodes().localNodeId()); - if (newRoutingNode == null) { - throw new IllegalStateException("cluster state does not contain this node - cannot write index meta state"); - } - Set indices = new HashSet<>(); - for (MutableShardRouting routing : newRoutingNode) { - indices.add(routing.index()); - } - // we have to check the meta data also: closed indices will not appear in the routing table, but we must still write the state if we have it written on disk previously - for (IndexMetaData indexMetaData : state.metaData()) { - if (previouslyWrittenIndices.contains(indexMetaData.getIndex()) && state.metaData().getIndices().get(indexMetaData.getIndex()).state().equals(IndexMetaData.State.CLOSE)) { - indices.add(indexMetaData.getIndex()); - } - } - return indices; - } - - public static Set getRelevantIndicesForMasterEligibleNode(ClusterState state) { - Set relevantIndices; - relevantIndices = new HashSet<>(); - // we have to iterate over the metadata to make sure we also capture closed indices - for (IndexMetaData indexMetaData : state.metaData()) { - relevantIndices.add(indexMetaData.getIndex()); - } - return relevantIndices; - } - - - public static class IndexMetaWriteInfo { - final IndexMetaData newMetaData; - final String reason; - final IndexMetaData previousMetaData; - - public IndexMetaWriteInfo(IndexMetaData newMetaData, IndexMetaData previousMetaData, String reason) { - this.newMetaData = newMetaData; - this.reason = reason; - this.previousMetaData = previousMetaData; - } - - public IndexMetaData getNewMetaData() { - return newMetaData; - } - - public String getReason() { - return reason; - } - } } diff --git a/src/test/java/org/elasticsearch/gateway/GatewayMetaStateTests.java b/src/test/java/org/elasticsearch/gateway/GatewayMetaStateTests.java deleted file mode 100644 index 06b958d47aa..00000000000 --- a/src/test/java/org/elasticsearch/gateway/GatewayMetaStateTests.java +++ /dev/null @@ -1,249 +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.gateway; - -import com.google.common.collect.ImmutableSet; -import org.elasticsearch.Version; -import org.elasticsearch.cluster.ClusterChangedEvent; -import org.elasticsearch.cluster.ClusterState; -import org.elasticsearch.cluster.metadata.IndexMetaData; -import org.elasticsearch.cluster.metadata.MetaData; -import org.elasticsearch.cluster.node.DiscoveryNodes; -import org.elasticsearch.cluster.routing.RoutingTable; -import org.elasticsearch.cluster.routing.allocation.AllocationService; -import org.elasticsearch.cluster.routing.allocation.decider.ClusterRebalanceAllocationDecider; -import org.elasticsearch.test.ElasticsearchAllocationTestCase; -import org.junit.Test; - -import java.util.*; - -import static org.elasticsearch.cluster.routing.ShardRoutingState.INITIALIZING; -import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder; -import static org.hamcrest.Matchers.equalTo; - -/** - * Test IndexMetaState for master and data only nodes return correct list of indices to write - * There are many parameters: - * - meta state is not in memory - * - meta state is in memory with old version/ new version - * - meta state is in memory with new version - * - version changed in cluster state event/ no change - * - node is data only node - * - node is master eligible - * for data only nodes: shard initializing on shard - */ -public class GatewayMetaStateTests extends ElasticsearchAllocationTestCase { - - ClusterChangedEvent generateEvent(boolean initializing, boolean versionChanged, boolean masterEligible) { - //ridiculous settings to make sure we don't run into uninitialized because fo default - AllocationService strategy = createAllocationService(settingsBuilder() - .put("cluster.routing.allocation.concurrent_recoveries", 100) - .put(ClusterRebalanceAllocationDecider.CLUSTER_ROUTING_ALLOCATION_ALLOW_REBALANCE, "always") - .put("cluster.routing.allocation.cluster_concurrent_rebalance", 100) - .put("cluster.routing.allocation.node_initial_primaries_recoveries", 100) - .build()); - ClusterState newClusterState, previousClusterState; - MetaData metaDataOldClusterState = MetaData.builder() - .put(IndexMetaData.builder("test").settings(settings(Version.CURRENT)).numberOfShards(5).numberOfReplicas(2)) - .build(); - - RoutingTable routingTableOldClusterState = RoutingTable.builder() - .addAsNew(metaDataOldClusterState.index("test")) - .build(); - - // assign all shards - ClusterState init = ClusterState.builder(org.elasticsearch.cluster.ClusterName.DEFAULT) - .metaData(metaDataOldClusterState) - .routingTable(routingTableOldClusterState) - .nodes(generateDiscoveryNodes(masterEligible)) - .build(); - // new cluster state will have initializing shards on node 1 - RoutingTable routingTableNewClusterState = strategy.reroute(init).routingTable(); - if (initializing == false) { - // pretend all initialized, nothing happened - ClusterState temp = ClusterState.builder(init).routingTable(routingTableNewClusterState).metaData(metaDataOldClusterState).build(); - routingTableNewClusterState = strategy.applyStartedShards(temp, temp.getRoutingNodes().shardsWithState(INITIALIZING)).routingTable(); - routingTableOldClusterState = routingTableNewClusterState; - - } else { - // nothing to do, we have one routing table with unassigned and one with initializing - } - - // create new meta data either with version changed or not - MetaData metaDataNewClusterState = MetaData.builder() - .put(init.metaData().index("test"), versionChanged) - .build(); - - - // create the cluster states with meta data and routing tables as computed before - previousClusterState = ClusterState.builder(init) - .metaData(metaDataOldClusterState) - .routingTable(routingTableOldClusterState) - .nodes(generateDiscoveryNodes(masterEligible)) - .build(); - newClusterState = ClusterState.builder(previousClusterState).routingTable(routingTableNewClusterState).metaData(metaDataNewClusterState).version(previousClusterState.getVersion() + 1).build(); - - ClusterChangedEvent event = new ClusterChangedEvent("test", newClusterState, previousClusterState); - assertThat(event.state().version(), equalTo(event.previousState().version() + 1)); - return event; - } - - ClusterChangedEvent generateCloseEvent(boolean masterEligible) { - //ridiculous settings to make sure we don't run into uninitialized because fo default - AllocationService strategy = createAllocationService(settingsBuilder() - .put("cluster.routing.allocation.concurrent_recoveries", 100) - .put(ClusterRebalanceAllocationDecider.CLUSTER_ROUTING_ALLOCATION_ALLOW_REBALANCE, "always") - .put("cluster.routing.allocation.cluster_concurrent_rebalance", 100) - .put("cluster.routing.allocation.node_initial_primaries_recoveries", 100) - .build()); - ClusterState newClusterState, previousClusterState; - MetaData metaDataIndexCreated = MetaData.builder() - .put(IndexMetaData.builder("test").settings(settings(Version.CURRENT)).numberOfShards(5).numberOfReplicas(2)) - .build(); - - RoutingTable routingTableIndexCreated = RoutingTable.builder() - .addAsNew(metaDataIndexCreated.index("test")) - .build(); - - // assign all shards - ClusterState init = ClusterState.builder(org.elasticsearch.cluster.ClusterName.DEFAULT) - .metaData(metaDataIndexCreated) - .routingTable(routingTableIndexCreated) - .nodes(generateDiscoveryNodes(masterEligible)) - .build(); - RoutingTable routingTableInitializing = strategy.reroute(init).routingTable(); - ClusterState temp = ClusterState.builder(init).routingTable(routingTableInitializing).build(); - RoutingTable routingTableStarted = strategy.applyStartedShards(temp, temp.getRoutingNodes().shardsWithState(INITIALIZING)).routingTable(); - - // create new meta data either with version changed or not - MetaData metaDataStarted = MetaData.builder() - .put(init.metaData().index("test"), true) - .build(); - - // create the cluster states with meta data and routing tables as computed before - MetaData metaDataClosed = MetaData.builder() - .put(IndexMetaData.builder("test").settings(settings(Version.CURRENT)).state(IndexMetaData.State.CLOSE).numberOfShards(5).numberOfReplicas(2)).version(metaDataStarted.version() + 1) - .build(); - previousClusterState = ClusterState.builder(init) - .metaData(metaDataStarted) - .routingTable(routingTableStarted) - .nodes(generateDiscoveryNodes(masterEligible)) - .build(); - newClusterState = ClusterState.builder(previousClusterState) - .routingTable(routingTableIndexCreated) - .metaData(metaDataClosed) - .version(previousClusterState.getVersion() + 1).build(); - - ClusterChangedEvent event = new ClusterChangedEvent("test", newClusterState, previousClusterState); - assertThat(event.state().version(), equalTo(event.previousState().version() + 1)); - return event; - } - - private DiscoveryNodes.Builder generateDiscoveryNodes(boolean masterEligible) { - Map masterNodeAttributes = new HashMap<>(); - masterNodeAttributes.put("master", "true"); - masterNodeAttributes.put("data", "true"); - Map dataNodeAttributes = new HashMap<>(); - dataNodeAttributes.put("master", "false"); - dataNodeAttributes.put("data", "true"); - return DiscoveryNodes.builder().put(newNode("node1", masterEligible ? masterNodeAttributes : dataNodeAttributes)).put(newNode("master_node", masterNodeAttributes)).localNodeId("node1").masterNodeId(masterEligible ? "node1" : "master_node"); - } - - public void assertState(ClusterChangedEvent event, - boolean stateInMemory, - boolean expectMetaData) throws Exception { - MetaData inMemoryMetaData = null; - ImmutableSet oldIndicesList = ImmutableSet.of(); - if (stateInMemory) { - inMemoryMetaData = event.previousState().metaData(); - ImmutableSet.Builder relevantIndices = ImmutableSet.builder(); - oldIndicesList = relevantIndices.addAll(GatewayMetaState.getRelevantIndices(event.previousState(), oldIndicesList)).build(); - } - Set newIndicesList = GatewayMetaState.getRelevantIndices(event.state(), oldIndicesList); - // third, get the actual write info - Iterator indices = GatewayMetaState.resolveStatesToBeWritten(oldIndicesList, newIndicesList, inMemoryMetaData, event.state().metaData()).iterator(); - - if (expectMetaData) { - assertThat(indices.hasNext(), equalTo(true)); - assertThat(indices.next().getNewMetaData().index(), equalTo("test")); - assertThat(indices.hasNext(), equalTo(false)); - } else { - assertThat(indices.hasNext(), equalTo(false)); - } - } - - @Test - public void testVersionChangeIsAlwaysWritten() throws Exception { - // test that version changes are always written - boolean initializing = randomBoolean(); - boolean versionChanged = true; - boolean stateInMemory = randomBoolean(); - boolean masterEligible = randomBoolean(); - boolean expectMetaData = true; - ClusterChangedEvent event = generateEvent(initializing, versionChanged, masterEligible); - assertState(event, stateInMemory, expectMetaData); - } - - @Test - public void testNewShardsAlwaysWritten() throws Exception { - // make sure new shards on data only node always written - boolean initializing = true; - boolean versionChanged = randomBoolean(); - boolean stateInMemory = randomBoolean(); - boolean masterEligible = false; - boolean expectMetaData = true; - ClusterChangedEvent event = generateEvent(initializing, versionChanged, masterEligible); - assertState(event, stateInMemory, expectMetaData); - } - - @Test - public void testAllUpToDateNothingWritten() throws Exception { - // make sure state is not written again if we wrote already - boolean initializing = false; - boolean versionChanged = false; - boolean stateInMemory = true; - boolean masterEligible = randomBoolean(); - boolean expectMetaData = false; - ClusterChangedEvent event = generateEvent(initializing, versionChanged, masterEligible); - assertState(event, stateInMemory, expectMetaData); - } - - @Test - public void testNoWriteIfNothingChanged() throws Exception { - boolean initializing = false; - boolean versionChanged = false; - boolean stateInMemory = true; - boolean masterEligible = randomBoolean(); - boolean expectMetaData = false; - ClusterChangedEvent event = generateEvent(initializing, versionChanged, masterEligible); - ClusterChangedEvent newEventWithNothingChanged = new ClusterChangedEvent("test cluster state", event.state(), event.state()); - assertState(newEventWithNothingChanged, stateInMemory, expectMetaData); - } - - @Test - public void testWriteClosedIndex() throws Exception { - // test that the closing of an index is written also on data only node - boolean masterEligible = randomBoolean(); - boolean expectMetaData = true; - boolean stateInMemory = true; - ClusterChangedEvent event = generateCloseEvent(masterEligible); - assertState(event, stateInMemory, expectMetaData); - } -} diff --git a/src/test/java/org/elasticsearch/gateway/MetaDataWriteDataNodesTests.java b/src/test/java/org/elasticsearch/gateway/MetaDataWriteDataNodesTests.java deleted file mode 100644 index 7947a6698c7..00000000000 --- a/src/test/java/org/elasticsearch/gateway/MetaDataWriteDataNodesTests.java +++ /dev/null @@ -1,354 +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.gateway; - -import com.carrotsearch.hppc.cursors.ObjectObjectCursor; -import com.google.common.base.Predicate; -import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse; -import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse; -import org.elasticsearch.cluster.metadata.IndexMetaData; -import org.elasticsearch.cluster.metadata.MetaData; -import org.elasticsearch.cluster.routing.allocation.decider.FilterAllocationDecider; -import org.elasticsearch.common.collect.ImmutableOpenMap; -import org.elasticsearch.common.settings.ImmutableSettings; -import org.elasticsearch.test.ElasticsearchIntegrationTest; -import org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope; -import org.elasticsearch.test.InternalTestCluster; -import org.junit.Test; - -import java.io.IOException; -import java.util.LinkedHashMap; -import java.util.Map; - -import static org.elasticsearch.client.Requests.clusterHealthRequest; -import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; -import static org.elasticsearch.test.ElasticsearchIntegrationTest.Scope; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; -import static org.hamcrest.Matchers.equalTo; - -/** - * - */ -@ClusterScope(scope = Scope.TEST, numDataNodes = 0) -public class MetaDataWriteDataNodesTests extends ElasticsearchIntegrationTest { - - @Test - public void testMetaWrittenAlsoOnDataNode() throws Exception { - // this test checks that index state is written on data only nodes - String masterNodeName = startMasterNode(); - String redNode = startDataNode("red"); - assertAcked(prepareCreate("test").setSettings(ImmutableSettings.builder().put("index.number_of_replicas", 0))); - index("test", "doc", "1", jsonBuilder().startObject().field("text", "some text").endObject()); - waitForConcreteMappingsOnAll("test", "doc", "text"); - ensureGreen("test"); - assertIndexInMetaState(redNode, "test"); - assertIndexInMetaState(masterNodeName, "test"); - //stop master node and start again with an empty data folder - ((InternalTestCluster) cluster()).stopCurrentMasterNode(); - String newMasterNode = startMasterNode(); - ensureGreen("test"); - // wait for mapping also on master becasue then we can be sure the state was written - waitForConcreteMappingsOnAll("test", "doc", "text"); - // check for meta data - assertIndexInMetaState(redNode, "test"); - assertIndexInMetaState(newMasterNode, "test"); - // check if index and doc is still there - ensureGreen("test"); - assertTrue(client().prepareGet("test", "doc", "1").get().isExists()); - } - - @Test - public void testMetaWrittenOnlyForIndicesOnNodesThatHaveAShard() throws Exception { - // this test checks that the index state is only written to a data only node if they have a shard of that index allocated on the node - String masterNode = startMasterNode(); - String blueNode = startDataNode("blue"); - String redNode = startDataNode("red"); - - assertAcked(prepareCreate("blue_index").setSettings(ImmutableSettings.builder().put("index.number_of_replicas", 0).put(FilterAllocationDecider.INDEX_ROUTING_INCLUDE_GROUP + "color", "blue"))); - index("blue_index", "doc", "1", jsonBuilder().startObject().field("text", "some text").endObject()); - assertAcked(prepareCreate("red_index").setSettings(ImmutableSettings.builder().put("index.number_of_replicas", 0).put(FilterAllocationDecider.INDEX_ROUTING_INCLUDE_GROUP + "color", "red"))); - index("red_index", "doc", "1", jsonBuilder().startObject().field("text", "some text").endObject()); - ensureGreen(); - waitForConcreteMappingsOnAll("blue_index", "doc", "text"); - waitForConcreteMappingsOnAll("red_index", "doc", "text"); - assertIndexNotInMetaState(blueNode, "red_index"); - assertIndexNotInMetaState(redNode, "blue_index"); - assertIndexInMetaState(blueNode, "blue_index"); - assertIndexInMetaState(redNode, "red_index"); - assertIndexInMetaState(masterNode, "red_index"); - assertIndexInMetaState(masterNode, "blue_index"); - - // not the index state for blue_index should only be written on blue_node and the for red_index only on red_node - // we restart red node and master but with empty data folders - stopNode(redNode); - ((InternalTestCluster) cluster()).stopCurrentMasterNode(); - masterNode = startMasterNode(); - redNode = startDataNode("red"); - - ensureGreen(); - assertIndexNotInMetaState(blueNode, "red_index"); - assertIndexInMetaState(blueNode, "blue_index"); - assertIndexNotInMetaState(redNode, "red_index"); - assertIndexNotInMetaState(redNode, "blue_index"); - assertIndexNotInMetaState(masterNode, "red_index"); - assertIndexInMetaState(masterNode, "blue_index"); - // check that blue index is still there - assertFalse(client().admin().indices().prepareExists("red_index").get().isExists()); - assertTrue(client().prepareGet("blue_index", "doc", "1").get().isExists()); - // red index should be gone - // if the blue node had stored the index state then cluster health would be red and red_index would exist - assertFalse(client().admin().indices().prepareExists("red_index").get().isExists()); - - } - - @Test - public void testMetaIsRemovedIfAllShardsFromIndexRemoved() throws Exception { - // this test checks that the index state is removed from a data only node once all shards have been allocated away from it - String masterNode = startMasterNode(); - String blueNode = startDataNode("blue"); - String redNode = startDataNode("red"); - - // create blue_index on blue_node and same for red - client().admin().cluster().health(clusterHealthRequest().waitForYellowStatus().waitForNodes("3")).get(); - assertAcked(prepareCreate("blue_index").setSettings(ImmutableSettings.builder().put("index.number_of_replicas", 0).put(FilterAllocationDecider.INDEX_ROUTING_INCLUDE_GROUP + "color", "blue"))); - index("blue_index", "doc", "1", jsonBuilder().startObject().field("text", "some text").endObject()); - assertAcked(prepareCreate("red_index").setSettings(ImmutableSettings.builder().put("index.number_of_replicas", 0).put(FilterAllocationDecider.INDEX_ROUTING_INCLUDE_GROUP + "color", "red"))); - index("red_index", "doc", "1", jsonBuilder().startObject().field("text", "some text").endObject()); - - ensureGreen(); - assertIndexNotInMetaState(redNode, "blue_index"); - assertIndexNotInMetaState(blueNode, "red_index"); - assertIndexInMetaState(redNode, "red_index"); - assertIndexInMetaState(blueNode, "blue_index"); - assertIndexInMetaState(masterNode, "red_index"); - assertIndexInMetaState(masterNode, "blue_index"); - - // now relocate blue_index to red_node and red_index to blue_node - logger.debug("relocating indices..."); - client().admin().indices().prepareUpdateSettings("blue_index").setSettings(ImmutableSettings.builder().put(FilterAllocationDecider.INDEX_ROUTING_INCLUDE_GROUP + "color", "red")).get(); - client().admin().indices().prepareUpdateSettings("red_index").setSettings(ImmutableSettings.builder().put(FilterAllocationDecider.INDEX_ROUTING_INCLUDE_GROUP + "color", "blue")).get(); - client().admin().cluster().prepareHealth().setWaitForRelocatingShards(0).get(); - ensureGreen(); - assertIndexNotInMetaState(redNode, "red_index"); - assertIndexNotInMetaState(blueNode, "blue_index"); - assertIndexInMetaState(redNode, "blue_index"); - assertIndexInMetaState(blueNode, "red_index"); - assertIndexInMetaState(masterNode, "red_index"); - assertIndexInMetaState(masterNode, "blue_index"); - waitForConcreteMappingsOnAll("blue_index", "doc", "text"); - waitForConcreteMappingsOnAll("red_index", "doc", "text"); - - //at this point the blue_index is on red node and the red_index on blue node - // now, when we start red and master node again but without data folder, the red index should be gone but the blue index should initialize fine - stopNode(redNode); - ((InternalTestCluster) cluster()).stopCurrentMasterNode(); - masterNode = startMasterNode(); - redNode = startDataNode("red"); - ensureGreen(); - assertIndexNotInMetaState(redNode, "blue_index"); - assertIndexNotInMetaState(blueNode, "blue_index"); - assertIndexNotInMetaState(redNode, "red_index"); - assertIndexInMetaState(blueNode, "red_index"); - assertIndexInMetaState(masterNode, "red_index"); - assertIndexNotInMetaState(masterNode, "blue_index"); - assertTrue(client().prepareGet("red_index", "doc", "1").get().isExists()); - // if the red_node had stored the index state then cluster health would be red and blue_index would exist - assertFalse(client().admin().indices().prepareExists("blue_index").get().isExists()); - } - - @Test - public void testMetaWrittenWhenIndexIsClosed() throws Exception { - String masterNode = startMasterNode(); - String redNodeDataPath = createTempDir().toString(); - String redNode = startDataNode("red", redNodeDataPath); - String blueNode = startDataNode("blue"); - // create red_index on red_node and same for red - client().admin().cluster().health(clusterHealthRequest().waitForYellowStatus().waitForNodes("3")).get(); - assertAcked(prepareCreate("red_index").setSettings(ImmutableSettings.builder().put("index.number_of_replicas", 0).put(FilterAllocationDecider.INDEX_ROUTING_INCLUDE_GROUP + "color", "red"))); - index("red_index", "doc", "1", jsonBuilder().startObject().field("text", "some text").endObject()); - - ensureGreen(); - assertIndexNotInMetaState(blueNode, "red_index"); - assertIndexInMetaState(redNode, "red_index"); - assertIndexInMetaState(masterNode, "red_index"); - - waitForConcreteMappingsOnAll("red_index", "doc", "text"); - client().admin().indices().prepareClose("red_index").get(); - // close the index - ClusterStateResponse clusterStateResponse = client().admin().cluster().prepareState().get(); - assertThat(clusterStateResponse.getState().getMetaData().index("red_index").getState().name(), equalTo(IndexMetaData.State.CLOSE.name())); - - // restart master with empty data folder and maybe red node - boolean restartRedNode = randomBoolean(); - //at this point the red_index on red node - if (restartRedNode) { - stopNode(redNode); - } - ((InternalTestCluster) cluster()).stopCurrentMasterNode(); - masterNode = startMasterNode(); - if (restartRedNode) { - redNode = startDataNode("red", redNodeDataPath); - } - - ensureGreen("red_index"); - assertIndexNotInMetaState(blueNode, "red_index"); - assertIndexInMetaState(redNode, "red_index"); - assertIndexInMetaState(masterNode, "red_index"); - clusterStateResponse = client().admin().cluster().prepareState().get(); - assertThat(clusterStateResponse.getState().getMetaData().index("red_index").getState().name(), equalTo(IndexMetaData.State.CLOSE.name())); - - // open the index again - client().admin().indices().prepareOpen("red_index").get(); - clusterStateResponse = client().admin().cluster().prepareState().get(); - assertThat(clusterStateResponse.getState().getMetaData().index("red_index").getState().name(), equalTo(IndexMetaData.State.OPEN.name())); - // restart again - ensureGreen(); - if (restartRedNode) { - stopNode(redNode); - } - ((InternalTestCluster) cluster()).stopCurrentMasterNode(); - masterNode = startMasterNode(); - if (restartRedNode) { - redNode = startDataNode("red", redNodeDataPath); - } - ensureGreen("red_index"); - assertIndexNotInMetaState(blueNode, "red_index"); - assertIndexInMetaState(redNode, "red_index"); - assertIndexInMetaState(masterNode, "red_index"); - clusterStateResponse = client().admin().cluster().prepareState().get(); - assertThat(clusterStateResponse.getState().getMetaData().index("red_index").getState().name(), equalTo(IndexMetaData.State.OPEN.name())); - assertTrue(client().prepareGet("red_index", "doc", "1").get().isExists()); - } - @Test - public void testMetaWrittenWhenIndexIsClosedAndMetaUpdated() throws Exception { - String masterNode = startMasterNode(); - String redNodeDataPath = createTempDir().toString(); - String redNode = startDataNode("red", redNodeDataPath); - // create red_index on red_node and same for red - client().admin().cluster().health(clusterHealthRequest().waitForYellowStatus().waitForNodes("2")).get(); - assertAcked(prepareCreate("red_index").setSettings(ImmutableSettings.builder().put("index.number_of_replicas", 0).put(FilterAllocationDecider.INDEX_ROUTING_INCLUDE_GROUP + "color", "red"))); - index("red_index", "doc", "1", jsonBuilder().startObject().field("text", "some text").endObject()); - - logger.info("--> wait for green red_index"); - ensureGreen(); - logger.info("--> wait for meta state written for red_index"); - assertIndexInMetaState(redNode, "red_index"); - assertIndexInMetaState(masterNode, "red_index"); - - waitForConcreteMappingsOnAll("red_index", "doc", "text"); - - logger.info("--> close red_index"); - client().admin().indices().prepareClose("red_index").get(); - // close the index - ClusterStateResponse clusterStateResponse = client().admin().cluster().prepareState().get(); - assertThat(clusterStateResponse.getState().getMetaData().index("red_index").getState().name(), equalTo(IndexMetaData.State.CLOSE.name())); - - logger.info("--> restart red node"); - stopNode(redNode); - redNode = startDataNode("red", redNodeDataPath); - client().admin().indices().preparePutMapping("red_index").setType("doc").setSource(jsonBuilder().startObject() - .startObject("properties") - .startObject("integer_field") - .field("type", "integer") - .endObject() - .endObject() - .endObject()).get(); - - GetMappingsResponse getMappingsResponse = client().admin().indices().prepareGetMappings("red_index").addTypes("doc").get(); - assertNotNull(((LinkedHashMap)(getMappingsResponse.getMappings().get("red_index").get("doc").getSourceAsMap().get("properties"))).get("integer_field")); - // restart master with empty data folder and maybe red node - ((InternalTestCluster) cluster()).stopCurrentMasterNode(); - masterNode = startMasterNode(); - - ensureGreen("red_index"); - assertIndexInMetaState(redNode, "red_index"); - assertIndexInMetaState(masterNode, "red_index"); - clusterStateResponse = client().admin().cluster().prepareState().get(); - assertThat(clusterStateResponse.getState().getMetaData().index("red_index").getState().name(), equalTo(IndexMetaData.State.CLOSE.name())); - getMappingsResponse = client().admin().indices().prepareGetMappings("red_index").addTypes("doc").get(); - assertNotNull(((LinkedHashMap)(getMappingsResponse.getMappings().get("red_index").get("doc").getSourceAsMap().get("properties"))).get("integer_field")); - - } - - private String startDataNode(String color) { - return startDataNode(color, createTempDir().toString()); - } - - private String startDataNode(String color, String newDataPath) { - ImmutableSettings.Builder settingsBuilder = ImmutableSettings.builder() - .put("node.data", true) - .put("node.master", false) - .put("node.color", color) - .put("path.data", newDataPath); - return internalCluster().startNode(settingsBuilder.build()); - } - - private String startMasterNode() { - ImmutableSettings.Builder settingsBuilder = ImmutableSettings.builder() - .put("node.data", false) - .put("node.master", true) - .put("path.data", createTempDir().toString()); - return internalCluster().startNode(settingsBuilder.build()); - } - - private void stopNode(String name) throws IOException { - internalCluster().stopRandomNode(InternalTestCluster.nameFilter(name)); - } - - protected void assertIndexNotInMetaState(String nodeName, String indexName) throws Exception { - assertMetaState(nodeName, indexName, false); - } - - protected void assertIndexInMetaState(String nodeName, String indexName) throws Exception { - assertMetaState(nodeName, indexName, true); - } - - private void assertMetaState(final String nodeName, final String indexName, final boolean shouldBe) throws Exception { - awaitBusy(new Predicate() { - @Override - public boolean apply(Object o) { - logger.info("checking if meta state exists..."); - return shouldBe == metaStateExists(nodeName, indexName); - } - }); - boolean inMetaSate = metaStateExists(nodeName, indexName); - if (shouldBe) { - assertTrue("expected " + indexName + " in meta state of node " + nodeName, inMetaSate); - } else { - assertFalse("expected " + indexName + " to not be in meta state of node " + nodeName, inMetaSate); - } - } - - private boolean metaStateExists(String nodeName, String indexName) { - GatewayMetaState redNodeMetaState = ((InternalTestCluster) cluster()).getInstance(GatewayMetaState.class, nodeName); - MetaData redNodeMetaData = null; - try { - redNodeMetaData = redNodeMetaState.loadMetaState(); - } catch (Exception e) { - fail("failed to load meta state"); - } - ImmutableOpenMap indices = redNodeMetaData.getIndices(); - boolean inMetaSate = false; - for (ObjectObjectCursor index : indices) { - inMetaSate = inMetaSate || index.key.equals(indexName); - } - return inMetaSate; - } -} From eb44e950d494d1edb16bf03a84f13f26ef093630 Mon Sep 17 00:00:00 2001 From: javanna Date: Wed, 29 Apr 2015 17:23:33 +0200 Subject: [PATCH 75/85] Java Api: remove unused private static class PartialField from SearchSourceBuilder Partial fields have been removed from master a while ago, this is a leftover. --- .../search/builder/SearchSourceBuilder.java | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java b/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java index 892cc2085ae..4f54fc1768f 100644 --- a/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java +++ b/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java @@ -885,34 +885,4 @@ public class SearchSourceBuilder implements ToXContent { return params; } } - - private static class PartialField { - private final String name; - private final String[] includes; - private final String[] excludes; - - private PartialField(String name, String[] includes, String[] excludes) { - this.name = name; - this.includes = includes; - this.excludes = 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}; - } - - public String name() { - return name; - } - - public String[] includes() { - return includes; - } - - public String[] excludes() { - return excludes; - } - } } From d4463602f68f039069d9fe8ceaf16a639fc52c9f Mon Sep 17 00:00:00 2001 From: Simon Willnauer Date: Wed, 29 Apr 2015 17:51:21 +0200 Subject: [PATCH 76/85] [TEST] Use a high shard delete timeout when clusterstates are delayed `IndiceStore#indexCleanup` uses a disruption scheme to delay cluster state processing. Yet, the delay is [1..2] seconds but tests are setting the shard deletion timeout to 1 second to speed up tests. This can cause random not reproducible failures in this test since the timeouts and delays are bascially overlapping. This commit adds a longer timeout for this test to prevent these problems. --- .../indices/store/IndicesStore.java | 97 ++++++++++--------- .../store/IndicesStoreIntegrationTests.java | 15 ++- 2 files changed, 60 insertions(+), 52 deletions(-) diff --git a/src/main/java/org/elasticsearch/indices/store/IndicesStore.java b/src/main/java/org/elasticsearch/indices/store/IndicesStore.java index 32e695a828c..36c9be862ee 100644 --- a/src/main/java/org/elasticsearch/indices/store/IndicesStore.java +++ b/src/main/java/org/elasticsearch/indices/store/IndicesStore.java @@ -332,56 +332,57 @@ public class IndicesStore extends AbstractComponent implements ClusterStateListe // make sure shard is really there before register cluster state observer if (indexShard == null) { channel.sendResponse(new ShardActiveResponse(false, clusterService.localNode())); - } - // create observer here. we need to register it here because we need to capture the current cluster state - // which will then be compared to the one that is applied when we call waitForNextChange(). if we create it - // later we might miss an update and wait forever in case no new cluster state comes in. - // in general, using a cluster state observer here is a workaround for the fact that we cannot listen on shard state changes explicitly. - // instead we wait for the cluster state changes because we know any shard state change will trigger or be - // triggered by a cluster state change. - ClusterStateObserver observer = new ClusterStateObserver(clusterService, request.timeout, logger); - // check if shard is active. if so, all is good - boolean shardActive = shardActive(indexShard); - if (shardActive) { - channel.sendResponse(new ShardActiveResponse(true, clusterService.localNode())); } else { - // shard is not active, might be POST_RECOVERY so check if cluster state changed inbetween or wait for next change - observer.waitForNextChange(new ClusterStateObserver.Listener() { - @Override - public void onNewClusterState(ClusterState state) { - sendResult(shardActive(getShard(request))); - } - - @Override - public void onClusterServiceClose() { - sendResult(false); - } - - @Override - public void onTimeout(TimeValue timeout) { - sendResult(shardActive(getShard(request))); - } - - public void sendResult(boolean shardActive) { - try { - channel.sendResponse(new ShardActiveResponse(shardActive, clusterService.localNode())); - } catch (IOException e) { - logger.error("failed send response for shard active while trying to delete shard {} - shard will probably not be removed", e, request.shardId); - } catch (EsRejectedExecutionException e) { - logger.error("failed send response for shard active while trying to delete shard {} - shard will probably not be removed", e, request.shardId); + // create observer here. we need to register it here because we need to capture the current cluster state + // which will then be compared to the one that is applied when we call waitForNextChange(). if we create it + // later we might miss an update and wait forever in case no new cluster state comes in. + // in general, using a cluster state observer here is a workaround for the fact that we cannot listen on shard state changes explicitly. + // instead we wait for the cluster state changes because we know any shard state change will trigger or be + // triggered by a cluster state change. + ClusterStateObserver observer = new ClusterStateObserver(clusterService, request.timeout, logger); + // check if shard is active. if so, all is good + boolean shardActive = shardActive(indexShard); + if (shardActive) { + channel.sendResponse(new ShardActiveResponse(true, clusterService.localNode())); + } else { + // shard is not active, might be POST_RECOVERY so check if cluster state changed inbetween or wait for next change + observer.waitForNextChange(new ClusterStateObserver.Listener() { + @Override + public void onNewClusterState(ClusterState state) { + sendResult(shardActive(getShard(request))); } - } - }, new ClusterStateObserver.ValidationPredicate() { - @Override - protected boolean validate(ClusterState newState) { - // the shard is not there in which case we want to send back a false (shard is not active), so the cluster state listener must be notified - // or the shard is active in which case we want to send back that the shard is active - // here we could also evaluate the cluster state and get the information from there. we - // don't do it because we would have to write another method for this that would have the same effect - IndexShard indexShard = getShard(request); - return indexShard == null || shardActive(indexShard); - } - }); + + @Override + public void onClusterServiceClose() { + sendResult(false); + } + + @Override + public void onTimeout(TimeValue timeout) { + sendResult(shardActive(getShard(request))); + } + + public void sendResult(boolean shardActive) { + try { + channel.sendResponse(new ShardActiveResponse(shardActive, clusterService.localNode())); + } catch (IOException e) { + logger.error("failed send response for shard active while trying to delete shard {} - shard will probably not be removed", e, request.shardId); + } catch (EsRejectedExecutionException e) { + logger.error("failed send response for shard active while trying to delete shard {} - shard will probably not be removed", e, request.shardId); + } + } + }, new ClusterStateObserver.ValidationPredicate() { + @Override + protected boolean validate(ClusterState newState) { + // the shard is not there in which case we want to send back a false (shard is not active), so the cluster state listener must be notified + // or the shard is active in which case we want to send back that the shard is active + // here we could also evaluate the cluster state and get the information from there. we + // don't do it because we would have to write another method for this that would have the same effect + IndexShard indexShard = getShard(request); + return indexShard == null || shardActive(indexShard); + } + }); + } } } diff --git a/src/test/java/org/elasticsearch/indices/store/IndicesStoreIntegrationTests.java b/src/test/java/org/elasticsearch/indices/store/IndicesStoreIntegrationTests.java index e1efe59776d..386c778b07e 100644 --- a/src/test/java/org/elasticsearch/indices/store/IndicesStoreIntegrationTests.java +++ b/src/test/java/org/elasticsearch/indices/store/IndicesStoreIntegrationTests.java @@ -31,6 +31,7 @@ import org.elasticsearch.cluster.routing.allocation.command.MoveAllocationComman import org.elasticsearch.common.Priority; import org.elasticsearch.common.settings.ImmutableSettings; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.discovery.DiscoveryService; import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.index.Index; @@ -45,6 +46,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import static org.elasticsearch.test.ElasticsearchIntegrationTest.Scope; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; @@ -58,7 +60,12 @@ public class IndicesStoreIntegrationTests extends ElasticsearchIntegrationTest { @Override protected Settings nodeSettings(int nodeOrdinal) { // simplify this and only use a single data path - return ImmutableSettings.settingsBuilder().put(super.nodeSettings(nodeOrdinal)).put("path.data", "").build(); + return ImmutableSettings.settingsBuilder().put(super.nodeSettings(nodeOrdinal)).put("path.data", "") + // by default this value is 1 sec in tests (30 sec in practice) but we adding disruption here + // which is between 1 and 2 sec can cause each of the shard deletion requests to timeout. + // to prevent this we are setting the timeout here to something highish ie. the default in practice + .put(IndicesStore.INDICES_STORE_DELETE_SHARD_TIMEOUT, new TimeValue(30, TimeUnit.SECONDS)) + .build(); } @Test @@ -97,9 +104,8 @@ public class IndicesStoreIntegrationTests extends ElasticsearchIntegrationTest { assertThat(Files.exists(indexDirectory(node_3, "test")), equalTo(false)); logger.info("--> move shard from node_1 to node_3, and wait for relocation to finish"); - SlowClusterStateProcessing disruption = null; - if (randomBoolean()) { - disruption = new SlowClusterStateProcessing(node_3, getRandom(), 0, 0, 1000, 2000); + if (randomBoolean()) { // sometimes add cluster-state delay to trigger observers in IndicesStore.ShardActiveRequestHandler + final SlowClusterStateProcessing disruption = new SlowClusterStateProcessing(node_3, getRandom(), 0, 0, 1000, 2000); internalCluster().setDisruptionScheme(disruption); disruption.startDisrupting(); } @@ -116,6 +122,7 @@ public class IndicesStoreIntegrationTests extends ElasticsearchIntegrationTest { assertThat(Files.exists(indexDirectory(node_2, "test")), equalTo(true)); assertThat(Files.exists(shardDirectory(node_3, "test", 0)), equalTo(true)); assertThat(Files.exists(indexDirectory(node_3, "test")), equalTo(true)); + } @Test From 3c3e9b63a7ae59c276cdf098ae96f24a6faaba6f Mon Sep 17 00:00:00 2001 From: David Pilato Date: Wed, 29 Apr 2015 18:10:02 +0200 Subject: [PATCH 77/85] fix: query string time zone not working If you define exactly the same date range query using either `DATE+0200` notation or `DATE` and set `timezone: +0200`, elasticsearch gives back different results: ``` DELETE foo PUT /foo { "mapping": { "tweets": { "properties": { "tweet_date": { "type": "date" } } } } } POST /foo/tweets/1/ { "tweet_date": "2015-04-05T23:00:00+0000" } POST /foo/tweets/2/ { "tweet_date": "2015-04-06T00:00:00+0000" } GET /foo/tweets/_search?pretty { "query": { "query_string": { "query": "tweet_date:[2015-04-06T00:00:00+0200 TO 2015-04-06T23:00:00+0200]" } } } GET /foo/tweets/_search?pretty { "query": { "query_string": { "query": "tweet_date:[2015-04-06T00:00:00 TO 2015-04-06T23:00:00]", "time_zone": "+0200" } } } ``` This PR fixes it and will also allow us to add the same feature to simple_query_string as well in another PR. Closes #10477. (cherry picked from commit 880f4a0) --- .../classic/MapperQueryParser.java | 13 +++++-- .../search/query/SearchQueryTests.java | 38 +++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/apache/lucene/queryparser/classic/MapperQueryParser.java b/src/main/java/org/apache/lucene/queryparser/classic/MapperQueryParser.java index c87f9144709..d55374cd5b9 100644 --- a/src/main/java/org/apache/lucene/queryparser/classic/MapperQueryParser.java +++ b/src/main/java/org/apache/lucene/queryparser/classic/MapperQueryParser.java @@ -39,6 +39,7 @@ import org.elasticsearch.common.lucene.search.Queries; import org.elasticsearch.common.unit.Fuzziness; import org.elasticsearch.index.mapper.FieldMapper; import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.mapper.core.DateFieldMapper; import org.elasticsearch.index.query.QueryParseContext; import org.elasticsearch.index.query.support.QueryParsers; @@ -131,9 +132,6 @@ public class MapperQueryParser extends QueryParser { setFuzzyMinSim(settings.fuzzyMinSim()); setFuzzyPrefixLength(settings.fuzzyPrefixLength()); setLocale(settings.locale()); - if (settings.timeZone() != null) { - setTimeZone(settings.timeZone().toTimeZone()); - } this.analyzeWildcard = settings.analyzeWildcard(); } @@ -377,7 +375,14 @@ public class MapperQueryParser extends QueryParser { } try { - return currentMapper.rangeQuery(part1, part2, startInclusive, endInclusive, parseContext); + Query rangeQuery; + if (currentMapper instanceof DateFieldMapper && settings.timeZone() != null) { + DateFieldMapper dateFieldMapper = (DateFieldMapper) this.currentMapper; + rangeQuery = dateFieldMapper.rangeQuery(part1, part2, startInclusive, endInclusive, settings.timeZone(), null, parseContext); + } else { + rangeQuery = currentMapper.rangeQuery(part1, part2, startInclusive, endInclusive, parseContext); + } + return rangeQuery; } catch (RuntimeException e) { if (settings.lenient()) { return null; diff --git a/src/test/java/org/elasticsearch/search/query/SearchQueryTests.java b/src/test/java/org/elasticsearch/search/query/SearchQueryTests.java index 6a575fc38c7..8d8e948f769 100644 --- a/src/test/java/org/elasticsearch/search/query/SearchQueryTests.java +++ b/src/test/java/org/elasticsearch/search/query/SearchQueryTests.java @@ -587,6 +587,44 @@ public class SearchQueryTests extends ElasticsearchIntegrationTest { assertHitCount(searchResponse, 1l); } + @Test // https://github.com/elasticsearch/elasticsearch/issues/10477 + public void testDateRangeInQueryStringWithTimeZone_10477() { + //the mapping needs to be provided upfront otherwise we are not sure how many failures we get back + //as with dynamic mappings some shards might be lacking behind and parse a different query + assertAcked(prepareCreate("test").addMapping( + "type", "past", "type=date" + )); + ensureGreen(); + + client().prepareIndex("test", "type", "1").setSource("past", "2015-04-05T23:00:00+0000").get(); + client().prepareIndex("test", "type", "2").setSource("past", "2015-04-06T00:00:00+0000").get(); + refresh(); + + // Timezone set with dates + SearchResponse searchResponse = client().prepareSearch() + .setQuery(queryStringQuery("past:[2015-04-06T00:00:00+0200 TO 2015-04-06T23:00:00+0200]")) + .get(); + assertHitCount(searchResponse, 2l); + + // Same timezone set with time_zone + searchResponse = client().prepareSearch() + .setQuery(queryStringQuery("past:[2015-04-06T00:00:00 TO 2015-04-06T23:00:00]").timeZone("+0200")) + .get(); + assertHitCount(searchResponse, 2l); + + // We set a timezone which will give no result + searchResponse = client().prepareSearch() + .setQuery(queryStringQuery("past:[2015-04-06T00:00:00-0200 TO 2015-04-06T23:00:00-0200]")) + .get(); + assertHitCount(searchResponse, 0l); + + // Same timezone set with time_zone but another timezone is set directly within dates which has the precedence + searchResponse = client().prepareSearch() + .setQuery(queryStringQuery("past:[2015-04-06T00:00:00-0200 TO 2015-04-06T23:00:00-0200]").timeZone("+0200")) + .get(); + assertHitCount(searchResponse, 0l); + } + @Test public void typeFilterTypeIndexedTests() throws Exception { typeFilterTests("not_analyzed"); From 6e1c99574180c7798a2d544f0f60dc4509f4f69b Mon Sep 17 00:00:00 2001 From: Lee Hinman Date: Wed, 29 Apr 2015 10:40:51 -0600 Subject: [PATCH 78/85] Clarify logging about disk thresholds in DiskThresholdDecider --- .../routing/allocation/decider/DiskThresholdDecider.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDecider.java b/src/main/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDecider.java index 726a588d1bf..a3969dcc232 100644 --- a/src/main/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDecider.java +++ b/src/main/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDecider.java @@ -142,19 +142,19 @@ public class DiskThresholdDecider extends AllocationDecider { private void warnAboutDiskIfNeeded(DiskUsage usage) { // Check absolute disk values if (usage.getFreeBytes() < DiskThresholdDecider.this.freeBytesThresholdHigh.bytes()) { - logger.warn("high disk watermark [{}] exceeded on {}, shards will be relocated away from this node", + logger.warn("high disk watermark [{} free] exceeded on {}, shards will be relocated away from this node", DiskThresholdDecider.this.freeBytesThresholdHigh, usage); } else if (usage.getFreeBytes() < DiskThresholdDecider.this.freeBytesThresholdLow.bytes()) { - logger.info("low disk watermark [{}] exceeded on {}, replicas will not be assigned to this node", + logger.info("low disk watermark [{} free] exceeded on {}, replicas will not be assigned to this node", DiskThresholdDecider.this.freeBytesThresholdLow, usage); } // Check percentage disk values if (usage.getFreeDiskAsPercentage() < DiskThresholdDecider.this.freeDiskThresholdHigh) { - logger.warn("high disk watermark [{}] exceeded on {}, shards will be relocated away from this node", + logger.warn("high disk watermark [{} free] exceeded on {}, shards will be relocated away from this node", Strings.format1Decimals(DiskThresholdDecider.this.freeDiskThresholdHigh, "%"), usage); } else if (usage.getFreeDiskAsPercentage() < DiskThresholdDecider.this.freeDiskThresholdLow) { - logger.info("low disk watermark [{}] exceeded on {}, replicas will not be assigned to this node", + logger.info("low disk watermark [{} free] exceeded on {}, replicas will not be assigned to this node", Strings.format1Decimals(DiskThresholdDecider.this.freeDiskThresholdLow, "%"), usage); } } From 478c253f8929682675298cd9e491963090b897b8 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Sun, 26 Apr 2015 21:04:41 -0400 Subject: [PATCH 79/85] Add support for cluster state diffs Adds support for calculating and sending diffs instead of full cluster state of the most frequently changing elements - cluster state, meta data and routing table. Closes #6295 --- .../reroute/ClusterRerouteResponse.java | 2 +- .../cluster/state/ClusterStateResponse.java | 2 +- .../state/TransportClusterStateAction.java | 15 +- .../indices/alias/get/GetAliasesResponse.java | 2 +- .../indices/create/CreateIndexRequest.java | 10 +- .../admin/indices/get/GetIndexResponse.java | 6 +- .../mapping/get/GetMappingsResponse.java | 4 +- .../get/GetIndexTemplatesResponse.java | 2 +- .../template/put/PutIndexTemplateRequest.java | 10 +- .../cluster/AbstractDiffable.java | 108 +++ .../elasticsearch/cluster/ClusterState.java | 272 ++++++-- .../java/org/elasticsearch/cluster/Diff.java | 42 ++ .../org/elasticsearch/cluster/Diffable.java | 42 ++ .../elasticsearch/cluster/DiffableUtils.java | 283 ++++++++ ...ompatibleClusterStateVersionException.java | 35 + .../cluster/block/ClusterBlocks.java | 75 ++- .../cluster/metadata/AliasMetaData.java | 85 ++- .../cluster/metadata/IndexMetaData.java | 234 ++++--- .../metadata/IndexTemplateMetaData.java | 105 +-- .../cluster/metadata/MappingMetaData.java | 48 +- .../cluster/metadata/MetaData.java | 266 +++++--- .../metadata/MetaDataCreateIndexService.java | 2 +- .../metadata/RepositoriesMetaData.java | 236 +++---- .../cluster/metadata/RepositoryMetaData.java | 21 + .../cluster/metadata/RestoreMetaData.java | 220 +++--- .../cluster/metadata/SnapshotMetaData.java | 223 ++++--- .../cluster/node/DiscoveryNodes.java | 73 +- .../cluster/routing/IndexRoutingTable.java | 72 +- .../routing/IndexShardRoutingTable.java | 22 + .../cluster/routing/RoutingTable.java | 90 ++- .../service/InternalClusterService.java | 8 +- .../ClusterDynamicSettingsModule.java | 1 + .../common/io/stream/StreamableReader.java | 30 + .../common/io/stream/Writeable.java | 30 + .../elasticsearch/discovery/Discovery.java | 3 +- .../discovery/DiscoveryService.java | 5 +- .../discovery/DiscoverySettings.java | 13 + .../discovery/local/LocalDiscovery.java | 46 +- .../discovery/zen/ZenDiscovery.java | 9 +- .../publish/PublishClusterStateAction.java | 194 ++++-- .../org/elasticsearch/gateway/Gateway.java | 2 +- .../gateway/LocalAllocateDangledIndices.java | 2 +- .../TransportNodesListGatewayMetaState.java | 2 +- .../get/RestGetRepositoriesAction.java | 2 +- .../indices/get/RestGetIndicesAction.java | 2 +- .../warmer/get/RestGetWarmerAction.java | 2 +- .../search/warmer/IndexWarmersMetaData.java | 318 +++++---- .../ClusterStateDiffPublishingTests.java | 625 ++++++++++++++++++ .../cluster/ClusterStateDiffTests.java | 534 +++++++++++++++ .../ClusterSerializationTests.java | 2 +- .../cluster/serialization/DiffableTests.java | 127 ++++ .../common/xcontent/XContentTestUtils.java | 100 +++ .../discovery/ZenUnicastDiscoveryTests.java | 1 + .../discovery/zen/ZenDiscoveryTests.java | 10 +- .../timestamp/TimestampMappingTests.java | 12 +- .../store/IndicesStoreIntegrationTests.java | 7 + .../template/SimpleIndexTemplateTests.java | 1 + .../DedicatedClusterSnapshotRestoreTests.java | 218 +++--- .../test/ElasticsearchIntegrationTest.java | 38 +- .../test/ElasticsearchTestCase.java | 14 + 60 files changed, 3831 insertions(+), 1134 deletions(-) create mode 100644 src/main/java/org/elasticsearch/cluster/AbstractDiffable.java create mode 100644 src/main/java/org/elasticsearch/cluster/Diff.java create mode 100644 src/main/java/org/elasticsearch/cluster/Diffable.java create mode 100644 src/main/java/org/elasticsearch/cluster/DiffableUtils.java create mode 100644 src/main/java/org/elasticsearch/cluster/IncompatibleClusterStateVersionException.java create mode 100644 src/main/java/org/elasticsearch/common/io/stream/StreamableReader.java create mode 100644 src/main/java/org/elasticsearch/common/io/stream/Writeable.java create mode 100644 src/test/java/org/elasticsearch/cluster/ClusterStateDiffPublishingTests.java create mode 100644 src/test/java/org/elasticsearch/cluster/ClusterStateDiffTests.java create mode 100644 src/test/java/org/elasticsearch/cluster/serialization/DiffableTests.java create mode 100644 src/test/java/org/elasticsearch/common/xcontent/XContentTestUtils.java diff --git a/src/main/java/org/elasticsearch/action/admin/cluster/reroute/ClusterRerouteResponse.java b/src/main/java/org/elasticsearch/action/admin/cluster/reroute/ClusterRerouteResponse.java index 79b31f620d5..28f9cb1db90 100644 --- a/src/main/java/org/elasticsearch/action/admin/cluster/reroute/ClusterRerouteResponse.java +++ b/src/main/java/org/elasticsearch/action/admin/cluster/reroute/ClusterRerouteResponse.java @@ -68,7 +68,7 @@ public class ClusterRerouteResponse extends AcknowledgedResponse { @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); - ClusterState.Builder.writeTo(state, out); + state.writeTo(out); writeAcknowledged(out); RoutingExplanations.writeTo(explanations, out); } diff --git a/src/main/java/org/elasticsearch/action/admin/cluster/state/ClusterStateResponse.java b/src/main/java/org/elasticsearch/action/admin/cluster/state/ClusterStateResponse.java index 861a84a9e71..e9aa9b723fa 100644 --- a/src/main/java/org/elasticsearch/action/admin/cluster/state/ClusterStateResponse.java +++ b/src/main/java/org/elasticsearch/action/admin/cluster/state/ClusterStateResponse.java @@ -62,6 +62,6 @@ public class ClusterStateResponse extends ActionResponse { public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); clusterName.writeTo(out); - ClusterState.Builder.writeTo(clusterState, out); + clusterState.writeTo(out); } } diff --git a/src/main/java/org/elasticsearch/action/admin/cluster/state/TransportClusterStateAction.java b/src/main/java/org/elasticsearch/action/admin/cluster/state/TransportClusterStateAction.java index 7b114c92d43..5c8905fd97b 100644 --- a/src/main/java/org/elasticsearch/action/admin/cluster/state/TransportClusterStateAction.java +++ b/src/main/java/org/elasticsearch/action/admin/cluster/state/TransportClusterStateAction.java @@ -19,7 +19,6 @@ package org.elasticsearch.action.admin.cluster.state; -import com.carrotsearch.hppc.cursors.ObjectCursor; import com.carrotsearch.hppc.cursors.ObjectObjectCursor; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; @@ -29,7 +28,6 @@ import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterService; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.block.ClusterBlockException; -import org.elasticsearch.cluster.block.ClusterBlockLevel; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.metadata.MetaData.Custom; @@ -39,11 +37,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; -import java.util.List; - -import static com.google.common.collect.Lists.newArrayList; -import static org.elasticsearch.cluster.metadata.MetaData.lookupFactorySafe; - /** * */ @@ -84,6 +77,7 @@ public class TransportClusterStateAction extends TransportMasterNodeReadOperatio logger.trace("Serving cluster state request using version {}", currentState.version()); ClusterState.Builder builder = ClusterState.builder(currentState.getClusterName()); builder.version(currentState.version()); + builder.uuid(currentState.uuid()); if (request.nodes()) { builder.nodes(currentState.nodes()); } @@ -122,10 +116,9 @@ public class TransportClusterStateAction extends TransportMasterNodeReadOperatio } // Filter our metadata that shouldn't be returned by API - for(ObjectCursor type : currentState.metaData().customs().keys()) { - Custom.Factory factory = lookupFactorySafe(type.value); - if(!factory.context().contains(MetaData.XContentContext.API)) { - mdBuilder.removeCustom(type.value); + for(ObjectObjectCursor custom : currentState.metaData().customs()) { + if(!custom.value.context().contains(MetaData.XContentContext.API)) { + mdBuilder.removeCustom(custom.key); } } diff --git a/src/main/java/org/elasticsearch/action/admin/indices/alias/get/GetAliasesResponse.java b/src/main/java/org/elasticsearch/action/admin/indices/alias/get/GetAliasesResponse.java index 765a9395afc..106e864a367 100644 --- a/src/main/java/org/elasticsearch/action/admin/indices/alias/get/GetAliasesResponse.java +++ b/src/main/java/org/elasticsearch/action/admin/indices/alias/get/GetAliasesResponse.java @@ -74,7 +74,7 @@ public class GetAliasesResponse extends ActionResponse { out.writeString(entry.key); out.writeVInt(entry.value.size()); for (AliasMetaData aliasMetaData : entry.value) { - AliasMetaData.Builder.writeTo(aliasMetaData, out); + aliasMetaData.writeTo(out); } } } diff --git a/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequest.java b/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequest.java index d79c2128611..60a265de785 100644 --- a/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequest.java +++ b/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequest.java @@ -396,11 +396,11 @@ public class CreateIndexRequest extends AcknowledgedRequest aliases((Map) entry.getValue()); } else { // maybe custom? - IndexMetaData.Custom.Factory factory = IndexMetaData.lookupFactory(name); - if (factory != null) { + IndexMetaData.Custom proto = IndexMetaData.lookupPrototype(name); + if (proto != null) { found = true; try { - customs.put(name, factory.fromMap((Map) entry.getValue())); + customs.put(name, proto.fromMap((Map) entry.getValue())); } catch (IOException e) { throw new ElasticsearchParseException("failed to parse custom metadata for [" + name + "]"); } @@ -448,7 +448,7 @@ public class CreateIndexRequest extends AcknowledgedRequest int customSize = in.readVInt(); for (int i = 0; i < customSize; i++) { String type = in.readString(); - IndexMetaData.Custom customIndexMetaData = IndexMetaData.lookupFactorySafe(type).readFrom(in); + IndexMetaData.Custom customIndexMetaData = IndexMetaData.lookupPrototypeSafe(type).readFrom(in); customs.put(type, customIndexMetaData); } int aliasesSize = in.readVInt(); @@ -472,7 +472,7 @@ public class CreateIndexRequest extends AcknowledgedRequest out.writeVInt(customs.size()); for (Map.Entry entry : customs.entrySet()) { out.writeString(entry.getKey()); - IndexMetaData.lookupFactorySafe(entry.getKey()).writeTo(entry.getValue(), out); + entry.getValue().writeTo(out); } out.writeVInt(aliases.size()); for (Alias alias : aliases) { diff --git a/src/main/java/org/elasticsearch/action/admin/indices/get/GetIndexResponse.java b/src/main/java/org/elasticsearch/action/admin/indices/get/GetIndexResponse.java index 35e6cfa4804..7080a694a11 100644 --- a/src/main/java/org/elasticsearch/action/admin/indices/get/GetIndexResponse.java +++ b/src/main/java/org/elasticsearch/action/admin/indices/get/GetIndexResponse.java @@ -134,7 +134,7 @@ public class GetIndexResponse extends ActionResponse { int valueSize = in.readVInt(); ImmutableOpenMap.Builder mappingEntryBuilder = ImmutableOpenMap.builder(); for (int j = 0; j < valueSize; j++) { - mappingEntryBuilder.put(in.readString(), MappingMetaData.readFrom(in)); + mappingEntryBuilder.put(in.readString(), MappingMetaData.PROTO.readFrom(in)); } mappingsMapBuilder.put(key, mappingEntryBuilder.build()); } @@ -181,7 +181,7 @@ public class GetIndexResponse extends ActionResponse { out.writeVInt(indexEntry.value.size()); for (ObjectObjectCursor mappingEntry : indexEntry.value) { out.writeString(mappingEntry.key); - MappingMetaData.writeTo(mappingEntry.value, out); + mappingEntry.value.writeTo(out); } } out.writeVInt(aliases.size()); @@ -189,7 +189,7 @@ public class GetIndexResponse extends ActionResponse { out.writeString(indexEntry.key); out.writeVInt(indexEntry.value.size()); for (AliasMetaData aliasEntry : indexEntry.value) { - AliasMetaData.Builder.writeTo(aliasEntry, out); + aliasEntry.writeTo(out); } } out.writeVInt(settings.size()); diff --git a/src/main/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponse.java b/src/main/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponse.java index b27577f8da3..30e9e24c493 100644 --- a/src/main/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponse.java +++ b/src/main/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponse.java @@ -59,7 +59,7 @@ public class GetMappingsResponse extends ActionResponse { int valueSize = in.readVInt(); ImmutableOpenMap.Builder typeMapBuilder = ImmutableOpenMap.builder(); for (int j = 0; j < valueSize; j++) { - typeMapBuilder.put(in.readString(), MappingMetaData.readFrom(in)); + typeMapBuilder.put(in.readString(), MappingMetaData.PROTO.readFrom(in)); } indexMapBuilder.put(key, typeMapBuilder.build()); } @@ -75,7 +75,7 @@ public class GetMappingsResponse extends ActionResponse { out.writeVInt(indexEntry.value.size()); for (ObjectObjectCursor typeEntry : indexEntry.value) { out.writeString(typeEntry.key); - MappingMetaData.writeTo(typeEntry.value, out); + typeEntry.value.writeTo(out); } } } diff --git a/src/main/java/org/elasticsearch/action/admin/indices/template/get/GetIndexTemplatesResponse.java b/src/main/java/org/elasticsearch/action/admin/indices/template/get/GetIndexTemplatesResponse.java index 56de19872f2..2ce6d8d2c1a 100644 --- a/src/main/java/org/elasticsearch/action/admin/indices/template/get/GetIndexTemplatesResponse.java +++ b/src/main/java/org/elasticsearch/action/admin/indices/template/get/GetIndexTemplatesResponse.java @@ -60,7 +60,7 @@ public class GetIndexTemplatesResponse extends ActionResponse { super.writeTo(out); out.writeVInt(indexTemplates.size()); for (IndexTemplateMetaData indexTemplate : indexTemplates) { - IndexTemplateMetaData.Builder.writeTo(indexTemplate, out); + indexTemplate.writeTo(out); } } } diff --git a/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequest.java b/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequest.java index 41dd9ec2b45..1b752855c20 100644 --- a/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequest.java +++ b/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequest.java @@ -292,10 +292,10 @@ public class PutIndexTemplateRequest extends MasterNodeOperationRequest) entry.getValue()); } else { // maybe custom? - IndexMetaData.Custom.Factory factory = IndexMetaData.lookupFactory(name); - if (factory != null) { + IndexMetaData.Custom proto = IndexMetaData.lookupPrototype(name); + if (proto != null) { try { - customs.put(name, factory.fromMap((Map) entry.getValue())); + customs.put(name, proto.fromMap((Map) entry.getValue())); } catch (IOException e) { throw new ElasticsearchParseException("failed to parse custom metadata for [" + name + "]"); } @@ -440,7 +440,7 @@ public class PutIndexTemplateRequest extends MasterNodeOperationRequest entry : customs.entrySet()) { out.writeString(entry.getKey()); - IndexMetaData.lookupFactorySafe(entry.getKey()).writeTo(entry.getValue(), out); + entry.getValue().writeTo(out); } out.writeVInt(aliases.size()); for (Alias alias : aliases) { diff --git a/src/main/java/org/elasticsearch/cluster/AbstractDiffable.java b/src/main/java/org/elasticsearch/cluster/AbstractDiffable.java new file mode 100644 index 00000000000..4e6da2bd569 --- /dev/null +++ b/src/main/java/org/elasticsearch/cluster/AbstractDiffable.java @@ -0,0 +1,108 @@ +/* + * 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.cluster; + +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.io.stream.StreamableReader; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +/** + * Abstract diffable object with simple diffs implementation that sends the entire object if object has changed or + * nothing is object remained the same. + */ +public abstract class AbstractDiffable> implements Diffable { + + @Override + public Diff diff(T previousState) { + if (this.get().equals(previousState)) { + return new CompleteDiff<>(); + } else { + return new CompleteDiff<>(get()); + } + } + + @Override + public Diff readDiffFrom(StreamInput in) throws IOException { + return new CompleteDiff<>(this, in); + } + + public static > Diff readDiffFrom(StreamableReader reader, StreamInput in) throws IOException { + return new CompleteDiff(reader, in); + } + + private static class CompleteDiff> implements Diff { + + @Nullable + private final T part; + + /** + * Creates simple diff with changes + */ + public CompleteDiff(T part) { + this.part = part; + } + + /** + * Creates simple diff without changes + */ + public CompleteDiff() { + this.part = null; + } + + /** + * Read simple diff from the stream + */ + public CompleteDiff(StreamableReader reader, StreamInput in) throws IOException { + if (in.readBoolean()) { + this.part = reader.readFrom(in); + } else { + this.part = null; + } + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + if (part != null) { + out.writeBoolean(true); + part.writeTo(out); + } else { + out.writeBoolean(false); + } + } + + @Override + public T apply(T part) { + if (this.part != null) { + return this.part; + } else { + return part; + } + } + } + + @SuppressWarnings("unchecked") + public T get() { + return (T) this; + } +} + diff --git a/src/main/java/org/elasticsearch/cluster/ClusterState.java b/src/main/java/org/elasticsearch/cluster/ClusterState.java index b90bc0bb2ac..4f63d9e00e3 100644 --- a/src/main/java/org/elasticsearch/cluster/ClusterState.java +++ b/src/main/java/org/elasticsearch/cluster/ClusterState.java @@ -22,6 +22,7 @@ package org.elasticsearch.cluster; import com.carrotsearch.hppc.cursors.ObjectCursor; import com.carrotsearch.hppc.cursors.ObjectObjectCursor; import com.google.common.collect.ImmutableSet; +import org.elasticsearch.cluster.DiffableUtils.KeyedReader; import org.elasticsearch.cluster.block.ClusterBlock; import org.elasticsearch.cluster.block.ClusterBlocks; import org.elasticsearch.cluster.metadata.IndexMetaData; @@ -55,7 +56,9 @@ import java.util.Map; /** * */ -public class ClusterState implements ToXContent { +public class ClusterState implements ToXContent, Diffable { + + public static final ClusterState PROTO = builder(ClusterName.DEFAULT).build(); public static enum ClusterStateStatus { UNKNOWN((byte) 0), @@ -74,47 +77,43 @@ public class ClusterState implements ToXContent { } } - public interface Custom { + public interface Custom extends Diffable, ToXContent { - interface Factory { - - String type(); - - T readFrom(StreamInput in) throws IOException; - - void writeTo(T customState, StreamOutput out) throws IOException; - - void toXContent(T customState, XContentBuilder builder, ToXContent.Params params); - } + String type(); } - private final static Map customFactories = new HashMap<>(); + private final static Map customPrototypes = new HashMap<>(); /** * Register a custom index meta data factory. Make sure to call it from a static block. */ - public static void registerFactory(String type, Custom.Factory factory) { - customFactories.put(type, factory); + public static void registerPrototype(String type, Custom proto) { + customPrototypes.put(type, proto); } @Nullable - public static Custom.Factory lookupFactory(String type) { - return customFactories.get(type); + public static T lookupPrototype(String type) { + //noinspection unchecked + return (T) customPrototypes.get(type); } - public static Custom.Factory lookupFactorySafe(String type) { - Custom.Factory factory = customFactories.get(type); - if (factory == null) { - throw new IllegalArgumentException("No custom state factory registered for type [" + type + "]"); + public static T lookupPrototypeSafe(String type) { + @SuppressWarnings("unchecked") + T proto = (T)customPrototypes.get(type); + if (proto == null) { + throw new IllegalArgumentException("No custom state prototype registered for type [" + type + "]"); } - return factory; + return proto; } + public static final String UNKNOWN_UUID = "_na_"; public static final long UNKNOWN_VERSION = -1; private final long version; + private final String uuid; + private final RoutingTable routingTable; private final DiscoveryNodes nodes; @@ -127,17 +126,20 @@ public class ClusterState implements ToXContent { private final ClusterName clusterName; + private final boolean wasReadFromDiff; + // built on demand private volatile RoutingNodes routingNodes; private volatile ClusterStateStatus status; - public ClusterState(long version, ClusterState state) { - this(state.clusterName, version, state.metaData(), state.routingTable(), state.nodes(), state.blocks(), state.customs()); + public ClusterState(long version, String uuid, ClusterState state) { + this(state.clusterName, version, uuid, state.metaData(), state.routingTable(), state.nodes(), state.blocks(), state.customs(), false); } - public ClusterState(ClusterName clusterName, long version, MetaData metaData, RoutingTable routingTable, DiscoveryNodes nodes, ClusterBlocks blocks, ImmutableOpenMap customs) { + public ClusterState(ClusterName clusterName, long version, String uuid, MetaData metaData, RoutingTable routingTable, DiscoveryNodes nodes, ClusterBlocks blocks, ImmutableOpenMap customs, boolean wasReadFromDiff) { this.version = version; + this.uuid = uuid; this.clusterName = clusterName; this.metaData = metaData; this.routingTable = routingTable; @@ -145,6 +147,7 @@ public class ClusterState implements ToXContent { this.blocks = blocks; this.customs = customs; this.status = ClusterStateStatus.UNKNOWN; + this.wasReadFromDiff = wasReadFromDiff; } public ClusterStateStatus status() { @@ -164,6 +167,14 @@ public class ClusterState implements ToXContent { return version(); } + /** + * This uuid is automatically generated for for each version of cluster state. It is used to make sure that + * we are applying diffs to the right previous state. + */ + public String uuid() { + return this.uuid; + } + public DiscoveryNodes nodes() { return this.nodes; } @@ -216,6 +227,11 @@ public class ClusterState implements ToXContent { return this.clusterName; } + // Used for testing and logging to determine how this cluster state was send over the wire + boolean wasReadFromDiff() { + return wasReadFromDiff; + } + /** * Returns a built (on demand) routing nodes view of the routing table. NOTE, the routing nodes * are mutable, use them just for read operations @@ -231,6 +247,8 @@ public class ClusterState implements ToXContent { public String prettyPrint() { StringBuilder sb = new StringBuilder(); sb.append("version: ").append(version).append("\n"); + sb.append("uuid: ").append(uuid).append("\n"); + sb.append("from_diff: ").append(wasReadFromDiff).append("\n"); sb.append("meta data version: ").append(metaData.version()).append("\n"); sb.append(nodes().prettyPrint()); sb.append(routingTable().prettyPrint()); @@ -302,14 +320,13 @@ public class ClusterState implements ToXContent { } } - - @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { EnumSet metrics = Metric.parseString(params.param("metric", "_all"), true); if (metrics.contains(Metric.VERSION)) { builder.field("version", version); + builder.field("uuid", uuid); } if (metrics.contains(Metric.MASTER_NODE)) { @@ -434,7 +451,7 @@ public class ClusterState implements ToXContent { for (ObjectObjectCursor cursor : metaData.customs()) { builder.startObject(cursor.key); - MetaData.lookupFactorySafe(cursor.key).toXContent(cursor.value, builder, params); + cursor.value.toXContent(builder, params); builder.endObject(); } @@ -473,7 +490,7 @@ public class ClusterState implements ToXContent { builder.startObject("nodes"); for (RoutingNode routingNode : readOnlyRoutingNodes()) { - builder.startArray(routingNode.nodeId(), XContentBuilder.FieldCaseConversion.NONE); + builder.startArray(routingNode.nodeId() == null ? "null" : routingNode.nodeId(), XContentBuilder.FieldCaseConversion.NONE); for (ShardRouting shardRouting : routingNode) { shardRouting.toXContent(builder, params); } @@ -486,7 +503,7 @@ public class ClusterState implements ToXContent { if (metrics.contains(Metric.CUSTOMS)) { for (ObjectObjectCursor cursor : customs) { builder.startObject(cursor.key); - lookupFactorySafe(cursor.key).toXContent(cursor.value, builder, params); + cursor.value.toXContent(builder, params); builder.endObject(); } } @@ -506,21 +523,25 @@ public class ClusterState implements ToXContent { private final ClusterName clusterName; private long version = 0; + private String uuid = UNKNOWN_UUID; private MetaData metaData = MetaData.EMPTY_META_DATA; private RoutingTable routingTable = RoutingTable.EMPTY_ROUTING_TABLE; private DiscoveryNodes nodes = DiscoveryNodes.EMPTY_NODES; private ClusterBlocks blocks = ClusterBlocks.EMPTY_CLUSTER_BLOCK; private final ImmutableOpenMap.Builder customs; + private boolean fromDiff; public Builder(ClusterState state) { this.clusterName = state.clusterName; this.version = state.version(); + this.uuid = state.uuid(); this.nodes = state.nodes(); this.routingTable = state.routingTable(); this.metaData = state.metaData(); this.blocks = state.blocks(); this.customs = ImmutableOpenMap.builder(state.customs()); + this.fromDiff = false; } public Builder(ClusterName clusterName) { @@ -574,6 +595,17 @@ public class ClusterState implements ToXContent { return this; } + public Builder incrementVersion() { + this.version = version + 1; + this.uuid = UNKNOWN_UUID; + return this; + } + + public Builder uuid(String uuid) { + this.uuid = uuid; + return this; + } + public Custom getCustom(String type) { return customs.get(type); } @@ -588,13 +620,26 @@ public class ClusterState implements ToXContent { return this; } + public Builder customs(ImmutableOpenMap customs) { + this.customs.putAll(customs); + return this; + } + + public Builder fromDiff(boolean fromDiff) { + this.fromDiff = fromDiff; + return this; + } + public ClusterState build() { - return new ClusterState(clusterName, version, metaData, routingTable, nodes, blocks, customs.build()); + if (UNKNOWN_UUID.equals(uuid)) { + uuid = Strings.randomBase64UUID(); + } + return new ClusterState(clusterName, version, uuid, metaData, routingTable, nodes, blocks, customs.build(), fromDiff); } public static byte[] toBytes(ClusterState state) throws IOException { BytesStreamOutput os = new BytesStreamOutput(); - writeTo(state, os); + state.writeTo(os); return os.bytes().toBytes(); } @@ -606,39 +651,152 @@ public class ClusterState implements ToXContent { return readFrom(new BytesStreamInput(data), localNode); } - public static void writeTo(ClusterState state, StreamOutput out) throws IOException { - state.clusterName.writeTo(out); - out.writeLong(state.version()); - MetaData.Builder.writeTo(state.metaData(), out); - RoutingTable.Builder.writeTo(state.routingTable(), out); - DiscoveryNodes.Builder.writeTo(state.nodes(), out); - ClusterBlocks.Builder.writeClusterBlocks(state.blocks(), out); - out.writeVInt(state.customs().size()); - for (ObjectObjectCursor cursor : state.customs()) { - out.writeString(cursor.key); - lookupFactorySafe(cursor.key).writeTo(cursor.value, out); - } - } - /** * @param in input stream * @param localNode used to set the local node in the cluster state. can be null. */ public static ClusterState readFrom(StreamInput in, @Nullable DiscoveryNode localNode) throws IOException { - ClusterName clusterName = ClusterName.readClusterName(in); + return PROTO.readFrom(in, localNode); + } + + } + + @Override + public Diff diff(ClusterState previousState) { + return new ClusterStateDiff(previousState, this); + } + + @Override + public Diff readDiffFrom(StreamInput in) throws IOException { + return new ClusterStateDiff(in, this); + } + + public ClusterState readFrom(StreamInput in, DiscoveryNode localNode) throws IOException { + ClusterName clusterName = ClusterName.readClusterName(in); + Builder builder = new Builder(clusterName); + builder.version = in.readLong(); + builder.uuid = in.readString(); + builder.metaData = MetaData.Builder.readFrom(in); + builder.routingTable = RoutingTable.Builder.readFrom(in); + builder.nodes = DiscoveryNodes.Builder.readFrom(in, localNode); + builder.blocks = ClusterBlocks.Builder.readClusterBlocks(in); + int customSize = in.readVInt(); + for (int i = 0; i < customSize; i++) { + String type = in.readString(); + Custom customIndexMetaData = lookupPrototypeSafe(type).readFrom(in); + builder.putCustom(type, customIndexMetaData); + } + return builder.build(); + } + + @Override + public ClusterState readFrom(StreamInput in) throws IOException { + return readFrom(in, nodes.localNode()); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + clusterName.writeTo(out); + out.writeLong(version); + out.writeString(uuid); + metaData.writeTo(out); + routingTable.writeTo(out); + nodes.writeTo(out); + blocks.writeTo(out); + out.writeVInt(customs.size()); + for (ObjectObjectCursor cursor : customs) { + out.writeString(cursor.key); + cursor.value.writeTo(out); + } + } + + private static class ClusterStateDiff implements Diff { + + private final long toVersion; + + private final String fromUuid; + + private final String toUuid; + + private final ClusterName clusterName; + + private final Diff routingTable; + + private final Diff nodes; + + private final Diff metaData; + + private final Diff blocks; + + private final Diff> customs; + + public ClusterStateDiff(ClusterState before, ClusterState after) { + fromUuid = before.uuid; + toUuid = after.uuid; + toVersion = after.version; + clusterName = after.clusterName; + routingTable = after.routingTable.diff(before.routingTable); + nodes = after.nodes.diff(before.nodes); + metaData = after.metaData.diff(before.metaData); + blocks = after.blocks.diff(before.blocks); + customs = DiffableUtils.diff(before.customs, after.customs); + } + + public ClusterStateDiff(StreamInput in, ClusterState proto) throws IOException { + clusterName = ClusterName.readClusterName(in); + fromUuid = in.readString(); + toUuid = in.readString(); + toVersion = in.readLong(); + routingTable = proto.routingTable.readDiffFrom(in); + nodes = proto.nodes.readDiffFrom(in); + metaData = proto.metaData.readDiffFrom(in); + blocks = proto.blocks.readDiffFrom(in); + customs = DiffableUtils.readImmutableOpenMapDiff(in, new KeyedReader() { + @Override + public Custom readFrom(StreamInput in, String key) throws IOException { + return lookupPrototypeSafe(key).readFrom(in); + } + + @Override + public Diff readDiffFrom(StreamInput in, String key) throws IOException { + return lookupPrototypeSafe(key).readDiffFrom(in); + } + }); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + clusterName.writeTo(out); + out.writeString(fromUuid); + out.writeString(toUuid); + out.writeLong(toVersion); + routingTable.writeTo(out); + nodes.writeTo(out); + metaData.writeTo(out); + blocks.writeTo(out); + customs.writeTo(out); + } + + @Override + public ClusterState apply(ClusterState state) { Builder builder = new Builder(clusterName); - builder.version = in.readLong(); - builder.metaData = MetaData.Builder.readFrom(in); - builder.routingTable = RoutingTable.Builder.readFrom(in); - builder.nodes = DiscoveryNodes.Builder.readFrom(in, localNode); - builder.blocks = ClusterBlocks.Builder.readClusterBlocks(in); - int customSize = in.readVInt(); - for (int i = 0; i < customSize; i++) { - String type = in.readString(); - Custom customIndexMetaData = lookupFactorySafe(type).readFrom(in); - builder.putCustom(type, customIndexMetaData); + if (toUuid.equals(state.uuid)) { + // no need to read the rest - cluster state didn't change + return state; } + if (fromUuid.equals(state.uuid) == false) { + throw new IncompatibleClusterStateVersionException(state.version, state.uuid, toVersion, fromUuid); + } + builder.uuid(toUuid); + builder.version(toVersion); + builder.routingTable(routingTable.apply(state.routingTable)); + builder.nodes(nodes.apply(state.nodes)); + builder.metaData(metaData.apply(state.metaData)); + builder.blocks(blocks.apply(state.blocks)); + builder.customs(customs.apply(state.customs)); + builder.fromDiff(true); return builder.build(); } } + } diff --git a/src/main/java/org/elasticsearch/cluster/Diff.java b/src/main/java/org/elasticsearch/cluster/Diff.java new file mode 100644 index 00000000000..2e571f43bca --- /dev/null +++ b/src/main/java/org/elasticsearch/cluster/Diff.java @@ -0,0 +1,42 @@ +/* + * 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.cluster; + +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +/** + * Represents difference between states of cluster state parts + */ +public interface Diff { + + /** + * Applies difference to the specified part and retunrs the resulted part + */ + T apply(T part); + + /** + * Writes the differences into the output stream + * @param out + * @throws IOException + */ + void writeTo(StreamOutput out) throws IOException; +} diff --git a/src/main/java/org/elasticsearch/cluster/Diffable.java b/src/main/java/org/elasticsearch/cluster/Diffable.java new file mode 100644 index 00000000000..7ce60047a2b --- /dev/null +++ b/src/main/java/org/elasticsearch/cluster/Diffable.java @@ -0,0 +1,42 @@ +/* + * 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.cluster; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.io.stream.StreamInput; + +import java.io.IOException; + +/** + * Cluster state part, changes in which can be serialized + */ +public interface Diffable extends Writeable { + + /** + * Returns serializable object representing differences between this and previousState + */ + Diff diff(T previousState); + + /** + * Reads the {@link org.elasticsearch.cluster.Diff} from StreamInput + */ + Diff readDiffFrom(StreamInput in) throws IOException; + +} diff --git a/src/main/java/org/elasticsearch/cluster/DiffableUtils.java b/src/main/java/org/elasticsearch/cluster/DiffableUtils.java new file mode 100644 index 00000000000..4e912a34f97 --- /dev/null +++ b/src/main/java/org/elasticsearch/cluster/DiffableUtils.java @@ -0,0 +1,283 @@ +/* + * 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.cluster; + +import com.carrotsearch.hppc.cursors.ObjectCursor; +import com.carrotsearch.hppc.cursors.ObjectObjectCursor; +import com.google.common.collect.ImmutableMap; +import org.elasticsearch.common.collect.ImmutableOpenMap; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.collect.Maps.newHashMap; + +public final class DiffableUtils { + private DiffableUtils() { + } + + /** + * Calculates diff between two ImmutableOpenMaps of Diffable objects + */ + public static > Diff> diff(ImmutableOpenMap before, ImmutableOpenMap after) { + assert after != null && before != null; + return new ImmutableOpenMapDiff<>(before, after); + } + + /** + * Calculates diff between two ImmutableMaps of Diffable objects + */ + public static > Diff> diff(ImmutableMap before, ImmutableMap after) { + assert after != null && before != null; + return new ImmutableMapDiff<>(before, after); + } + + /** + * Loads an object that represents difference between two ImmutableOpenMaps + */ + public static > Diff> readImmutableOpenMapDiff(StreamInput in, KeyedReader keyedReader) throws IOException { + return new ImmutableOpenMapDiff<>(in, keyedReader); + } + + /** + * Loads an object that represents difference between two ImmutableMaps + */ + public static > Diff> readImmutableMapDiff(StreamInput in, KeyedReader keyedReader) throws IOException { + return new ImmutableMapDiff<>(in, keyedReader); + } + + /** + * Loads an object that represents difference between two ImmutableOpenMaps + */ + public static > Diff> readImmutableOpenMapDiff(StreamInput in, T proto) throws IOException { + return new ImmutableOpenMapDiff<>(in, new PrototypeReader<>(proto)); + } + + /** + * Loads an object that represents difference between two ImmutableMaps + */ + public static > Diff> readImmutableMapDiff(StreamInput in, T proto) throws IOException { + return new ImmutableMapDiff<>(in, new PrototypeReader<>(proto)); + } + + /** + * A reader that can deserialize an object. The reader can select the deserialization type based on the key. It's + * used in custom metadata deserialization. + */ + public interface KeyedReader { + + /** + * reads an object of the type T from the stream input + */ + T readFrom(StreamInput in, String key) throws IOException; + + /** + * reads an object that respresents differences between two objects with the type T from the stream input + */ + Diff readDiffFrom(StreamInput in, String key) throws IOException; + } + + /** + * Implementation of the KeyedReader that is using a prototype object for reading operations + * + * Note: this implementation is ignoring the key. + */ + public static class PrototypeReader> implements KeyedReader { + private T proto; + + public PrototypeReader(T proto) { + this.proto = proto; + } + + @Override + public T readFrom(StreamInput in, String key) throws IOException { + return proto.readFrom(in); + } + + @Override + public Diff readDiffFrom(StreamInput in, String key) throws IOException { + return proto.readDiffFrom(in); + } + } + + /** + * Represents differences between two ImmutableMaps of diffable objects + * + * @param the diffable object + */ + private static class ImmutableMapDiff> extends MapDiff> { + + protected ImmutableMapDiff(StreamInput in, KeyedReader reader) throws IOException { + super(in, reader); + } + + public ImmutableMapDiff(ImmutableMap before, ImmutableMap after) { + assert after != null && before != null; + for (String key : before.keySet()) { + if (!after.containsKey(key)) { + deletes.add(key); + } + } + for (ImmutableMap.Entry partIter : after.entrySet()) { + T beforePart = before.get(partIter.getKey()); + if (beforePart == null) { + adds.put(partIter.getKey(), partIter.getValue()); + } else if (partIter.getValue().equals(beforePart) == false) { + diffs.put(partIter.getKey(), partIter.getValue().diff(beforePart)); + } + } + } + + @Override + public ImmutableMap apply(ImmutableMap map) { + HashMap builder = newHashMap(); + builder.putAll(map); + + for (String part : deletes) { + builder.remove(part); + } + + for (Map.Entry> diff : diffs.entrySet()) { + builder.put(diff.getKey(), diff.getValue().apply(builder.get(diff.getKey()))); + } + + for (Map.Entry additon : adds.entrySet()) { + builder.put(additon.getKey(), additon.getValue()); + } + return ImmutableMap.copyOf(builder); + } + } + + /** + * Represents differences between two ImmutableOpenMap of diffable objects + * + * @param the diffable object + */ + private static class ImmutableOpenMapDiff> extends MapDiff> { + + protected ImmutableOpenMapDiff(StreamInput in, KeyedReader reader) throws IOException { + super(in, reader); + } + + public ImmutableOpenMapDiff(ImmutableOpenMap before, ImmutableOpenMap after) { + assert after != null && before != null; + for (ObjectCursor key : before.keys()) { + if (!after.containsKey(key.value)) { + deletes.add(key.value); + } + } + for (ObjectObjectCursor partIter : after) { + T beforePart = before.get(partIter.key); + if (beforePart == null) { + adds.put(partIter.key, partIter.value); + } else if (partIter.value.equals(beforePart) == false) { + diffs.put(partIter.key, partIter.value.diff(beforePart)); + } + } + } + + @Override + public ImmutableOpenMap apply(ImmutableOpenMap map) { + ImmutableOpenMap.Builder builder = ImmutableOpenMap.builder(); + builder.putAll(map); + + for (String part : deletes) { + builder.remove(part); + } + + for (Map.Entry> diff : diffs.entrySet()) { + builder.put(diff.getKey(), diff.getValue().apply(builder.get(diff.getKey()))); + } + + for (Map.Entry additon : adds.entrySet()) { + builder.put(additon.getKey(), additon.getValue()); + } + return builder.build(); + } + } + + /** + * Represents differences between two maps of diffable objects + * + * This class is used as base class for different map implementations + * + * @param the diffable object + */ + private static abstract class MapDiff, M> implements Diff { + + protected final List deletes; + protected final Map> diffs; + protected final Map adds; + + protected MapDiff() { + deletes = newArrayList(); + diffs = newHashMap(); + adds = newHashMap(); + } + + protected MapDiff(StreamInput in, KeyedReader reader) throws IOException { + deletes = newArrayList(); + diffs = newHashMap(); + adds = newHashMap(); + int deletesCount = in.readVInt(); + for (int i = 0; i < deletesCount; i++) { + deletes.add(in.readString()); + } + + int diffsCount = in.readVInt(); + for (int i = 0; i < diffsCount; i++) { + String key = in.readString(); + Diff diff = reader.readDiffFrom(in, key); + diffs.put(key, diff); + } + + int addsCount = in.readVInt(); + for (int i = 0; i < addsCount; i++) { + String key = in.readString(); + T part = reader.readFrom(in, key); + adds.put(key, part); + } + } + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeVInt(deletes.size()); + for (String delete : deletes) { + out.writeString(delete); + } + + out.writeVInt(diffs.size()); + for (Map.Entry> entry : diffs.entrySet()) { + out.writeString(entry.getKey()); + entry.getValue().writeTo(out); + } + + out.writeVInt(adds.size()); + for (Map.Entry entry : adds.entrySet()) { + out.writeString(entry.getKey()); + entry.getValue().writeTo(out); + } + } + } +} diff --git a/src/main/java/org/elasticsearch/cluster/IncompatibleClusterStateVersionException.java b/src/main/java/org/elasticsearch/cluster/IncompatibleClusterStateVersionException.java new file mode 100644 index 00000000000..92f5897bf2e --- /dev/null +++ b/src/main/java/org/elasticsearch/cluster/IncompatibleClusterStateVersionException.java @@ -0,0 +1,35 @@ +/* + * 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.cluster; + +import org.elasticsearch.ElasticsearchException; + +/** + * Thrown by {@link Diffable#readDiffAndApply(org.elasticsearch.common.io.stream.StreamInput)} method + */ +public class IncompatibleClusterStateVersionException extends ElasticsearchException { + public IncompatibleClusterStateVersionException(String msg) { + super(msg); + } + + public IncompatibleClusterStateVersionException(long expectedVersion, String expectedUuid, long receivedVersion, String receivedUuid) { + super("Expected diff for version " + expectedVersion + " with uuid " + expectedUuid + " got version " + receivedVersion + " and uuid " + receivedUuid); + } +} diff --git a/src/main/java/org/elasticsearch/cluster/block/ClusterBlocks.java b/src/main/java/org/elasticsearch/cluster/block/ClusterBlocks.java index bb7d332de4f..95c0ba7127e 100644 --- a/src/main/java/org/elasticsearch/cluster/block/ClusterBlocks.java +++ b/src/main/java/org/elasticsearch/cluster/block/ClusterBlocks.java @@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.collect.Sets; +import org.elasticsearch.cluster.AbstractDiffable; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaDataIndexStateService; import org.elasticsearch.common.io.stream.StreamInput; @@ -36,10 +37,12 @@ import java.util.Set; /** * Represents current cluster level blocks to block dirty operations done against the cluster. */ -public class ClusterBlocks { +public class ClusterBlocks extends AbstractDiffable { public static final ClusterBlocks EMPTY_CLUSTER_BLOCK = new ClusterBlocks(ImmutableSet.of(), ImmutableMap.>of()); + public static final ClusterBlocks PROTO = EMPTY_CLUSTER_BLOCK; + private final ImmutableSet global; private final ImmutableMap> indicesBlocks; @@ -203,6 +206,43 @@ public class ClusterBlocks { return new ClusterBlockException(builder.build()); } + @Override + public void writeTo(StreamOutput out) throws IOException { + writeBlockSet(global, out); + out.writeVInt(indicesBlocks.size()); + for (Map.Entry> entry : indicesBlocks.entrySet()) { + out.writeString(entry.getKey()); + writeBlockSet(entry.getValue(), out); + } + } + + private static void writeBlockSet(ImmutableSet blocks, StreamOutput out) throws IOException { + out.writeVInt(blocks.size()); + for (ClusterBlock block : blocks) { + block.writeTo(out); + } + } + + @Override + public ClusterBlocks readFrom(StreamInput in) throws IOException { + ImmutableSet global = readBlockSet(in); + ImmutableMap.Builder> indicesBuilder = ImmutableMap.builder(); + int size = in.readVInt(); + for (int j = 0; j < size; j++) { + indicesBuilder.put(in.readString().intern(), readBlockSet(in)); + } + return new ClusterBlocks(global, indicesBuilder.build()); + } + + private static ImmutableSet readBlockSet(StreamInput in) throws IOException { + ImmutableSet.Builder builder = ImmutableSet.builder(); + int size = in.readVInt(); + for (int i = 0; i < size; i++) { + builder.add(ClusterBlock.readClusterBlock(in)); + } + return builder.build(); + } + static class ImmutableLevelHolder { static final ImmutableLevelHolder EMPTY = new ImmutableLevelHolder(ImmutableSet.of(), ImmutableMap.>of()); @@ -313,38 +353,7 @@ public class ClusterBlocks { } public static ClusterBlocks readClusterBlocks(StreamInput in) throws IOException { - ImmutableSet global = readBlockSet(in); - ImmutableMap.Builder> indicesBuilder = ImmutableMap.builder(); - int size = in.readVInt(); - for (int j = 0; j < size; j++) { - indicesBuilder.put(in.readString().intern(), readBlockSet(in)); - } - return new ClusterBlocks(global, indicesBuilder.build()); - } - - public static void writeClusterBlocks(ClusterBlocks blocks, StreamOutput out) throws IOException { - writeBlockSet(blocks.global(), out); - out.writeVInt(blocks.indices().size()); - for (Map.Entry> entry : blocks.indices().entrySet()) { - out.writeString(entry.getKey()); - writeBlockSet(entry.getValue(), out); - } - } - - private static void writeBlockSet(ImmutableSet blocks, StreamOutput out) throws IOException { - out.writeVInt(blocks.size()); - for (ClusterBlock block : blocks) { - block.writeTo(out); - } - } - - private static ImmutableSet readBlockSet(StreamInput in) throws IOException { - ImmutableSet.Builder builder = ImmutableSet.builder(); - int size = in.readVInt(); - for (int i = 0; i < size; i++) { - builder.add(ClusterBlock.readClusterBlock(in)); - } - return builder.build(); + return PROTO.readFrom(in); } } } diff --git a/src/main/java/org/elasticsearch/cluster/metadata/AliasMetaData.java b/src/main/java/org/elasticsearch/cluster/metadata/AliasMetaData.java index 008935ec026..0f7e55c8087 100644 --- a/src/main/java/org/elasticsearch/cluster/metadata/AliasMetaData.java +++ b/src/main/java/org/elasticsearch/cluster/metadata/AliasMetaData.java @@ -21,6 +21,7 @@ package org.elasticsearch.cluster.metadata; import com.google.common.collect.ImmutableSet; import org.elasticsearch.ElasticsearchGenerationException; +import org.elasticsearch.cluster.AbstractDiffable; import org.elasticsearch.common.Strings; import org.elasticsearch.common.compress.CompressedString; import org.elasticsearch.common.io.stream.StreamInput; @@ -38,7 +39,9 @@ import java.util.Set; /** * */ -public class AliasMetaData { +public class AliasMetaData extends AbstractDiffable { + + public static final AliasMetaData PROTO = new AliasMetaData("", null, null, null); private final String alias; @@ -146,6 +149,48 @@ public class AliasMetaData { return result; } + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(alias()); + if (filter() != null) { + out.writeBoolean(true); + filter.writeTo(out); + } else { + out.writeBoolean(false); + } + if (indexRouting() != null) { + out.writeBoolean(true); + out.writeString(indexRouting()); + } else { + out.writeBoolean(false); + } + if (searchRouting() != null) { + out.writeBoolean(true); + out.writeString(searchRouting()); + } else { + out.writeBoolean(false); + } + + } + + @Override + public AliasMetaData readFrom(StreamInput in) throws IOException { + String alias = in.readString(); + CompressedString filter = null; + if (in.readBoolean()) { + filter = CompressedString.readCompressedString(in); + } + String indexRouting = null; + if (in.readBoolean()) { + indexRouting = in.readString(); + } + String searchRouting = null; + if (in.readBoolean()) { + searchRouting = in.readString(); + } + return new AliasMetaData(alias, filter, indexRouting, searchRouting); + } + public static class Builder { private final String alias; @@ -294,44 +339,12 @@ public class AliasMetaData { return builder.build(); } - public static void writeTo(AliasMetaData aliasMetaData, StreamOutput out) throws IOException { - out.writeString(aliasMetaData.alias()); - if (aliasMetaData.filter() != null) { - out.writeBoolean(true); - aliasMetaData.filter.writeTo(out); - } else { - out.writeBoolean(false); - } - if (aliasMetaData.indexRouting() != null) { - out.writeBoolean(true); - out.writeString(aliasMetaData.indexRouting()); - } else { - out.writeBoolean(false); - } - if (aliasMetaData.searchRouting() != null) { - out.writeBoolean(true); - out.writeString(aliasMetaData.searchRouting()); - } else { - out.writeBoolean(false); - } - + public void writeTo(AliasMetaData aliasMetaData, StreamOutput out) throws IOException { + aliasMetaData.writeTo(out); } public static AliasMetaData readFrom(StreamInput in) throws IOException { - String alias = in.readString(); - CompressedString filter = null; - if (in.readBoolean()) { - filter = CompressedString.readCompressedString(in); - } - String indexRouting = null; - if (in.readBoolean()) { - indexRouting = in.readString(); - } - String searchRouting = null; - if (in.readBoolean()) { - searchRouting = in.readString(); - } - return new AliasMetaData(alias, filter, indexRouting, searchRouting); + return PROTO.readFrom(in); } } diff --git a/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java b/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java index d6bcacf1615..fe76d0f3f2b 100644 --- a/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java +++ b/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java @@ -24,6 +24,9 @@ import com.carrotsearch.hppc.cursors.ObjectObjectCursor; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import org.elasticsearch.Version; +import org.elasticsearch.cluster.Diff; +import org.elasticsearch.cluster.Diffable; +import org.elasticsearch.cluster.DiffableUtils; import org.elasticsearch.cluster.block.ClusterBlock; import org.elasticsearch.cluster.block.ClusterBlockLevel; import org.elasticsearch.cluster.node.DiscoveryNodeFilters; @@ -59,60 +62,54 @@ import static org.elasticsearch.common.settings.ImmutableSettings.*; /** * */ -public class IndexMetaData { +public class IndexMetaData implements Diffable { + public static final IndexMetaData PROTO = IndexMetaData.builder("") + .settings(ImmutableSettings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(1).numberOfReplicas(0).build(); - public interface Custom { + public interface Custom extends Diffable, ToXContent { String type(); - interface Factory { + Custom fromMap(Map map) throws IOException; - String type(); + Custom fromXContent(XContentParser parser) throws IOException; - T readFrom(StreamInput in) throws IOException; - - void writeTo(T customIndexMetaData, StreamOutput out) throws IOException; - - T fromMap(Map map) throws IOException; - - T fromXContent(XContentParser parser) throws IOException; - - void toXContent(T customIndexMetaData, XContentBuilder builder, ToXContent.Params params) throws IOException; - - /** - * Merges from first to second, with first being more important, i.e., if something exists in first and second, - * first will prevail. - */ - T merge(T first, T second); - } + /** + * Merges from this to another, with this being more important, i.e., if something exists in this and another, + * this will prevail. + */ + Custom mergeWith(Custom another); } - public static Map customFactories = new HashMap<>(); + public static Map customPrototypes = new HashMap<>(); static { // register non plugin custom metadata - registerFactory(IndexWarmersMetaData.TYPE, IndexWarmersMetaData.FACTORY); + registerPrototype(IndexWarmersMetaData.TYPE, IndexWarmersMetaData.PROTO); } /** * Register a custom index meta data factory. Make sure to call it from a static block. */ - public static void registerFactory(String type, Custom.Factory factory) { - customFactories.put(type, factory); + public static void registerPrototype(String type, Custom proto) { + customPrototypes.put(type, proto); } @Nullable - public static Custom.Factory lookupFactory(String type) { - return customFactories.get(type); + public static T lookupPrototype(String type) { + //noinspection unchecked + return (T) customPrototypes.get(type); } - public static Custom.Factory lookupFactorySafe(String type) { - Custom.Factory factory = customFactories.get(type); - if (factory == null) { - throw new IllegalArgumentException("No custom index metadata factoy registered for type [" + type + "]"); + public static T lookupPrototypeSafe(String type) { + //noinspection unchecked + T proto = (T) customPrototypes.get(type); + if (proto == null) { + throw new IllegalArgumentException("No custom metadata prototype registered for type [" + type + "]"); } - return factory; + return proto; } public static final ClusterBlock INDEX_READ_ONLY_BLOCK = new ClusterBlock(5, "index read-only (api)", false, false, RestStatus.FORBIDDEN, EnumSet.of(ClusterBlockLevel.WRITE, ClusterBlockLevel.METADATA_WRITE)); @@ -451,7 +448,9 @@ public class IndexMetaData { if (state != that.state) { return false; } - + if (!customs.equals(that.customs)) { + return false; + } return true; } @@ -465,6 +464,126 @@ public class IndexMetaData { return result; } + @Override + public Diff diff(IndexMetaData previousState) { + return new IndexMetaDataDiff(previousState, this); + } + + @Override + public Diff readDiffFrom(StreamInput in) throws IOException { + return new IndexMetaDataDiff(in); + } + + private static class IndexMetaDataDiff implements Diff { + + private final String index; + private final long version; + private final State state; + private final Settings settings; + private final Diff> mappings; + private final Diff> aliases; + private Diff> customs; + + public IndexMetaDataDiff(IndexMetaData before, IndexMetaData after) { + index = after.index; + version = after.version; + state = after.state; + settings = after.settings; + mappings = DiffableUtils.diff(before.mappings, after.mappings); + aliases = DiffableUtils.diff(before.aliases, after.aliases); + customs = DiffableUtils.diff(before.customs, after.customs); + } + + public IndexMetaDataDiff(StreamInput in) throws IOException { + index = in.readString(); + version = in.readLong(); + state = State.fromId(in.readByte()); + settings = ImmutableSettings.readSettingsFromStream(in); + mappings = DiffableUtils.readImmutableOpenMapDiff(in, MappingMetaData.PROTO); + aliases = DiffableUtils.readImmutableOpenMapDiff(in, AliasMetaData.PROTO); + customs = DiffableUtils.readImmutableOpenMapDiff(in, new DiffableUtils.KeyedReader() { + @Override + public Custom readFrom(StreamInput in, String key) throws IOException { + return lookupPrototypeSafe(key).readFrom(in); + } + + @Override + public Diff readDiffFrom(StreamInput in, String key) throws IOException { + return lookupPrototypeSafe(key).readDiffFrom(in); + } + }); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(index); + out.writeLong(version); + out.writeByte(state.id); + ImmutableSettings.writeSettingsToStream(settings, out); + mappings.writeTo(out); + aliases.writeTo(out); + customs.writeTo(out); + } + + @Override + public IndexMetaData apply(IndexMetaData part) { + Builder builder = builder(index); + builder.version(version); + builder.state(state); + builder.settings(settings); + builder.mappings.putAll(mappings.apply(part.mappings)); + builder.aliases.putAll(aliases.apply(part.aliases)); + builder.customs.putAll(customs.apply(part.customs)); + return builder.build(); + } + } + + @Override + public IndexMetaData readFrom(StreamInput in) throws IOException { + Builder builder = new Builder(in.readString()); + builder.version(in.readLong()); + builder.state(State.fromId(in.readByte())); + builder.settings(readSettingsFromStream(in)); + int mappingsSize = in.readVInt(); + for (int i = 0; i < mappingsSize; i++) { + MappingMetaData mappingMd = MappingMetaData.PROTO.readFrom(in); + builder.putMapping(mappingMd); + } + int aliasesSize = in.readVInt(); + for (int i = 0; i < aliasesSize; i++) { + AliasMetaData aliasMd = AliasMetaData.Builder.readFrom(in); + builder.putAlias(aliasMd); + } + int customSize = in.readVInt(); + for (int i = 0; i < customSize; i++) { + String type = in.readString(); + Custom customIndexMetaData = lookupPrototypeSafe(type).readFrom(in); + builder.putCustom(type, customIndexMetaData); + } + return builder.build(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(index); + out.writeLong(version); + out.writeByte(state.id()); + writeSettingsToStream(settings, out); + out.writeVInt(mappings.size()); + for (ObjectCursor cursor : mappings.values()) { + cursor.value.writeTo(out); + } + out.writeVInt(aliases.size()); + for (ObjectCursor cursor : aliases.values()) { + cursor.value.writeTo(out); + } + out.writeVInt(customs.size()); + for (ObjectObjectCursor cursor : customs) { + out.writeString(cursor.key); + cursor.value.writeTo(out); + } + } + public static Builder builder(String index) { return new Builder(index); } @@ -660,7 +779,7 @@ public class IndexMetaData { for (ObjectObjectCursor cursor : indexMetaData.customs()) { builder.startObject(cursor.key, XContentBuilder.FieldCaseConversion.NONE); - lookupFactorySafe(cursor.key).toXContent(cursor.value, builder, params); + cursor.value.toXContent(builder, params); builder.endObject(); } @@ -707,12 +826,13 @@ public class IndexMetaData { } } else { // check if its a custom index metadata - Custom.Factory factory = lookupFactory(currentFieldName); - if (factory == null) { + Custom proto = lookupPrototype(currentFieldName); + if (proto == null) { //TODO warn parser.skipChildren(); } else { - builder.putCustom(factory.type(), factory.fromXContent(parser)); + Custom custom = proto.fromXContent(parser); + builder.putCustom(custom.type(), custom); } } } else if (token == XContentParser.Token.START_ARRAY) { @@ -741,47 +861,7 @@ public class IndexMetaData { } public static IndexMetaData readFrom(StreamInput in) throws IOException { - Builder builder = new Builder(in.readString()); - builder.version(in.readLong()); - builder.state(State.fromId(in.readByte())); - builder.settings(readSettingsFromStream(in)); - int mappingsSize = in.readVInt(); - for (int i = 0; i < mappingsSize; i++) { - MappingMetaData mappingMd = MappingMetaData.readFrom(in); - builder.putMapping(mappingMd); - } - int aliasesSize = in.readVInt(); - for (int i = 0; i < aliasesSize; i++) { - AliasMetaData aliasMd = AliasMetaData.Builder.readFrom(in); - builder.putAlias(aliasMd); - } - int customSize = in.readVInt(); - for (int i = 0; i < customSize; i++) { - String type = in.readString(); - Custom customIndexMetaData = lookupFactorySafe(type).readFrom(in); - builder.putCustom(type, customIndexMetaData); - } - return builder.build(); - } - - public static void writeTo(IndexMetaData indexMetaData, StreamOutput out) throws IOException { - out.writeString(indexMetaData.index()); - out.writeLong(indexMetaData.version()); - out.writeByte(indexMetaData.state().id()); - writeSettingsToStream(indexMetaData.settings(), out); - out.writeVInt(indexMetaData.mappings().size()); - for (ObjectCursor cursor : indexMetaData.mappings().values()) { - MappingMetaData.writeTo(cursor.value, out); - } - out.writeVInt(indexMetaData.aliases().size()); - for (ObjectCursor cursor : indexMetaData.aliases().values()) { - AliasMetaData.Builder.writeTo(cursor.value, out); - } - out.writeVInt(indexMetaData.customs().size()); - for (ObjectObjectCursor cursor : indexMetaData.customs()) { - out.writeString(cursor.key); - lookupFactorySafe(cursor.key).writeTo(cursor.value, out); - } + return PROTO.readFrom(in); } } diff --git a/src/main/java/org/elasticsearch/cluster/metadata/IndexTemplateMetaData.java b/src/main/java/org/elasticsearch/cluster/metadata/IndexTemplateMetaData.java index 582e008550d..54150ee6a1e 100644 --- a/src/main/java/org/elasticsearch/cluster/metadata/IndexTemplateMetaData.java +++ b/src/main/java/org/elasticsearch/cluster/metadata/IndexTemplateMetaData.java @@ -21,7 +21,7 @@ package org.elasticsearch.cluster.metadata; import com.carrotsearch.hppc.cursors.ObjectCursor; import com.carrotsearch.hppc.cursors.ObjectObjectCursor; import com.google.common.collect.Sets; -import org.elasticsearch.Version; +import org.elasticsearch.cluster.AbstractDiffable; import org.elasticsearch.common.collect.ImmutableOpenMap; import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.compress.CompressedString; @@ -42,7 +42,9 @@ import java.util.Set; /** * */ -public class IndexTemplateMetaData { +public class IndexTemplateMetaData extends AbstractDiffable { + + public static final IndexTemplateMetaData PROTO = IndexTemplateMetaData.builder("").build(); private final String name; @@ -161,11 +163,57 @@ public class IndexTemplateMetaData { return result; } + @Override + public IndexTemplateMetaData readFrom(StreamInput in) throws IOException { + Builder builder = new Builder(in.readString()); + builder.order(in.readInt()); + builder.template(in.readString()); + builder.settings(ImmutableSettings.readSettingsFromStream(in)); + int mappingsSize = in.readVInt(); + for (int i = 0; i < mappingsSize; i++) { + builder.putMapping(in.readString(), CompressedString.readCompressedString(in)); + } + int aliasesSize = in.readVInt(); + for (int i = 0; i < aliasesSize; i++) { + AliasMetaData aliasMd = AliasMetaData.Builder.readFrom(in); + builder.putAlias(aliasMd); + } + int customSize = in.readVInt(); + for (int i = 0; i < customSize; i++) { + String type = in.readString(); + IndexMetaData.Custom customIndexMetaData = IndexMetaData.lookupPrototypeSafe(type).readFrom(in); + builder.putCustom(type, customIndexMetaData); + } + return builder.build(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(name); + out.writeInt(order); + out.writeString(template); + ImmutableSettings.writeSettingsToStream(settings, out); + out.writeVInt(mappings.size()); + for (ObjectObjectCursor cursor : mappings) { + out.writeString(cursor.key); + cursor.value.writeTo(out); + } + out.writeVInt(aliases.size()); + for (ObjectCursor cursor : aliases.values()) { + cursor.value.writeTo(out); + } + out.writeVInt(customs.size()); + for (ObjectObjectCursor cursor : customs) { + out.writeString(cursor.key); + cursor.value.writeTo(out); + } + } + public static class Builder { private static final Set VALID_FIELDS = Sets.newHashSet("template", "order", "mappings", "settings"); static { - VALID_FIELDS.addAll(IndexMetaData.customFactories.keySet()); + VALID_FIELDS.addAll(IndexMetaData.customPrototypes.keySet()); } private String name; @@ -305,7 +353,7 @@ public class IndexTemplateMetaData { for (ObjectObjectCursor cursor : indexTemplateMetaData.customs()) { builder.startObject(cursor.key, XContentBuilder.FieldCaseConversion.NONE); - IndexMetaData.lookupFactorySafe(cursor.key).toXContent(cursor.value, builder, params); + cursor.value.toXContent(builder, params); builder.endObject(); } @@ -347,12 +395,13 @@ public class IndexTemplateMetaData { } } else { // check if its a custom index metadata - IndexMetaData.Custom.Factory factory = IndexMetaData.lookupFactory(currentFieldName); - if (factory == null) { + IndexMetaData.Custom proto = IndexMetaData.lookupPrototype(currentFieldName); + if (proto == null) { //TODO warn parser.skipChildren(); } else { - builder.putCustom(factory.type(), factory.fromXContent(parser)); + IndexMetaData.Custom custom = proto.fromXContent(parser); + builder.putCustom(custom.type(), custom); } } } else if (token == XContentParser.Token.START_ARRAY) { @@ -401,47 +450,7 @@ public class IndexTemplateMetaData { } public static IndexTemplateMetaData readFrom(StreamInput in) throws IOException { - Builder builder = new Builder(in.readString()); - builder.order(in.readInt()); - builder.template(in.readString()); - builder.settings(ImmutableSettings.readSettingsFromStream(in)); - int mappingsSize = in.readVInt(); - for (int i = 0; i < mappingsSize; i++) { - builder.putMapping(in.readString(), CompressedString.readCompressedString(in)); - } - int aliasesSize = in.readVInt(); - for (int i = 0; i < aliasesSize; i++) { - AliasMetaData aliasMd = AliasMetaData.Builder.readFrom(in); - builder.putAlias(aliasMd); - } - int customSize = in.readVInt(); - for (int i = 0; i < customSize; i++) { - String type = in.readString(); - IndexMetaData.Custom customIndexMetaData = IndexMetaData.lookupFactorySafe(type).readFrom(in); - builder.putCustom(type, customIndexMetaData); - } - return builder.build(); - } - - public static void writeTo(IndexTemplateMetaData indexTemplateMetaData, StreamOutput out) throws IOException { - out.writeString(indexTemplateMetaData.name()); - out.writeInt(indexTemplateMetaData.order()); - out.writeString(indexTemplateMetaData.template()); - ImmutableSettings.writeSettingsToStream(indexTemplateMetaData.settings(), out); - out.writeVInt(indexTemplateMetaData.mappings().size()); - for (ObjectObjectCursor cursor : indexTemplateMetaData.mappings()) { - out.writeString(cursor.key); - cursor.value.writeTo(out); - } - out.writeVInt(indexTemplateMetaData.aliases().size()); - for (ObjectCursor cursor : indexTemplateMetaData.aliases().values()) { - AliasMetaData.Builder.writeTo(cursor.value, out); - } - out.writeVInt(indexTemplateMetaData.customs().size()); - for (ObjectObjectCursor cursor : indexTemplateMetaData.customs()) { - out.writeString(cursor.key); - IndexMetaData.lookupFactorySafe(cursor.key).writeTo(cursor.value, out); - } + return PROTO.readFrom(in); } } diff --git a/src/main/java/org/elasticsearch/cluster/metadata/MappingMetaData.java b/src/main/java/org/elasticsearch/cluster/metadata/MappingMetaData.java index f80c6072bfc..7225a43d5ef 100644 --- a/src/main/java/org/elasticsearch/cluster/metadata/MappingMetaData.java +++ b/src/main/java/org/elasticsearch/cluster/metadata/MappingMetaData.java @@ -19,8 +19,10 @@ package org.elasticsearch.cluster.metadata; +import com.google.common.collect.Maps; import org.elasticsearch.Version; import org.elasticsearch.action.TimestampParsingException; +import org.elasticsearch.cluster.AbstractDiffable; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; import org.elasticsearch.common.compress.CompressedString; @@ -38,14 +40,18 @@ import org.elasticsearch.index.mapper.internal.TimestampFieldMapper; import java.io.IOException; import java.util.Arrays; +import java.util.HashMap; import java.util.Map; +import static com.google.common.collect.Maps.newHashMap; import static org.elasticsearch.common.xcontent.support.XContentMapValues.nodeBooleanValue; /** * Mapping configuration for a type. */ -public class MappingMetaData { +public class MappingMetaData extends AbstractDiffable { + + public static final MappingMetaData PROTO = new MappingMetaData(); public static class Id { @@ -317,6 +323,15 @@ public class MappingMetaData { initMappers(withoutType); } + private MappingMetaData() { + this.type = ""; + try { + this.source = new CompressedString(""); + } catch (IOException ex) { + throw new IllegalStateException("Cannot create MappingMetaData prototype", ex); + } + } + private void initMappers(Map withoutType) { if (withoutType.containsKey("_id")) { String path = null; @@ -532,34 +547,35 @@ public class MappingMetaData { } } - public static void writeTo(MappingMetaData mappingMd, StreamOutput out) throws IOException { - out.writeString(mappingMd.type()); - mappingMd.source().writeTo(out); + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(type()); + source().writeTo(out); // id - if (mappingMd.id().hasPath()) { + if (id().hasPath()) { out.writeBoolean(true); - out.writeString(mappingMd.id().path()); + out.writeString(id().path()); } else { out.writeBoolean(false); } // routing - out.writeBoolean(mappingMd.routing().required()); - if (mappingMd.routing().hasPath()) { + out.writeBoolean(routing().required()); + if (routing().hasPath()) { out.writeBoolean(true); - out.writeString(mappingMd.routing().path()); + out.writeString(routing().path()); } else { out.writeBoolean(false); } // timestamp - out.writeBoolean(mappingMd.timestamp().enabled()); - out.writeOptionalString(mappingMd.timestamp().path()); - out.writeString(mappingMd.timestamp().format()); - out.writeOptionalString(mappingMd.timestamp().defaultTimestamp()); + out.writeBoolean(timestamp().enabled()); + out.writeOptionalString(timestamp().path()); + out.writeString(timestamp().format()); + out.writeOptionalString(timestamp().defaultTimestamp()); // TODO Remove the test in elasticsearch 2.0.0 if (out.getVersion().onOrAfter(Version.V_1_5_0)) { - out.writeOptionalBoolean(mappingMd.timestamp().ignoreMissing()); + out.writeOptionalBoolean(timestamp().ignoreMissing()); } - out.writeBoolean(mappingMd.hasParentField()); + out.writeBoolean(hasParentField()); } @Override @@ -588,7 +604,7 @@ public class MappingMetaData { return result; } - public static MappingMetaData readFrom(StreamInput in) throws IOException { + public MappingMetaData readFrom(StreamInput in) throws IOException { String type = in.readString(); CompressedString source = CompressedString.readCompressedString(in); // id diff --git a/src/main/java/org/elasticsearch/cluster/metadata/MetaData.java b/src/main/java/org/elasticsearch/cluster/metadata/MetaData.java index ea25a6d5256..97a1367d8e8 100644 --- a/src/main/java/org/elasticsearch/cluster/metadata/MetaData.java +++ b/src/main/java/org/elasticsearch/cluster/metadata/MetaData.java @@ -25,7 +25,9 @@ import com.carrotsearch.hppc.cursors.ObjectCursor; import com.carrotsearch.hppc.cursors.ObjectObjectCursor; import com.google.common.base.Predicate; import com.google.common.collect.*; +import org.elasticsearch.cluster.*; import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.cluster.DiffableUtils.KeyedReader; import org.elasticsearch.cluster.block.ClusterBlock; import org.elasticsearch.cluster.block.ClusterBlockLevel; import org.elasticsearch.common.Nullable; @@ -55,7 +57,9 @@ import static org.elasticsearch.common.settings.ImmutableSettings.*; /** * */ -public class MetaData implements Iterable { +public class MetaData implements Iterable, Diffable { + + public static final MetaData PROTO = builder().build(); public static final String ALL = "_all"; @@ -67,60 +71,51 @@ public class MetaData implements Iterable { GATEWAY, /* Custom metadata should be stored as part of a snapshot */ - SNAPSHOT; + SNAPSHOT } public static EnumSet API_ONLY = EnumSet.of(XContentContext.API); public static EnumSet API_AND_GATEWAY = EnumSet.of(XContentContext.API, XContentContext.GATEWAY); public static EnumSet API_AND_SNAPSHOT = EnumSet.of(XContentContext.API, XContentContext.SNAPSHOT); - public interface Custom { + public interface Custom extends Diffable, ToXContent { - abstract class Factory { + String type(); - public abstract String type(); + Custom fromXContent(XContentParser parser) throws IOException; - public abstract T readFrom(StreamInput in) throws IOException; - - public abstract void writeTo(T customIndexMetaData, StreamOutput out) throws IOException; - - public abstract T fromXContent(XContentParser parser) throws IOException; - - public abstract void toXContent(T customIndexMetaData, XContentBuilder builder, ToXContent.Params params) throws IOException; - - public EnumSet context() { - return API_ONLY; - } - } + EnumSet context(); } - public static Map customFactories = new HashMap<>(); + public static Map customPrototypes = new HashMap<>(); static { // register non plugin custom metadata - registerFactory(RepositoriesMetaData.TYPE, RepositoriesMetaData.FACTORY); - registerFactory(SnapshotMetaData.TYPE, SnapshotMetaData.FACTORY); - registerFactory(RestoreMetaData.TYPE, RestoreMetaData.FACTORY); + registerPrototype(RepositoriesMetaData.TYPE, RepositoriesMetaData.PROTO); + registerPrototype(SnapshotMetaData.TYPE, SnapshotMetaData.PROTO); + registerPrototype(RestoreMetaData.TYPE, RestoreMetaData.PROTO); } /** * Register a custom index meta data factory. Make sure to call it from a static block. */ - public static void registerFactory(String type, Custom.Factory factory) { - customFactories.put(type, factory); + public static void registerPrototype(String type, Custom proto) { + customPrototypes.put(type, proto); } @Nullable - public static Custom.Factory lookupFactory(String type) { - return customFactories.get(type); + public static T lookupPrototype(String type) { + //noinspection unchecked + return (T) customPrototypes.get(type); } - public static Custom.Factory lookupFactorySafe(String type) { - Custom.Factory factory = customFactories.get(type); - if (factory == null) { - throw new IllegalArgumentException("No custom index metadata factory registered for type [" + type + "]"); + public static T lookupPrototypeSafe(String type) { + //noinspection unchecked + T proto = (T) customPrototypes.get(type); + if (proto == null) { + throw new IllegalArgumentException("No custom metadata prototype registered for type [" + type + "]"); } - return factory; + return proto; } @@ -644,14 +639,22 @@ public class MetaData implements Iterable { /** * Translates the provided indices or aliases, eventually containing wildcard expressions, into actual indices. * - * @param indicesOptions how the aliases or indices need to be resolved to concrete indices + * @param indicesOptions how the aliases or indices need to be resolved to concrete indices * @param aliasesOrIndices the aliases or indices to be resolved to concrete indices * @return the obtained concrete indices +<<<<<<< HEAD * @throws IndexMissingException if one of the aliases or indices is missing and the provided indices options * don't allow such a case, or if the final result of the indices resolution is no indices and the indices options * don't allow such a case. * @throws IllegalArgumentException if one of the aliases resolve to multiple indices and the provided * indices options don't allow such a case. +======= + * @throws IndexMissingException if one of the aliases or indices is missing and the provided indices options + * don't allow such a case, or if the final result of the indices resolution is no indices and the indices options + * don't allow such a case. + * @throws ElasticsearchIllegalArgumentException if one of the aliases resolve to multiple indices and the provided + * indices options don't allow such a case. +>>>>>>> Add support for cluster state diffs */ public String[] concreteIndices(IndicesOptions indicesOptions, String... aliasesOrIndices) throws IndexMissingException, IllegalArgumentException { if (indicesOptions.expandWildcardsOpen() || indicesOptions.expandWildcardsClosed()) { @@ -1139,14 +1142,14 @@ public class MetaData implements Iterable { // Check if any persistent metadata needs to be saved int customCount1 = 0; for (ObjectObjectCursor cursor : metaData1.customs) { - if (customFactories.get(cursor.key).context().contains(XContentContext.GATEWAY)) { + if (customPrototypes.get(cursor.key).context().contains(XContentContext.GATEWAY)) { if (!cursor.value.equals(metaData2.custom(cursor.key))) return false; customCount1++; } } int customCount2 = 0; for (ObjectObjectCursor cursor : metaData2.customs) { - if (customFactories.get(cursor.key).context().contains(XContentContext.GATEWAY)) { + if (customPrototypes.get(cursor.key).context().contains(XContentContext.GATEWAY)) { customCount2++; } } @@ -1154,6 +1157,129 @@ public class MetaData implements Iterable { return true; } + @Override + public Diff diff(MetaData previousState) { + return new MetaDataDiff(previousState, this); + } + + @Override + public Diff readDiffFrom(StreamInput in) throws IOException { + return new MetaDataDiff(in); + } + + private static class MetaDataDiff implements Diff { + + private long version; + + private String uuid; + + private Settings transientSettings; + private Settings persistentSettings; + private Diff> indices; + private Diff> templates; + private Diff> customs; + + + public MetaDataDiff(MetaData before, MetaData after) { + uuid = after.uuid; + version = after.version; + transientSettings = after.transientSettings; + persistentSettings = after.persistentSettings; + indices = DiffableUtils.diff(before.indices, after.indices); + templates = DiffableUtils.diff(before.templates, after.templates); + customs = DiffableUtils.diff(before.customs, after.customs); + } + + public MetaDataDiff(StreamInput in) throws IOException { + uuid = in.readString(); + version = in.readLong(); + transientSettings = ImmutableSettings.readSettingsFromStream(in); + persistentSettings = ImmutableSettings.readSettingsFromStream(in); + indices = DiffableUtils.readImmutableOpenMapDiff(in, IndexMetaData.PROTO); + templates = DiffableUtils.readImmutableOpenMapDiff(in, IndexTemplateMetaData.PROTO); + customs = DiffableUtils.readImmutableOpenMapDiff(in, new KeyedReader() { + @Override + public Custom readFrom(StreamInput in, String key) throws IOException { + return lookupPrototypeSafe(key).readFrom(in); + } + + @Override + public Diff readDiffFrom(StreamInput in, String key) throws IOException { + return lookupPrototypeSafe(key).readDiffFrom(in); + } + }); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(uuid); + out.writeLong(version); + ImmutableSettings.writeSettingsToStream(transientSettings, out); + ImmutableSettings.writeSettingsToStream(persistentSettings, out); + indices.writeTo(out); + templates.writeTo(out); + customs.writeTo(out); + } + + @Override + public MetaData apply(MetaData part) { + Builder builder = builder(); + builder.uuid(uuid); + builder.version(version); + builder.transientSettings(transientSettings); + builder.persistentSettings(persistentSettings); + builder.indices(indices.apply(part.indices)); + builder.templates(templates.apply(part.templates)); + builder.customs(customs.apply(part.customs)); + return builder.build(); + } + } + + @Override + public MetaData readFrom(StreamInput in) throws IOException { + Builder builder = new Builder(); + builder.version = in.readLong(); + builder.uuid = in.readString(); + builder.transientSettings(readSettingsFromStream(in)); + builder.persistentSettings(readSettingsFromStream(in)); + int size = in.readVInt(); + for (int i = 0; i < size; i++) { + builder.put(IndexMetaData.Builder.readFrom(in), false); + } + size = in.readVInt(); + for (int i = 0; i < size; i++) { + builder.put(IndexTemplateMetaData.Builder.readFrom(in)); + } + int customSize = in.readVInt(); + for (int i = 0; i < customSize; i++) { + String type = in.readString(); + Custom customIndexMetaData = lookupPrototypeSafe(type).readFrom(in); + builder.putCustom(type, customIndexMetaData); + } + return builder.build(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeLong(version); + out.writeString(uuid); + writeSettingsToStream(transientSettings, out); + writeSettingsToStream(persistentSettings, out); + out.writeVInt(indices.size()); + for (IndexMetaData indexMetaData : this) { + indexMetaData.writeTo(out); + } + out.writeVInt(templates.size()); + for (ObjectCursor cursor : templates.values()) { + cursor.value.writeTo(out); + } + out.writeVInt(customs.size()); + for (ObjectObjectCursor cursor : customs) { + out.writeString(cursor.key); + cursor.value.writeTo(out); + } + } + public static Builder builder() { return new Builder(); } @@ -1225,6 +1351,11 @@ public class MetaData implements Iterable { return this; } + public Builder indices(ImmutableOpenMap indices) { + this.indices.putAll(indices); + return this; + } + public Builder put(IndexTemplateMetaData.Builder template) { return put(template.build()); } @@ -1239,6 +1370,11 @@ public class MetaData implements Iterable { return this; } + public Builder templates(ImmutableOpenMap templates) { + this.templates.putAll(templates); + return this; + } + public Custom getCustom(String type) { return customs.get(type); } @@ -1253,6 +1389,11 @@ public class MetaData implements Iterable { return this; } + public Builder customs(ImmutableOpenMap customs) { + this.customs.putAll(customs); + return this; + } + public Builder updateSettings(Settings settings, String... indices) { if (indices == null || indices.length == 0) { indices = this.indices.keys().toArray(String.class); @@ -1305,6 +1446,11 @@ public class MetaData implements Iterable { return this; } + public Builder uuid(String uuid) { + this.uuid = uuid; + return this; + } + public Builder generateUuidIfNeeded() { if (uuid.equals("_na_")) { uuid = Strings.randomBase64UUID(); @@ -1363,10 +1509,10 @@ public class MetaData implements Iterable { } for (ObjectObjectCursor cursor : metaData.customs()) { - Custom.Factory factory = lookupFactorySafe(cursor.key); - if (factory.context().contains(context)) { + Custom proto = lookupPrototypeSafe(cursor.key); + if (proto.context().contains(context)) { builder.startObject(cursor.key); - factory.toXContent(cursor.value, builder, params); + cursor.value.toXContent(builder, params); builder.endObject(); } } @@ -1410,12 +1556,13 @@ public class MetaData implements Iterable { } } else { // check if its a custom index metadata - Custom.Factory factory = lookupFactory(currentFieldName); - if (factory == null) { + Custom proto = lookupPrototype(currentFieldName); + if (proto == null) { //TODO warn parser.skipChildren(); } else { - builder.putCustom(factory.type(), factory.fromXContent(parser)); + Custom custom = proto.fromXContent(parser); + builder.putCustom(custom.type(), custom); } } } else if (token.isValue()) { @@ -1430,46 +1577,7 @@ public class MetaData implements Iterable { } public static MetaData readFrom(StreamInput in) throws IOException { - Builder builder = new Builder(); - builder.version = in.readLong(); - builder.uuid = in.readString(); - builder.transientSettings(readSettingsFromStream(in)); - builder.persistentSettings(readSettingsFromStream(in)); - int size = in.readVInt(); - for (int i = 0; i < size; i++) { - builder.put(IndexMetaData.Builder.readFrom(in), false); - } - size = in.readVInt(); - for (int i = 0; i < size; i++) { - builder.put(IndexTemplateMetaData.Builder.readFrom(in)); - } - int customSize = in.readVInt(); - for (int i = 0; i < customSize; i++) { - String type = in.readString(); - Custom customIndexMetaData = lookupFactorySafe(type).readFrom(in); - builder.putCustom(type, customIndexMetaData); - } - return builder.build(); - } - - public static void writeTo(MetaData metaData, StreamOutput out) throws IOException { - out.writeLong(metaData.version); - out.writeString(metaData.uuid); - writeSettingsToStream(metaData.transientSettings(), out); - writeSettingsToStream(metaData.persistentSettings(), out); - out.writeVInt(metaData.indices.size()); - for (IndexMetaData indexMetaData : metaData) { - IndexMetaData.Builder.writeTo(indexMetaData, out); - } - out.writeVInt(metaData.templates.size()); - for (ObjectCursor cursor : metaData.templates.values()) { - IndexTemplateMetaData.Builder.writeTo(cursor.value, out); - } - out.writeVInt(metaData.customs().size()); - for (ObjectObjectCursor cursor : metaData.customs()) { - out.writeString(cursor.key); - lookupFactorySafe(cursor.key).writeTo(cursor.value, out); - } + return PROTO.readFrom(in); } } } diff --git a/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java b/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java index 9fcb5182180..732561f66f1 100644 --- a/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java +++ b/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java @@ -272,7 +272,7 @@ public class MetaDataCreateIndexService extends AbstractComponent { if (existing == null) { customs.put(type, custom); } else { - IndexMetaData.Custom merged = IndexMetaData.lookupFactorySafe(type).merge(existing, custom); + IndexMetaData.Custom merged = existing.mergeWith(custom); customs.put(type, merged); } } diff --git a/src/main/java/org/elasticsearch/cluster/metadata/RepositoriesMetaData.java b/src/main/java/org/elasticsearch/cluster/metadata/RepositoriesMetaData.java index 81b11fc14b1..51cd5db086b 100644 --- a/src/main/java/org/elasticsearch/cluster/metadata/RepositoriesMetaData.java +++ b/src/main/java/org/elasticsearch/cluster/metadata/RepositoriesMetaData.java @@ -21,6 +21,8 @@ package org.elasticsearch.cluster.metadata; import com.google.common.collect.ImmutableList; import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.cluster.AbstractDiffable; +import org.elasticsearch.cluster.metadata.MetaData.Custom; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.ImmutableSettings; @@ -39,11 +41,11 @@ import java.util.Map; /** * Contains metadata about registered snapshot repositories */ -public class RepositoriesMetaData implements MetaData.Custom { +public class RepositoriesMetaData extends AbstractDiffable implements MetaData.Custom { public static final String TYPE = "repositories"; - public static final Factory FACTORY = new Factory(); + public static final RepositoriesMetaData PROTO = new RepositoriesMetaData(); private final ImmutableList repositories; @@ -80,122 +82,132 @@ public class RepositoriesMetaData implements MetaData.Custom { return null; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RepositoriesMetaData that = (RepositoriesMetaData) o; + + return repositories.equals(that.repositories); + + } + + @Override + public int hashCode() { + return repositories.hashCode(); + } + /** - * Repository metadata factory + * {@inheritDoc} */ - public static class Factory extends MetaData.Custom.Factory { + @Override + public String type() { + return TYPE; + } - /** - * {@inheritDoc} - */ - @Override - public String type() { - return TYPE; + /** + * {@inheritDoc} + */ + @Override + public Custom readFrom(StreamInput in) throws IOException { + RepositoryMetaData[] repository = new RepositoryMetaData[in.readVInt()]; + for (int i = 0; i < repository.length; i++) { + repository[i] = RepositoryMetaData.readFrom(in); } + return new RepositoriesMetaData(repository); + } - /** - * {@inheritDoc} - */ - @Override - public RepositoriesMetaData readFrom(StreamInput in) throws IOException { - RepositoryMetaData[] repository = new RepositoryMetaData[in.readVInt()]; - for (int i = 0; i < repository.length; i++) { - repository[i] = RepositoryMetaData.readFrom(in); - } - return new RepositoriesMetaData(repository); - } - - /** - * {@inheritDoc} - */ - @Override - public void writeTo(RepositoriesMetaData repositories, StreamOutput out) throws IOException { - out.writeVInt(repositories.repositories().size()); - for (RepositoryMetaData repository : repositories.repositories()) { - repository.writeTo(out); - } - } - - /** - * {@inheritDoc} - */ - @Override - public RepositoriesMetaData fromXContent(XContentParser parser) throws IOException { - XContentParser.Token token; - List repository = new ArrayList<>(); - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - String name = parser.currentName(); - if (parser.nextToken() != XContentParser.Token.START_OBJECT) { - throw new ElasticsearchParseException("failed to parse repository [" + name + "], expected object"); - } - String type = null; - Settings settings = ImmutableSettings.EMPTY; - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - String currentFieldName = parser.currentName(); - if ("type".equals(currentFieldName)) { - if (parser.nextToken() != XContentParser.Token.VALUE_STRING) { - throw new ElasticsearchParseException("failed to parse repository [" + name + "], unknown type"); - } - type = parser.text(); - } else if ("settings".equals(currentFieldName)) { - if (parser.nextToken() != XContentParser.Token.START_OBJECT) { - throw new ElasticsearchParseException("failed to parse repository [" + name + "], incompatible params"); - } - settings = ImmutableSettings.settingsBuilder().put(SettingsLoader.Helper.loadNestedFromMap(parser.mapOrdered())).build(); - } else { - throw new ElasticsearchParseException("failed to parse repository [" + name + "], unknown field [" + currentFieldName + "]"); - } - } else { - throw new ElasticsearchParseException("failed to parse repository [" + name + "]"); - } - } - if (type == null) { - throw new ElasticsearchParseException("failed to parse repository [" + name + "], missing repository type"); - } - repository.add(new RepositoryMetaData(name, type, settings)); - } else { - throw new ElasticsearchParseException("failed to parse repositories"); - } - } - return new RepositoriesMetaData(repository.toArray(new RepositoryMetaData[repository.size()])); - } - - /** - * {@inheritDoc} - */ - @Override - public void toXContent(RepositoriesMetaData customIndexMetaData, XContentBuilder builder, ToXContent.Params params) throws IOException { - for (RepositoryMetaData repository : customIndexMetaData.repositories()) { - toXContent(repository, builder, params); - } - } - - @Override - public EnumSet context() { - return MetaData.API_AND_GATEWAY; - } - - /** - * Serializes information about a single repository - * - * @param repository repository metadata - * @param builder XContent builder - * @param params serialization parameters - * @throws IOException - */ - public void toXContent(RepositoryMetaData repository, XContentBuilder builder, ToXContent.Params params) throws IOException { - builder.startObject(repository.name(), XContentBuilder.FieldCaseConversion.NONE); - builder.field("type", repository.type()); - builder.startObject("settings"); - for (Map.Entry settingEntry : repository.settings().getAsMap().entrySet()) { - builder.field(settingEntry.getKey(), settingEntry.getValue()); - } - builder.endObject(); - - builder.endObject(); + /** + * {@inheritDoc} + */ + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeVInt(repositories.size()); + for (RepositoryMetaData repository : repositories) { + repository.writeTo(out); } } + /** + * {@inheritDoc} + */ + @Override + public RepositoriesMetaData fromXContent(XContentParser parser) throws IOException { + XContentParser.Token token; + List repository = new ArrayList<>(); + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + String name = parser.currentName(); + if (parser.nextToken() != XContentParser.Token.START_OBJECT) { + throw new ElasticsearchParseException("failed to parse repository [" + name + "], expected object"); + } + String type = null; + Settings settings = ImmutableSettings.EMPTY; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + String currentFieldName = parser.currentName(); + if ("type".equals(currentFieldName)) { + if (parser.nextToken() != XContentParser.Token.VALUE_STRING) { + throw new ElasticsearchParseException("failed to parse repository [" + name + "], unknown type"); + } + type = parser.text(); + } else if ("settings".equals(currentFieldName)) { + if (parser.nextToken() != XContentParser.Token.START_OBJECT) { + throw new ElasticsearchParseException("failed to parse repository [" + name + "], incompatible params"); + } + settings = ImmutableSettings.settingsBuilder().put(SettingsLoader.Helper.loadNestedFromMap(parser.mapOrdered())).build(); + } else { + throw new ElasticsearchParseException("failed to parse repository [" + name + "], unknown field [" + currentFieldName + "]"); + } + } else { + throw new ElasticsearchParseException("failed to parse repository [" + name + "]"); + } + } + if (type == null) { + throw new ElasticsearchParseException("failed to parse repository [" + name + "], missing repository type"); + } + repository.add(new RepositoryMetaData(name, type, settings)); + } else { + throw new ElasticsearchParseException("failed to parse repositories"); + } + } + return new RepositoriesMetaData(repository.toArray(new RepositoryMetaData[repository.size()])); + } + + /** + * {@inheritDoc} + */ + @Override + public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { + for (RepositoryMetaData repository : repositories) { + toXContent(repository, builder, params); + } + return builder; + } + + @Override + public EnumSet context() { + return MetaData.API_AND_GATEWAY; + } + + /** + * Serializes information about a single repository + * + * @param repository repository metadata + * @param builder XContent builder + * @param params serialization parameters + * @throws IOException + */ + public static void toXContent(RepositoryMetaData repository, XContentBuilder builder, ToXContent.Params params) throws IOException { + builder.startObject(repository.name(), XContentBuilder.FieldCaseConversion.NONE); + builder.field("type", repository.type()); + builder.startObject("settings"); + for (Map.Entry settingEntry : repository.settings().getAsMap().entrySet()) { + builder.field(settingEntry.getKey(), settingEntry.getValue()); + } + builder.endObject(); + + builder.endObject(); + } } diff --git a/src/main/java/org/elasticsearch/cluster/metadata/RepositoryMetaData.java b/src/main/java/org/elasticsearch/cluster/metadata/RepositoryMetaData.java index ea50b30ba88..a283f1f43c1 100644 --- a/src/main/java/org/elasticsearch/cluster/metadata/RepositoryMetaData.java +++ b/src/main/java/org/elasticsearch/cluster/metadata/RepositoryMetaData.java @@ -99,4 +99,25 @@ public class RepositoryMetaData { out.writeString(type); ImmutableSettings.writeSettingsToStream(settings, out); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RepositoryMetaData that = (RepositoryMetaData) o; + + if (!name.equals(that.name)) return false; + if (!type.equals(that.type)) return false; + return settings.equals(that.settings); + + } + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + type.hashCode(); + result = 31 * result + settings.hashCode(); + return result; + } } \ No newline at end of file diff --git a/src/main/java/org/elasticsearch/cluster/metadata/RestoreMetaData.java b/src/main/java/org/elasticsearch/cluster/metadata/RestoreMetaData.java index 642136d7b7e..51fd5e0514a 100644 --- a/src/main/java/org/elasticsearch/cluster/metadata/RestoreMetaData.java +++ b/src/main/java/org/elasticsearch/cluster/metadata/RestoreMetaData.java @@ -21,6 +21,7 @@ package org.elasticsearch.cluster.metadata; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import org.elasticsearch.cluster.AbstractDiffable; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.ToXContent; @@ -29,16 +30,17 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.shard.ShardId; import java.io.IOException; +import java.util.EnumSet; import java.util.Map; /** * Meta data about restore processes that are currently executing */ -public class RestoreMetaData implements MetaData.Custom { +public class RestoreMetaData extends AbstractDiffable implements MetaData.Custom { public static final String TYPE = "restore"; - public static final Factory FACTORY = new Factory(); + public static final RestoreMetaData PROTO = new RestoreMetaData(); private final ImmutableList entries; @@ -394,124 +396,122 @@ public class RestoreMetaData implements MetaData.Custom { } /** - * Restore metadata factory + * {@inheritDoc} */ - public static class Factory extends MetaData.Custom.Factory { + @Override + public String type() { + return TYPE; + } - /** - * {@inheritDoc} - */ - @Override - public String type() { - return TYPE; - } - - /** - * {@inheritDoc} - */ - @Override - public RestoreMetaData readFrom(StreamInput in) throws IOException { - Entry[] entries = new Entry[in.readVInt()]; - for (int i = 0; i < entries.length; i++) { - SnapshotId snapshotId = SnapshotId.readSnapshotId(in); - State state = State.fromValue(in.readByte()); - int indices = in.readVInt(); - ImmutableList.Builder indexBuilder = ImmutableList.builder(); - for (int j = 0; j < indices; j++) { - indexBuilder.add(in.readString()); - } - ImmutableMap.Builder builder = ImmutableMap.builder(); - int shards = in.readVInt(); - for (int j = 0; j < shards; j++) { - ShardId shardId = ShardId.readShardId(in); - ShardRestoreStatus shardState = ShardRestoreStatus.readShardRestoreStatus(in); - builder.put(shardId, shardState); - } - entries[i] = new Entry(snapshotId, state, indexBuilder.build(), builder.build()); + /** + * {@inheritDoc} + */ + @Override + public RestoreMetaData readFrom(StreamInput in) throws IOException { + Entry[] entries = new Entry[in.readVInt()]; + for (int i = 0; i < entries.length; i++) { + SnapshotId snapshotId = SnapshotId.readSnapshotId(in); + State state = State.fromValue(in.readByte()); + int indices = in.readVInt(); + ImmutableList.Builder indexBuilder = ImmutableList.builder(); + for (int j = 0; j < indices; j++) { + indexBuilder.add(in.readString()); } - return new RestoreMetaData(entries); - } - - /** - * {@inheritDoc} - */ - @Override - public void writeTo(RestoreMetaData repositories, StreamOutput out) throws IOException { - out.writeVInt(repositories.entries().size()); - for (Entry entry : repositories.entries()) { - entry.snapshotId().writeTo(out); - out.writeByte(entry.state().value()); - out.writeVInt(entry.indices().size()); - for (String index : entry.indices()) { - out.writeString(index); - } - out.writeVInt(entry.shards().size()); - for (Map.Entry shardEntry : entry.shards().entrySet()) { - shardEntry.getKey().writeTo(out); - shardEntry.getValue().writeTo(out); - } + ImmutableMap.Builder builder = ImmutableMap.builder(); + int shards = in.readVInt(); + for (int j = 0; j < shards; j++) { + ShardId shardId = ShardId.readShardId(in); + ShardRestoreStatus shardState = ShardRestoreStatus.readShardRestoreStatus(in); + builder.put(shardId, shardState); } + entries[i] = new Entry(snapshotId, state, indexBuilder.build(), builder.build()); } + return new RestoreMetaData(entries); + } - /** - * {@inheritDoc} - */ - @Override - public RestoreMetaData fromXContent(XContentParser parser) throws IOException { - throw new UnsupportedOperationException(); - } - - /** - * {@inheritDoc} - */ - @Override - public void toXContent(RestoreMetaData customIndexMetaData, XContentBuilder builder, ToXContent.Params params) throws IOException { - builder.startArray("snapshots"); - for (Entry entry : customIndexMetaData.entries()) { - toXContent(entry, builder, params); + /** + * {@inheritDoc} + */ + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeVInt(entries.size()); + for (Entry entry : entries) { + entry.snapshotId().writeTo(out); + out.writeByte(entry.state().value()); + out.writeVInt(entry.indices().size()); + for (String index : entry.indices()) { + out.writeString(index); } - builder.endArray(); - } - - /** - * Serializes single restore operation - * - * @param entry restore operation metadata - * @param builder XContent builder - * @param params serialization parameters - * @throws IOException - */ - public void toXContent(Entry entry, XContentBuilder builder, ToXContent.Params params) throws IOException { - builder.startObject(); - builder.field("snapshot", entry.snapshotId().getSnapshot()); - builder.field("repository", entry.snapshotId().getRepository()); - builder.field("state", entry.state()); - builder.startArray("indices"); - { - for (String index : entry.indices()) { - builder.value(index); - } + out.writeVInt(entry.shards().size()); + for (Map.Entry shardEntry : entry.shards().entrySet()) { + shardEntry.getKey().writeTo(out); + shardEntry.getValue().writeTo(out); } - builder.endArray(); - builder.startArray("shards"); - { - for (Map.Entry shardEntry : entry.shards.entrySet()) { - ShardId shardId = shardEntry.getKey(); - ShardRestoreStatus status = shardEntry.getValue(); - builder.startObject(); - { - builder.field("index", shardId.getIndex()); - builder.field("shard", shardId.getId()); - builder.field("state", status.state()); - } - builder.endObject(); - } - } - - builder.endArray(); - builder.endObject(); } } + /** + * {@inheritDoc} + */ + @Override + public RestoreMetaData fromXContent(XContentParser parser) throws IOException { + throw new UnsupportedOperationException(); + } + @Override + public EnumSet context() { + return MetaData.API_ONLY; + } + + /** + * {@inheritDoc} + */ + @Override + public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { + builder.startArray("snapshots"); + for (Entry entry : entries) { + toXContent(entry, builder, params); + } + builder.endArray(); + return builder; + } + + /** + * Serializes single restore operation + * + * @param entry restore operation metadata + * @param builder XContent builder + * @param params serialization parameters + * @throws IOException + */ + public void toXContent(Entry entry, XContentBuilder builder, ToXContent.Params params) throws IOException { + builder.startObject(); + builder.field("snapshot", entry.snapshotId().getSnapshot()); + builder.field("repository", entry.snapshotId().getRepository()); + builder.field("state", entry.state()); + builder.startArray("indices"); + { + for (String index : entry.indices()) { + builder.value(index); + } + } + builder.endArray(); + builder.startArray("shards"); + { + for (Map.Entry shardEntry : entry.shards.entrySet()) { + ShardId shardId = shardEntry.getKey(); + ShardRestoreStatus status = shardEntry.getValue(); + builder.startObject(); + { + builder.field("index", shardId.getIndex()); + builder.field("shard", shardId.getId()); + builder.field("state", status.state()); + } + builder.endObject(); + } + } + + builder.endArray(); + builder.endObject(); + } } diff --git a/src/main/java/org/elasticsearch/cluster/metadata/SnapshotMetaData.java b/src/main/java/org/elasticsearch/cluster/metadata/SnapshotMetaData.java index b1bcc92b8bd..b23c58710a0 100644 --- a/src/main/java/org/elasticsearch/cluster/metadata/SnapshotMetaData.java +++ b/src/main/java/org/elasticsearch/cluster/metadata/SnapshotMetaData.java @@ -21,6 +21,8 @@ package org.elasticsearch.cluster.metadata; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import org.elasticsearch.cluster.AbstractDiffable; +import org.elasticsearch.cluster.metadata.MetaData.Custom; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.ToXContent; @@ -30,6 +32,7 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.shard.ShardId; import java.io.IOException; +import java.util.EnumSet; import java.util.Map; import static com.google.common.collect.Maps.newHashMap; @@ -37,10 +40,10 @@ import static com.google.common.collect.Maps.newHashMap; /** * Meta data about snapshots that are currently executing */ -public class SnapshotMetaData implements MetaData.Custom { +public class SnapshotMetaData extends AbstractDiffable implements MetaData.Custom { public static final String TYPE = "snapshots"; - public static final Factory FACTORY = new Factory(); + public static final SnapshotMetaData PROTO = new SnapshotMetaData(); @Override public boolean equals(Object o) { @@ -329,123 +332,123 @@ public class SnapshotMetaData implements MetaData.Custom { return null; } + @Override + public String type() { + return TYPE; + } - public static class Factory extends MetaData.Custom.Factory { - - @Override - public String type() { - return TYPE; //To change body of implemented methods use File | Settings | File Templates. - } - - @Override - public SnapshotMetaData readFrom(StreamInput in) throws IOException { - Entry[] entries = new Entry[in.readVInt()]; - for (int i = 0; i < entries.length; i++) { - SnapshotId snapshotId = SnapshotId.readSnapshotId(in); - boolean includeGlobalState = in.readBoolean(); - State state = State.fromValue(in.readByte()); - int indices = in.readVInt(); - ImmutableList.Builder indexBuilder = ImmutableList.builder(); - for (int j = 0; j < indices; j++) { - indexBuilder.add(in.readString()); - } - long startTime = in.readLong(); - ImmutableMap.Builder builder = ImmutableMap.builder(); - int shards = in.readVInt(); - for (int j = 0; j < shards; j++) { - ShardId shardId = ShardId.readShardId(in); - String nodeId = in.readOptionalString(); - State shardState = State.fromValue(in.readByte()); - builder.put(shardId, new ShardSnapshotStatus(nodeId, shardState)); - } - entries[i] = new Entry(snapshotId, includeGlobalState, state, indexBuilder.build(), startTime, builder.build()); + @Override + public SnapshotMetaData readFrom(StreamInput in) throws IOException { + Entry[] entries = new Entry[in.readVInt()]; + for (int i = 0; i < entries.length; i++) { + SnapshotId snapshotId = SnapshotId.readSnapshotId(in); + boolean includeGlobalState = in.readBoolean(); + State state = State.fromValue(in.readByte()); + int indices = in.readVInt(); + ImmutableList.Builder indexBuilder = ImmutableList.builder(); + for (int j = 0; j < indices; j++) { + indexBuilder.add(in.readString()); } - return new SnapshotMetaData(entries); - } - - @Override - public void writeTo(SnapshotMetaData repositories, StreamOutput out) throws IOException { - out.writeVInt(repositories.entries().size()); - for (Entry entry : repositories.entries()) { - entry.snapshotId().writeTo(out); - out.writeBoolean(entry.includeGlobalState()); - out.writeByte(entry.state().value()); - out.writeVInt(entry.indices().size()); - for (String index : entry.indices()) { - out.writeString(index); - } - out.writeLong(entry.startTime()); - out.writeVInt(entry.shards().size()); - for (Map.Entry shardEntry : entry.shards().entrySet()) { - shardEntry.getKey().writeTo(out); - out.writeOptionalString(shardEntry.getValue().nodeId()); - out.writeByte(shardEntry.getValue().state().value()); - } + long startTime = in.readLong(); + ImmutableMap.Builder builder = ImmutableMap.builder(); + int shards = in.readVInt(); + for (int j = 0; j < shards; j++) { + ShardId shardId = ShardId.readShardId(in); + String nodeId = in.readOptionalString(); + State shardState = State.fromValue(in.readByte()); + builder.put(shardId, new ShardSnapshotStatus(nodeId, shardState)); } + entries[i] = new Entry(snapshotId, includeGlobalState, state, indexBuilder.build(), startTime, builder.build()); } + return new SnapshotMetaData(entries); + } - @Override - public SnapshotMetaData fromXContent(XContentParser parser) throws IOException { - throw new UnsupportedOperationException(); - } - - static final class Fields { - static final XContentBuilderString REPOSITORY = new XContentBuilderString("repository"); - static final XContentBuilderString SNAPSHOTS = new XContentBuilderString("snapshots"); - static final XContentBuilderString SNAPSHOT = new XContentBuilderString("snapshot"); - static final XContentBuilderString INCLUDE_GLOBAL_STATE = new XContentBuilderString("include_global_state"); - static final XContentBuilderString STATE = new XContentBuilderString("state"); - static final XContentBuilderString INDICES = new XContentBuilderString("indices"); - static final XContentBuilderString START_TIME_MILLIS = new XContentBuilderString("start_time_millis"); - static final XContentBuilderString START_TIME = new XContentBuilderString("start_time"); - static final XContentBuilderString SHARDS = new XContentBuilderString("shards"); - static final XContentBuilderString INDEX = new XContentBuilderString("index"); - static final XContentBuilderString SHARD = new XContentBuilderString("shard"); - static final XContentBuilderString NODE = new XContentBuilderString("node"); - } - - @Override - public void toXContent(SnapshotMetaData customIndexMetaData, XContentBuilder builder, ToXContent.Params params) throws IOException { - builder.startArray(Fields.SNAPSHOTS); - for (Entry entry : customIndexMetaData.entries()) { - toXContent(entry, builder, params); + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeVInt(entries.size()); + for (Entry entry : entries) { + entry.snapshotId().writeTo(out); + out.writeBoolean(entry.includeGlobalState()); + out.writeByte(entry.state().value()); + out.writeVInt(entry.indices().size()); + for (String index : entry.indices()) { + out.writeString(index); } - builder.endArray(); - } - - public void toXContent(Entry entry, XContentBuilder builder, ToXContent.Params params) throws IOException { - builder.startObject(); - builder.field(Fields.REPOSITORY, entry.snapshotId().getRepository()); - builder.field(Fields.SNAPSHOT, entry.snapshotId().getSnapshot()); - builder.field(Fields.INCLUDE_GLOBAL_STATE, entry.includeGlobalState()); - builder.field(Fields.STATE, entry.state()); - builder.startArray(Fields.INDICES); - { - for (String index : entry.indices()) { - builder.value(index); - } + out.writeLong(entry.startTime()); + out.writeVInt(entry.shards().size()); + for (Map.Entry shardEntry : entry.shards().entrySet()) { + shardEntry.getKey().writeTo(out); + out.writeOptionalString(shardEntry.getValue().nodeId()); + out.writeByte(shardEntry.getValue().state().value()); } - builder.endArray(); - builder.timeValueField(Fields.START_TIME_MILLIS, Fields.START_TIME, entry.startTime()); - builder.startArray(Fields.SHARDS); - { - for (Map.Entry shardEntry : entry.shards.entrySet()) { - ShardId shardId = shardEntry.getKey(); - ShardSnapshotStatus status = shardEntry.getValue(); - builder.startObject(); - { - builder.field(Fields.INDEX, shardId.getIndex()); - builder.field(Fields.SHARD, shardId.getId()); - builder.field(Fields.STATE, status.state()); - builder.field(Fields.NODE, status.nodeId()); - } - builder.endObject(); - } - } - builder.endArray(); - builder.endObject(); } } + @Override + public SnapshotMetaData fromXContent(XContentParser parser) throws IOException { + throw new UnsupportedOperationException(); + } + @Override + public EnumSet context() { + return MetaData.API_ONLY; + } + + static final class Fields { + static final XContentBuilderString REPOSITORY = new XContentBuilderString("repository"); + static final XContentBuilderString SNAPSHOTS = new XContentBuilderString("snapshots"); + static final XContentBuilderString SNAPSHOT = new XContentBuilderString("snapshot"); + static final XContentBuilderString INCLUDE_GLOBAL_STATE = new XContentBuilderString("include_global_state"); + static final XContentBuilderString STATE = new XContentBuilderString("state"); + static final XContentBuilderString INDICES = new XContentBuilderString("indices"); + static final XContentBuilderString START_TIME_MILLIS = new XContentBuilderString("start_time_millis"); + static final XContentBuilderString START_TIME = new XContentBuilderString("start_time"); + static final XContentBuilderString SHARDS = new XContentBuilderString("shards"); + static final XContentBuilderString INDEX = new XContentBuilderString("index"); + static final XContentBuilderString SHARD = new XContentBuilderString("shard"); + static final XContentBuilderString NODE = new XContentBuilderString("node"); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { + builder.startArray(Fields.SNAPSHOTS); + for (Entry entry : entries) { + toXContent(entry, builder, params); + } + builder.endArray(); + return builder; + } + + public void toXContent(Entry entry, XContentBuilder builder, ToXContent.Params params) throws IOException { + builder.startObject(); + builder.field(Fields.REPOSITORY, entry.snapshotId().getRepository()); + builder.field(Fields.SNAPSHOT, entry.snapshotId().getSnapshot()); + builder.field(Fields.INCLUDE_GLOBAL_STATE, entry.includeGlobalState()); + builder.field(Fields.STATE, entry.state()); + builder.startArray(Fields.INDICES); + { + for (String index : entry.indices()) { + builder.value(index); + } + } + builder.endArray(); + builder.timeValueField(Fields.START_TIME_MILLIS, Fields.START_TIME, entry.startTime()); + builder.startArray(Fields.SHARDS); + { + for (Map.Entry shardEntry : entry.shards.entrySet()) { + ShardId shardId = shardEntry.getKey(); + ShardSnapshotStatus status = shardEntry.getValue(); + builder.startObject(); + { + builder.field(Fields.INDEX, shardId.getIndex()); + builder.field(Fields.SHARD, shardId.getId()); + builder.field(Fields.STATE, status.state()); + builder.field(Fields.NODE, status.nodeId()); + } + builder.endObject(); + } + } + builder.endArray(); + builder.endObject(); + } } diff --git a/src/main/java/org/elasticsearch/cluster/node/DiscoveryNodes.java b/src/main/java/org/elasticsearch/cluster/node/DiscoveryNodes.java index 2831af8183d..8692e5fb006 100644 --- a/src/main/java/org/elasticsearch/cluster/node/DiscoveryNodes.java +++ b/src/main/java/org/elasticsearch/cluster/node/DiscoveryNodes.java @@ -25,6 +25,7 @@ import com.carrotsearch.hppc.cursors.ObjectObjectCursor; import com.google.common.collect.ImmutableList; import com.google.common.collect.UnmodifiableIterator; import org.elasticsearch.Version; +import org.elasticsearch.cluster.AbstractDiffable; import org.elasticsearch.common.Booleans; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.collect.ImmutableOpenMap; @@ -44,9 +45,10 @@ import static com.google.common.collect.Lists.newArrayList; * This class holds all {@link DiscoveryNode} in the cluster and provides convenience methods to * access, modify merge / diff discovery nodes. */ -public class DiscoveryNodes implements Iterable { +public class DiscoveryNodes extends AbstractDiffable implements Iterable { public static final DiscoveryNodes EMPTY_NODES = builder().build(); + public static final DiscoveryNodes PROTO = EMPTY_NODES; private final ImmutableOpenMap nodes; private final ImmutableOpenMap dataNodes; @@ -567,6 +569,44 @@ public class DiscoveryNodes implements Iterable { } } + public void writeTo(StreamOutput out) throws IOException { + if (masterNodeId == null) { + out.writeBoolean(false); + } else { + out.writeBoolean(true); + out.writeString(masterNodeId); + } + out.writeVInt(nodes.size()); + for (DiscoveryNode node : this) { + node.writeTo(out); + } + } + + public DiscoveryNodes readFrom(StreamInput in, DiscoveryNode localNode) throws IOException { + Builder builder = new Builder(); + if (in.readBoolean()) { + builder.masterNodeId(in.readString()); + } + if (localNode != null) { + builder.localNodeId(localNode.id()); + } + int size = in.readVInt(); + for (int i = 0; i < size; i++) { + DiscoveryNode node = DiscoveryNode.readNode(in); + if (localNode != null && node.id().equals(localNode.id())) { + // reuse the same instance of our address and local node id for faster equality + node = localNode; + } + builder.put(node); + } + return builder.build(); + } + + @Override + public DiscoveryNodes readFrom(StreamInput in) throws IOException { + return readFrom(in, localNode()); + } + public static Builder builder() { return new Builder(); } @@ -631,37 +671,8 @@ public class DiscoveryNodes implements Iterable { return new DiscoveryNodes(nodes.build(), dataNodesBuilder.build(), masterNodesBuilder.build(), masterNodeId, localNodeId, minNodeVersion, minNonClientNodeVersion); } - public static void writeTo(DiscoveryNodes nodes, StreamOutput out) throws IOException { - if (nodes.masterNodeId() == null) { - out.writeBoolean(false); - } else { - out.writeBoolean(true); - out.writeString(nodes.masterNodeId); - } - out.writeVInt(nodes.size()); - for (DiscoveryNode node : nodes) { - node.writeTo(out); - } - } - public static DiscoveryNodes readFrom(StreamInput in, @Nullable DiscoveryNode localNode) throws IOException { - Builder builder = new Builder(); - if (in.readBoolean()) { - builder.masterNodeId(in.readString()); - } - if (localNode != null) { - builder.localNodeId(localNode.id()); - } - int size = in.readVInt(); - for (int i = 0; i < size; i++) { - DiscoveryNode node = DiscoveryNode.readNode(in); - if (localNode != null && node.id().equals(localNode.id())) { - // reuse the same instance of our address and local node id for faster equality - node = localNode; - } - builder.put(node); - } - return builder.build(); + return PROTO.readFrom(in, localNode); } } } diff --git a/src/main/java/org/elasticsearch/cluster/routing/IndexRoutingTable.java b/src/main/java/org/elasticsearch/cluster/routing/IndexRoutingTable.java index de4ed5434e1..6aaa260c4b5 100644 --- a/src/main/java/org/elasticsearch/cluster/routing/IndexRoutingTable.java +++ b/src/main/java/org/elasticsearch/cluster/routing/IndexRoutingTable.java @@ -25,6 +25,7 @@ import com.carrotsearch.hppc.cursors.IntObjectCursor; import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; import com.google.common.collect.UnmodifiableIterator; +import org.elasticsearch.cluster.AbstractDiffable; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.common.collect.ImmutableOpenIntMap; @@ -55,7 +56,9 @@ import static com.google.common.collect.Lists.newArrayList; * represented as {@link ShardRouting}. *

    */ -public class IndexRoutingTable implements Iterable { +public class IndexRoutingTable extends AbstractDiffable implements Iterable { + + public static final IndexRoutingTable PROTO = builder("").build(); private final String index; private final ShardShuffler shuffler; @@ -314,9 +317,51 @@ public class IndexRoutingTable implements Iterable { return new GroupShardsIterator(set); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + IndexRoutingTable that = (IndexRoutingTable) o; + + if (!index.equals(that.index)) return false; + if (!shards.equals(that.shards)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = index.hashCode(); + result = 31 * result + shards.hashCode(); + return result; + } + public void validate() throws RoutingValidationException { } + @Override + public IndexRoutingTable readFrom(StreamInput in) throws IOException { + String index = in.readString(); + Builder builder = new Builder(index); + + int size = in.readVInt(); + for (int i = 0; i < size; i++) { + builder.addIndexShard(IndexShardRoutingTable.Builder.readFromThin(in, index)); + } + + return builder.build(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(index); + out.writeVInt(shards.size()); + for (IndexShardRoutingTable indexShard : this) { + IndexShardRoutingTable.Builder.writeToThin(indexShard, out); + } + } + public static Builder builder(String index) { return new Builder(index); } @@ -338,30 +383,7 @@ public class IndexRoutingTable implements Iterable { * @throws IOException if something happens during read */ public static IndexRoutingTable readFrom(StreamInput in) throws IOException { - String index = in.readString(); - Builder builder = new Builder(index); - - int size = in.readVInt(); - for (int i = 0; i < size; i++) { - builder.addIndexShard(IndexShardRoutingTable.Builder.readFromThin(in, index)); - } - - return builder.build(); - } - - /** - * Writes an {@link IndexRoutingTable} to a {@link StreamOutput}. - * - * @param index {@link IndexRoutingTable} to write - * @param out {@link StreamOutput} to write to - * @throws IOException if something happens during write - */ - public static void writeTo(IndexRoutingTable index, StreamOutput out) throws IOException { - out.writeString(index.index()); - out.writeVInt(index.shards.size()); - for (IndexShardRoutingTable indexShard : index) { - IndexShardRoutingTable.Builder.writeToThin(indexShard, out); - } + return PROTO.readFrom(in); } /** diff --git a/src/main/java/org/elasticsearch/cluster/routing/IndexShardRoutingTable.java b/src/main/java/org/elasticsearch/cluster/routing/IndexShardRoutingTable.java index 00e50b76129..2371b96f5b0 100644 --- a/src/main/java/org/elasticsearch/cluster/routing/IndexShardRoutingTable.java +++ b/src/main/java/org/elasticsearch/cluster/routing/IndexShardRoutingTable.java @@ -347,6 +347,28 @@ public class IndexShardRoutingTable implements Iterable { return new PlainShardIterator(shardId, ordered); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + IndexShardRoutingTable that = (IndexShardRoutingTable) o; + + if (primaryAllocatedPostApi != that.primaryAllocatedPostApi) return false; + if (!shardId.equals(that.shardId)) return false; + if (!shards.equals(that.shards)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = shardId.hashCode(); + result = 31 * result + shards.hashCode(); + result = 31 * result + (primaryAllocatedPostApi ? 1 : 0); + return result; + } + /** * Returns true iff all shards in the routing table are started otherwise false */ diff --git a/src/main/java/org/elasticsearch/cluster/routing/RoutingTable.java b/src/main/java/org/elasticsearch/cluster/routing/RoutingTable.java index 9f1b5db6c6b..25a8bac2f88 100644 --- a/src/main/java/org/elasticsearch/cluster/routing/RoutingTable.java +++ b/src/main/java/org/elasticsearch/cluster/routing/RoutingTable.java @@ -21,7 +21,7 @@ package org.elasticsearch.cluster.routing; import com.carrotsearch.hppc.IntSet; import com.google.common.collect.*; -import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.*; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.common.io.stream.StreamInput; @@ -44,7 +44,9 @@ import static com.google.common.collect.Maps.newHashMap; * * @see IndexRoutingTable */ -public class RoutingTable implements Iterable { +public class RoutingTable implements Iterable, Diffable { + + public static RoutingTable PROTO = builder().build(); public static final RoutingTable EMPTY_ROUTING_TABLE = builder().build(); @@ -254,6 +256,66 @@ public class RoutingTable implements Iterable { return new GroupShardsIterator(set); } + @Override + public Diff diff(RoutingTable previousState) { + return new RoutingTableDiff(previousState, this); + } + + @Override + public Diff readDiffFrom(StreamInput in) throws IOException { + return new RoutingTableDiff(in); + } + + @Override + public RoutingTable readFrom(StreamInput in) throws IOException { + Builder builder = new Builder(); + builder.version = in.readLong(); + int size = in.readVInt(); + for (int i = 0; i < size; i++) { + IndexRoutingTable index = IndexRoutingTable.Builder.readFrom(in); + builder.add(index); + } + + return builder.build(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeLong(version); + out.writeVInt(indicesRouting.size()); + for (IndexRoutingTable index : indicesRouting.values()) { + index.writeTo(out); + } + } + + private static class RoutingTableDiff implements Diff { + + private final long version; + + private final Diff> indicesRouting; + + public RoutingTableDiff(RoutingTable before, RoutingTable after) { + version = after.version; + indicesRouting = DiffableUtils.diff(before.indicesRouting, after.indicesRouting); + } + + public RoutingTableDiff(StreamInput in) throws IOException { + version = in.readLong(); + indicesRouting = DiffableUtils.readImmutableMapDiff(in, IndexRoutingTable.PROTO); + } + + @Override + public RoutingTable apply(RoutingTable part) { + return new RoutingTable(version, indicesRouting.apply(part.indicesRouting)); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeLong(version); + indicesRouting.writeTo(out); + } + } + public static Builder builder() { return new Builder(); } @@ -403,6 +465,11 @@ public class RoutingTable implements Iterable { return this; } + public Builder indicesRouting(ImmutableMap indicesRouting) { + this.indicesRouting.putAll(indicesRouting); + return this; + } + public Builder remove(String index) { indicesRouting.remove(index); return this; @@ -422,23 +489,7 @@ public class RoutingTable implements Iterable { } public static RoutingTable readFrom(StreamInput in) throws IOException { - Builder builder = new Builder(); - builder.version = in.readLong(); - int size = in.readVInt(); - for (int i = 0; i < size; i++) { - IndexRoutingTable index = IndexRoutingTable.Builder.readFrom(in); - builder.add(index); - } - - return builder.build(); - } - - public static void writeTo(RoutingTable table, StreamOutput out) throws IOException { - out.writeLong(table.version); - out.writeVInt(table.indicesRouting.size()); - for (IndexRoutingTable index : table.indicesRouting.values()) { - IndexRoutingTable.Builder.writeTo(index, out); - } + return PROTO.readFrom(in); } } @@ -450,5 +501,4 @@ public class RoutingTable implements Iterable { return sb.toString(); } - } diff --git a/src/main/java/org/elasticsearch/cluster/service/InternalClusterService.java b/src/main/java/org/elasticsearch/cluster/service/InternalClusterService.java index 17350ba6c04..b1823e5d74e 100644 --- a/src/main/java/org/elasticsearch/cluster/service/InternalClusterService.java +++ b/src/main/java/org/elasticsearch/cluster/service/InternalClusterService.java @@ -401,7 +401,7 @@ public class InternalClusterService extends AbstractLifecycleComponent { + /** + * Reads a copy of an object with the same type form the stream input + * + * The caller object remains unchanged. + */ + T readFrom(StreamInput in) throws IOException; +} diff --git a/src/main/java/org/elasticsearch/common/io/stream/Writeable.java b/src/main/java/org/elasticsearch/common/io/stream/Writeable.java new file mode 100644 index 00000000000..9025315dc43 --- /dev/null +++ b/src/main/java/org/elasticsearch/common/io/stream/Writeable.java @@ -0,0 +1,30 @@ +/* + * 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.common.io.stream; + +import java.io.IOException; + +public interface Writeable extends StreamableReader { + + /** + * Writes the current object into the output stream out + */ + void writeTo(StreamOutput out) throws IOException; +} diff --git a/src/main/java/org/elasticsearch/discovery/Discovery.java b/src/main/java/org/elasticsearch/discovery/Discovery.java index dfd51e6348f..36b8e5da6f5 100644 --- a/src/main/java/org/elasticsearch/discovery/Discovery.java +++ b/src/main/java/org/elasticsearch/discovery/Discovery.java @@ -19,6 +19,7 @@ package org.elasticsearch.discovery; +import org.elasticsearch.cluster.ClusterChangedEvent; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.routing.allocation.AllocationService; @@ -59,7 +60,7 @@ public interface Discovery extends LifecycleComponent { * The {@link AckListener} allows to keep track of the ack received from nodes, and verify whether * they updated their own cluster state or not. */ - void publish(ClusterState clusterState, AckListener ackListener); + void publish(ClusterChangedEvent clusterChangedEvent, AckListener ackListener); public static interface AckListener { void onNodeAck(DiscoveryNode node, @Nullable Throwable t); diff --git a/src/main/java/org/elasticsearch/discovery/DiscoveryService.java b/src/main/java/org/elasticsearch/discovery/DiscoveryService.java index 1f7207abd5b..a95c313447b 100644 --- a/src/main/java/org/elasticsearch/discovery/DiscoveryService.java +++ b/src/main/java/org/elasticsearch/discovery/DiscoveryService.java @@ -21,6 +21,7 @@ package org.elasticsearch.discovery; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchTimeoutException; +import org.elasticsearch.cluster.ClusterChangedEvent; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.block.ClusterBlock; import org.elasticsearch.cluster.node.DiscoveryNode; @@ -132,9 +133,9 @@ public class DiscoveryService extends AbstractLifecycleComponent implem private static final ConcurrentMap clusterGroups = ConcurrentCollections.newConcurrentMap(); + private volatile ClusterState lastProcessedClusterState; + @Inject public LocalDiscovery(Settings settings, ClusterName clusterName, TransportService transportService, ClusterService clusterService, DiscoveryNodeService discoveryNodeService, Version version, DiscoverySettings discoverySettings) { @@ -273,7 +279,7 @@ public class LocalDiscovery extends AbstractLifecycleComponent implem } @Override - public void publish(ClusterState clusterState, final Discovery.AckListener ackListener) { + public void publish(ClusterChangedEvent clusterChangedEvent, final Discovery.AckListener ackListener) { if (!master) { throw new IllegalStateException("Shouldn't publish state when not master"); } @@ -286,7 +292,7 @@ public class LocalDiscovery extends AbstractLifecycleComponent implem } nodesToPublishTo.add(localDiscovery.localNode); } - publish(members, clusterState, new AckClusterStatePublishResponseHandler(nodesToPublishTo, ackListener)); + publish(members, clusterChangedEvent, new AckClusterStatePublishResponseHandler(nodesToPublishTo, ackListener)); } } @@ -299,17 +305,47 @@ public class LocalDiscovery extends AbstractLifecycleComponent implem return members.toArray(new LocalDiscovery[members.size()]); } - private void publish(LocalDiscovery[] members, ClusterState clusterState, final BlockingClusterStatePublishResponseHandler publishResponseHandler) { + private void publish(LocalDiscovery[] members, ClusterChangedEvent clusterChangedEvent, final BlockingClusterStatePublishResponseHandler publishResponseHandler) { try { // we do the marshaling intentionally, to check it works well... - final byte[] clusterStateBytes = Builder.toBytes(clusterState); + byte[] clusterStateBytes = null; + byte[] clusterStateDiffBytes = null; + ClusterState clusterState = clusterChangedEvent.state(); for (final LocalDiscovery discovery : members) { if (discovery.master) { continue; } - final ClusterState nodeSpecificClusterState = ClusterState.Builder.fromBytes(clusterStateBytes, discovery.localNode); + ClusterState newNodeSpecificClusterState = null; + synchronized (this) { + // we do the marshaling intentionally, to check it works well... + // check if we publsihed cluster state at least once and node was in the cluster when we published cluster state the last time + if (discovery.lastProcessedClusterState != null && clusterChangedEvent.previousState().nodes().nodeExists(discovery.localNode.id())) { + // both conditions are true - which means we can try sending cluster state as diffs + if (clusterStateDiffBytes == null) { + Diff diff = clusterState.diff(clusterChangedEvent.previousState()); + BytesStreamOutput os = new BytesStreamOutput(); + diff.writeTo(os); + clusterStateDiffBytes = os.bytes().toBytes(); + } + try { + newNodeSpecificClusterState = discovery.lastProcessedClusterState.readDiffFrom(new BytesStreamInput(clusterStateDiffBytes)).apply(discovery.lastProcessedClusterState); + logger.debug("sending diff cluster state version with size {} to [{}]", clusterStateDiffBytes.length, discovery.localNode.getName()); + } catch (IncompatibleClusterStateVersionException ex) { + logger.warn("incompatible cluster state version - resending complete cluster state", ex); + } + } + if (newNodeSpecificClusterState == null) { + if (clusterStateBytes == null) { + clusterStateBytes = Builder.toBytes(clusterState); + } + newNodeSpecificClusterState = ClusterState.Builder.fromBytes(clusterStateBytes, discovery.localNode); + } + discovery.lastProcessedClusterState = newNodeSpecificClusterState; + } + final ClusterState nodeSpecificClusterState = newNodeSpecificClusterState; + nodeSpecificClusterState.status(ClusterState.ClusterStateStatus.RECEIVED); // ignore cluster state messages that do not include "me", not in the game yet... if (nodeSpecificClusterState.nodes().localNode() != null) { diff --git a/src/main/java/org/elasticsearch/discovery/zen/ZenDiscovery.java b/src/main/java/org/elasticsearch/discovery/zen/ZenDiscovery.java index 0defcb7edd5..5bec60abf04 100644 --- a/src/main/java/org/elasticsearch/discovery/zen/ZenDiscovery.java +++ b/src/main/java/org/elasticsearch/discovery/zen/ZenDiscovery.java @@ -22,7 +22,6 @@ package org.elasticsearch.discovery.zen; import com.google.common.base.Objects; import com.google.common.collect.Lists; import com.google.common.collect.Sets; -import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.cluster.*; import org.elasticsearch.cluster.block.ClusterBlocks; @@ -329,12 +328,12 @@ public class ZenDiscovery extends AbstractLifecycleComponent implemen @Override - public void publish(ClusterState clusterState, AckListener ackListener) { - if (!clusterState.getNodes().localNodeMaster()) { + public void publish(ClusterChangedEvent clusterChangedEvent, AckListener ackListener) { + if (!clusterChangedEvent.state().getNodes().localNodeMaster()) { throw new IllegalStateException("Shouldn't publish state when not master"); } - nodesFD.updateNodesAndPing(clusterState); - publishClusterState.publish(clusterState, ackListener); + nodesFD.updateNodesAndPing(clusterChangedEvent.state()); + publishClusterState.publish(clusterChangedEvent, ackListener); } /** diff --git a/src/main/java/org/elasticsearch/discovery/zen/publish/PublishClusterStateAction.java b/src/main/java/org/elasticsearch/discovery/zen/publish/PublishClusterStateAction.java index fd1ba85c25c..c4ad8895e79 100644 --- a/src/main/java/org/elasticsearch/discovery/zen/publish/PublishClusterStateAction.java +++ b/src/main/java/org/elasticsearch/discovery/zen/publish/PublishClusterStateAction.java @@ -21,8 +21,12 @@ package org.elasticsearch.discovery.zen.publish; import com.google.common.collect.Maps; import org.elasticsearch.Version; +import org.elasticsearch.cluster.ClusterChangedEvent; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.Diff; +import org.elasticsearch.cluster.IncompatibleClusterStateVersionException; import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.compress.Compressor; @@ -40,10 +44,13 @@ import org.elasticsearch.discovery.zen.DiscoveryNodesProvider; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.*; +import java.io.IOException; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; /** * @@ -83,73 +90,43 @@ public class PublishClusterStateAction extends AbstractComponent { transportService.removeHandler(ACTION_NAME); } - public void publish(ClusterState clusterState, final Discovery.AckListener ackListener) { - Set nodesToPublishTo = new HashSet<>(clusterState.nodes().size()); + public void publish(ClusterChangedEvent clusterChangedEvent, final Discovery.AckListener ackListener) { + Set nodesToPublishTo = new HashSet<>(clusterChangedEvent.state().nodes().size()); DiscoveryNode localNode = nodesProvider.nodes().localNode(); - for (final DiscoveryNode node : clusterState.nodes()) { + for (final DiscoveryNode node : clusterChangedEvent.state().nodes()) { if (node.equals(localNode)) { continue; } nodesToPublishTo.add(node); } - publish(clusterState, nodesToPublishTo, new AckClusterStatePublishResponseHandler(nodesToPublishTo, ackListener)); + publish(clusterChangedEvent, nodesToPublishTo, new AckClusterStatePublishResponseHandler(nodesToPublishTo, ackListener)); } - private void publish(final ClusterState clusterState, final Set nodesToPublishTo, + private void publish(final ClusterChangedEvent clusterChangedEvent, final Set nodesToPublishTo, final BlockingClusterStatePublishResponseHandler publishResponseHandler) { Map serializedStates = Maps.newHashMap(); + Map serializedDiffs = Maps.newHashMap(); + final ClusterState clusterState = clusterChangedEvent.state(); + final ClusterState previousState = clusterChangedEvent.previousState(); final AtomicBoolean timedOutWaitingForNodes = new AtomicBoolean(false); final TimeValue publishTimeout = discoverySettings.getPublishTimeout(); + final boolean sendFullVersion = !discoverySettings.getPublishDiff() || previousState == null; + Diff diff = null; for (final DiscoveryNode node : nodesToPublishTo) { // try and serialize the cluster state once (or per version), so we don't serialize it // per node when we send it over the wire, compress it while we are at it... - BytesReference bytes = serializedStates.get(node.version()); - if (bytes == null) { - try { - BytesStreamOutput bStream = new BytesStreamOutput(); - StreamOutput stream = CompressorFactory.defaultCompressor().streamOutput(bStream); - stream.setVersion(node.version()); - ClusterState.Builder.writeTo(clusterState, stream); - stream.close(); - bytes = bStream.bytes(); - serializedStates.put(node.version(), bytes); - } catch (Throwable e) { - logger.warn("failed to serialize cluster_state before publishing it to node {}", e, node); - publishResponseHandler.onFailure(node, e); - continue; + // we don't send full version if node didn't exist in the previous version of cluster state + if (sendFullVersion || !previousState.nodes().nodeExists(node.id())) { + sendFullClusterState(clusterState, serializedStates, node, timedOutWaitingForNodes, publishTimeout, publishResponseHandler); + } else { + if (diff == null) { + diff = clusterState.diff(previousState); } - } - try { - TransportRequestOptions options = TransportRequestOptions.options().withType(TransportRequestOptions.Type.STATE).withCompress(false); - // no need to put a timeout on the options here, because we want the response to eventually be received - // and not log an error if it arrives after the timeout - transportService.sendRequest(node, ACTION_NAME, - new BytesTransportRequest(bytes, node.version()), - options, // no need to compress, we already compressed the bytes - - new EmptyTransportResponseHandler(ThreadPool.Names.SAME) { - - @Override - public void handleResponse(TransportResponse.Empty response) { - if (timedOutWaitingForNodes.get()) { - logger.debug("node {} responded for cluster state [{}] (took longer than [{}])", node, clusterState.version(), publishTimeout); - } - publishResponseHandler.onResponse(node); - } - - @Override - public void handleException(TransportException exp) { - logger.debug("failed to send cluster state to {}", exp, node); - publishResponseHandler.onFailure(node, exp); - } - }); - } catch (Throwable t) { - logger.debug("error sending cluster state to {}", t, node); - publishResponseHandler.onFailure(node, t); + sendClusterStateDiff(clusterState, diff, serializedDiffs, node, timedOutWaitingForNodes, publishTimeout, publishResponseHandler); } } @@ -171,7 +148,107 @@ public class PublishClusterStateAction extends AbstractComponent { } } + private void sendFullClusterState(ClusterState clusterState, @Nullable Map serializedStates, + DiscoveryNode node, AtomicBoolean timedOutWaitingForNodes, TimeValue publishTimeout, + BlockingClusterStatePublishResponseHandler publishResponseHandler) { + BytesReference bytes = null; + if (serializedStates != null) { + bytes = serializedStates.get(node.version()); + } + if (bytes == null) { + try { + bytes = serializeFullClusterState(clusterState, node.version()); + if (serializedStates != null) { + serializedStates.put(node.version(), bytes); + } + } catch (Throwable e) { + logger.warn("failed to serialize cluster_state before publishing it to node {}", e, node); + publishResponseHandler.onFailure(node, e); + return; + } + } + publishClusterStateToNode(clusterState, bytes, node, timedOutWaitingForNodes, publishTimeout, publishResponseHandler, false); + } + + private void sendClusterStateDiff(ClusterState clusterState, Diff diff, Map serializedDiffs, DiscoveryNode node, + AtomicBoolean timedOutWaitingForNodes, TimeValue publishTimeout, + BlockingClusterStatePublishResponseHandler publishResponseHandler) { + BytesReference bytes = serializedDiffs.get(node.version()); + if (bytes == null) { + try { + bytes = serializeDiffClusterState(diff, node.version()); + serializedDiffs.put(node.version(), bytes); + } catch (Throwable e) { + logger.warn("failed to serialize diff of cluster_state before publishing it to node {}", e, node); + publishResponseHandler.onFailure(node, e); + return; + } + } + publishClusterStateToNode(clusterState, bytes, node, timedOutWaitingForNodes, publishTimeout, publishResponseHandler, true); + } + + private void publishClusterStateToNode(final ClusterState clusterState, BytesReference bytes, + final DiscoveryNode node, final AtomicBoolean timedOutWaitingForNodes, + final TimeValue publishTimeout, + final BlockingClusterStatePublishResponseHandler publishResponseHandler, + final boolean sendDiffs) { + try { + TransportRequestOptions options = TransportRequestOptions.options().withType(TransportRequestOptions.Type.STATE).withCompress(false); + // no need to put a timeout on the options here, because we want the response to eventually be received + // and not log an error if it arrives after the timeout + transportService.sendRequest(node, ACTION_NAME, + new BytesTransportRequest(bytes, node.version()), + options, // no need to compress, we already compressed the bytes + + new EmptyTransportResponseHandler(ThreadPool.Names.SAME) { + + @Override + public void handleResponse(TransportResponse.Empty response) { + if (timedOutWaitingForNodes.get()) { + logger.debug("node {} responded for cluster state [{}] (took longer than [{}])", node, clusterState.version(), publishTimeout); + } + publishResponseHandler.onResponse(node); + } + + @Override + public void handleException(TransportException exp) { + if (sendDiffs && exp.unwrapCause() instanceof IncompatibleClusterStateVersionException) { + logger.debug("resending full cluster state to node {} reason {}", node, exp.getDetailedMessage()); + sendFullClusterState(clusterState, null, node, timedOutWaitingForNodes, publishTimeout, publishResponseHandler); + } else { + logger.debug("failed to send cluster state to {}", exp, node); + publishResponseHandler.onFailure(node, exp); + } + } + }); + } catch (Throwable t) { + logger.warn("error sending cluster state to {}", t, node); + publishResponseHandler.onFailure(node, t); + } + } + + public static BytesReference serializeFullClusterState(ClusterState clusterState, Version nodeVersion) throws IOException { + BytesStreamOutput bStream = new BytesStreamOutput(); + StreamOutput stream = CompressorFactory.defaultCompressor().streamOutput(bStream); + stream.setVersion(nodeVersion); + stream.writeBoolean(true); + clusterState.writeTo(stream); + stream.close(); + return bStream.bytes(); + } + + public static BytesReference serializeDiffClusterState(Diff diff, Version nodeVersion) throws IOException { + BytesStreamOutput bStream = new BytesStreamOutput(); + StreamOutput stream = CompressorFactory.defaultCompressor().streamOutput(bStream); + stream.setVersion(nodeVersion); + stream.writeBoolean(false); + diff.writeTo(stream); + stream.close(); + return bStream.bytes(); + } + private class PublishClusterStateRequestHandler implements TransportRequestHandler { + private ClusterState lastSeenClusterState; @Override public void messageReceived(BytesTransportRequest request, final TransportChannel channel) throws Exception { @@ -183,11 +260,24 @@ public class PublishClusterStateAction extends AbstractComponent { in = request.bytes().streamInput(); } in.setVersion(request.version()); - ClusterState clusterState = ClusterState.Builder.readFrom(in, nodesProvider.nodes().localNode()); - clusterState.status(ClusterState.ClusterStateStatus.RECEIVED); - logger.debug("received cluster state version {}", clusterState.version()); + synchronized (this) { + // If true we received full cluster state - otherwise diffs + if (in.readBoolean()) { + lastSeenClusterState = ClusterState.Builder.readFrom(in, nodesProvider.nodes().localNode()); + logger.debug("received full cluster state version {} with size {}", lastSeenClusterState.version(), request.bytes().length()); + } else if (lastSeenClusterState != null) { + Diff diff = lastSeenClusterState.readDiffFrom(in); + lastSeenClusterState = diff.apply(lastSeenClusterState); + logger.debug("received diff cluster state version {} with uuid {}, diff size {}", lastSeenClusterState.version(), lastSeenClusterState.uuid(), request.bytes().length()); + } else { + logger.debug("received diff for but don't have any local cluster state - requesting full state"); + throw new IncompatibleClusterStateVersionException("have no local cluster state"); + } + lastSeenClusterState.status(ClusterState.ClusterStateStatus.RECEIVED); + } + try { - listener.onNewClusterState(clusterState, new NewClusterStateListener.NewStateProcessed() { + listener.onNewClusterState(lastSeenClusterState, new NewClusterStateListener.NewStateProcessed() { @Override public void onNewClusterStateProcessed() { try { @@ -207,7 +297,7 @@ public class PublishClusterStateAction extends AbstractComponent { } }); } catch (Exception e) { - logger.warn("unexpected error while processing cluster state version [{}]", e, clusterState.version()); + logger.warn("unexpected error while processing cluster state version [{}]", e, lastSeenClusterState.version()); try { channel.sendResponse(e); } catch (Throwable e1) { diff --git a/src/main/java/org/elasticsearch/gateway/Gateway.java b/src/main/java/org/elasticsearch/gateway/Gateway.java index cd15bccdc4a..139b5763489 100644 --- a/src/main/java/org/elasticsearch/gateway/Gateway.java +++ b/src/main/java/org/elasticsearch/gateway/Gateway.java @@ -31,7 +31,7 @@ import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.NodeEnvironment; -import org.elasticsearch.indices.IndicesService; + import java.nio.file.Path; diff --git a/src/main/java/org/elasticsearch/gateway/LocalAllocateDangledIndices.java b/src/main/java/org/elasticsearch/gateway/LocalAllocateDangledIndices.java index 43dec7edb51..5538ef6d043 100644 --- a/src/main/java/org/elasticsearch/gateway/LocalAllocateDangledIndices.java +++ b/src/main/java/org/elasticsearch/gateway/LocalAllocateDangledIndices.java @@ -198,7 +198,7 @@ public class LocalAllocateDangledIndices extends AbstractComponent { fromNode.writeTo(out); out.writeVInt(indices.length); for (IndexMetaData indexMetaData : indices) { - IndexMetaData.Builder.writeTo(indexMetaData, out); + indexMetaData.writeTo(out); } } } diff --git a/src/main/java/org/elasticsearch/gateway/TransportNodesListGatewayMetaState.java b/src/main/java/org/elasticsearch/gateway/TransportNodesListGatewayMetaState.java index 36372009f87..900a2e7ffc7 100644 --- a/src/main/java/org/elasticsearch/gateway/TransportNodesListGatewayMetaState.java +++ b/src/main/java/org/elasticsearch/gateway/TransportNodesListGatewayMetaState.java @@ -221,7 +221,7 @@ public class TransportNodesListGatewayMetaState extends TransportNodesOperationA out.writeBoolean(false); } else { out.writeBoolean(true); - MetaData.Builder.writeTo(metaData, out); + metaData.writeTo(out); } } } diff --git a/src/main/java/org/elasticsearch/rest/action/admin/cluster/repositories/get/RestGetRepositoriesAction.java b/src/main/java/org/elasticsearch/rest/action/admin/cluster/repositories/get/RestGetRepositoriesAction.java index be4e1b4e3f3..85b46925b5f 100644 --- a/src/main/java/org/elasticsearch/rest/action/admin/cluster/repositories/get/RestGetRepositoriesAction.java +++ b/src/main/java/org/elasticsearch/rest/action/admin/cluster/repositories/get/RestGetRepositoriesAction.java @@ -58,7 +58,7 @@ public class RestGetRepositoriesAction extends BaseRestHandler { public RestResponse buildResponse(GetRepositoriesResponse response, XContentBuilder builder) throws Exception { builder.startObject(); for (RepositoryMetaData repositoryMetaData : response.repositories()) { - RepositoriesMetaData.FACTORY.toXContent(repositoryMetaData, builder, request); + RepositoriesMetaData.toXContent(repositoryMetaData, builder, request); } builder.endObject(); diff --git a/src/main/java/org/elasticsearch/rest/action/admin/indices/get/RestGetIndicesAction.java b/src/main/java/org/elasticsearch/rest/action/admin/indices/get/RestGetIndicesAction.java index 7e4e56710b7..dd1dca34bbc 100644 --- a/src/main/java/org/elasticsearch/rest/action/admin/indices/get/RestGetIndicesAction.java +++ b/src/main/java/org/elasticsearch/rest/action/admin/indices/get/RestGetIndicesAction.java @@ -146,7 +146,7 @@ public class RestGetIndicesAction extends BaseRestHandler { builder.startObject(Fields.WARMERS); if (warmers != null) { for (IndexWarmersMetaData.Entry warmer : warmers) { - IndexWarmersMetaData.FACTORY.toXContent(warmer, builder, params); + IndexWarmersMetaData.toXContent(warmer, builder, params); } } builder.endObject(); diff --git a/src/main/java/org/elasticsearch/rest/action/admin/indices/warmer/get/RestGetWarmerAction.java b/src/main/java/org/elasticsearch/rest/action/admin/indices/warmer/get/RestGetWarmerAction.java index 7023eecedd4..be83ccbe4b5 100644 --- a/src/main/java/org/elasticsearch/rest/action/admin/indices/warmer/get/RestGetWarmerAction.java +++ b/src/main/java/org/elasticsearch/rest/action/admin/indices/warmer/get/RestGetWarmerAction.java @@ -72,7 +72,7 @@ public class RestGetWarmerAction extends BaseRestHandler { builder.startObject(entry.key, XContentBuilder.FieldCaseConversion.NONE); builder.startObject(IndexWarmersMetaData.TYPE, XContentBuilder.FieldCaseConversion.NONE); for (IndexWarmersMetaData.Entry warmerEntry : entry.value) { - IndexWarmersMetaData.FACTORY.toXContent(warmerEntry, builder, request); + IndexWarmersMetaData.toXContent(warmerEntry, builder, request); } builder.endObject(); builder.endObject(); diff --git a/src/main/java/org/elasticsearch/search/warmer/IndexWarmersMetaData.java b/src/main/java/org/elasticsearch/search/warmer/IndexWarmersMetaData.java index de56f823eac..ef1ef44ffb9 100644 --- a/src/main/java/org/elasticsearch/search/warmer/IndexWarmersMetaData.java +++ b/src/main/java/org/elasticsearch/search/warmer/IndexWarmersMetaData.java @@ -22,7 +22,9 @@ package org.elasticsearch.search.warmer; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import org.elasticsearch.Version; +import org.elasticsearch.cluster.AbstractDiffable; import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; @@ -33,16 +35,33 @@ import org.elasticsearch.common.xcontent.*; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; /** */ -public class IndexWarmersMetaData implements IndexMetaData.Custom { +public class IndexWarmersMetaData extends AbstractDiffable implements IndexMetaData.Custom { public static final String TYPE = "warmers"; - public static final Factory FACTORY = new Factory(); + public static final IndexWarmersMetaData PROTO = new IndexWarmersMetaData(); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + IndexWarmersMetaData that = (IndexWarmersMetaData) o; + + return entries.equals(that.entries); + + } + + @Override + public int hashCode() { + return entries.hashCode(); + } public static class Entry { private final String name; @@ -74,6 +93,29 @@ public class IndexWarmersMetaData implements IndexMetaData.Custom { public Boolean queryCache() { return this.queryCache; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Entry entry = (Entry) o; + + if (!name.equals(entry.name)) return false; + if (!Arrays.equals(types, entry.types)) return false; + if (!source.equals(entry.source)) return false; + return !(queryCache != null ? !queryCache.equals(entry.queryCache) : entry.queryCache != null); + + } + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + Arrays.hashCode(types); + result = 31 * result + source.hashCode(); + result = 31 * result + (queryCache != null ? queryCache.hashCode() : 0); + return result; + } } private final ImmutableList entries; @@ -92,149 +134,143 @@ public class IndexWarmersMetaData implements IndexMetaData.Custom { return TYPE; } - public static class Factory implements IndexMetaData.Custom.Factory { - - @Override - public String type() { - return TYPE; + @Override + public IndexWarmersMetaData readFrom(StreamInput in) throws IOException { + Entry[] entries = new Entry[in.readVInt()]; + for (int i = 0; i < entries.length; i++) { + String name = in.readString(); + String[] types = in.readStringArray(); + BytesReference source = null; + if (in.readBoolean()) { + source = in.readBytesReference(); + } + Boolean queryCache; + queryCache = in.readOptionalBoolean(); + entries[i] = new Entry(name, types, queryCache, source); } + return new IndexWarmersMetaData(entries); + } - @Override - public IndexWarmersMetaData readFrom(StreamInput in) throws IOException { - Entry[] entries = new Entry[in.readVInt()]; - for (int i = 0; i < entries.length; i++) { - String name = in.readString(); - String[] types = in.readStringArray(); - BytesReference source = null; - if (in.readBoolean()) { - source = in.readBytesReference(); - } - Boolean queryCache = null; - queryCache = in.readOptionalBoolean(); - entries[i] = new Entry(name, types, queryCache, source); - } - return new IndexWarmersMetaData(entries); - } - - @Override - public void writeTo(IndexWarmersMetaData warmers, StreamOutput out) throws IOException { - out.writeVInt(warmers.entries().size()); - for (Entry entry : warmers.entries()) { - out.writeString(entry.name()); - out.writeStringArray(entry.types()); - if (entry.source() == null) { - out.writeBoolean(false); - } else { - out.writeBoolean(true); - out.writeBytesReference(entry.source()); - } - out.writeOptionalBoolean(entry.queryCache()); - } - } - - @Override - public IndexWarmersMetaData fromMap(Map map) throws IOException { - // if it starts with the type, remove it - if (map.size() == 1 && map.containsKey(TYPE)) { - map = (Map) map.values().iterator().next(); - } - XContentBuilder builder = XContentFactory.smileBuilder().map(map); - try (XContentParser parser = XContentFactory.xContent(XContentType.SMILE).createParser(builder.bytes())) { - // move to START_OBJECT - parser.nextToken(); - return fromXContent(parser); - } - } - - @Override - public IndexWarmersMetaData fromXContent(XContentParser parser) throws IOException { - // we get here after we are at warmers token - String currentFieldName = null; - XContentParser.Token token; - List entries = new ArrayList<>(); - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - currentFieldName = parser.currentName(); - } else if (token == XContentParser.Token.START_OBJECT) { - String name = currentFieldName; - List types = new ArrayList<>(2); - BytesReference source = null; - Boolean queryCache = null; - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - currentFieldName = parser.currentName(); - } else if (token == XContentParser.Token.START_ARRAY) { - if ("types".equals(currentFieldName)) { - while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { - types.add(parser.text()); - } - } - } else if (token == XContentParser.Token.START_OBJECT) { - if ("source".equals(currentFieldName)) { - XContentBuilder builder = XContentFactory.jsonBuilder().map(parser.mapOrdered()); - source = builder.bytes(); - } - } else if (token == XContentParser.Token.VALUE_EMBEDDED_OBJECT) { - if ("source".equals(currentFieldName)) { - source = new BytesArray(parser.binaryValue()); - } - } else if (token.isValue()) { - if ("queryCache".equals(currentFieldName) || "query_cache".equals(currentFieldName)) { - queryCache = parser.booleanValue(); - } - } - } - entries.add(new Entry(name, types.size() == 0 ? Strings.EMPTY_ARRAY : types.toArray(new String[types.size()]), queryCache, source)); - } - } - return new IndexWarmersMetaData(entries.toArray(new Entry[entries.size()])); - } - - @Override - public void toXContent(IndexWarmersMetaData warmers, XContentBuilder builder, ToXContent.Params params) throws IOException { - //No need, IndexMetaData already writes it - //builder.startObject(TYPE, XContentBuilder.FieldCaseConversion.NONE); - for (Entry entry : warmers.entries()) { - toXContent(entry, builder, params); - } - //No need, IndexMetaData already writes it - //builder.endObject(); - } - - public void toXContent(Entry entry, XContentBuilder builder, ToXContent.Params params) throws IOException { - boolean binary = params.paramAsBoolean("binary", false); - builder.startObject(entry.name(), XContentBuilder.FieldCaseConversion.NONE); - builder.field("types", entry.types()); - if (entry.queryCache() != null) { - builder.field("queryCache", entry.queryCache()); - } - builder.field("source"); - if (binary) { - builder.value(entry.source()); + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeVInt(entries().size()); + for (Entry entry : entries()) { + out.writeString(entry.name()); + out.writeStringArray(entry.types()); + if (entry.source() == null) { + out.writeBoolean(false); } else { - Map mapping = XContentFactory.xContent(entry.source()).createParser(entry.source()).mapOrderedAndClose(); - builder.map(mapping); + out.writeBoolean(true); + out.writeBytesReference(entry.source()); } - builder.endObject(); - } - - @Override - public IndexWarmersMetaData merge(IndexWarmersMetaData first, IndexWarmersMetaData second) { - List entries = Lists.newArrayList(); - entries.addAll(first.entries()); - for (Entry secondEntry : second.entries()) { - boolean found = false; - for (Entry firstEntry : first.entries()) { - if (firstEntry.name().equals(secondEntry.name())) { - found = true; - break; - } - } - if (!found) { - entries.add(secondEntry); - } - } - return new IndexWarmersMetaData(entries.toArray(new Entry[entries.size()])); + out.writeOptionalBoolean(entry.queryCache()); } } + + @Override + public IndexWarmersMetaData fromMap(Map map) throws IOException { + // if it starts with the type, remove it + if (map.size() == 1 && map.containsKey(TYPE)) { + map = (Map) map.values().iterator().next(); + } + XContentBuilder builder = XContentFactory.smileBuilder().map(map); + try (XContentParser parser = XContentFactory.xContent(XContentType.SMILE).createParser(builder.bytes())) { + // move to START_OBJECT + parser.nextToken(); + return fromXContent(parser); + } + } + + @Override + public IndexWarmersMetaData fromXContent(XContentParser parser) throws IOException { + // we get here after we are at warmers token + String currentFieldName = null; + XContentParser.Token token; + List entries = new ArrayList<>(); + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.START_OBJECT) { + String name = currentFieldName; + List types = new ArrayList<>(2); + BytesReference source = null; + Boolean queryCache = null; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.START_ARRAY) { + if ("types".equals(currentFieldName)) { + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + types.add(parser.text()); + } + } + } else if (token == XContentParser.Token.START_OBJECT) { + if ("source".equals(currentFieldName)) { + XContentBuilder builder = XContentFactory.jsonBuilder().map(parser.mapOrdered()); + source = builder.bytes(); + } + } else if (token == XContentParser.Token.VALUE_EMBEDDED_OBJECT) { + if ("source".equals(currentFieldName)) { + source = new BytesArray(parser.binaryValue()); + } + } else if (token.isValue()) { + if ("queryCache".equals(currentFieldName) || "query_cache".equals(currentFieldName)) { + queryCache = parser.booleanValue(); + } + } + } + entries.add(new Entry(name, types.size() == 0 ? Strings.EMPTY_ARRAY : types.toArray(new String[types.size()]), queryCache, source)); + } + } + return new IndexWarmersMetaData(entries.toArray(new Entry[entries.size()])); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { + //No need, IndexMetaData already writes it + //builder.startObject(TYPE, XContentBuilder.FieldCaseConversion.NONE); + for (Entry entry : entries()) { + toXContent(entry, builder, params); + } + //No need, IndexMetaData already writes it + //builder.endObject(); + return builder; + } + + public static void toXContent(Entry entry, XContentBuilder builder, ToXContent.Params params) throws IOException { + boolean binary = params.paramAsBoolean("binary", false); + builder.startObject(entry.name(), XContentBuilder.FieldCaseConversion.NONE); + builder.field("types", entry.types()); + if (entry.queryCache() != null) { + builder.field("queryCache", entry.queryCache()); + } + builder.field("source"); + if (binary) { + builder.value(entry.source()); + } else { + Map mapping = XContentFactory.xContent(entry.source()).createParser(entry.source()).mapOrderedAndClose(); + builder.map(mapping); + } + builder.endObject(); + } + + @Override + public IndexMetaData.Custom mergeWith(IndexMetaData.Custom other) { + IndexWarmersMetaData second = (IndexWarmersMetaData) other; + List entries = Lists.newArrayList(); + entries.addAll(entries()); + for (Entry secondEntry : second.entries()) { + boolean found = false; + for (Entry firstEntry : entries()) { + if (firstEntry.name().equals(secondEntry.name())) { + found = true; + break; + } + } + if (!found) { + entries.add(secondEntry); + } + } + return new IndexWarmersMetaData(entries.toArray(new Entry[entries.size()])); + } } diff --git a/src/test/java/org/elasticsearch/cluster/ClusterStateDiffPublishingTests.java b/src/test/java/org/elasticsearch/cluster/ClusterStateDiffPublishingTests.java new file mode 100644 index 00000000000..33008fd63d2 --- /dev/null +++ b/src/test/java/org/elasticsearch/cluster/ClusterStateDiffPublishingTests.java @@ -0,0 +1,625 @@ +/* + * 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.cluster; + +import com.google.common.collect.ImmutableMap; +import org.elasticsearch.Version; +import org.elasticsearch.cluster.block.ClusterBlocks; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.collect.ImmutableOpenMap; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.settings.ImmutableSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.discovery.Discovery; +import org.elasticsearch.discovery.DiscoverySettings; +import org.elasticsearch.discovery.zen.DiscoveryNodesProvider; +import org.elasticsearch.discovery.zen.publish.PublishClusterStateAction; +import org.elasticsearch.node.service.NodeService; +import org.elasticsearch.node.settings.NodeSettingsService; +import org.elasticsearch.test.ElasticsearchTestCase; +import org.elasticsearch.test.junit.annotations.TestLogging; +import org.elasticsearch.test.transport.MockTransportService; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportConnectionListener; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.transport.local.LocalTransport; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.google.common.collect.Maps.newHashMap; +import static org.hamcrest.Matchers.*; + +public class ClusterStateDiffPublishingTests extends ElasticsearchTestCase { + + protected ThreadPool threadPool; + protected Map nodes = newHashMap(); + + public static class MockNode { + public final DiscoveryNode discoveryNode; + public final MockTransportService service; + public final PublishClusterStateAction action; + public final MockDiscoveryNodesProvider nodesProvider; + + public MockNode(DiscoveryNode discoveryNode, MockTransportService service, PublishClusterStateAction action, MockDiscoveryNodesProvider nodesProvider) { + this.discoveryNode = discoveryNode; + this.service = service; + this.action = action; + this.nodesProvider = nodesProvider; + } + + public void connectTo(DiscoveryNode node) { + service.connectToNode(node); + nodesProvider.addNode(node); + } + } + + public MockNode createMockNode(final String name, Settings settings, Version version) throws Exception { + return createMockNode(name, settings, version, new PublishClusterStateAction.NewClusterStateListener() { + @Override + public void onNewClusterState(ClusterState clusterState, NewStateProcessed newStateProcessed) { + logger.debug("Node [{}] onNewClusterState version [{}], uuid [{}]", name, clusterState.version(), clusterState.uuid()); + newStateProcessed.onNewClusterStateProcessed(); + } + }); + } + + public MockNode createMockNode(String name, Settings settings, Version version, PublishClusterStateAction.NewClusterStateListener listener) throws Exception { + MockTransportService service = buildTransportService( + ImmutableSettings.builder().put(settings).put("name", name, TransportService.SETTING_TRACE_LOG_INCLUDE, "", TransportService.SETTING_TRACE_LOG_EXCLUDE, "NOTHING").build(), + version + ); + DiscoveryNode discoveryNode = new DiscoveryNode(name, name, service.boundAddress().publishAddress(), ImmutableMap.of(), version); + MockDiscoveryNodesProvider nodesProvider = new MockDiscoveryNodesProvider(discoveryNode); + PublishClusterStateAction action = buildPublishClusterStateAction(settings, service, nodesProvider, listener); + MockNode node = new MockNode(discoveryNode, service, action, nodesProvider); + nodesProvider.addNode(discoveryNode); + final CountDownLatch latch = new CountDownLatch(nodes.size() * 2 + 1); + TransportConnectionListener waitForConnection = new TransportConnectionListener() { + @Override + public void onNodeConnected(DiscoveryNode node) { + latch.countDown(); + } + + @Override + public void onNodeDisconnected(DiscoveryNode node) { + fail("disconnect should not be called " + node); + } + }; + node.service.addConnectionListener(waitForConnection); + for (MockNode curNode : nodes.values()) { + curNode.service.addConnectionListener(waitForConnection); + curNode.connectTo(node.discoveryNode); + node.connectTo(curNode.discoveryNode); + } + node.connectTo(node.discoveryNode); + assertThat("failed to wait for all nodes to connect", latch.await(5, TimeUnit.SECONDS), equalTo(true)); + for (MockNode curNode : nodes.values()) { + curNode.service.removeConnectionListener(waitForConnection); + } + node.service.removeConnectionListener(waitForConnection); + if (nodes.put(name, node) != null) { + fail("Node with the name " + name + " already exist"); + } + return node; + } + + public MockTransportService service(String name) { + MockNode node = nodes.get(name); + if (node != null) { + return node.service; + } + return null; + } + + public PublishClusterStateAction action(String name) { + MockNode node = nodes.get(name); + if (node != null) { + return node.action; + } + return null; + } + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + threadPool = new ThreadPool(getClass().getName()); + } + + @Override + @After + public void tearDown() throws Exception { + super.tearDown(); + for (MockNode curNode : nodes.values()) { + curNode.action.close(); + curNode.service.close(); + } + terminate(threadPool); + } + + protected MockTransportService buildTransportService(Settings settings, Version version) { + MockTransportService transportService = new MockTransportService(settings, new LocalTransport(settings, threadPool, version), threadPool); + transportService.start(); + return transportService; + } + + protected PublishClusterStateAction buildPublishClusterStateAction(Settings settings, MockTransportService transportService, MockDiscoveryNodesProvider nodesProvider, + PublishClusterStateAction.NewClusterStateListener listener) { + DiscoverySettings discoverySettings = new DiscoverySettings(settings, new NodeSettingsService(settings)); + return new PublishClusterStateAction(settings, transportService, nodesProvider, listener, discoverySettings); + } + + + static class MockDiscoveryNodesProvider implements DiscoveryNodesProvider { + + private DiscoveryNodes discoveryNodes = DiscoveryNodes.EMPTY_NODES; + + public MockDiscoveryNodesProvider(DiscoveryNode localNode) { + discoveryNodes = DiscoveryNodes.builder().put(localNode).localNodeId(localNode.id()).build(); + } + + public void addNode(DiscoveryNode node) { + discoveryNodes = DiscoveryNodes.builder(discoveryNodes).put(node).build(); + } + + @Override + public DiscoveryNodes nodes() { + return discoveryNodes; + } + + @Override + public NodeService nodeService() { + assert false; + throw new UnsupportedOperationException("Shouldn't be here"); + } + } + + + @Test + @TestLogging("cluster:DEBUG,discovery.zen.publish:DEBUG") + public void testSimpleClusterStatePublishing() throws Exception { + MockNewClusterStateListener mockListenerA = new MockNewClusterStateListener(); + MockNode nodeA = createMockNode("nodeA", ImmutableSettings.EMPTY, Version.CURRENT, mockListenerA); + + MockNewClusterStateListener mockListenerB = new MockNewClusterStateListener(); + MockNode nodeB = createMockNode("nodeB", ImmutableSettings.EMPTY, Version.CURRENT, mockListenerB); + + // Initial cluster state + DiscoveryNodes discoveryNodes = DiscoveryNodes.builder().put(nodeA.discoveryNode).localNodeId(nodeA.discoveryNode.id()).build(); + ClusterState clusterState = ClusterState.builder(new ClusterName("test")).nodes(discoveryNodes).build(); + + // cluster state update - add nodeB + discoveryNodes = DiscoveryNodes.builder(discoveryNodes).put(nodeB.discoveryNode).build(); + ClusterState previousClusterState = clusterState; + clusterState = ClusterState.builder(clusterState).nodes(discoveryNodes).incrementVersion().build(); + mockListenerB.add(new NewClusterStateExpectation() { + @Override + public void check(ClusterState clusterState, PublishClusterStateAction.NewClusterStateListener.NewStateProcessed newStateProcessed) { + assertFalse(clusterState.wasReadFromDiff()); + } + }); + publishStateDiffAndWait(nodeA.action, clusterState, previousClusterState); + + // cluster state update - add block + previousClusterState = clusterState; + clusterState = ClusterState.builder(clusterState).blocks(ClusterBlocks.builder().addGlobalBlock(MetaData.CLUSTER_READ_ONLY_BLOCK)).incrementVersion().build(); + mockListenerB.add(new NewClusterStateExpectation() { + @Override + public void check(ClusterState clusterState, PublishClusterStateAction.NewClusterStateListener.NewStateProcessed newStateProcessed) { + assertTrue(clusterState.wasReadFromDiff()); + assertThat(clusterState.blocks().global().size(), equalTo(1)); + } + }); + publishStateDiffAndWait(nodeA.action, clusterState, previousClusterState); + + // cluster state update - remove block + previousClusterState = clusterState; + clusterState = ClusterState.builder(clusterState).blocks(ClusterBlocks.EMPTY_CLUSTER_BLOCK).incrementVersion().build(); + mockListenerB.add(new NewClusterStateExpectation() { + @Override + public void check(ClusterState clusterState, PublishClusterStateAction.NewClusterStateListener.NewStateProcessed newStateProcessed) { + assertTrue(clusterState.wasReadFromDiff()); + assertThat(clusterState.blocks().global().size(), equalTo(0)); + } + }); + publishStateDiffAndWait(nodeA.action, clusterState, previousClusterState); + + // Adding new node - this node should get full cluster state while nodeB should still be getting diffs + + MockNewClusterStateListener mockListenerC = new MockNewClusterStateListener(); + MockNode nodeC = createMockNode("nodeC", ImmutableSettings.EMPTY, Version.CURRENT, mockListenerC); + + // cluster state update 3 - register node C + previousClusterState = clusterState; + discoveryNodes = DiscoveryNodes.builder(discoveryNodes).put(nodeC.discoveryNode).build(); + clusterState = ClusterState.builder(clusterState).nodes(discoveryNodes).incrementVersion().build(); + mockListenerB.add(new NewClusterStateExpectation() { + @Override + public void check(ClusterState clusterState, PublishClusterStateAction.NewClusterStateListener.NewStateProcessed newStateProcessed) { + assertTrue(clusterState.wasReadFromDiff()); + assertThat(clusterState.blocks().global().size(), equalTo(0)); + } + }); + mockListenerC.add(new NewClusterStateExpectation() { + @Override + public void check(ClusterState clusterState, PublishClusterStateAction.NewClusterStateListener.NewStateProcessed newStateProcessed) { + // First state + assertFalse(clusterState.wasReadFromDiff()); + } + }); + publishStateDiffAndWait(nodeA.action, clusterState, previousClusterState); + + // cluster state update 4 - update settings + previousClusterState = clusterState; + MetaData metaData = MetaData.builder(clusterState.metaData()).transientSettings(ImmutableSettings.settingsBuilder().put("foo", "bar").build()).build(); + clusterState = ClusterState.builder(clusterState).metaData(metaData).incrementVersion().build(); + NewClusterStateExpectation expectation = new NewClusterStateExpectation() { + @Override + public void check(ClusterState clusterState, PublishClusterStateAction.NewClusterStateListener.NewStateProcessed newStateProcessed) { + assertTrue(clusterState.wasReadFromDiff()); + assertThat(clusterState.blocks().global().size(), equalTo(0)); + } + }; + mockListenerB.add(expectation); + mockListenerC.add(expectation); + publishStateDiffAndWait(nodeA.action, clusterState, previousClusterState); + + // cluster state update - skipping one version change - should request full cluster state + previousClusterState = ClusterState.builder(clusterState).incrementVersion().build(); + clusterState = ClusterState.builder(clusterState).incrementVersion().build(); + expectation = new NewClusterStateExpectation() { + @Override + public void check(ClusterState clusterState, PublishClusterStateAction.NewClusterStateListener.NewStateProcessed newStateProcessed) { + assertFalse(clusterState.wasReadFromDiff()); + } + }; + mockListenerB.add(expectation); + mockListenerC.add(expectation); + publishStateDiffAndWait(nodeA.action, clusterState, previousClusterState); + + // cluster state update - skipping one version change - should request full cluster state + previousClusterState = ClusterState.builder(clusterState).incrementVersion().build(); + clusterState = ClusterState.builder(clusterState).incrementVersion().build(); + expectation = new NewClusterStateExpectation() { + @Override + public void check(ClusterState clusterState, PublishClusterStateAction.NewClusterStateListener.NewStateProcessed newStateProcessed) { + assertFalse(clusterState.wasReadFromDiff()); + } + }; + mockListenerB.add(expectation); + mockListenerC.add(expectation); + publishStateDiffAndWait(nodeA.action, clusterState, previousClusterState); + + // node B becomes the master and sends a version of the cluster state that goes back + discoveryNodes = DiscoveryNodes.builder(discoveryNodes) + .put(nodeA.discoveryNode) + .put(nodeB.discoveryNode) + .put(nodeC.discoveryNode) + .build(); + previousClusterState = ClusterState.builder(new ClusterName("test")).nodes(discoveryNodes).build(); + clusterState = ClusterState.builder(clusterState).nodes(discoveryNodes).incrementVersion().build(); + expectation = new NewClusterStateExpectation() { + @Override + public void check(ClusterState clusterState, PublishClusterStateAction.NewClusterStateListener.NewStateProcessed newStateProcessed) { + assertFalse(clusterState.wasReadFromDiff()); + } + }; + mockListenerA.add(expectation); + mockListenerC.add(expectation); + publishStateDiffAndWait(nodeB.action, clusterState, previousClusterState); + } + + @Test + @TestLogging("cluster:DEBUG,discovery.zen.publish:DEBUG") + public void testUnexpectedDiffPublishing() throws Exception { + + MockNode nodeA = createMockNode("nodeA", ImmutableSettings.EMPTY, Version.CURRENT, new PublishClusterStateAction.NewClusterStateListener() { + @Override + public void onNewClusterState(ClusterState clusterState, NewStateProcessed newStateProcessed) { + fail("Shouldn't send cluster state to myself"); + } + }); + + MockNewClusterStateListener mockListenerB = new MockNewClusterStateListener(); + MockNode nodeB = createMockNode("nodeB", ImmutableSettings.EMPTY, Version.CURRENT, mockListenerB); + + // Initial cluster state with both states - the second node still shouldn't get diff even though it's present in the previous cluster state + DiscoveryNodes discoveryNodes = DiscoveryNodes.builder().put(nodeA.discoveryNode).put(nodeB.discoveryNode).localNodeId(nodeA.discoveryNode.id()).build(); + ClusterState previousClusterState = ClusterState.builder(new ClusterName("test")).nodes(discoveryNodes).build(); + ClusterState clusterState = ClusterState.builder(previousClusterState).incrementVersion().build(); + mockListenerB.add(new NewClusterStateExpectation() { + @Override + public void check(ClusterState clusterState, PublishClusterStateAction.NewClusterStateListener.NewStateProcessed newStateProcessed) { + assertFalse(clusterState.wasReadFromDiff()); + } + }); + publishStateDiffAndWait(nodeA.action, clusterState, previousClusterState); + + // cluster state update - add block + previousClusterState = clusterState; + clusterState = ClusterState.builder(clusterState).blocks(ClusterBlocks.builder().addGlobalBlock(MetaData.CLUSTER_READ_ONLY_BLOCK)).incrementVersion().build(); + mockListenerB.add(new NewClusterStateExpectation() { + @Override + public void check(ClusterState clusterState, PublishClusterStateAction.NewClusterStateListener.NewStateProcessed newStateProcessed) { + assertTrue(clusterState.wasReadFromDiff()); + } + }); + publishStateDiffAndWait(nodeA.action, clusterState, previousClusterState); + } + + @Test + @TestLogging("cluster:DEBUG,discovery.zen.publish:DEBUG") + public void testDisablingDiffPublishing() throws Exception { + Settings noDiffPublishingSettings = ImmutableSettings.builder().put(DiscoverySettings.PUBLISH_DIFF_ENABLE, false).build(); + + MockNode nodeA = createMockNode("nodeA", noDiffPublishingSettings, Version.CURRENT, new PublishClusterStateAction.NewClusterStateListener() { + @Override + public void onNewClusterState(ClusterState clusterState, NewStateProcessed newStateProcessed) { + fail("Shouldn't send cluster state to myself"); + } + }); + + MockNode nodeB = createMockNode("nodeB", noDiffPublishingSettings, Version.CURRENT, new PublishClusterStateAction.NewClusterStateListener() { + @Override + public void onNewClusterState(ClusterState clusterState, NewStateProcessed newStateProcessed) { + logger.debug("Got cluster state update, version [{}], guid [{}], from diff [{}]", clusterState.version(), clusterState.uuid(), clusterState.wasReadFromDiff()); + assertFalse(clusterState.wasReadFromDiff()); + newStateProcessed.onNewClusterStateProcessed(); + } + }); + + // Initial cluster state + DiscoveryNodes discoveryNodes = DiscoveryNodes.builder().put(nodeA.discoveryNode).localNodeId(nodeA.discoveryNode.id()).build(); + ClusterState clusterState = ClusterState.builder(new ClusterName("test")).nodes(discoveryNodes).build(); + + // cluster state update - add nodeB + discoveryNodes = DiscoveryNodes.builder(discoveryNodes).put(nodeB.discoveryNode).build(); + ClusterState previousClusterState = clusterState; + clusterState = ClusterState.builder(clusterState).nodes(discoveryNodes).incrementVersion().build(); + publishStateDiffAndWait(nodeA.action, clusterState, previousClusterState); + + // cluster state update - add block + previousClusterState = clusterState; + clusterState = ClusterState.builder(clusterState).blocks(ClusterBlocks.builder().addGlobalBlock(MetaData.CLUSTER_READ_ONLY_BLOCK)).incrementVersion().build(); + publishStateDiffAndWait(nodeA.action, clusterState, previousClusterState); + } + + + @Test + @TestLogging("cluster:DEBUG,discovery.zen.publish:DEBUG") + public void testSimultaneousClusterStatePublishing() throws Exception { + int numberOfNodes = randomIntBetween(2, 10); + int numberOfIterations = randomIntBetween(50, 200); + Settings settings = ImmutableSettings.builder().put(DiscoverySettings.PUBLISH_TIMEOUT, "100ms").put(DiscoverySettings.PUBLISH_DIFF_ENABLE, true).build(); + MockNode[] nodes = new MockNode[numberOfNodes]; + DiscoveryNodes.Builder discoveryNodesBuilder = DiscoveryNodes.builder(); + for (int i = 0; i < nodes.length; i++) { + final String name = "node" + i; + nodes[i] = createMockNode(name, settings, Version.CURRENT, new PublishClusterStateAction.NewClusterStateListener() { + @Override + public synchronized void onNewClusterState(ClusterState clusterState, NewStateProcessed newStateProcessed) { + assertProperMetaDataForVersion(clusterState.metaData(), clusterState.version()); + if (randomInt(10) < 2) { + // Cause timeouts from time to time + try { + Thread.sleep(randomInt(110)); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + newStateProcessed.onNewClusterStateProcessed(); + } + }); + discoveryNodesBuilder.put(nodes[i].discoveryNode); + } + + AssertingAckListener[] listeners = new AssertingAckListener[numberOfIterations]; + DiscoveryNodes discoveryNodes = discoveryNodesBuilder.build(); + MetaData metaData = MetaData.EMPTY_META_DATA; + ClusterState clusterState = ClusterState.builder(new ClusterName("test")).metaData(metaData).build(); + ClusterState previousState; + for (int i = 0; i < numberOfIterations; i++) { + previousState = clusterState; + metaData = buildMetaDataForVersion(metaData, i + 1); + clusterState = ClusterState.builder(clusterState).incrementVersion().metaData(metaData).nodes(discoveryNodes).build(); + listeners[i] = publishStateDiff(nodes[0].action, clusterState, previousState); + } + + for (int i = 0; i < numberOfIterations; i++) { + listeners[i].await(1, TimeUnit.SECONDS); + } + } + + @Test + @TestLogging("cluster:DEBUG,discovery.zen.publish:DEBUG") + public void testSerializationFailureDuringDiffPublishing() throws Exception { + + MockNode nodeA = createMockNode("nodeA", ImmutableSettings.EMPTY, Version.CURRENT, new PublishClusterStateAction.NewClusterStateListener() { + @Override + public void onNewClusterState(ClusterState clusterState, NewStateProcessed newStateProcessed) { + fail("Shouldn't send cluster state to myself"); + } + }); + + MockNewClusterStateListener mockListenerB = new MockNewClusterStateListener(); + MockNode nodeB = createMockNode("nodeB", ImmutableSettings.EMPTY, Version.CURRENT, mockListenerB); + + // Initial cluster state with both states - the second node still shouldn't get diff even though it's present in the previous cluster state + DiscoveryNodes discoveryNodes = DiscoveryNodes.builder().put(nodeA.discoveryNode).put(nodeB.discoveryNode).localNodeId(nodeA.discoveryNode.id()).build(); + ClusterState previousClusterState = ClusterState.builder(new ClusterName("test")).nodes(discoveryNodes).build(); + ClusterState clusterState = ClusterState.builder(previousClusterState).incrementVersion().build(); + mockListenerB.add(new NewClusterStateExpectation() { + @Override + public void check(ClusterState clusterState, PublishClusterStateAction.NewClusterStateListener.NewStateProcessed newStateProcessed) { + assertFalse(clusterState.wasReadFromDiff()); + } + }); + publishStateDiffAndWait(nodeA.action, clusterState, previousClusterState); + + // cluster state update - add block + previousClusterState = clusterState; + clusterState = ClusterState.builder(clusterState).blocks(ClusterBlocks.builder().addGlobalBlock(MetaData.CLUSTER_READ_ONLY_BLOCK)).incrementVersion().build(); + mockListenerB.add(new NewClusterStateExpectation() { + @Override + public void check(ClusterState clusterState, PublishClusterStateAction.NewClusterStateListener.NewStateProcessed newStateProcessed) { + assertTrue(clusterState.wasReadFromDiff()); + } + }); + + ClusterState unserializableClusterState = new ClusterState(clusterState.version(), clusterState.uuid(), clusterState) { + @Override + public Diff diff(ClusterState previousState) { + return new Diff() { + @Override + public ClusterState apply(ClusterState part) { + fail("this diff shouldn't be applied"); + return part; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + throw new IOException("Simulated failure of diff serialization"); + } + }; + } + }; + List> errors = publishStateDiff(nodeA.action, unserializableClusterState, previousClusterState).awaitErrors(1, TimeUnit.SECONDS); + assertThat(errors.size(), equalTo(1)); + assertThat(errors.get(0).v2().getMessage(), containsString("Simulated failure of diff serialization")); + } + + private MetaData buildMetaDataForVersion(MetaData metaData, long version) { + ImmutableOpenMap.Builder indices = ImmutableOpenMap.builder(metaData.indices()); + indices.put("test" + version, IndexMetaData.builder("test" + version).settings(ImmutableSettings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards((int) version).numberOfReplicas(0).build()); + return MetaData.builder(metaData) + .transientSettings(ImmutableSettings.builder().put("test", version).build()) + .indices(indices.build()) + .build(); + } + + private void assertProperMetaDataForVersion(MetaData metaData, long version) { + for (long i = 1; i <= version; i++) { + assertThat(metaData.index("test" + i), notNullValue()); + assertThat(metaData.index("test" + i).numberOfShards(), equalTo((int) i)); + } + assertThat(metaData.index("test" + (version + 1)), nullValue()); + assertThat(metaData.transientSettings().get("test"), equalTo(Long.toString(version))); + } + + public void publishStateDiffAndWait(PublishClusterStateAction action, ClusterState state, ClusterState previousState) throws InterruptedException { + publishStateDiff(action, state, previousState).await(1, TimeUnit.SECONDS); + } + + public AssertingAckListener publishStateDiff(PublishClusterStateAction action, ClusterState state, ClusterState previousState) throws InterruptedException { + AssertingAckListener assertingAckListener = new AssertingAckListener(state.nodes().getSize() - 1); + ClusterChangedEvent changedEvent = new ClusterChangedEvent("test update", state, previousState); + action.publish(changedEvent, assertingAckListener); + return assertingAckListener; + } + + public static class AssertingAckListener implements Discovery.AckListener { + private final List> errors = new CopyOnWriteArrayList<>(); + private final AtomicBoolean timeoutOccured = new AtomicBoolean(); + private final CountDownLatch countDown; + + public AssertingAckListener(int nodeCount) { + countDown = new CountDownLatch(nodeCount); + } + + @Override + public void onNodeAck(DiscoveryNode node, @Nullable Throwable t) { + if (t != null) { + errors.add(new Tuple<>(node, t)); + } + countDown.countDown(); + } + + @Override + public void onTimeout() { + timeoutOccured.set(true); + // Fast forward the counter - no reason to wait here + long currentCount = countDown.getCount(); + for (long i = 0; i < currentCount; i++) { + countDown.countDown(); + } + } + + public void await(long timeout, TimeUnit unit) throws InterruptedException { + assertThat(awaitErrors(timeout, unit), emptyIterable()); + } + + public List> awaitErrors(long timeout, TimeUnit unit) throws InterruptedException { + countDown.await(timeout, unit); + assertFalse(timeoutOccured.get()); + return errors; + } + + } + + public interface NewClusterStateExpectation { + void check(ClusterState clusterState, PublishClusterStateAction.NewClusterStateListener.NewStateProcessed newStateProcessed); + } + + public static class MockNewClusterStateListener implements PublishClusterStateAction.NewClusterStateListener { + CopyOnWriteArrayList expectations = new CopyOnWriteArrayList(); + + @Override + public void onNewClusterState(ClusterState clusterState, NewStateProcessed newStateProcessed) { + final NewClusterStateExpectation expectation; + try { + expectation = expectations.remove(0); + } catch (ArrayIndexOutOfBoundsException ex) { + fail("Unexpected cluster state update " + clusterState.prettyPrint()); + return; + } + expectation.check(clusterState, newStateProcessed); + newStateProcessed.onNewClusterStateProcessed(); + } + + public void add(NewClusterStateExpectation expectation) { + expectations.add(expectation); + } + } + + public static class DelegatingClusterState extends ClusterState { + + public DelegatingClusterState(ClusterState clusterState) { + super(clusterState.version(), clusterState.uuid(), clusterState); + } + + + } + +} diff --git a/src/test/java/org/elasticsearch/cluster/ClusterStateDiffTests.java b/src/test/java/org/elasticsearch/cluster/ClusterStateDiffTests.java new file mode 100644 index 00000000000..84df1eaf209 --- /dev/null +++ b/src/test/java/org/elasticsearch/cluster/ClusterStateDiffTests.java @@ -0,0 +1,534 @@ +/* + * 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.cluster; + +import com.carrotsearch.hppc.cursors.ObjectCursor; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.elasticsearch.Version; +import org.elasticsearch.cluster.block.ClusterBlock; +import org.elasticsearch.cluster.block.ClusterBlocks; +import org.elasticsearch.cluster.metadata.*; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.cluster.routing.*; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.collect.ImmutableOpenMap; +import org.elasticsearch.common.io.stream.BytesStreamInput; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.settings.ImmutableSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.LocalTransportAddress; +import org.elasticsearch.discovery.DiscoverySettings; +import org.elasticsearch.gateway.GatewayService; +import org.elasticsearch.index.query.FilterBuilders; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.search.warmer.IndexWarmersMetaData; +import org.elasticsearch.test.ElasticsearchIntegrationTest; +import org.junit.Test; + +import java.util.List; + +import static org.elasticsearch.cluster.metadata.AliasMetaData.newAliasMetaDataBuilder; +import static org.elasticsearch.common.xcontent.XContentTestUtils.convertToMap; +import static org.elasticsearch.common.xcontent.XContentTestUtils.mapsEqualIgnoringArrayOrder; +import static org.elasticsearch.test.VersionUtils.randomVersion; +import static org.hamcrest.Matchers.equalTo; + + +@ElasticsearchIntegrationTest.ClusterScope(scope = ElasticsearchIntegrationTest.Scope.SUITE, numDataNodes = 0, numClientNodes = 0) +public class ClusterStateDiffTests extends ElasticsearchIntegrationTest { + + @Test + public void testClusterStateDiffSerialization() throws Exception { + DiscoveryNode masterNode = new DiscoveryNode("master", new LocalTransportAddress("master"), Version.CURRENT); + DiscoveryNode otherNode = new DiscoveryNode("other", new LocalTransportAddress("other"), Version.CURRENT); + DiscoveryNodes discoveryNodes = DiscoveryNodes.builder().put(masterNode).put(otherNode).localNodeId(masterNode.id()).build(); + ClusterState clusterState = ClusterState.builder(new ClusterName("test")).nodes(discoveryNodes).build(); + ClusterState clusterStateFromDiffs = ClusterState.Builder.fromBytes(ClusterState.Builder.toBytes(clusterState), otherNode); + + int iterationCount = randomIntBetween(10, 300); + for (int iteration = 0; iteration < iterationCount; iteration++) { + ClusterState previousClusterState = clusterState; + ClusterState previousClusterStateFromDiffs = clusterStateFromDiffs; + int changesCount = randomIntBetween(1, 4); + ClusterState.Builder builder = null; + for (int i = 0; i < changesCount; i++) { + if (i > 0) { + clusterState = builder.build(); + } + switch (randomInt(4)) { + case 0: + builder = randomNodes(clusterState); + break; + case 1: + builder = randomRoutingTable(clusterState); + break; + case 2: + builder = randomBlocks(clusterState); + break; + case 3: + case 4: + builder = randomMetaDataChanges(clusterState); + break; + default: + throw new IllegalArgumentException("Shouldn't be here"); + } + } + clusterState = builder.incrementVersion().build(); + + if (randomIntBetween(0, 10) < 1) { + // Update cluster state via full serialization from time to time + clusterStateFromDiffs = ClusterState.Builder.fromBytes(ClusterState.Builder.toBytes(clusterState), previousClusterStateFromDiffs.nodes().localNode()); + } else { + // Update cluster states using diffs + Diff diffBeforeSerialization = clusterState.diff(previousClusterState); + BytesStreamOutput os = new BytesStreamOutput(); + diffBeforeSerialization.writeTo(os); + byte[] diffBytes = os.bytes().toBytes(); + Diff diff; + try (BytesStreamInput input = new BytesStreamInput(diffBytes)) { + diff = previousClusterStateFromDiffs.readDiffFrom(input); + clusterStateFromDiffs = diff.apply(previousClusterStateFromDiffs); + } + } + + + try { + // Check non-diffable elements + assertThat(clusterStateFromDiffs.version(), equalTo(clusterState.version())); + assertThat(clusterStateFromDiffs.uuid(), equalTo(clusterState.uuid())); + + // Check nodes + assertThat(clusterStateFromDiffs.nodes().nodes(), equalTo(clusterState.nodes().nodes())); + assertThat(clusterStateFromDiffs.nodes().localNodeId(), equalTo(previousClusterStateFromDiffs.nodes().localNodeId())); + assertThat(clusterStateFromDiffs.nodes().nodes(), equalTo(clusterState.nodes().nodes())); + for (ObjectCursor node : clusterStateFromDiffs.nodes().nodes().keys()) { + DiscoveryNode node1 = clusterState.nodes().get(node.value); + DiscoveryNode node2 = clusterStateFromDiffs.nodes().get(node.value); + assertThat(node1.version(), equalTo(node2.version())); + assertThat(node1.address(), equalTo(node2.address())); + assertThat(node1.attributes(), equalTo(node2.attributes())); + } + + // Check routing table + assertThat(clusterStateFromDiffs.routingTable().version(), equalTo(clusterState.routingTable().version())); + assertThat(clusterStateFromDiffs.routingTable().indicesRouting(), equalTo(clusterState.routingTable().indicesRouting())); + + // Check cluster blocks + assertThat(clusterStateFromDiffs.blocks().global(), equalTo(clusterStateFromDiffs.blocks().global())); + assertThat(clusterStateFromDiffs.blocks().indices(), equalTo(clusterStateFromDiffs.blocks().indices())); + assertThat(clusterStateFromDiffs.blocks().disableStatePersistence(), equalTo(clusterStateFromDiffs.blocks().disableStatePersistence())); + + // Check metadata + assertThat(clusterStateFromDiffs.metaData().version(), equalTo(clusterState.metaData().version())); + assertThat(clusterStateFromDiffs.metaData().uuid(), equalTo(clusterState.metaData().uuid())); + assertThat(clusterStateFromDiffs.metaData().transientSettings(), equalTo(clusterState.metaData().transientSettings())); + assertThat(clusterStateFromDiffs.metaData().persistentSettings(), equalTo(clusterState.metaData().persistentSettings())); + assertThat(clusterStateFromDiffs.metaData().indices(), equalTo(clusterState.metaData().indices())); + assertThat(clusterStateFromDiffs.metaData().templates(), equalTo(clusterState.metaData().templates())); + assertThat(clusterStateFromDiffs.metaData().customs(), equalTo(clusterState.metaData().customs())); + assertThat(clusterStateFromDiffs.metaData().aliases(), equalTo(clusterState.metaData().aliases())); + + // JSON Serialization test - make sure that both states produce similar JSON + assertThat(mapsEqualIgnoringArrayOrder(convertToMap(clusterStateFromDiffs), convertToMap(clusterState)), equalTo(true)); + + // Smoke test - we cannot compare bytes to bytes because some elements might get serialized in different order + // however, serialized size should remain the same + assertThat(ClusterState.Builder.toBytes(clusterStateFromDiffs).length, equalTo(ClusterState.Builder.toBytes(clusterState).length)); + } catch (AssertionError error) { + logger.error("Cluster state:\n{}\nCluster state from diffs:\n{}", clusterState.toString(), clusterStateFromDiffs.toString()); + throw error; + } + } + + logger.info("Final cluster state:[{}]", clusterState.toString()); + + } + + private ClusterState.Builder randomNodes(ClusterState clusterState) { + DiscoveryNodes.Builder nodes = DiscoveryNodes.builder(clusterState.nodes()); + List nodeIds = randomSubsetOf(randomInt(clusterState.nodes().nodes().size() - 1), clusterState.nodes().nodes().keys().toArray(String.class)); + for (String nodeId : nodeIds) { + if (nodeId.startsWith("node-")) { + if (randomBoolean()) { + nodes.remove(nodeId); + } else { + nodes.put(new DiscoveryNode(nodeId, new LocalTransportAddress(randomAsciiOfLength(10)), randomVersion(random()))); + } + } + } + int additionalNodeCount = randomIntBetween(1, 20); + for (int i = 0; i < additionalNodeCount; i++) { + nodes.put(new DiscoveryNode("node-" + randomAsciiOfLength(10), new LocalTransportAddress(randomAsciiOfLength(10)), randomVersion(random()))); + } + return ClusterState.builder(clusterState).nodes(nodes); + } + + private ClusterState.Builder randomRoutingTable(ClusterState clusterState) { + RoutingTable.Builder builder = RoutingTable.builder(clusterState.routingTable()); + int numberOfIndices = clusterState.routingTable().indicesRouting().size(); + if (numberOfIndices > 0) { + List randomIndices = randomSubsetOf(randomInt(numberOfIndices - 1), clusterState.routingTable().indicesRouting().keySet().toArray(new String[numberOfIndices])); + for (String index : randomIndices) { + if (randomBoolean()) { + builder.remove(index); + } else { + builder.add(randomIndexRoutingTable(index, clusterState.nodes().nodes().keys().toArray(String.class))); + } + } + } + int additionalIndexCount = randomIntBetween(1, 20); + for (int i = 0; i < additionalIndexCount; i++) { + builder.add(randomIndexRoutingTable("index-" + randomInt(), clusterState.nodes().nodes().keys().toArray(String.class))); + } + return ClusterState.builder(clusterState).routingTable(builder.build()); + } + + private IndexRoutingTable randomIndexRoutingTable(String index, String[] nodeIds) { + IndexRoutingTable.Builder builder = IndexRoutingTable.builder(index); + int shardCount = randomInt(10); + + for (int i = 0; i < shardCount; i++) { + IndexShardRoutingTable.Builder indexShard = new IndexShardRoutingTable.Builder(new ShardId(index, i), randomBoolean()); + int replicaCount = randomIntBetween(1, 10); + for (int j = 0; j < replicaCount; j++) { + indexShard.addShard( + new MutableShardRouting(index, i, randomFrom(nodeIds), j == 0, ShardRoutingState.fromValue((byte) randomIntBetween(1, 4)), 1)); + } + builder.addIndexShard(indexShard.build()); + } + return builder.build(); + } + + private ClusterState.Builder randomBlocks(ClusterState clusterState) { + ClusterBlocks.Builder builder = ClusterBlocks.builder().blocks(clusterState.blocks()); + int globalBlocksCount = clusterState.blocks().global().size(); + if (globalBlocksCount > 0) { + List blocks = randomSubsetOf(randomInt(globalBlocksCount - 1), clusterState.blocks().global().toArray(new ClusterBlock[globalBlocksCount])); + for (ClusterBlock block : blocks) { + builder.removeGlobalBlock(block); + } + } + int additionalGlobalBlocksCount = randomIntBetween(1, 3); + for (int i = 0; i < additionalGlobalBlocksCount; i++) { + builder.addGlobalBlock(randomGlobalBlock()); + } + return ClusterState.builder(clusterState).blocks(builder); + } + + private ClusterBlock randomGlobalBlock() { + switch (randomInt(2)) { + case 0: + return DiscoverySettings.NO_MASTER_BLOCK_ALL; + case 1: + return DiscoverySettings.NO_MASTER_BLOCK_WRITES; + default: + return GatewayService.STATE_NOT_RECOVERED_BLOCK; + } + } + + private ClusterState.Builder randomMetaDataChanges(ClusterState clusterState) { + MetaData metaData = clusterState.metaData(); + int changesCount = randomIntBetween(1, 10); + for (int i = 0; i < changesCount; i++) { + switch (randomInt(3)) { + case 0: + metaData = randomMetaDataSettings(metaData); + break; + case 1: + metaData = randomIndices(metaData); + break; + case 2: + metaData = randomTemplates(metaData); + break; + case 3: + metaData = randomMetaDataCustoms(metaData); + break; + default: + throw new IllegalArgumentException("Shouldn't be here"); + } + } + return ClusterState.builder(clusterState).metaData(MetaData.builder(metaData).version(metaData.version() + 1).build()); + } + + private Settings randomSettings(Settings settings) { + ImmutableSettings.Builder builder = ImmutableSettings.builder(); + if (randomBoolean()) { + builder.put(settings); + } + int settingsCount = randomInt(10); + for (int i = 0; i < settingsCount; i++) { + builder.put(randomAsciiOfLength(10), randomAsciiOfLength(10)); + } + return builder.build(); + + } + + private MetaData randomMetaDataSettings(MetaData metaData) { + if (randomBoolean()) { + return MetaData.builder(metaData).persistentSettings(randomSettings(metaData.persistentSettings())).build(); + } else { + return MetaData.builder(metaData).transientSettings(randomSettings(metaData.transientSettings())).build(); + } + } + + private interface RandomPart { + /** + * Returns list of parts from metadata + */ + ImmutableOpenMap parts(MetaData metaData); + + /** + * Puts the part back into metadata + */ + MetaData.Builder put(MetaData.Builder builder, T part); + + /** + * Remove the part from metadata + */ + MetaData.Builder remove(MetaData.Builder builder, String name); + + /** + * Returns a random part with the specified name + */ + T randomCreate(String name); + + /** + * Makes random modifications to the part + */ + T randomChange(T part); + + } + + private MetaData randomParts(MetaData metaData, String prefix, RandomPart randomPart) { + MetaData.Builder builder = MetaData.builder(metaData); + ImmutableOpenMap parts = randomPart.parts(metaData); + int partCount = parts.size(); + if (partCount > 0) { + List randomParts = randomSubsetOf(randomInt(partCount - 1), randomPart.parts(metaData).keys().toArray(String.class)); + for (String part : randomParts) { + if (randomBoolean()) { + randomPart.remove(builder, part); + } else { + randomPart.put(builder, randomPart.randomChange(parts.get(part))); + } + } + } + int additionalPartCount = randomIntBetween(1, 20); + for (int i = 0; i < additionalPartCount; i++) { + String name = randomName(prefix); + randomPart.put(builder, randomPart.randomCreate(name)); + } + return builder.build(); + } + + private MetaData randomIndices(MetaData metaData) { + return randomParts(metaData, "index", new RandomPart() { + + @Override + public ImmutableOpenMap parts(MetaData metaData) { + return metaData.indices(); + } + + @Override + public MetaData.Builder put(MetaData.Builder builder, IndexMetaData part) { + return builder.put(part, true); + } + + @Override + public MetaData.Builder remove(MetaData.Builder builder, String name) { + return builder.remove(name); + } + + @Override + public IndexMetaData randomCreate(String name) { + IndexMetaData.Builder builder = IndexMetaData.builder(name); + ImmutableSettings.Builder settingsBuilder = ImmutableSettings.builder(); + setRandomSettings(getRandom(), settingsBuilder); + settingsBuilder.put(randomSettings(ImmutableSettings.EMPTY)).put(IndexMetaData.SETTING_VERSION_CREATED, randomVersion(random())); + builder.settings(settingsBuilder); + builder.numberOfShards(randomIntBetween(1, 10)).numberOfReplicas(randomInt(10)); + int aliasCount = randomInt(10); + if (randomBoolean()) { + builder.putCustom(IndexWarmersMetaData.TYPE, randomWarmers()); + } + for (int i = 0; i < aliasCount; i++) { + builder.putAlias(randomAlias()); + } + return builder.build(); + } + + @Override + public IndexMetaData randomChange(IndexMetaData part) { + IndexMetaData.Builder builder = IndexMetaData.builder(part); + switch (randomIntBetween(0, 3)) { + case 0: + builder.settings(ImmutableSettings.builder().put(part.settings()).put(randomSettings(ImmutableSettings.EMPTY))); + break; + case 1: + if (randomBoolean() && part.aliases().isEmpty() == false) { + builder.removeAlias(randomFrom(part.aliases().keys().toArray(String.class))); + } else { + builder.putAlias(AliasMetaData.builder(randomAsciiOfLength(10))); + } + break; + case 2: + builder.settings(ImmutableSettings.builder().put(part.settings()).put(IndexMetaData.SETTING_UUID, Strings.randomBase64UUID())); + break; + case 3: + builder.putCustom(IndexWarmersMetaData.TYPE, randomWarmers()); + break; + default: + throw new IllegalArgumentException("Shouldn't be here"); + } + return builder.build(); + } + }); + } + + private IndexWarmersMetaData randomWarmers() { + if (randomBoolean()) { + return new IndexWarmersMetaData( + new IndexWarmersMetaData.Entry( + randomName("warm"), + new String[]{randomName("type")}, + randomBoolean(), + new BytesArray(randomAsciiOfLength(1000))) + ); + } else { + return new IndexWarmersMetaData(); + } + } + + private MetaData randomTemplates(MetaData metaData) { + return randomParts(metaData, "template", new RandomPart() { + @Override + public ImmutableOpenMap parts(MetaData metaData) { + return metaData.templates(); + } + + @Override + public MetaData.Builder put(MetaData.Builder builder, IndexTemplateMetaData part) { + return builder.put(part); + } + + @Override + public MetaData.Builder remove(MetaData.Builder builder, String name) { + return builder.removeTemplate(name); + } + + @Override + public IndexTemplateMetaData randomCreate(String name) { + IndexTemplateMetaData.Builder builder = IndexTemplateMetaData.builder(name); + builder.order(randomInt(1000)) + .template(randomName("temp")) + .settings(randomSettings(ImmutableSettings.EMPTY)); + int aliasCount = randomIntBetween(0, 10); + for (int i = 0; i < aliasCount; i++) { + builder.putAlias(randomAlias()); + } + if (randomBoolean()) { + builder.putCustom(IndexWarmersMetaData.TYPE, randomWarmers()); + } + return builder.build(); + } + + @Override + public IndexTemplateMetaData randomChange(IndexTemplateMetaData part) { + IndexTemplateMetaData.Builder builder = new IndexTemplateMetaData.Builder(part); + builder.order(randomInt(1000)); + return builder.build(); + } + }); + } + + private AliasMetaData randomAlias() { + AliasMetaData.Builder builder = newAliasMetaDataBuilder(randomName("alias")); + if (randomBoolean()) { + builder.filter(FilterBuilders.termFilter("test", randomRealisticUnicodeOfCodepointLength(10)).toString()); + } + if (randomBoolean()) { + builder.routing(randomAsciiOfLength(10)); + } + return builder.build(); + } + + private MetaData randomMetaDataCustoms(final MetaData metaData) { + return randomParts(metaData, "custom", new RandomPart() { + + @Override + public ImmutableOpenMap parts(MetaData metaData) { + return metaData.customs(); + } + + @Override + public MetaData.Builder put(MetaData.Builder builder, MetaData.Custom part) { + if (part instanceof SnapshotMetaData) { + return builder.putCustom(SnapshotMetaData.TYPE, part); + } else if (part instanceof RepositoriesMetaData) { + return builder.putCustom(RepositoriesMetaData.TYPE, part); + } else if (part instanceof RestoreMetaData) { + return builder.putCustom(RestoreMetaData.TYPE, part); + } + throw new IllegalArgumentException("Unknown custom part " + part); + } + + @Override + public MetaData.Builder remove(MetaData.Builder builder, String name) { + return builder.removeCustom(name); + } + + @Override + public MetaData.Custom randomCreate(String name) { + switch (randomIntBetween(0, 2)) { + case 0: + return new SnapshotMetaData(new SnapshotMetaData.Entry( + new SnapshotId(randomName("repo"), randomName("snap")), + randomBoolean(), + SnapshotMetaData.State.fromValue((byte) randomIntBetween(0, 6)), + ImmutableList.of(), + Math.abs(randomLong()), + ImmutableMap.of())); + case 1: + return new RepositoriesMetaData(); + case 2: + return new RestoreMetaData(new RestoreMetaData.Entry( + new SnapshotId(randomName("repo"), randomName("snap")), + RestoreMetaData.State.fromValue((byte) randomIntBetween(0, 3)), + ImmutableList.of(), + ImmutableMap.of())); + default: + throw new IllegalArgumentException("Shouldn't be here"); + } + } + + @Override + public MetaData.Custom randomChange(MetaData.Custom part) { + return part; + } + }); + } + + private String randomName(String prefix) { + return prefix + Strings.randomBase64UUID(getRandom()); + } +} \ No newline at end of file diff --git a/src/test/java/org/elasticsearch/cluster/serialization/ClusterSerializationTests.java b/src/test/java/org/elasticsearch/cluster/serialization/ClusterSerializationTests.java index cbbff463f20..83a27850591 100644 --- a/src/test/java/org/elasticsearch/cluster/serialization/ClusterSerializationTests.java +++ b/src/test/java/org/elasticsearch/cluster/serialization/ClusterSerializationTests.java @@ -81,7 +81,7 @@ public class ClusterSerializationTests extends ElasticsearchAllocationTestCase { RoutingTable source = strategy.reroute(clusterState).routingTable(); BytesStreamOutput outStream = new BytesStreamOutput(); - RoutingTable.Builder.writeTo(source, outStream); + source.writeTo(outStream); BytesStreamInput inStream = new BytesStreamInput(outStream.bytes().toBytes()); RoutingTable target = RoutingTable.Builder.readFrom(inStream); diff --git a/src/test/java/org/elasticsearch/cluster/serialization/DiffableTests.java b/src/test/java/org/elasticsearch/cluster/serialization/DiffableTests.java new file mode 100644 index 00000000000..d87d900a0e8 --- /dev/null +++ b/src/test/java/org/elasticsearch/cluster/serialization/DiffableTests.java @@ -0,0 +1,127 @@ +/* + * 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.cluster.serialization; + +import com.google.common.collect.ImmutableMap; +import org.elasticsearch.cluster.Diff; +import org.elasticsearch.cluster.DiffableUtils; +import org.elasticsearch.cluster.DiffableUtils.KeyedReader; +import org.elasticsearch.cluster.AbstractDiffable; +import org.elasticsearch.common.collect.ImmutableOpenMap; +import org.elasticsearch.common.io.stream.*; +import org.elasticsearch.test.ElasticsearchTestCase; +import org.junit.Test; + +import java.io.IOException; +import java.util.Map; + +import static com.google.common.collect.Maps.newHashMap; +import static org.hamcrest.CoreMatchers.equalTo; + +public class DiffableTests extends ElasticsearchTestCase { + + @Test + public void testImmutableMapDiff() throws IOException { + ImmutableMap.Builder builder = ImmutableMap.builder(); + builder.put("foo", new TestDiffable("1")); + builder.put("bar", new TestDiffable("2")); + builder.put("baz", new TestDiffable("3")); + ImmutableMap before = builder.build(); + Map map = newHashMap(); + map.putAll(before); + map.remove("bar"); + map.put("baz", new TestDiffable("4")); + map.put("new", new TestDiffable("5")); + ImmutableMap after = ImmutableMap.copyOf(map); + Diff diff = DiffableUtils.diff(before, after); + BytesStreamOutput out = new BytesStreamOutput(); + diff.writeTo(out); + BytesStreamInput in = new BytesStreamInput(out.bytes()); + ImmutableMap serialized = DiffableUtils.readImmutableMapDiff(in, TestDiffable.PROTO).apply(before); + assertThat(serialized.size(), equalTo(3)); + assertThat(serialized.get("foo").value(), equalTo("1")); + assertThat(serialized.get("baz").value(), equalTo("4")); + assertThat(serialized.get("new").value(), equalTo("5")); + } + + @Test + public void testImmutableOpenMapDiff() throws IOException { + ImmutableOpenMap.Builder builder = ImmutableOpenMap.builder(); + builder.put("foo", new TestDiffable("1")); + builder.put("bar", new TestDiffable("2")); + builder.put("baz", new TestDiffable("3")); + ImmutableOpenMap before = builder.build(); + builder = ImmutableOpenMap.builder(before); + builder.remove("bar"); + builder.put("baz", new TestDiffable("4")); + builder.put("new", new TestDiffable("5")); + ImmutableOpenMap after = builder.build(); + Diff diff = DiffableUtils.diff(before, after); + BytesStreamOutput out = new BytesStreamOutput(); + diff.writeTo(out); + BytesStreamInput in = new BytesStreamInput(out.bytes()); + ImmutableOpenMap serialized = DiffableUtils.readImmutableOpenMapDiff(in, new KeyedReader() { + @Override + public TestDiffable readFrom(StreamInput in, String key) throws IOException { + return new TestDiffable(in.readString()); + } + + @Override + public Diff readDiffFrom(StreamInput in, String key) throws IOException { + return AbstractDiffable.readDiffFrom(new StreamableReader() { + @Override + public TestDiffable readFrom(StreamInput in) throws IOException { + return new TestDiffable(in.readString()); + } + }, in); + } + }).apply(before); + assertThat(serialized.size(), equalTo(3)); + assertThat(serialized.get("foo").value(), equalTo("1")); + assertThat(serialized.get("baz").value(), equalTo("4")); + assertThat(serialized.get("new").value(), equalTo("5")); + + } + public static class TestDiffable extends AbstractDiffable { + + public static final TestDiffable PROTO = new TestDiffable(""); + + private final String value; + + public TestDiffable(String value) { + this.value = value; + } + + public String value() { + return value; + } + + @Override + public TestDiffable readFrom(StreamInput in) throws IOException { + return new TestDiffable(in.readString()); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(value); + } + } + +} diff --git a/src/test/java/org/elasticsearch/common/xcontent/XContentTestUtils.java b/src/test/java/org/elasticsearch/common/xcontent/XContentTestUtils.java new file mode 100644 index 00000000000..9ebffe58783 --- /dev/null +++ b/src/test/java/org/elasticsearch/common/xcontent/XContentTestUtils.java @@ -0,0 +1,100 @@ +/* + * 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.common.xcontent; + +import com.carrotsearch.ant.tasks.junit4.dependencies.com.google.common.collect.Lists; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.common.xcontent.ToXContent.EMPTY_PARAMS; + +public final class XContentTestUtils { + private XContentTestUtils() { + + } + + public static Map convertToMap(ToXContent part) throws IOException { + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.startObject(); + part.toXContent(builder, EMPTY_PARAMS); + builder.endObject(); + return XContentHelper.convertToMap(builder.bytes(), false).v2(); + } + + + /** + * Compares to maps generated from XContentObjects. The order of elements in arrays is ignored + */ + public static boolean mapsEqualIgnoringArrayOrder(Map first, Map second) { + if (first.size() != second.size()) { + return false; + } + + for (String key : first.keySet()) { + if (objectsEqualIgnoringArrayOrder(first.get(key), second.get(key)) == false) { + return false; + } + } + return true; + } + + @SuppressWarnings("unchecked") + private static boolean objectsEqualIgnoringArrayOrder(Object first, Object second) { + if (first == null ) { + return second == null; + } else if (first instanceof List) { + if (second instanceof List) { + List secondList = Lists.newArrayList((List) second); + List firstList = (List) first; + if (firstList.size() == secondList.size()) { + for (Object firstObj : firstList) { + boolean found = false; + for (Object secondObj : secondList) { + if (objectsEqualIgnoringArrayOrder(firstObj, secondObj)) { + secondList.remove(secondObj); + found = true; + break; + } + } + if (found == false) { + return false; + } + } + return secondList.isEmpty(); + } else { + return false; + } + } else { + return false; + } + } else if (first instanceof Map) { + if (second instanceof Map) { + return mapsEqualIgnoringArrayOrder((Map) first, (Map) second); + } else { + return false; + } + } else { + return first.equals(second); + } + } + +} diff --git a/src/test/java/org/elasticsearch/discovery/ZenUnicastDiscoveryTests.java b/src/test/java/org/elasticsearch/discovery/ZenUnicastDiscoveryTests.java index f265869ec75..f1e7a249c59 100644 --- a/src/test/java/org/elasticsearch/discovery/ZenUnicastDiscoveryTests.java +++ b/src/test/java/org/elasticsearch/discovery/ZenUnicastDiscoveryTests.java @@ -28,6 +28,7 @@ import org.elasticsearch.test.ElasticsearchIntegrationTest; import org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope; import org.elasticsearch.test.ElasticsearchIntegrationTest.Scope; import org.elasticsearch.test.discovery.ClusterDiscoveryConfiguration; +import org.elasticsearch.test.junit.annotations.TestLogging; import org.junit.Before; import org.junit.Test; diff --git a/src/test/java/org/elasticsearch/discovery/zen/ZenDiscoveryTests.java b/src/test/java/org/elasticsearch/discovery/zen/ZenDiscoveryTests.java index 58e177b1115..228faa8cf4d 100644 --- a/src/test/java/org/elasticsearch/discovery/zen/ZenDiscoveryTests.java +++ b/src/test/java/org/elasticsearch/discovery/zen/ZenDiscoveryTests.java @@ -32,9 +32,6 @@ import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.common.Priority; import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.common.compress.CompressorFactory; -import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.ImmutableSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.LocalTransportAddress; @@ -196,12 +193,7 @@ public class ZenDiscoveryTests extends ElasticsearchIntegrationTest { .put(new DiscoveryNode("abc", new LocalTransportAddress("abc"), Version.CURRENT)).masterNodeId("abc"); ClusterState.Builder builder = ClusterState.builder(state); builder.nodes(nodes); - BytesStreamOutput bStream = new BytesStreamOutput(); - StreamOutput stream = CompressorFactory.defaultCompressor().streamOutput(bStream); - stream.setVersion(node.version()); - ClusterState.Builder.writeTo(builder.build(), stream); - stream.close(); - BytesReference bytes = bStream.bytes(); + BytesReference bytes = PublishClusterStateAction.serializeFullClusterState(builder.build(), node.version()); final CountDownLatch latch = new CountDownLatch(1); final AtomicReference reference = new AtomicReference<>(); diff --git a/src/test/java/org/elasticsearch/index/mapper/timestamp/TimestampMappingTests.java b/src/test/java/org/elasticsearch/index/mapper/timestamp/TimestampMappingTests.java index c5adf8cb50e..c97fa5b789d 100644 --- a/src/test/java/org/elasticsearch/index/mapper/timestamp/TimestampMappingTests.java +++ b/src/test/java/org/elasticsearch/index/mapper/timestamp/TimestampMappingTests.java @@ -443,11 +443,11 @@ public class TimestampMappingTests extends ElasticsearchSingleNodeTest { new MappingMetaData.Id(null), new MappingMetaData.Routing(false, null), timestamp, false); BytesStreamOutput out = new BytesStreamOutput(); - MappingMetaData.writeTo(expected, out); + expected.writeTo(out); out.close(); BytesReference bytes = out.bytes(); - MappingMetaData metaData = MappingMetaData.readFrom(new BytesStreamInput(bytes)); + MappingMetaData metaData = MappingMetaData.PROTO.readFrom(new BytesStreamInput(bytes)); assertThat(metaData, is(expected)); } @@ -460,11 +460,11 @@ public class TimestampMappingTests extends ElasticsearchSingleNodeTest { new MappingMetaData.Id(null), new MappingMetaData.Routing(false, null), timestamp, false); BytesStreamOutput out = new BytesStreamOutput(); - MappingMetaData.writeTo(expected, out); + expected.writeTo(out); out.close(); BytesReference bytes = out.bytes(); - MappingMetaData metaData = MappingMetaData.readFrom(new BytesStreamInput(bytes)); + MappingMetaData metaData = MappingMetaData.PROTO.readFrom(new BytesStreamInput(bytes)); assertThat(metaData, is(expected)); } @@ -477,11 +477,11 @@ public class TimestampMappingTests extends ElasticsearchSingleNodeTest { new MappingMetaData.Id(null), new MappingMetaData.Routing(false, null), timestamp, false); BytesStreamOutput out = new BytesStreamOutput(); - MappingMetaData.writeTo(expected, out); + expected.writeTo(out); out.close(); BytesReference bytes = out.bytes(); - MappingMetaData metaData = MappingMetaData.readFrom(new BytesStreamInput(bytes)); + MappingMetaData metaData = MappingMetaData.PROTO.readFrom(new BytesStreamInput(bytes)); assertThat(metaData, is(expected)); } diff --git a/src/test/java/org/elasticsearch/indices/store/IndicesStoreIntegrationTests.java b/src/test/java/org/elasticsearch/indices/store/IndicesStoreIntegrationTests.java index 386c778b07e..9e05d915803 100644 --- a/src/test/java/org/elasticsearch/indices/store/IndicesStoreIntegrationTests.java +++ b/src/test/java/org/elasticsearch/indices/store/IndicesStoreIntegrationTests.java @@ -42,6 +42,7 @@ import org.elasticsearch.test.InternalTestCluster; import org.elasticsearch.test.disruption.SlowClusterStateProcessing; import org.junit.Test; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; @@ -68,6 +69,12 @@ public class IndicesStoreIntegrationTests extends ElasticsearchIntegrationTest { .build(); } + @Override + protected void ensureClusterStateConsistency() throws IOException { + // testShardActiveElseWhere might change the state of a non-master node + // so we cannot check state consistency of this cluster + } + @Test public void indexCleanup() throws Exception { final String masterNode = internalCluster().startNode(ImmutableSettings.builder().put("node.data", false)); diff --git a/src/test/java/org/elasticsearch/indices/template/SimpleIndexTemplateTests.java b/src/test/java/org/elasticsearch/indices/template/SimpleIndexTemplateTests.java index ce96576ce15..bd664694c9f 100644 --- a/src/test/java/org/elasticsearch/indices/template/SimpleIndexTemplateTests.java +++ b/src/test/java/org/elasticsearch/indices/template/SimpleIndexTemplateTests.java @@ -284,6 +284,7 @@ public class SimpleIndexTemplateTests extends ElasticsearchIntegrationTest { } @Test + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/pull/8802") public void testBrokenMapping() throws Exception { // clean all templates setup by the framework. client().admin().indices().prepareDeleteTemplate("*").get(); diff --git a/src/test/java/org/elasticsearch/snapshots/DedicatedClusterSnapshotRestoreTests.java b/src/test/java/org/elasticsearch/snapshots/DedicatedClusterSnapshotRestoreTests.java index ff8264fdc03..8d569275aea 100644 --- a/src/test/java/org/elasticsearch/snapshots/DedicatedClusterSnapshotRestoreTests.java +++ b/src/test/java/org/elasticsearch/snapshots/DedicatedClusterSnapshotRestoreTests.java @@ -38,7 +38,9 @@ import org.elasticsearch.client.Client; import org.elasticsearch.cluster.ClusterService; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ProcessedClusterStateUpdateTask; +import org.elasticsearch.cluster.AbstractDiffable; import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.metadata.MetaData.Custom; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Priority; import org.elasticsearch.common.io.stream.StreamInput; @@ -748,7 +750,7 @@ public class DedicatedClusterSnapshotRestoreTests extends AbstractSnapshotTests )); } - public static abstract class TestCustomMetaData implements MetaData.Custom { + public static abstract class TestCustomMetaData extends AbstractDiffable implements MetaData.Custom { private final String data; protected TestCustomMetaData(String data) { @@ -776,194 +778,182 @@ public class DedicatedClusterSnapshotRestoreTests extends AbstractSnapshotTests return data.hashCode(); } - public static abstract class TestCustomMetaDataFactory extends MetaData.Custom.Factory { + protected abstract TestCustomMetaData newTestCustomMetaData(String data); - protected abstract TestCustomMetaData newTestCustomMetaData(String data); + @Override + public Custom readFrom(StreamInput in) throws IOException { + return newTestCustomMetaData(in.readString()); + } - @Override - public T readFrom(StreamInput in) throws IOException { - return (T) newTestCustomMetaData(in.readString()); - } + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(getData()); + } - @Override - public void writeTo(T metadata, StreamOutput out) throws IOException { - out.writeString(metadata.getData()); - } - - @Override - public T fromXContent(XContentParser parser) throws IOException { - XContentParser.Token token; - String data = null; - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - String currentFieldName = parser.currentName(); - if ("data".equals(currentFieldName)) { - if (parser.nextToken() != XContentParser.Token.VALUE_STRING) { - throw new ElasticsearchParseException("failed to parse snapshottable metadata, invalid data type"); - } - data = parser.text(); - } else { - throw new ElasticsearchParseException("failed to parse snapshottable metadata, unknown field [" + currentFieldName + "]"); + @Override + public Custom fromXContent(XContentParser parser) throws IOException { + XContentParser.Token token; + String data = null; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + String currentFieldName = parser.currentName(); + if ("data".equals(currentFieldName)) { + if (parser.nextToken() != XContentParser.Token.VALUE_STRING) { + throw new ElasticsearchParseException("failed to parse snapshottable metadata, invalid data type"); } + data = parser.text(); } else { - throw new ElasticsearchParseException("failed to parse snapshottable metadata"); + throw new ElasticsearchParseException("failed to parse snapshottable metadata, unknown field [" + currentFieldName + "]"); } + } else { + throw new ElasticsearchParseException("failed to parse snapshottable metadata"); } - if (data == null) { - throw new ElasticsearchParseException("failed to parse snapshottable metadata, data not found"); - } - return (T) newTestCustomMetaData(data); } + if (data == null) { + throw new ElasticsearchParseException("failed to parse snapshottable metadata, data not found"); + } + return newTestCustomMetaData(data); + } - @Override - public void toXContent(T metadata, XContentBuilder builder, ToXContent.Params params) throws IOException { - builder.field("data", metadata.getData()); - } + @Override + public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { + builder.field("data", getData()); + return builder; } } + static { - MetaData.registerFactory(SnapshottableMetadata.TYPE, SnapshottableMetadata.FACTORY); - MetaData.registerFactory(NonSnapshottableMetadata.TYPE, NonSnapshottableMetadata.FACTORY); - MetaData.registerFactory(SnapshottableGatewayMetadata.TYPE, SnapshottableGatewayMetadata.FACTORY); - MetaData.registerFactory(NonSnapshottableGatewayMetadata.TYPE, NonSnapshottableGatewayMetadata.FACTORY); - MetaData.registerFactory(SnapshotableGatewayNoApiMetadata.TYPE, SnapshotableGatewayNoApiMetadata.FACTORY); + MetaData.registerPrototype(SnapshottableMetadata.TYPE, SnapshottableMetadata.PROTO); + MetaData.registerPrototype(NonSnapshottableMetadata.TYPE, NonSnapshottableMetadata.PROTO); + MetaData.registerPrototype(SnapshottableGatewayMetadata.TYPE, SnapshottableGatewayMetadata.PROTO); + MetaData.registerPrototype(NonSnapshottableGatewayMetadata.TYPE, NonSnapshottableGatewayMetadata.PROTO); + MetaData.registerPrototype(SnapshotableGatewayNoApiMetadata.TYPE, SnapshotableGatewayNoApiMetadata.PROTO); } public static class SnapshottableMetadata extends TestCustomMetaData { public static final String TYPE = "test_snapshottable"; - public static final Factory FACTORY = new Factory(); + public static final SnapshottableMetadata PROTO = new SnapshottableMetadata(""); public SnapshottableMetadata(String data) { super(data); } - private static class Factory extends TestCustomMetaDataFactory { + @Override + public String type() { + return TYPE; + } - @Override - public String type() { - return TYPE; - } + @Override + protected TestCustomMetaData newTestCustomMetaData(String data) { + return new SnapshottableMetadata(data); + } - @Override - protected TestCustomMetaData newTestCustomMetaData(String data) { - return new SnapshottableMetadata(data); - } - - @Override - public EnumSet context() { - return MetaData.API_AND_SNAPSHOT; - } + @Override + public EnumSet context() { + return MetaData.API_AND_SNAPSHOT; } } public static class NonSnapshottableMetadata extends TestCustomMetaData { public static final String TYPE = "test_non_snapshottable"; - public static final Factory FACTORY = new Factory(); + public static final NonSnapshottableMetadata PROTO = new NonSnapshottableMetadata(""); public NonSnapshottableMetadata(String data) { super(data); } - private static class Factory extends TestCustomMetaDataFactory { + @Override + public String type() { + return TYPE; + } - @Override - public String type() { - return TYPE; - } + @Override + protected NonSnapshottableMetadata newTestCustomMetaData(String data) { + return new NonSnapshottableMetadata(data); + } - @Override - protected NonSnapshottableMetadata newTestCustomMetaData(String data) { - return new NonSnapshottableMetadata(data); - } + @Override + public EnumSet context() { + return MetaData.API_ONLY; } } public static class SnapshottableGatewayMetadata extends TestCustomMetaData { public static final String TYPE = "test_snapshottable_gateway"; - public static final Factory FACTORY = new Factory(); + public static final SnapshottableGatewayMetadata PROTO = new SnapshottableGatewayMetadata(""); public SnapshottableGatewayMetadata(String data) { super(data); } - private static class Factory extends TestCustomMetaDataFactory { + @Override + public String type() { + return TYPE; + } - @Override - public String type() { - return TYPE; - } + @Override + protected TestCustomMetaData newTestCustomMetaData(String data) { + return new SnapshottableGatewayMetadata(data); + } - @Override - protected TestCustomMetaData newTestCustomMetaData(String data) { - return new SnapshottableGatewayMetadata(data); - } - - @Override - public EnumSet context() { - return EnumSet.of(MetaData.XContentContext.API, MetaData.XContentContext.SNAPSHOT, MetaData.XContentContext.GATEWAY); - } + @Override + public EnumSet context() { + return EnumSet.of(MetaData.XContentContext.API, MetaData.XContentContext.SNAPSHOT, MetaData.XContentContext.GATEWAY); } } public static class NonSnapshottableGatewayMetadata extends TestCustomMetaData { public static final String TYPE = "test_non_snapshottable_gateway"; - public static final Factory FACTORY = new Factory(); + public static final NonSnapshottableGatewayMetadata PROTO = new NonSnapshottableGatewayMetadata(""); public NonSnapshottableGatewayMetadata(String data) { super(data); } - private static class Factory extends TestCustomMetaDataFactory { - - @Override - public String type() { - return TYPE; - } - - @Override - protected NonSnapshottableGatewayMetadata newTestCustomMetaData(String data) { - return new NonSnapshottableGatewayMetadata(data); - } - - @Override - public EnumSet context() { - return MetaData.API_AND_GATEWAY; - } - + @Override + public String type() { + return TYPE; } + + @Override + protected NonSnapshottableGatewayMetadata newTestCustomMetaData(String data) { + return new NonSnapshottableGatewayMetadata(data); + } + + @Override + public EnumSet context() { + return MetaData.API_AND_GATEWAY; + } + } public static class SnapshotableGatewayNoApiMetadata extends TestCustomMetaData { public static final String TYPE = "test_snapshottable_gateway_no_api"; - public static final Factory FACTORY = new Factory(); + public static final SnapshotableGatewayNoApiMetadata PROTO = new SnapshotableGatewayNoApiMetadata(""); public SnapshotableGatewayNoApiMetadata(String data) { super(data); } - private static class Factory extends TestCustomMetaDataFactory { + @Override + public String type() { + return TYPE; + } - @Override - public String type() { - return TYPE; - } - - @Override - protected SnapshotableGatewayNoApiMetadata newTestCustomMetaData(String data) { - return new SnapshotableGatewayNoApiMetadata(data); - } - - @Override - public EnumSet context() { - return EnumSet.of(MetaData.XContentContext.GATEWAY, MetaData.XContentContext.SNAPSHOT); - } + @Override + protected SnapshotableGatewayNoApiMetadata newTestCustomMetaData(String data) { + return new SnapshotableGatewayNoApiMetadata(data); + } + @Override + public EnumSet context() { + return EnumSet.of(MetaData.XContentContext.GATEWAY, MetaData.XContentContext.SNAPSHOT); } } diff --git a/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java b/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java index 9e68d16caa0..f30a47755ed 100644 --- a/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java +++ b/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java @@ -166,6 +166,8 @@ import java.util.concurrent.atomic.AtomicLong; import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_REPLICAS; import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_SHARDS; import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder; +import static org.elasticsearch.common.xcontent.XContentTestUtils.convertToMap; +import static org.elasticsearch.common.xcontent.XContentTestUtils.mapsEqualIgnoringArrayOrder; import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures; @@ -357,7 +359,7 @@ public abstract class ElasticsearchIntegrationTest extends ElasticsearchTestCase * Creates a randomized index template. This template is used to pass in randomized settings on a * per index basis. Allows to enable/disable the randomization for number of shards and replicas */ - private void randomIndexTemplate() throws IOException { + public void randomIndexTemplate() throws IOException { // TODO move settings for random directory etc here into the index based randomized settings. if (cluster().size() > 0) { @@ -647,6 +649,7 @@ public abstract class ElasticsearchIntegrationTest extends ElasticsearchTestCase .transientSettings().getAsMap().size(), equalTo(0)); } ensureClusterSizeConsistency(); + ensureClusterStateConsistency(); cluster().wipe(); // wipe after to make sure we fail in the test that didn't ack the delete if (afterClass || currentClusterScope == Scope.TEST) { cluster().close(); @@ -1085,8 +1088,8 @@ public abstract class ElasticsearchIntegrationTest extends ElasticsearchTestCase */ public void setMinimumMasterNodes(int n) { assertTrue(client().admin().cluster().prepareUpdateSettings().setTransientSettings( - settingsBuilder().put(ElectMasterService.DISCOVERY_ZEN_MINIMUM_MASTER_NODES, n)) - .get().isAcknowledged()); + settingsBuilder().put(ElectMasterService.DISCOVERY_ZEN_MINIMUM_MASTER_NODES, n)) + .get().isAcknowledged()); } /** @@ -1133,6 +1136,35 @@ public abstract class ElasticsearchIntegrationTest extends ElasticsearchTestCase } } + /** + * Verifies that all nodes that have the same version of the cluster state as master have same cluster state + */ + protected void ensureClusterStateConsistency() throws IOException { + if (cluster() != null) { + ClusterState masterClusterState = client().admin().cluster().prepareState().all().get().getState(); + Map masterStateMap = convertToMap(masterClusterState); + int masterClusterStateSize = ClusterState.Builder.toBytes(masterClusterState).length; + for (Client client : cluster()) { + ClusterState localClusterState = client.admin().cluster().prepareState().all().setLocal(true).get().getState(); + if (masterClusterState.version() == localClusterState.version()) { + try { + assertThat(masterClusterState.uuid(), equalTo(localClusterState.uuid())); + // We cannot compare serialization bytes since serialization order of maps is not guaranteed + // but we can compare serialization sizes - they should be the same + int localClusterStateSize = ClusterState.Builder.toBytes(localClusterState).length; + assertThat(masterClusterStateSize, equalTo(localClusterStateSize)); + + // Compare JSON serialization + assertThat(mapsEqualIgnoringArrayOrder(masterStateMap, convertToMap(localClusterState)), equalTo(true)); + } catch (AssertionError error) { + logger.error("Cluster state from master:\n{}\nLocal cluster state:\n{}", masterClusterState.toString(), localClusterState.toString()); + throw error; + } + } + } + } + } + /** * Ensures the cluster is in a searchable state for the given indices. This means a searchable copy of each * shard is available on the cluster. diff --git a/src/test/java/org/elasticsearch/test/ElasticsearchTestCase.java b/src/test/java/org/elasticsearch/test/ElasticsearchTestCase.java index 1276089b182..0f71b7239e0 100644 --- a/src/test/java/org/elasticsearch/test/ElasticsearchTestCase.java +++ b/src/test/java/org/elasticsearch/test/ElasticsearchTestCase.java @@ -71,6 +71,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import static com.google.common.collect.Lists.newArrayList; /** * Base testcase for randomized unit testing with Elasticsearch @@ -595,4 +596,17 @@ public abstract class ElasticsearchTestCase extends LuceneTestCase { return threadGroup.getName(); } } + + /** + * Returns size random values + */ + public static List randomSubsetOf(int size, T... values) { + if (size > values.length) { + throw new IllegalArgumentException("Can\'t pick " + size + " random objects from a list of " + values.length + " objects"); + } + List list = newArrayList(values); + Collections.shuffle(list); + return list.subList(0, size); + } + } From 58eed45ee58bf12b01a1f05719806c82d719040d Mon Sep 17 00:00:00 2001 From: Simon Willnauer Date: Mon, 27 Apr 2015 10:23:18 +0200 Subject: [PATCH 80/85] [TEST] Move XContentTestUtils.java into o.e.test folder Classes referenced by the Test base classes must be under this package otherwise the test jar can't be used in a 3rd party application. --- .../org/elasticsearch/cluster/ClusterStateDiffTests.java | 4 ++-- .../elasticsearch/test/ElasticsearchIntegrationTest.java | 4 ++-- .../{common/xcontent => test}/XContentTestUtils.java | 6 +++++- 3 files changed, 9 insertions(+), 5 deletions(-) rename src/test/java/org/elasticsearch/{common/xcontent => test}/XContentTestUtils.java (93%) diff --git a/src/test/java/org/elasticsearch/cluster/ClusterStateDiffTests.java b/src/test/java/org/elasticsearch/cluster/ClusterStateDiffTests.java index 84df1eaf209..b49b7586dc3 100644 --- a/src/test/java/org/elasticsearch/cluster/ClusterStateDiffTests.java +++ b/src/test/java/org/elasticsearch/cluster/ClusterStateDiffTests.java @@ -48,8 +48,8 @@ import org.junit.Test; import java.util.List; import static org.elasticsearch.cluster.metadata.AliasMetaData.newAliasMetaDataBuilder; -import static org.elasticsearch.common.xcontent.XContentTestUtils.convertToMap; -import static org.elasticsearch.common.xcontent.XContentTestUtils.mapsEqualIgnoringArrayOrder; +import static org.elasticsearch.test.XContentTestUtils.convertToMap; +import static org.elasticsearch.test.XContentTestUtils.mapsEqualIgnoringArrayOrder; import static org.elasticsearch.test.VersionUtils.randomVersion; import static org.hamcrest.Matchers.equalTo; diff --git a/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java b/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java index f30a47755ed..2550bf8a417 100644 --- a/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java +++ b/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java @@ -166,8 +166,8 @@ import java.util.concurrent.atomic.AtomicLong; import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_REPLICAS; import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_SHARDS; import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder; -import static org.elasticsearch.common.xcontent.XContentTestUtils.convertToMap; -import static org.elasticsearch.common.xcontent.XContentTestUtils.mapsEqualIgnoringArrayOrder; +import static org.elasticsearch.test.XContentTestUtils.convertToMap; +import static org.elasticsearch.test.XContentTestUtils.mapsEqualIgnoringArrayOrder; import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures; diff --git a/src/test/java/org/elasticsearch/common/xcontent/XContentTestUtils.java b/src/test/java/org/elasticsearch/test/XContentTestUtils.java similarity index 93% rename from src/test/java/org/elasticsearch/common/xcontent/XContentTestUtils.java rename to src/test/java/org/elasticsearch/test/XContentTestUtils.java index 9ebffe58783..1f1b8eff710 100644 --- a/src/test/java/org/elasticsearch/common/xcontent/XContentTestUtils.java +++ b/src/test/java/org/elasticsearch/test/XContentTestUtils.java @@ -17,9 +17,13 @@ * under the License. */ -package org.elasticsearch.common.xcontent; +package org.elasticsearch.test; import com.carrotsearch.ant.tasks.junit4.dependencies.com.google.common.collect.Lists; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentHelper; import java.io.IOException; import java.util.List; From 9828e955f3537e09955551e7fd23c6b243ec9c7a Mon Sep 17 00:00:00 2001 From: Simon Willnauer Date: Mon, 27 Apr 2015 14:23:42 +0200 Subject: [PATCH 81/85] [TEST] enable host name resolving to gain consistent transport addresses in clusterstate --- src/test/java/org/elasticsearch/test/InternalTestCluster.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/org/elasticsearch/test/InternalTestCluster.java b/src/test/java/org/elasticsearch/test/InternalTestCluster.java index 5d2d00c4870..cc487f30258 100644 --- a/src/test/java/org/elasticsearch/test/InternalTestCluster.java +++ b/src/test/java/org/elasticsearch/test/InternalTestCluster.java @@ -295,6 +295,7 @@ public final class InternalTestCluster extends TestCluster { builder.put("http.port", BASE_PORT+101 + "-" + (BASE_PORT+200)); builder.put("config.ignore_system_properties", true); builder.put("node.mode", NODE_MODE); + builder.put("network.address.serialization.resolve", true); // this makes adresses in the clusterstate consistent builder.put("http.pipelining", enableHttpPipelining); builder.put("plugins." + PluginsService.LOAD_PLUGIN_FROM_CLASSPATH, false); builder.put(NodeEnvironment.SETTING_CUSTOM_DATA_PATH_ENABLED, true); From 38be1e8a1a8fe870538487285cabd5e5f6c88e53 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Mon, 27 Apr 2015 08:47:09 -0400 Subject: [PATCH 82/85] Test: remove reference to the local node before comparing cluster states in ensureClusterStateConsistency --- .../test/ElasticsearchIntegrationTest.java | 12 +++++++++--- .../org/elasticsearch/test/InternalTestCluster.java | 1 - 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java b/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java index 2550bf8a417..0a5208f6763 100644 --- a/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java +++ b/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java @@ -1142,20 +1142,26 @@ public abstract class ElasticsearchIntegrationTest extends ElasticsearchTestCase protected void ensureClusterStateConsistency() throws IOException { if (cluster() != null) { ClusterState masterClusterState = client().admin().cluster().prepareState().all().get().getState(); + byte[] masterClusterStateBytes = ClusterState.Builder.toBytes(masterClusterState); + // remove local node reference + masterClusterState = ClusterState.Builder.fromBytes(masterClusterStateBytes, null); Map masterStateMap = convertToMap(masterClusterState); int masterClusterStateSize = ClusterState.Builder.toBytes(masterClusterState).length; for (Client client : cluster()) { ClusterState localClusterState = client.admin().cluster().prepareState().all().setLocal(true).get().getState(); + byte[] localClusterStateBytes = ClusterState.Builder.toBytes(localClusterState); + // remove local node reference + localClusterState = ClusterState.Builder.fromBytes(localClusterStateBytes, null); + Map localStateMap = convertToMap(localClusterState); + int localClusterStateSize = localClusterStateBytes.length; if (masterClusterState.version() == localClusterState.version()) { try { assertThat(masterClusterState.uuid(), equalTo(localClusterState.uuid())); // We cannot compare serialization bytes since serialization order of maps is not guaranteed // but we can compare serialization sizes - they should be the same - int localClusterStateSize = ClusterState.Builder.toBytes(localClusterState).length; assertThat(masterClusterStateSize, equalTo(localClusterStateSize)); - // Compare JSON serialization - assertThat(mapsEqualIgnoringArrayOrder(masterStateMap, convertToMap(localClusterState)), equalTo(true)); + assertThat(mapsEqualIgnoringArrayOrder(masterStateMap, localStateMap), equalTo(true)); } catch (AssertionError error) { logger.error("Cluster state from master:\n{}\nLocal cluster state:\n{}", masterClusterState.toString(), localClusterState.toString()); throw error; diff --git a/src/test/java/org/elasticsearch/test/InternalTestCluster.java b/src/test/java/org/elasticsearch/test/InternalTestCluster.java index cc487f30258..5d2d00c4870 100644 --- a/src/test/java/org/elasticsearch/test/InternalTestCluster.java +++ b/src/test/java/org/elasticsearch/test/InternalTestCluster.java @@ -295,7 +295,6 @@ public final class InternalTestCluster extends TestCluster { builder.put("http.port", BASE_PORT+101 + "-" + (BASE_PORT+200)); builder.put("config.ignore_system_properties", true); builder.put("node.mode", NODE_MODE); - builder.put("network.address.serialization.resolve", true); // this makes adresses in the clusterstate consistent builder.put("http.pipelining", enableHttpPipelining); builder.put("plugins." + PluginsService.LOAD_PLUGIN_FROM_CLASSPATH, false); builder.put(NodeEnvironment.SETTING_CUSTOM_DATA_PATH_ENABLED, true); From bac135261cf4c0a04e4d55f1062a94b2179153b3 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Tue, 28 Apr 2015 12:18:43 -0400 Subject: [PATCH 83/85] Test: make sure that tests are not affected by changing in address resolution settings --- .../transport/InetSocketTransportAddress.java | 4 ++ .../test/ElasticsearchIntegrationTest.java | 53 +++++++++++-------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/elasticsearch/common/transport/InetSocketTransportAddress.java b/src/main/java/org/elasticsearch/common/transport/InetSocketTransportAddress.java index 1bc519435de..bfa4233d917 100644 --- a/src/main/java/org/elasticsearch/common/transport/InetSocketTransportAddress.java +++ b/src/main/java/org/elasticsearch/common/transport/InetSocketTransportAddress.java @@ -38,6 +38,10 @@ public class InetSocketTransportAddress implements TransportAddress { InetSocketTransportAddress.resolveAddress = resolveAddress; } + public static boolean getResolveAddress() { + return resolveAddress; + } + private InetSocketAddress address; InetSocketTransportAddress() { diff --git a/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java b/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java index 0a5208f6763..f160e4cb653 100644 --- a/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java +++ b/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java @@ -1141,34 +1141,41 @@ public abstract class ElasticsearchIntegrationTest extends ElasticsearchTestCase */ protected void ensureClusterStateConsistency() throws IOException { if (cluster() != null) { - ClusterState masterClusterState = client().admin().cluster().prepareState().all().get().getState(); - byte[] masterClusterStateBytes = ClusterState.Builder.toBytes(masterClusterState); - // remove local node reference - masterClusterState = ClusterState.Builder.fromBytes(masterClusterStateBytes, null); - Map masterStateMap = convertToMap(masterClusterState); - int masterClusterStateSize = ClusterState.Builder.toBytes(masterClusterState).length; - for (Client client : cluster()) { - ClusterState localClusterState = client.admin().cluster().prepareState().all().setLocal(true).get().getState(); - byte[] localClusterStateBytes = ClusterState.Builder.toBytes(localClusterState); + boolean getResolvedAddress = InetSocketTransportAddress.getResolveAddress(); + try { + InetSocketTransportAddress.setResolveAddress(false); + ClusterState masterClusterState = client().admin().cluster().prepareState().all().get().getState(); + byte[] masterClusterStateBytes = ClusterState.Builder.toBytes(masterClusterState); // remove local node reference - localClusterState = ClusterState.Builder.fromBytes(localClusterStateBytes, null); - Map localStateMap = convertToMap(localClusterState); - int localClusterStateSize = localClusterStateBytes.length; - if (masterClusterState.version() == localClusterState.version()) { - try { - assertThat(masterClusterState.uuid(), equalTo(localClusterState.uuid())); - // We cannot compare serialization bytes since serialization order of maps is not guaranteed - // but we can compare serialization sizes - they should be the same - assertThat(masterClusterStateSize, equalTo(localClusterStateSize)); - // Compare JSON serialization - assertThat(mapsEqualIgnoringArrayOrder(masterStateMap, localStateMap), equalTo(true)); - } catch (AssertionError error) { - logger.error("Cluster state from master:\n{}\nLocal cluster state:\n{}", masterClusterState.toString(), localClusterState.toString()); - throw error; + masterClusterState = ClusterState.Builder.fromBytes(masterClusterStateBytes, null); + Map masterStateMap = convertToMap(masterClusterState); + int masterClusterStateSize = ClusterState.Builder.toBytes(masterClusterState).length; + for (Client client : cluster()) { + ClusterState localClusterState = client.admin().cluster().prepareState().all().setLocal(true).get().getState(); + byte[] localClusterStateBytes = ClusterState.Builder.toBytes(localClusterState); + // remove local node reference + localClusterState = ClusterState.Builder.fromBytes(localClusterStateBytes, null); + Map localStateMap = convertToMap(localClusterState); + int localClusterStateSize = localClusterStateBytes.length; + if (masterClusterState.version() == localClusterState.version()) { + try { + assertThat(masterClusterState.uuid(), equalTo(localClusterState.uuid())); + // We cannot compare serialization bytes since serialization order of maps is not guaranteed + // but we can compare serialization sizes - they should be the same + assertThat(masterClusterStateSize, equalTo(localClusterStateSize)); + // Compare JSON serialization + assertThat(mapsEqualIgnoringArrayOrder(masterStateMap, localStateMap), equalTo(true)); + } catch (AssertionError error) { + logger.error("Cluster state from master:\n{}\nLocal cluster state:\n{}", masterClusterState.toString(), localClusterState.toString()); + throw error; + } } } + } finally { + InetSocketTransportAddress.setResolveAddress(getResolvedAddress); } } + } /** From 8e5543dea05124f99552912c1c445add2e122dde Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Tue, 28 Apr 2015 15:59:57 -0400 Subject: [PATCH 84/85] Test: ignore cluster state differences on the nodes that disconnected from the master --- .../org/elasticsearch/test/ElasticsearchIntegrationTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java b/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java index f160e4cb653..187afe1b658 100644 --- a/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java +++ b/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java @@ -1150,6 +1150,7 @@ public abstract class ElasticsearchIntegrationTest extends ElasticsearchTestCase masterClusterState = ClusterState.Builder.fromBytes(masterClusterStateBytes, null); Map masterStateMap = convertToMap(masterClusterState); int masterClusterStateSize = ClusterState.Builder.toBytes(masterClusterState).length; + String masterId = masterClusterState.nodes().masterNodeId(); for (Client client : cluster()) { ClusterState localClusterState = client.admin().cluster().prepareState().all().setLocal(true).get().getState(); byte[] localClusterStateBytes = ClusterState.Builder.toBytes(localClusterState); @@ -1157,7 +1158,8 @@ public abstract class ElasticsearchIntegrationTest extends ElasticsearchTestCase localClusterState = ClusterState.Builder.fromBytes(localClusterStateBytes, null); Map localStateMap = convertToMap(localClusterState); int localClusterStateSize = localClusterStateBytes.length; - if (masterClusterState.version() == localClusterState.version()) { + // Check that the non-master node has the same version of the cluster state as the master and that this node didn't disconnect from the master + if (masterClusterState.version() == localClusterState.version() && localClusterState.nodes().nodes().containsKey(masterId)) { try { assertThat(masterClusterState.uuid(), equalTo(localClusterState.uuid())); // We cannot compare serialization bytes since serialization order of maps is not guaranteed From 351a4d3315e66989dce9b47f10e3057db4eb72df Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Wed, 29 Apr 2015 13:33:54 -0400 Subject: [PATCH 85/85] [DOCS] Fix movavg images and naming --- .../reducers_movavg}/double_0.2beta.png | Bin .../reducers_movavg}/double_0.7beta.png | Bin .../double_prediction_global.png | Bin .../double_prediction_local.png | Bin .../reducers_movavg}/linear_100window.png | Bin .../reducers_movavg}/linear_10window.png | Bin .../reducers_movavg}/movavg_100window.png | Bin .../reducers_movavg}/movavg_10window.png | Bin .../reducers_movavg}/simple_prediction.png | Bin .../reducers_movavg}/single_0.2alpha.png | Bin .../reducers_movavg}/single_0.7alpha.png | Bin .../search/aggregations/reducer.asciidoc | 4 ++-- ...r.asciidoc => movavg-aggregation.asciidoc} | 22 +++++++++--------- 13 files changed, 13 insertions(+), 13 deletions(-) rename docs/reference/{search/aggregations/reducer/images => images/reducers_movavg}/double_0.2beta.png (100%) rename docs/reference/{search/aggregations/reducer/images => images/reducers_movavg}/double_0.7beta.png (100%) rename docs/reference/{search/aggregations/reducer/images => images/reducers_movavg}/double_prediction_global.png (100%) rename docs/reference/{search/aggregations/reducer/images => images/reducers_movavg}/double_prediction_local.png (100%) rename docs/reference/{search/aggregations/reducer/images => images/reducers_movavg}/linear_100window.png (100%) rename docs/reference/{search/aggregations/reducer/images => images/reducers_movavg}/linear_10window.png (100%) rename docs/reference/{search/aggregations/reducer/images => images/reducers_movavg}/movavg_100window.png (100%) rename docs/reference/{search/aggregations/reducer/images => images/reducers_movavg}/movavg_10window.png (100%) rename docs/reference/{search/aggregations/reducer/images => images/reducers_movavg}/simple_prediction.png (100%) rename docs/reference/{search/aggregations/reducer/images => images/reducers_movavg}/single_0.2alpha.png (100%) rename docs/reference/{search/aggregations/reducer/images => images/reducers_movavg}/single_0.7alpha.png (100%) rename docs/reference/search/aggregations/reducer/{movavg-reducer.asciidoc => movavg-aggregation.asciidoc} (95%) diff --git a/docs/reference/search/aggregations/reducer/images/double_0.2beta.png b/docs/reference/images/reducers_movavg/double_0.2beta.png similarity index 100% rename from docs/reference/search/aggregations/reducer/images/double_0.2beta.png rename to docs/reference/images/reducers_movavg/double_0.2beta.png diff --git a/docs/reference/search/aggregations/reducer/images/double_0.7beta.png b/docs/reference/images/reducers_movavg/double_0.7beta.png similarity index 100% rename from docs/reference/search/aggregations/reducer/images/double_0.7beta.png rename to docs/reference/images/reducers_movavg/double_0.7beta.png diff --git a/docs/reference/search/aggregations/reducer/images/double_prediction_global.png b/docs/reference/images/reducers_movavg/double_prediction_global.png similarity index 100% rename from docs/reference/search/aggregations/reducer/images/double_prediction_global.png rename to docs/reference/images/reducers_movavg/double_prediction_global.png diff --git a/docs/reference/search/aggregations/reducer/images/double_prediction_local.png b/docs/reference/images/reducers_movavg/double_prediction_local.png similarity index 100% rename from docs/reference/search/aggregations/reducer/images/double_prediction_local.png rename to docs/reference/images/reducers_movavg/double_prediction_local.png diff --git a/docs/reference/search/aggregations/reducer/images/linear_100window.png b/docs/reference/images/reducers_movavg/linear_100window.png similarity index 100% rename from docs/reference/search/aggregations/reducer/images/linear_100window.png rename to docs/reference/images/reducers_movavg/linear_100window.png diff --git a/docs/reference/search/aggregations/reducer/images/linear_10window.png b/docs/reference/images/reducers_movavg/linear_10window.png similarity index 100% rename from docs/reference/search/aggregations/reducer/images/linear_10window.png rename to docs/reference/images/reducers_movavg/linear_10window.png diff --git a/docs/reference/search/aggregations/reducer/images/movavg_100window.png b/docs/reference/images/reducers_movavg/movavg_100window.png similarity index 100% rename from docs/reference/search/aggregations/reducer/images/movavg_100window.png rename to docs/reference/images/reducers_movavg/movavg_100window.png diff --git a/docs/reference/search/aggregations/reducer/images/movavg_10window.png b/docs/reference/images/reducers_movavg/movavg_10window.png similarity index 100% rename from docs/reference/search/aggregations/reducer/images/movavg_10window.png rename to docs/reference/images/reducers_movavg/movavg_10window.png diff --git a/docs/reference/search/aggregations/reducer/images/simple_prediction.png b/docs/reference/images/reducers_movavg/simple_prediction.png similarity index 100% rename from docs/reference/search/aggregations/reducer/images/simple_prediction.png rename to docs/reference/images/reducers_movavg/simple_prediction.png diff --git a/docs/reference/search/aggregations/reducer/images/single_0.2alpha.png b/docs/reference/images/reducers_movavg/single_0.2alpha.png similarity index 100% rename from docs/reference/search/aggregations/reducer/images/single_0.2alpha.png rename to docs/reference/images/reducers_movavg/single_0.2alpha.png diff --git a/docs/reference/search/aggregations/reducer/images/single_0.7alpha.png b/docs/reference/images/reducers_movavg/single_0.7alpha.png similarity index 100% rename from docs/reference/search/aggregations/reducer/images/single_0.7alpha.png rename to docs/reference/images/reducers_movavg/single_0.7alpha.png diff --git a/docs/reference/search/aggregations/reducer.asciidoc b/docs/reference/search/aggregations/reducer.asciidoc index d460fd5e450..11b0826e9eb 100644 --- a/docs/reference/search/aggregations/reducer.asciidoc +++ b/docs/reference/search/aggregations/reducer.asciidoc @@ -1,5 +1,5 @@ [[search-aggregations-reducer]] -include::reducer/derivative.asciidoc[] +include::reducer/derivative-aggregation.asciidoc[] include::reducer/max-bucket-aggregation.asciidoc[] -include::reducer/movavg-reducer.asciidoc[] +include::reducer/movavg-aggregation.asciidoc[] diff --git a/docs/reference/search/aggregations/reducer/movavg-reducer.asciidoc b/docs/reference/search/aggregations/reducer/movavg-aggregation.asciidoc similarity index 95% rename from docs/reference/search/aggregations/reducer/movavg-reducer.asciidoc rename to docs/reference/search/aggregations/reducer/movavg-aggregation.asciidoc index a01141f0fec..9b2f89ca43e 100644 --- a/docs/reference/search/aggregations/reducer/movavg-reducer.asciidoc +++ b/docs/reference/search/aggregations/reducer/movavg-aggregation.asciidoc @@ -132,14 +132,14 @@ track the data and only smooth out small scale fluctuations: [[movavg_10window]] .Moving average with window of size 10 -image::images/movavg_10window.png[] +image::images/reducers_movavg/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[] +image::images/reducers_movavg/movavg_100window.png[] ==== Linear @@ -166,7 +166,7 @@ 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[] +image::images/reducers_movavg/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, @@ -174,7 +174,7 @@ although typically less than the `simple` model: [[linear_100window]] .Linear moving average with window of size 100 -image::images/linear_100window.png[] +image::images/reducers_movavg/linear_100window.png[] ==== Single Exponential @@ -204,11 +204,11 @@ The default value of `alpha` is `0.5`, and the setting accepts any float from 0- [[single_0.2alpha]] .Single Exponential moving average with window of size 10, alpha = 0.2 -image::images/single_0.2alpha.png[] +image::images/reducers_movavg/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[] +image::images/reducers_movavg/single_0.7alpha.png[] ==== Double Exponential @@ -247,11 +247,11 @@ values emphasize short-term trends. This will become more apparently when you a [[double_0.2beta]] .Double Exponential moving average with window of size 100, alpha = 0.5, beta = 0.2 -image::images/double_0.2beta.png[] +image::images/reducers_movavg/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[] +image::images/reducers_movavg/double_0.7beta.png[] === Prediction @@ -279,7 +279,7 @@ 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[] +image::images/reducers_movavg/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 @@ -287,11 +287,11 @@ 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[] +image::images/reducers_movavg/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 +image::images/reducers_movavg/double_prediction_global.png[] \ No newline at end of file

DE?qi7@jtkci%XpNhO6BtS zY-=W>2xoCYiXZqqjKD{N6TEeF=HPFBtWsFUWP-}Zy|RMvu*i}?wZ=r}mU^ek|B&L^ zIYpu@SB|fekBbBRYeN^ex;wz8`%!^{eH`_vJpyzi<2p+dv0_57@-_ z6GRoK(*EYC^-OO^C0l0@jpm zca*#^e_ZZ?GHL9h<$Ow0U*teWl4o4xfe0v&?l=L{t~-MW0d+sMD$)$T1A zmkYQv$=CXYCGxcb1K*g6o!%PKnY8Bp3391Jy?`570g0R_hT51=Nx_ z+}wHM_q$rxB2D-O6E_upt~*IA<6f#sE#o3xi{zhWW=?WAHQ}P0+4Y;Fbr*F&^e(oZ z2ReS#JQ=#13w7^xVw+3Fw2pcWk#MtZtQOsU-T@(s)HvlQd1P7c5-;xp@W$D4UMXfvhEZT za-g4|euV7XyY)>Ado;S@0L#ncfQ4#rA2HisFa!^J)g!}%=*?sl+a*u|K~Aa#ot^Nn^Z1@@$p``0f4 zoc<%qNibATooIhHZw;i1TlQ+2w0ro<=b=HVDnGiN;?d)<#TY4l!5jpLj{>_Ltuk)a z&~14;bdXqHZ6Eo7GZB|s?LDr`>&kkIdbq`(`K|HzP*`H{(SP}ufD4t?1%zZ|6l-Bg z=|Gr#xc-L?*CHXTR@zmcOuNJ}DPIFrDkjBq79)-yO9B1_t42!3eKY|!8PL8`UXXrS zW>&*|B$9?!j1;5c7atdj;t1ces}^ez}qJvVeA(7k25r4S6GCd zT(jcGZ-krbL7H90XgG`0Y7vBIQ`Gc6Jy02G=8sC%r^Z1k*{5D;yxSvt4Z9i767Hx~ zhC8}g2d;x){JEqM6KqQx5^Bz}`}1Mdi}MPY(A_-hp3L1`KkD8TgiPqGN~tacb=+X>j>x}Ot?`pc(fkCWLrzm>~-M%6}JzKXVc3!bL0@27vb`mzrY zjanDNM9(k>)1Mc7k7r(w$0pZ0CiOPEKZ`c*uq7*gYlIeUF`q1^_Q!$&<~s_fWNT`F zb*zGAODI}-+M23Lc>*9TAxfG7qwNP)U>)I6+)_ziW+$qz>> zToIzOV4k;hAp`}-RY%=49v|Tm(aUIInnz5u5pTdiQtP|49mucJR-vXo#NDp#*j0y` znbBZn?54S$=`)JeqLoB$9|s>3xVm70Wr0xRZkWp!a@ zxy-%GWPEJc&+=>zDc&OLn;Np`uHl!sIAnR2>vk@ni>u9ZO!o=q9p2l4o36;H+fjU} zTT>$y+KfyC4+4)3gJS+_v0ka$%b>Bnk=V1HGpR!`!CqyklNb8Tf(@rCOb|Y8c!xLK z;u(2x@A0aVtov^tujGytHs8cDKo)=j(Tf!5u>-DpF*Bl9MiT5*N8(Vdo?q3aK@bK| zq2q$!kwR);*lqb^zg(n9nwv!pdGM*byD#uJOalBGgp(i>(DcKNCIya`l}<3J;#J&C ziW}1X<`B?*tQcr1S7UHmE|e=*YN^$=h(xV^I^py0ar@c_m3;Q>xiL3RXI^G}FzyX_ z5G0+>Wfn6tGu?%w4PXoqgb^Fxu!BveZ(m4Ea>da$b^NMENzqF*6_3?mh!)gt=ZwE( zi;-kHxrrZ|S_2`|d^899(?nY7T>v2RaMJkWb%+1@LnX88>_JQS5Eq*#w|i521J{~O(uc6*Z+O4-*raxI?czUM9f*AGu7ha{B=Td)~*P(BYd7r!ZE=Fo8S*0%dXt4V$S$Ub40}QX zWSBA%qWPc5mk_+FcZ@9fUkwIB>=edmJUvDf$$8({dYY-g=AWi6%m{g60R%#xx}I>K ziQzz#-EbM7_kqcpi4VTxDH-ezhNpkCyTym>J3ThQd^P;Q1LXpN+MpZYRC`K0)XFfs z;O6rXXDtn}CsK%U>|49Iut`c-iLsEYljEv~QsSbp|yW>kl1?cA91FIUkm>-Z-T5?GiHer0Nz?DPbcbhyYJT8-P-D6xI0$)(B# z4BheR(BQJ^-ZAC2vuhXf!wP%K9;{Zl!44#YMBq%DG3CZm*xB()XEp@i<5<}zQ7ZNl_0|K zLB9OT<`JUm?^H(j3k1Bzxt6K$>n(R!Qc;>qHnT~OR_A?=bS}xyl9&#x{gk~a1_-*~ z8$U2N^}mJlm7CdPO&LmfCxZvvH6{Zw_0T*Ks+i}}g~m?EPhQ z{DXrU>^2{U$f1?^+UCbuzm5YSo}L5i^Blf*QJ5bSrX#6>8wHvrtap7Mwao{&BeuMP zpL3o^VOOJQ8XeV1WO8Ym2ZGTINZJVs)FPvz`$aRcV@VgCYMQ>Ta4b5i>hfilO4slPXGrp^e@_YV^fi5Z-Ya;P4OK6X{~u$*d26;kM`5NW=F zf|Tx01ViIN`BEeF<9x4!Wp(p_b@Y6|Z0j|Fr25y`0r_Lr#q_-+?WP&Szi}SZu!A}| zlNY4J$!@Y3)J1j8UQNF?l>j|7aOzH(rfsq%_?r?zxv+t;GEMeDpf!T^+iXzT0lS@& z{nd84&UyZF%Oh|OO&EuwPNB%0K3?DKWu=0@6(%+A)=#w8VXHy^|91gM!#5QCl}k3h z3<{7lb@|(AC@DBkEsj1X5?oGO+pLpPa6{hN!h1A5Q$J!JY~Vbs87d0mhdm^ z=x8t}AfRDzp?krL(FgNi)pdDp(NG|l0li=c@m1&&cidW$^o%pM z!b+XkdKEu*R^`sp#HNZB6QPL@cOg-gWHBEta1#>f0FH-OTK1OJGOc~pAW6Iani%#q zpiIamZZ$HhEIQLGvX7WS-wxIxUTs7=V|iVR$$4sDGa)G3ZMV{h$-+&++YSpDTa7{A zkxbu78}xC*w%OlaHb0f$K&(^pnK?prvSnxZ;Pbf-RMEJZ^?6x~>7E53`VBK!z_7N~ z^3&*GS|YmxiRX6H(n?~QDJ#pgQmzltIO3JYn8BcJnDNeCg+_ zWHgguMeJDPYLmo!lg`PF(h9tk?K7Q3u(cuqSXpCP`SgfJIB!k_{7joS#}sgP<9yO( zfG7sgEQ_L_jp%91=}qWq<71&GgmV>jNMJwldGp&A@^1+*f+>W>X zFop_DJIBrnRILANo5GBB-C>I|u|SPh^m~l5@ek#hhYO=f=ZEJk!%D-o&HGPI>T9Y} zm-qT4HEMU`B)wKVVuVjD7nw4eM`r}}Xbnl}nY_Pe3jqK4JI%AfUu@ zc}TX^#-CJo!r(R42xpxD0KB5)#Ay&Sr})b?}% zf4{Go-VFCKL$xl$CPEyI~y;AM~*jb_YiIKPS>DCU@Y{VVGcTaoBVH;NUe*QwFTR zXy7m;J&K&k8LMh?12n#;2mYy93HS(F0mF#QRjSA-3W=?1r`mR7%R|B!1~_LN$|{A7 zj8Dmvf}@|J&%qt=A=Vd4RW=ragVh{FaeT->-xy^p+DpB5kDO;62W3gM-WjKhZ1?sm zcK39EOzs9vJA!HEODOuJdV72{I{?SBWq;9JGR zN{PRww*C!9Wl1Sp2m{mgx{GnV-9&pm*%=%-9Lj;dsR&gA^cUY0pJ0j~mSQ$TWE}Xy zyJs0KSfE@CP}?b)tEjb9gws83YOGn#UmuIY;y6rO3FnHKshFzpHYSU>GGM-Al^>r#PY-yW_9x{h|Co{00yT|Zw2 zQ}Ar(vu{Zj;Q`f0coeKczY2JyYtdcDJKEw0b9Db?u?4_g+uq_tyw2!>`GI5oZ1!1C}29G2Dj>()N1TJi-!1m6CCOI9e zw%XGuU?0$dWf4#{;Z9U8;okZDh6nb>n&z#4r||eNQ_=i}2NrZ7O-h`^6df?gv!vuH z!#-yqL45*4f-$)v1w9A~`;i}Ew3M?El>;pX?-jERU6z;5lEuh4fb-GJ@EXe|M(y88 z^0&6_VHD^m4s&J`-`oAl8aKFI+tAuF()4-2-l?6l;9Oej?Qb*WCymiYRB>PB?Qt!y ziqLY_!#aOVewPibA$;e>sTEDIN~Y`&&Z${7DW`5n0m=5Vmhu`sif@Mye?`o?XI8GlC{trlpR3_c63zm4?9bDL$JPOHL^ArCAuZjY*L6rMd4 zC==d|^kdJ6vNvl&{CK&C|CQmI8lHytMn~IS7rrv4C8!1q=)c8ZR~BF!>gYNGq8OMi zjt#MiRP50CIlv9*snH9{KMFhUx8#1=`Gy{b(hgCJZCQ$$^GNm(NNAptptFY-VbHo# z7fG8Ai@M~AV{N;XOi+1J&H>)+VVW(d!Jey zQ!AlUrc8CGQ;cFSRnfY8T~RxF2pbd7t!Z@}G%8O>&=>6WMy#uS@sj^8rpSQ`9BHvp zT@D29yZF7bIh%JLx=f4eq&c#@~ zj&aiu%efGu|8LjG?h)@u=H(Q#FlS&PHT_DWv`x}@OtP5q`{TZF;(R%@&}Fx988fz4 zZLTCsVZYV|DDOZ(4O#p2Lh*Q4*pU0!p7K=gd)9}oj}O*XJi3tU$u;=8?2%v3hJsNz z#9EWUIV$^MXdM$81&ZQ4-eym+OmMC-X{iCyKRp2#7yu_|m)^(2BbGjMa>*#-mmhOe z(P~2EoKWwWx%n$^r>+L$JGp&5wI}TPTpNRtP2Sm{uJ!~Cl7kkgkC5{_OmAaWlj!E% zG0Uz~F={jhLdHaZ6n1#GF&Qd%c4~D}c1)b7(Zgp;E(N(%8-r5sp}az3G9LfOEDl|_ z@*;)!dL~?Wv3Av;qQZq|?57SfPoSnlWNU8hocVHQ1*+-h5z~%Oq}hrXdeuGOfW#Pl z=;=}Cy@6KDHoCH5V_b6R(k#crZu#52)bY#a1{qfbdQT$=|H#K&%%XUelmRX{a6(TD z9op}8`qB{iBi+MLf%^Nm?=iPKv5^w@woP%>y%5ojX1cY1`h^*iS&>@+e5k$J>m^;?o^6!_3#8 zmll_{V3=yKGqLy^MvqBe!;TpTC(|N@${IIev;d`608;uk4K)9vidI@nobERes>cUp z-~&n7qCw&B6j!(w7hDoNiLAn^?(YL&ck|DB`OA;9=voCLK_Qt*-_~OaSecTvxT#YX z!ADz#`V8!_yjPc5u_qF3{;|Pz;IjzBBW0XvIufQ53`rHzZeuw9yq3if54sw$4V~PBVLa>4 zfsmKtb|@x%yQBwo%Ccl)w0^v-hss6&d4)JIF)XLsAPvy*aNz@654BkE)B3)%yxg5Q za(LW8UGRFKe#Z^0h(8O|z-XSl;7k8l2M4}P6^UIViLkJy7!}Pp`~kirFbF%bdy-%y z8o*qD!*S3sX$t4g_FKHHF5mUv=~RGHh{1MIpDau}8+tpke+Jj?a>>ju2(0rPrNuAA zvX0Q^XPa+qO*)K1-B#pYFr3$}B-#!A4h+cahLYadnzG$tB^i#wQaAn@2Rb76n$XCO z5M)>I`n%C6WPMaDokHB~G)$(}QsDm+$H5fiAeTbj0jUFuqak~pZZj;$I+jDX^*3$z zG&BCWq#a&XhsmVF)lgKCM2IB`tE7C))FFPC(+j9iva&VSk%p&em-p2f%FPFMr~VjO z9y!EyHyl^)YN-^VyR}*&y!*K*6-R=I-QJ|iN+=g}us7-s-ox5gEp4FK+|4JX zXYz;A()nxm#g+X#uk8fWOh2aQZds@Qh|()$b`tj)_`Ce&p(25DAqNY4gbQ9!fX`4h zx_xXp@BS$6OJZz=q`fH9VWe_PK-GVnU4a{6*k%JZD-2DM@J!ugn;VB;Lq}0UZcfoZ z*3UoiY$~}i^rXk-jDDWQ|KMnifngZ0P7>?iD)Q=U5p1h#_^Tb4WgZ(d-Hv~a5l%|9Yd~`=w8E~;5C6||$ zRMo+7%@5JD5f&nc`CL78hkfFE^|3^MyMS$M2&wX$V@-`!AZgQOMXiOgBJWrFq{bVF8gK@Hw%DRmG zTF!^|cG@WWE_kqgufEckwR!BTS{-^;)A zimBEk?vJxO<+`z}ojr+miVNi+!!Q+tT{HL~P?i=&BXoQ_5;HW`*+pQ_J9#Z}%7%^c zjhc!ew}*_IWTDvHrs@<(tVHu>oHqRzW`YTT`j9x3s`=iyI$?fTz38J={&?yre9t&& zFezB#)~pvvHJvW$Fbdu}=H#vX_IN^(nA9=5`BhjAif5*~l9OZQpAhr3J zBe{7!c0Ej`YXxe(1s{#t~-dTZA_CgSKaRxF#rxABAN8f9BNiN@>hXnK3B(_VE9 z+JoTRpeM6VQrOKb$0{QE5TICf;uMF~cDIiD$yVKc-a=!|oZ~}0-)X-_wyqf3p#Lu$ z!vR}I+Z)oWfc@H-N28paX*^lv#8hF&6N^Aw1wr}N<&7LvqqP=R%fdkTGhjQ5R1T*%&|#sQ-&w%|_sy@V1LtLFQ)^4`J9AF0%`Z_Pf&mNfLyeS!SU zYD!;?EjiBes!=y%nr$7$Jfm%7QWtKG9{(=1@~lC&ds}e;6cyv_FNT( zfVKl?oNW@T3gu8^(`{`knDPVJ-NOmk)qNu9$ga?&@pSSDfiU6U_GQ=)M# zk9b?O20DQFSuY4Mi)J~iP<8Nz#ROH{Dk3Vx3w~1q#}+WkBndDv%Z%5&NaP((r z8fi8su*aLVl)ubJKlMzTX>`R*;7Q(VPRtd^o$brbFL;j>p+ZTGo`q!mI)FvhC_Xi_{fEzu5;|#M+XKL!mzJgSF z7(Hir_&v~3;1b`@Cm%On2Xd(?t^wXSJ9au#v*2nSqWJt1UxK44-FQ#r6A`u`#-C>rCs+VIlOLD4?T#|5F$*CRZU(m_~{^9C}$w)S?s& zte6R7c^dVXQ4Z5bfY{#z{wX;n(YPAvyUhsGF~m8z`PiRf1uzHFg;xLXrG^ESQbO8xQld+h=9FsH$s9 zjS}+T#>+y%o!L%mVZX9$sAObc{DksxStl%l>oRM1Q#yVCBc|bI>mqB}>rjHM?0V_@ z%9+>RtM04FUUBi=)OpH}M2HCKnEUkjOLLw)A5C9>c|oCA4hZ9(t$X0<2wT(0#DM2rQ5eK9v*2W=#ju1T&>+8+9ip{nTk zf?EQk`ZReAk*l~nAgK#y_9l?^(@&O#FVeaAhMQ5cA)pU}E!`fjpLs3)|Miw zx_7jDlyef1=S-HDn(6;_?}5!EjDK2;utI;A;^W%Z>RO#pg7Nj_^ZX&y>8YrF9>niM z!z6Yh--r2Vo3h{Q*(kvO>iXgXgad2))uLO+c?0j8|&1eze`zK`gSIs$rXX6C+=^ z#0;OH*lc=7&U!iQvfb9%Lh>-sAb8;cDMKwaFP&>iQS;1{Z2uFS-jy@QPel0>_YNw1 zo(V_kYz#l2k2o6ethZWxjnyTI2^1A95Pll;%Kn`=30*z)@wh6>15Q2|fh_IvIB%YN zLj}ls!GM1d8_dA$_x&wCa}(O#;z`N1TCFSn(s+WZDc82w9gGyuOb3+kDlf0NsCv=R z@T!i7vJ^e9C-cuIZ+u0s3W7te-;s6c8U7aFy|%+VJi`XvKJpt?U5jA=25L)xNH@`F zWz9tS6dK>D7yRKb3A*{8Z4~j5yznXGJE{J!tafEaV;hjP_scj?yYNxcVbX?4B^P)x z$I!Y-04#Bx@pX2evY!;WwWfrx-2<<;9`VeTppPV}B~Qy;j<47HXf|PFt5^2kYBKx` zY&2VG=0L*3!q+zCDg_=CK)9><@5J^ET!(}{rwA4;=}mHg3NrTIq~6=O7eB>`1!(?+ zXFom`8*z3ANSwx5?%mmf>U=x43M zX#rWGw+PB0SDg~3K??6+?y1-*p2XoTo7g%Nd#OWG8?bv4z^{sXcJ%Mp>Gx^mB9v;0 zPVc4W7e@00QT`uH?cH2=CzkUHIBvg&E&x^)j^(s{7X8*+U=Pn-Ch`KOA!3USG{CT2 zqVlu(_;rF!*Io_LQ9#@&MU>fgoDv^@XtIWY8TRkrs&MXxo$y$8zA+O;YXtLwGf%6+ zbzKe zP*$Q(?|b_gAFUWjKP|66;iXU?xPr}n^T`X9bmYkZ#2mZT5JEC1|H6iAff>he$l+V2Q9PC;@#Mj63df1ZpmqnC_Kn+YH%IwEbbL>57-Zro?D;m^v3p5K^UIQ+374ooV$U*cM*eWGrxhSMEY z%st@)QM=+FJ9o)xnhkl#Id;D-c60I1#$<6is1ZAI@56k&S~1rVo6&3aONm;oe0I-(Yj5q zzB(yn+K#y>1IiJI%3100M3nb1<+}S`N3v(h8_h!2|{rrbuAxLRl)3e=$W8n4l zLYf)rv{ed?S9V=P5>F7Xm_-rju|FfISEk(qmX?n~m|waZ6S#bQ6|&NDBpiTrfHSKA zixHKX_j(S7>6%Hz_s2F5`#d6o+<3Wy$m?;al{r&Ww1s%%kC^^GYyv5h2-Vo>M#_E% z(pH?rNl6p3xwYuO-}OamEz#%jffl_HY<_#O*;y|6=}vNkU1A}KMX<7Axa0+r7*XW+ z)1ovbWpYo_&uteVE6HxAAQbUrwx#R!n3W@3pW-ac?ySA(ih(KM?Pt84a>b<(mnbON z9D^QB>_j{8k^Z~9&TFnX?R~*}6E3~eYeFhvd^c{!c*N6RVqsjN>Nr@#$i=SQLTpHmUgJ)-YGWIuANtAT_bGu-I|Q+K_T2L%KOG8(rYM*!2lkyPS^0F7MW ztJHJQFoqXa7l*(f=tAD&OX3&koY*X2*P8-MEBaueXenX?`|MHgL#i5^J2f^ADKvuz z#t9jiq%&o>#^#<7^TqdqA9Z_a|8Y8MV#nV%Tg`jX^&pyF|GB8A>A!_BM-~!}j`|yQ zM+UCv*1g`vnVxaq>ev_tFj0jlk|xgu8VAe?M=Jqk;A^8(d^I`HBpD_^0z=meIm3uV zGpAODHj!C13LGKlF%d7{)m6d%J5RG$Pyi*JprbNCcw!&!*=V-Ay)8sTTX+U)y_^Z?fKVJ~kBc?t{?>!D4(DbS}(2Xyg zwQ&s?Gs^S&=i=uWC=2}?fCLaN^uc6?*xh$Vetiw=on$6M6%9-no{!}9g=@R@_H1uJ zaXTFuSNJkWzc;hl%0dWmPKOxR%se*QH*RpC2+z_oAnV8yAPt$)N;1l+(6Ap%T3ni* zJ6eLX(5(-{Y*g#G{fk3!q;y=AiZgap@_07#6mL}E<~k${(5MpjSaF=kxp4rmhZh(Y z&C-?jq2uzF4;*`P6i6gVI@JbrzNil1EBdLs%VHMAF4{tmlh9r}%4UbKLY){ELdF>& zXCTNG2JC~%8kTREHo*NcekF7rViQ=^5Huf;H~;6I%@qK6U!)54ZQVk-)$o)>vd zWUzpLVEw|l&f!r^^?MN8!*unvJf!$WymN_-L{GW%sJd5NOTwBcWh{ z#6j@CQZhn3D?Rjl^|ML#z;)wzn2u=a874}UAgz(;Jo8;`}1r_<9 z_H}#O%|!~BQd#^_JlYDiLqxoFZOhInD2rxs$~Ktz&rcNc`T#=?96sWJ1^AyU0!k-9 zd@`P|v|9zER-FJ>E`j&SW$*{Lu}zS!*y!K0G~<6oL=Y@_FO*-4cQs1o(h!U-cj_4Z zil8`m1mAA@xN?OmmQX}$>6p`D7y*R-*ptZZDC|P?rM)! zQ_Sv>`%O@;pZG)x2gbd2I-q>#8xOs@ACW3r@{cRdG^=N`$+FS*jnCt*CMCLlH$Fgf z3-B(A2JCTbe_)DI4RY0JNleeT_5tmj<<@7PIT#P23qO@|uwp^-haYebJ&rdMxIGr{<3XQ!6 z1>gV#Wkm*P=Ev6rUv?o==f91R`;3l!A1yW|YR~i7-6XuJ;aNI!-7$W*s@q>wa=#5G z&Aw=b-beSIb%d9xBvxA zGz_@dK+~JPJJq@lr8Z7^ycM)=KW9!KjoARZ8AC?aVMYuW8U7MgcEVmJj+B@F^%#05 z*H}cxQhu5)6;}XcOV`|6FQ5@+2j7X?cHJF4r4QP-8`ePFMQ;V z{p-?0tncihg1i9JOt^4Dw8wC(dH%}?EGHciO~*@j9up_l)5kl?Z2A+F#oc(BSpg2q zT}U#)jCuuJ{WO53AG`*6+~V-B8hl2c{;xB=g{m83|LXQOFo}4hETn(TaAA*utqR|a zw$jg&bl-IwaaZ%2Wdq}*;|=VVd8rS9042_BeR>e$Ncg~|y<$O-LH>oFX@D+HD@=t# zEF2uDld0L=n_-yznhv^}fn`Qd3;X{@gfc|J{eqG-1gaHf9^u?Zxju-@Rk{L(_U3}B zG4WbbezY7lM5z?_RF>N=Y?x0wilemTe{L5u21c)$>%K9YhAZ^%E66(2KACi$x>~+i z|2%9ih$i!&^}n9J%x_RgTR#+s{jyQjqwJkeldGr`;s5(zo|u44bhwyM@!U@$9s1gU z^<8|#F;0SW!;1$hMq(~#J4~B8P_I@Nr7m+M4X-PX?ug`xH|q*^M`M#A-#)@BhtkJe z-Zy8S>)i1L9_bIr$%U+j6$9rB4B&sZ=~8R}9RUzs#R~M~0*RmREwOmsD0#QgT{d9_ zJ`P!2mN23rOR;CJhK9-+{lCsC}QChl>yCZWL47{LtL&XcVT$ zb_BNR;|S&>szVIl$V(~(UHcxrs{e6<7;&rUqcUR$WyK{7`gf%=Ar z=HbvK+nk%dsL?1%`c6ht>|Gfo6NB-t&?fu4h~<$fTQ>hL2KX$B;`>GO9IUXzLm={} z7~vB3=_APiZ!e1eh6WS`CR&Vv3Tna%YcL~{Zdnx!$$f6bd56c5K{#01N#-bRI{xE2_U?W6-=r8Z_@)Qa`hF|&rImH^=Zo>Mx2yn9r$_MT~X%t z1c~TrxEnHTHf#N%|4<6RzyWql)Ig!Gb;JwR;D@Rl+?$D{aa$D!wvtiRSSZippBHVbC4GYw$A6>}0TE$T zErG6#xUgpr;{6qt8i1y?sem1Q!{lmr}#)@o~aJ8LfnQIiyO=MHBI39A1N?m8B*qR$4_&6AzoX+<>< zPpP>DZBeS4 zMo#TGL4T1>W|z_-vK_cdWD z8se?u?AT=P%WKZIZ}9%ss?M_p!+V$ZJoOcQGU6)jP&XG?y#Ai{ue}&hAW!x;$vxWX zARoE27pKdOZYwIvOm+KT%Z~)uDLjEcd-K4M;y&ij#dKJh8__0|TOfqzk!yD69KIre zDk4W#?XF#|l<@CZBM0UNj^7U?*}f=mafQyFi$Zd{Xrw&;-27TWqlQ;?D*lz+<|N1= zhAqFjw2(g9ssL>W&)fF9D6KY0w@u02J6qWC71&=3n(C#+lYzS9r|U!1Ej}E8&pMih zOP0SxL0$!EbX(dJlTvX{_3?yP(gR}`;b;l zzFj^WEG;;D;4m^UP1I?nzw2zNcf-~eI+$CBfXyFGNu(AjzE73Z$2%E`-@ zxn%vne1qbl?mqfHb&y5g<*Yu7kM#Av9$@0I$s3K+yyvz9g=6kfOcd7F%Uv0pT~+#g z;DC~MJaz$YykGF*&Tk-^{sW;Z9C&Mxq<5pH=;7Z}0<2VuK9@SMZzn{h;)#m--JsTP zlr83OB`?_aX#xHpD7!ojEf7D2ZWrV|yxmLu|Ch+n0C!u&Rel+?vn2w&Z2u&gH)RL2{NN6yNTJMg(#*6!72rkN&PRo%7z-f~#mhAoYUNrnWQQ{7LMfd1s>0)h4W$r56W4@>Xp?lRoaYHYc zYsrov-Lv!gXDiELyXuCW;2ISmW`hJM#060Gx!DE$Y%A8Y@Sqi<2yhGFZ)(fR((73n zt`%vc+~h82bq{tte*FGnd`qA=eI2%1*-z>so6)fLZdT$K7~ zprLyu9R{6Ci1?nQA9YO_y?+&;UUpXY!u;(FT9i8|ybLJ32^721mMXb8r8YHmLp_Z& za9Ousye+a{tbY>O$Pnv{o{y{ISMyz4E#Y&E6uGJH%dCb2wM_Vk|m(HDroYPG=8AtOt*@wJ2 zGrr&TwzmP0HT=Qg?1SDZKOei@+4>8DN7W*wu7!V$^w)~XEBmIE{FeoT0v4bYuJlrM z{=`|&6Xv@sG2ssNgj6-w-Db4?%G~u#q*{0{&#g*qVl>Xz@L3wNe0Sp0>-iZf>hOQ; zy=7CJThKO&O9<}nE`z%zxI4iK?(PmDNN|VXPH=a3cMIb}~0^?j)Y-ofabieu2#e4^5v2PK=M5h=M@e^VVs(=@7=0H; zhsMnptI16i|LzHi)`MWwOmk1MwRfB9_11c+lCI8QY%R{d-d~T(b<(hT%2Sca;2~0P z^>@f9^>=1IqWt-D3{AH=x=b%J{uTu%_onpGwo1w4{-5Y6f4?2?^2S4BKCMM8{WinGv(SgM^ zC+<{)6ly=Oqxteg5xFBKn%QZ){=w(Uwb++*AWxj>{qy-Sx$Bq@}Zy+A*+&wzcr4mH$U)(&Xty2Ns?WG3QZpeb5F&qTXvfW{==P%zvzswr2@Pm+U|CP{Ih@i;fN&llC_CrfpsqRoidc7w;%~g>-g_*;(q%&_BFMel zn-@v&n1Y8354`!^ZyhuBwEhd72Ehu@RsqQ2nepYHXjm_FJ`P|d2NJ^nOR!%w#+A*G z+Xa9}4q1%=tp9Dl@}vKT2?ZPjvlHm3FD~q1VV{G@#T^?Ult!M%sJ4Fj#dCZQ1dWkG z^8i2D)ER|$is=PjeWH<>;PQ+_DD_4CHU_%ae7b(2l=^_IH~=tcm**-ti*ii@rPkYz zwIb#NZ}t%Jp>HB9{>QSjfmzqyA4dPTGU5~;WaaXGWyE##QTvSvdv{wCf$iIh&`4U( zur2=<>3tO|gz>X2dk?m7LQ}`VbV2882LEYJe&?zg_=vo+>G37In zK<(InZu~;w;|`p7ThQG3I&VFZUln?AB3Tr#xR~smO0TJ|>yII^#b#xQVSB4ZJYnpfw?m1rUspLz$3qbUaANp*aSG>`-D6effhLS8D13^_yc7ieD%|O?sr`0)MBCf4bQxL z{)mfKjrZ1g9J=HW74s|$EXS@LQk2lZ@);SWXI`fP#XD9 ztw&iV(%3JnKUH@>2cZNqmloW?1D ziO6+$2({-Te#rZt8w|tTWGQG0YZ^!^E#F=_(OJ&HS4$m~sC!CGm)niom4W z=-C=T@!~Cv1$w)tgi}jLgzj`V1;ihwC0(CitYoQE>zo=qQc6q|k^-qfp00=53=p0S ztM+vSMNnH!6PELrQ=NP`3jj300qwPnEB;hi{=uNR4DRAk*WG&Q*8DvKweVmM#Et@5w@XN=+}d zweCu?Qpq9Sk4iQS=GH`er<_QFmrgOjXh9zhNABArl=e0+-7tWL_P(arVD#s0vgL5M zjMUACfRcb&s3Y)sHC+;4;fSl~6y1L_!rYZF+JR)hUQ?9tA9IEN5xG(=cB`F_e*}hN zAhJ84nVOf`D%eOjPwh)8L+#lTqvcGiPyzY^>eQR`wVABUap2CtF&QFe;$RUD!?;oc zyfuxg`?#0_sP7^_&=D znZFmC-IM!y(_}g;KOdvX5pnl8=%?5wY3TP~Q2ClD2@+G9&r&^V4&E0+4!T;0O6Nz) z4%I8F>={(sb!{1zJ+Ept(u6$1wjzX{8px}d`Nl7RRGJE^gIARhdJ2x_+%n|zl+i)#;1h{4abD?W1}O}cwGA`H|nk$(D=Kj{xQbfck&t*!$J zd;(5ua(ZQ`P-;@gl$T1gGU}dnUGPy|ny+1x%%DTZYEj(52(K+wk` z4qEK5rNShAX~NV>;hkHX)PEW_Tz zO&|W70?ccc#af1+zga5^F?6@2`=l-&O7P(ROW_#I;a{unqkTUR>axQ*S{Niy^h6}P>$OTpCgTe=_a z=v%afVZ09D7e68M*s_rdXaZ_YH0;af_~HvdLn;AOgZ@#&Lc;%?4p_@^mA-J&L|m#Q zp)kzK?aqR?l{*fmpg&Rud-Ol!cmR%XsofAaZc?dZECa4Vdn|MA6&VC_Va(VWjzJt6 zo`NXY-ot@N;&b=ttb3du1!2=(3>Lip<~z9n$o_9hq#F7aBp!zgrtF5*T?}0fkq#L+ zl`(g~|B|@33`)H|!_T^fgJ-a7kL?W10Y{brTxz>4)HPFZiK9 zLf~SMNHeVB^C;oC_efcP>aHoH*)Y~oe7uc9$_))uC<^Kyu237VhJ{iqBP;=F1{)8O zbDuH2tZ zrLp1=Ov6_JW3xJ>C)TcF+4tl}dLk7IiF_lQKDp9XDSAI}qhVK!TfGBU#9-qSGcUhq z?xM3k_IxWx$9J{^2^~&W&~;~pZpN+r6`gkF0hlDS@ChE@g;pLUO#Avk#XR4Fp5Wek za6ka-``Dy+u&)>VI3o1nNYtDW?Pck?8oH9v8D9EMKR7Vh*yxrD8tH0a@|4Jm$C_{& zQ^1L|!a^03&O{D+{rnbv#Zd6K;#+&GmqoNVRK!;Qp<5WGP+|!HxCmjMK(v^~M?7h?M{UYO=dt>vBJdO|62b~Of$ran%l zYE*B=tSlD?ei>8o36lQV>a4=dBCaLBqRJBvn-zv@!n9f(4)_7#i33*_m;C$@9wdB=VCbB*)P9vFw}%H@?{7Rvzx?t z!+)naF^_BvJB{SLE(5(>{RJDrdRJc)BR=jG<2_{Gc5GU==saE3vhk@et;ix?+RR0m zH_%r?8o2H2FT3y>>@~w}jV1Lc1ZTl=>L{yJy0!A*$5Pu&RN{$3blpcJjWAU;6c$7q zM?NwUBELBBNxL@3`-jfMt;l#i8LZf}wQXs4y($kP4GCE?0chZ@RPYWbdss4B+m6>a z(fI>8Y4YW&A2-5e)l$^0@B30Axo>_L5r5RFzI1fIXL5?B5?{!S4X8wn!W>qi?8Y>Q zw|e;H##`DktOF>;XnS44p{HLtaon5gx;C%eRDM;7E+rT-6EC}&+{-TsjanZ~VD5WF z4;vY8K;+@(RT0of25#~gWNx23k=Do*RT;{T(J9R2sVpB{@`Vd~0Zw zq~A*AI3`rfw6+aH2(Ft(O7C|uDs6b$k9cQKxn?!0eZ2RhHdeeiUe z9v4}EaT@mIRkIJUU3T*kzdxr4?g!CN(6w!iMuO3Ipucm%&_n@dg=unOpucTXz z=Xz1VnyDKc`OoWbM}f^(G$}I}KD_mzk!veFlf0C?R6k28K!buv&CoR*sq^>b$>}uW zK??!pTkW-)6}p)5OVjCuyi^Qh6z(6>9%UqqFjr@aytER|mHNMI3K>qOTEy%>c+l#wx zkqR9abtUf=VW~YgL#4RY7rP#@GLj`qjb8IL#K88XgkgaSjFZ{_>ZWscedAaDLx*gIefUqaeB_-UiP$U$Lxd5^^BHl{H9snj?BHB}^7ST} zyh3ll72|#lu^z%!chd@`P*yKm0k;ECwm0b7?9&byYbM+DFEEwH6jcO*-P zVfxpt%}|il8NNJFh}KzR_G%yD!Z?xO5OE`2EH55kL}{yLEf~)(j@194mi}XL6b{YiA)he*3WEiAl=?#($?c z+tELYaB-M}*`!DUyg^Vb3@*;RziT*P-lh}3^)#`R*YLkCnWp4qfI}%QpJS*biSm^Q#s;;f+<$r@}sLRA|mZZWp z+n{RkHX3O8pjRWH4>`3XOcX8aDkEBS9>{1ftiO~_f| z{*TXGe+sj6$5TIHZ>8(G7#W*7gPQPN`ACsyc>NlHEN$}p&jR8 zPT_vlAmk`XQAXQSJE-Pzox?I4jb=pErUJiP;Utch*GFP6iQ-|mxeLosGC|fqB^7i( z@&n9?a1;(Mpl^m!FUwb5RL)7K&d{ zwY)sp5UC2t2N9XuHrEPBP#5^3R#Iy7<|C%00;tE1S9%ImnvnSa?c>!5fC0_6BvnoeIn0rGi2zCw z&2k5U50s@8xwIt!YxCzDX<^LJpLi-syN}*hs!*m9O&eu-Dl}&~KTV9-XQt@vNLDE@ znk!#aT8d8e1Z_?x(cVXl?=1{X#XFw3pJ3n4Qt;OLPDc^qbXLM>$!H(7{wWJ*azVI5 zWkXg5+3(FrmRPyfIbw5p06X0@iDpQgB7qPNR8Xi-8{YV1dYXw5SEJTctP5$n%;GPW z!%bF8vagy|W6VL|5f-xnY56Hvz!s>Lc|z;YxGSXjtq3gR-OPp}nCitDF0Mu=;BYN5 zNOIZ|Gh0Fq=fxb`L=ABM5?h%o3Q@y5mX)5~QsT+ZKRqF@*)K>%sxH9LpM=xex~| zAVoCuc7=QFl3?a73hYkwTbh|Mz3sM48KT?zSx(2c(?=C?hXfSM4-f1l8ngyKmmZxMf6e+3sS$mWpX%np_b?siXG?;nHwI6@4dv)>x%B z(5%#v2Wjh}Q^LikRP(e;EY7iT{qyXDw+}IC2LGkeZQa1gW7~ znlS|KXV>nrYFC+8w$}TjJU4~r*k2cijZ7np`Z<5Ku8B)&x2u9P~!}H^!)xK3*L% z)uF$BAIx1N7wQbK7;zjaEGt4N1jv)!5m<^WPXL6*{qt}T2N<#&c%^t;>tWzEcklC~ zE$)|S@(q;UvE-o)2GgtJEzi5G=Br3b;GI%?(3FrG8Mkj7k>7C3#>i;`7t&F(N~e?a z=4b2Z#tn>`0vgPQFGow?uAUiuX@o$lD@r(ABJdm#!C=AB?)sq%q78>4CR*DBE}HRZ zWn)oeWm}*GMl?>UZW6o8D$+tojLZ`5b$Bv|8U8^?uwa3b=RyDgaat~umyf$MT%JaB zw>~y~Lyf3jl`j6n*R6{}^g-YjYHFyP7)x+v=;87#DY7NlwWaC|h|h0Er?xEy%Y2@4 zHnexeJDa6zbq=X<$J{zFTy}Jbj}=LEfcu71ZVWf-9y(QQWg!sX~JIgI+Dsjx+Q% z_(X&bpY0a7Hyqp)^*#pN*>JH)?=n%qkH4?ZN5s3{RTE7^>o(<64X5I^c}h6w z>pXoT>bjm$c!N3qiyK^;AyeLv-+NeT=T9ZG&&u`MarR;($j;qay3 z9VWS^kkDH*6T&2L$7>E5k+;>04CKB$6Bsb7uR!v3^-nxds+;k0X3>Gzx(uaMa60JKT$(%%JppzCwNwjhp3|s% zsiO6mGpe?_G)Bq))6Zxqszz+ZKk?m!rWQ)7d#FbMQv@2ra~GAQnNM;Ac5E@l;t@UW zB8!-QL^~8`(q#vI?y16SCChP|OA`2msh;SiBPeT28uz&^AG9(Muiu4~*D)e-3yXs$ zbtRHn9gEkJNPUQb8~eTe#5U7LSt0RP$P+R_zi%rWu9CI|vZ?KTU25L27)akYyBm+T zA^!uh`HqfmAEu=`%lg@)JgA^dgxHZNBOG`HS%=YVFEzj_Vz2b0wVVz;#VfSA|Kb?7lIe^|MeST{8P{VL?OVh4_4=o># zZ?M(3WNhM&)l!;UjiKe9P&;upeu#8vZv3l6yw7i;2I5p^Xfl?_Y$Zn20w=5Wlg(D7 z(P(I8L7`NEmVI5Ao0%!c(_K<#lat26Mj%Lg!w+>24g6tw!~X<5*6SnefB~}>?kGps zJ&_b9)Qt1=R$jOD)xU{}M*PIFD)P~fy9YL5q1&uh)vbP!h|DV9qUmhQeA|-s!D8{3 z@Ow`|srX|`%G4v5`>C~v2(?AWo%~Rf;}@zn=E`uH+D^C{7g#2e#P`3Ax2{-y-?5w@ zTpT8l5Klmp&yF+IyLZCi<4A;4S%Y~BMXzS*w^?GfyF2{sYY9htwy+y-m@@5l^=5M> z-I3ii)$#`TwD1L@6W&$`t%uxN!ZL@+T-ZP#g(oyQw>fq-Mxx0i|0$(B5n^=Lpuidn05tfO{3sAoA zZnL!FF;Gmd+aW|D0!7)cgE$7S>A=d6qwHTjynh8AK)rKneMi+*srz08}+DO;klc~ z9I73ueOS8HpE=)EI5XDIaZd!cV0B+sVjts~9a0i@r*^BCgpcUwg8FiN@K|m3PBig% zMZrEO{^@(ShLj955gnwiLauRppx5LwK7V%@ayCwx?_yXvk^&*_3(>^t*GwAZ8aF5H+ZM<5jafYX`iCr`m7J2EIH=bi#->|Nxn6Kqi;^w6ssb+y(I*!QJqDyO}70%17 zif&8!IzCT^t3?<)WLCT6uyzD{r^@wz^vH7#|7Wcj~x9d|v}66dGOZ z+X4-WVUhhSkCS*yW?BmOsb2JE2dgSwKe+!1f}!4-zsL)IZCmZdsFlwsgIAGox3S<) zmCOPS;v_@XiIRnO%-Ue5(h!qkuC{s=z$`cJ0+w>MA4XlFLtBmJ5?3hIhvlBh^y*Li zH&Zv+NwEEhB+N0cguIx%<_KtmZaIvVkYL2y7zGG4PsXZ0gee`8wdVljC((fLs z6eo~;{!eTHn`bRz5K18_T2&$Qj1rs?TOd~HB9LzLG-o6A**I7OcwGfqZgS!MD7C@63zh&)Hm3T4OUWU`Ua_HMDm~GVS7Je)b;2_g81&&*9 zSx+78#g^-bhlW`K9Qt}@_SdE+jbtP>? zskq_wg?ZX7B;~4*LMcCGHM#^3Sx7gw+F2iY_$P~Uo}nhS`G#^?>sIHlxSJ&{T5^$d zFS0G(MdG1vp4^FbcD`4a5yp+}nyD&4E06W?c05vfzhiK>HPrqF6DrC<=*{mFtZb|;GgBnY70rS0`tLoJXe zZaO4fID(#VE@|RE0B`-AZYf=_z+7k&bM@G%OIw`r(}M)sO!H*iY(>dd)Sgnk1eg3^ z)no7`;t}whv%B)luAn?Do$6B;_k-=l<;v+9vXjlr=Ca0%OA#}TVtR+wI-^OXvfDqL zPZQiiZCa^5UH%--b}2g0QaoReKl3w7sd&r^H9;0=i2`6acbRp}6{xzeuEIcXi3jQP z8^U2)JxqTw!(DH*(eKFX-GP+GUFV0mOPTR(Hg)SIDgGdPcJ@)ti6QMDo6~jj`nDrd z+ZinHH48e57{E=e>g1iNLc~qkGZ1xE2T{5Ly>Inuta`O}y2e1*NAi2Cbl1H&lBJN( zeRHj}dWSSJ%zB-1uP)PA>s|XA48XjL?{NU=YuiJYC_}^nyx&_+cdjB*g0O*5i4ufH zo(Ruze=`MiVNq{k)maF%H>%z3d&<4tBcO;@;b$TWK0bEssK!TI=w5vrY$CaV{6pN` zkh`}r%I2B!iN~seU(E5U4R1Yv&~9Pjv3Qq^uzWsuloa*Uk<;(` zyl)b!^fgoe?XRBew(3`!exrQw_C8V*0S8V-4}%WY?XVYY=d`&*s4d1l1RGflpj8(H zV}N*8ysJcgM&?ALerc=QdirxdL}rPPM`<-0end%B5zV zAg)h=%%eA7gVE zWzmgW>liOW#CFb#vYAE?2*n6*Vi<-`1R>{7cglYdS;|!4kF1plvJ{&rn*%NM2Q(upy5%7U3ZIjPZto@@is15ayQC>(o?b zeV@sQr%7Lr=;fpq63*Lj0}H0nOv(2BvQ>!9P2e$wPNkLy?Lg0q&EoZPtH4RBJnOpc z84W+_L3mhE9H>D)r0eq`hJ9=eDNI((lMs=wDMD+{o1*k;() zp2fe&?X4Mm++m2EOU1>Y>GsaKiHx_9&9Gq_?2jkcz@txdIF^5kGTU>|S>y<0un_tk z1T)T9+jb-qhjGYUn6)i~uW8mjG8|y%h!(`~rwy+S0}-GN)8`ts?THs3p!MG+MJQnZ9T7{9(BJFtVS&m@! z#rQ+^^->*J>C_miE(nRrb;Bof^AXNXXn6_Izc54;x^_2JU7ZuS;%H0{3@y}3G2OeF zOXHWHYEU5iLnF3uc}rUqJ4w|;$V-9i1_MXDR)}M=ncsH0H|p*Aan^p zr*MP{GfCg}2RXFdOMBV)>r&7ck1g=c5#rWLGpTTEd;7-kXZ{0;2|ty+I!Y-@P#}pWVBQuNYzcv`3~9QPX}GCR@9|nD zj49yg@<$ouZ;$;JhBEJ|Q!6$6;MpNugq)hjn8_v=t^QPPA*_OvPF#~((!CvD9f3Rm z^h}loJkQ{|HOCO0szpK6B1@f}Sx%Sf$q+y?(sT(qn+%}j;BnHO+IpeaDZzRQypOKo zEneEVB*3|gQHcYO%R~%DxQt3!io1@3NR={ehiWw zuIgexC1TId#_{Ir0&gVoIa8&{_lHkSk-5(!>pZ0iz0%8>92;kQ3(5V+9Je*P=?AA~ zzW#|@+M7N}PnJ8l`Qa6O+cWUR6>t=nutR${vABeyX`}OP7_pGeMH80;)_XCbD4(G_R za#u5aN2>>{X8+>R-gG`5-0muanU?Rci`)%E_B06xV>PMlwMY?>cwr(rMl;d($w=j@ z9U|(R;sL%yqsa{mr=t&)c)KMI7nAMX-8aOi{YT~9woUB(*>=>Vgryk06z@`Nf7jEq z9HH3fvQ9PPcchtq18Y5L)Q0PLlXUGMw-9=eKA3H4j9~a>vz1wZLu08sMYY%P8T=M0 zOmCr&`ODF9cj~kvl)Lyw>vSQXqiUaO{BgxTMsN-(tlTg1dx1JaH0Us=#pJS7=pwy~ z6M;)6AvpcXZ&qi#-~QIqiXObjH61=M_ocvoqaqGQ_~x_D+e2X$1rR+a5}A`8R(a{A zg_v;xQ(W7B6Pc5@$>j#kO&LjgHdMo2O{aD{xZ$?--bU%PCA=%)?j5o4OU8Fv*Q_Cx zNqFd*M6&NgSPp|NS}%sj>&$A^lbO_)E+cJF*fx()O7x zdqtV*!A2<#hb_*j8+pVF8hQk;Sl#9D7BwOH>W)SdxHE&KcfyRT+De&uI-FLi{Ae0H zn}j~ZQ3ShbToc%egwDV;fE)h9!7XN@lXA3rS5%N z5@g!bL#zppW;DK+>BT>)kj|8b8gZEEu1uS|qBaEJH6fOIblDstuA7)5Ii&E2XW5Bl zM9JtqEZwJzfE{vJ=@YI2;s=PaGpCC6kDqZQ2UGTJR_tqBo+ZkfTVJW}yFP8XU{D z*Xs=rSOg-c_cs>R;CI$~rhuU9u2~C6%Dwy0dKtk||Kd5?**NLJUiHCctGoSFj4k|r zRZ;S=TDPG5n|WD!r>*Uw^VH%5M2+9&C7VNiFmCt>Mk z$=|MZuL+JQ+4R}oMcDBMBY$zz&TbU`Ay5^F8P6G$&ECcsue8-?+A6XrK03fuB5HE+ zZoc!^Bo1*G4lAlU29m$?T`I%SPI(Wl49rbj_YXaeblSZ077qC!GIP# zi|JdHO?HDof^D%;c+=vuP7XqP9d~l3c=obe^3^WcM|D{?YYa8c8$>#q{EvsiNswmt7txAHMICA=Sc)(JXaCVmId z$H!TL^5lW%gL_wRzp^ju>8}^bL@A*+8>0?$XOj`Szxg|l!15R8h4^#;QW!VoQ$y{Z zWq6q8}+WhmrCtKN$ZeOm70eS zms88U5gtIQ6A$ZxuXNdfZ}`GB$D|d)vrCX@xS6A#>mt3xZgDlSZMBH2xzP z9fh6PTuOx<{|Kn-NiuiS%{Ls0Q%hUZm`1oH@vIAp{~o^GDfWSWxlAfz-Kx%xO%85U z!K}6FVOx2K*TDJ1vOVK>0eIJy_dnlqq|D7DT6z(#k5q{i7RzG9IlC745I@m8O0xoi z0eZW}mfG_`=avciMeVZ7|2d*Bgchv)h0{(a=?DMPA&#YR3WtgtTeW*b9=kV4uWzZ! zL`8(yK3N!@mj7gV;HW)mk=3x4piTApmq|dB2Le%^VMg;w^8I}HVnm~Z9u6iWCpXH~ zVS2ESc~gRTEp8Yl7DJ#ro=r81Jng2ARRy`cA3E_4B0(?&pxE%K8b0{5lGqUjYNxSd zA}x8uJf}l0Z!axJR0J)L>04h_bpzF8csDF=Q9kDXZ$SY_g$5ZAK_bFoA7Aw z9)*VrX>>A6SE57TxYLy`CoxHymbfv>j=g7De?KNs=aFO01=sfv-rfQ6^P{O`ZoTCke(yv72rj1)J;V!MO_!rFaTuFUGY z4ov^Z-Zk9!a&uXQmy4schGO2=7JHKpW9=ntaR=KVkS)B=;J8F8>FJz$!_yU^7h{yK>gKBnZmhNbmm7*Ccjk z1ooM%RSBq+o6mLeNleBUc)22XJ=$08;gDDT#3Ka##t3n&Du2&6cr|7t70NTxQrZq6=6D10TXgn03=sP&|Rp{5ekR0Vyg9142&l*vXx zbzt^=2c@gev{W98rn$ge zMUy%IsFdxvd_*1aFEGwA!A^?q7k_wUwQbX>+VB=!G^SQ#LV{9|^#x|roe{Ac>ITr) z5C*CrRhn%{SPeK#)_GN^XvIU*0)_mQK~ z^~Y8h@tDDXJcoGZ6SC>I-ua(Yyw|2QU=DMev-7K6nn!RV(P{o}Uf{Mn`vk}Q_OKMc zFP`?F9f?ffyh-i&IX&Seu&)fMYT(V6Yx7i>)kS3cSPHWiEA}iaAz1#if64Q}2udSS z+xZv0@V{~+n{a|nvP9wj+LD~C@w*!L^EmA#`?|3a{*Uf%Yvcl_n8L<(b1c?>bkf%c zwLwEgs&~FUC8?J5AAjP9Ks-S_RvMQ}oPE8@4M9TE(jC(7G}-cD=E&A#A@sX6-0SWQ zYpGcO(`!L!RMYvaF?>4O>VN&)f3E4>VED14W^OJS8%qBJ@Bj04T?%MM9u+MVRR33B z|Ftpy_f!9uhyVY_2AG$`-?{S)4EMi{m-jlJqa*a z&}8GF{U4kApBk=20t4IA`Q4I_dj7wL<>P~nwC`zxibHkPKgRTbUngE*fF`C%w^sfC z@U#Ev?dexvjJ4CLsS?Wn%)@_VI4Zq=p;>qJ;{Sftzb;Q3Cdr37t64eW#u5@#3VW_} zsOJ)1*T7`V4dPBp^lL6QyRx3oE~V>Pae3v-ggV5n)E7m$YSQi^q2i1FR^w$Jk~urs zrNXh^jMcw^na!fv6}UF#f5Y&hBlX8S{W-M=LVpS6XkR6e;xZU?S4CEO zPU}S9fpwt-X8AM>A_Jbb$kj}6BA70$Ke_Z=hNo2>m%6@9JE^r_abA`M5wUS0@Cjf4 zc+uTVi{vo4O5frnT*fqb6k%8WvkBwisu@Y@&pB5P4Z?g59|%VWv+ zI6T8ZVpr92)deN7`;oX*&NcMr*75DHydRs5ckouLsGgy;!_etj2Gp@u9UiSSVP{s3 zSjF8rEFO1UK-V9`+`fiYX!;d_^zF+Nc9er=ae<&B7PU%8HvXk1#I@9hu)j5iJ&o+q zflYbNhX^lPGM%6;&2*@`C)~lPiC?4GNpz0hSOrg3$JtiM6n^Sqfo#VMWU%5b1Nh6=`?w2?Gofkxs*#Lc)i?kt}^ev(kuw zFR)5_91u#HJL9e^(+c3#)1&441yC*TIjc*|&hA8yaHleQb)zl&Tb8xB=|0MN8E4Zu z2~2hnyZrCu`ocWUI%l@ANPi8po9ui#288!MQxbG~wK9C~j)8ArU>dqa(0>p+)4vIz znHdxs-xFE@ZK)+j%#VAjG+kv>-nQ({V8;O6W8du74%lb$V!)6N|I&FY?WsxvPXV~~ zn16$B_cB4Gu}Gd@wSR>3bkF~^6JI+DC}K&!Y_$U^FCG1|Ucb8;Al8>j<^uPcJkJ|B zWRS77RWdB@W6Gb7hx&61S5;4Lh5phB5HOzB>gmd+Y7+n5w6ELMQH#@_y}YXc@`k`| z_3%|8etcXjq$C+id4}Djq^!B!2P`@&J&<~KwMXk{>L$f4(v2f|yVa~zT=EhqK*Om~2j}?kz z?6_x16X2{S@U6}PQkAeh^ccu!+~y!M*cyfEV`Zq@5;{?Yle z-g_(%#q)ZDJ@u1hXIKB3SPNCB6#w5Kce%UB7TleazhQT)_YjRYv!k7}X+4DOXjvQX z>P9Lz~2%dOoZo4l|PjZ?OY9@rrbaU~!Cuzv;UW-pUK|^_oBpQ#< zcb#I$>Y_=`mQ1`YW@*IKl(mcdyzp{)PcuAw%DpEKQB1L12P+k*P5x;mNw;{)k6pB& z;;Af{IjD~Yivyewmudx06LX8W6{7HhUwvgFR?tqPfR#OSsd3VdDZ=p)>E4OBr}LAn zj?pRH0f8t)ixD(lT3v%_3f_=HVvKEkY6$7t2G8lA!j-#=(in+B38U^%K4Wyk!WAa% z&gqx*f9+xF{&<|{z=9)jeaF*rwPZJ@Wam(bPooy=Scg8hl@!R8mJuVO`xCtuRN(2A zgRYTqd0D8awn@-oUQ_Bd^GDOONXT4+m3kE1@Q$0UO4u1Vz$L_*3IX84VExW{4!DW(Bs<9))-b9FIq>BjhcLs8i#wV6V~Y_=|GD@lU(qD7G>?gA0Vj4{7!^1&=DP7 zZW2=kahb>03U_yryopCe+baxA$mYBWM;;pV>B%v;-1)*KA==5Zz&gZ2H>guDQS%Er7&G{5i7iS$hT&%3^j<_YIzI zPVE}8l2~&L0vdXu7~3Co>Wg*@14YYZ&Uw!d1wkE(SBLi4T}`rtj^ppV4>=e2mnWqJ z7gU;-)1IL=aiHiFS}XCt-WKvG@foc^(+M&6Vj^QWwt5q5^w^IQtvcGI(ErG#2CmY~ z?LSqz)=+6=w1tD2ZtB)02|Qke{Qxtp%I!9PIdZAX~1h(2_xl)h5f5rT=gP(oTd=4mM4RZyf}S zAGr|6bLMUxV8m^ocr6OwSa61vc!+1b-Yjyj8cS#PGQ2s_Krf5(`9GJexe~xLp&}W@ zP9mS45<(a#_i2TFrDxWCn%Dt<=(VD)=x4_eNLG(4>B~j-IV%Ch^XAnY zuK~lC2@XTQ!7<2O%|QqiQt@+Rg#3}C0 zYv&ZwFYNJA6shl5o+|l3T@-!Hr4~=D?_fzWWH_8l11+O6HY9`B-?3$iBYraF3(ON4Kv` zr&2}>K^o zHNFLdFa^#)!*{x1%FOZ4u*sCKG0vSFU?T>)>9X~bQ~lTYuB)SJ4*~bbEs$m` z7~eZhR~}csS9aj&t8Mxn#j8TN-jfxzft)R>GT@vzJ@3}m|BR?1u3LXAW7_g46pCW# zUYxZMR9k~##dFB85h@)?0xflO$XOz&r_P`&Ts+b}nCRMbW)M z(>;)?p7GHdHH^>eCQEW%-Cu{t4;26E_7BzRSb4d|EUv3T?IB)Xq`j=%9QvlOj>&w9 zj)%M2uM3{h8~4RuK?Rp+$eiy{7&3&s;o-$#Iq_QrL5B=B!Q)@SEq=C~cBRL*EOPs| zZReDy{vD1v^hd5Er|l&$+i@3yPJi=MJq=DBuOr;;IhpSQ^SK$t@rZmCkQOho=KYe` zv-)2Fi4<%Ii#ZublXuq7mKXBdY0W<~d)gZA7fd*L8KH<3!e7XqxX!K}ymD|dY(#N! zHC@ig`M`vli+wlcOxWiXNAodsTrNmZ28mh9iCklc6-C1a_P zbe}iqJq!6;hPL+WZOZ!P+WD}$MIi=waOE+V*IvKW-t3R)d1+7SqV}iis)Tn>PxpPl zJ!jf1Fyhi#BI&L|?U9q9J-eGr97`h0L}gqJ9`J{#$zu9V>yksFN8^2A$o<2@+^$+N zkYpIM2U$XO`}nW_5$T!Oi=Bd{OlJ~{8sXFen)dm`Z`W6|9jY(x{f}BL^)$E!1nRyX zBt{K8*VGJ?w9DH(E*+3`ZZ4EDhB})r+eYW-i{;3^U$8KdLm3OMhfoc@2GvVeT@l&` zu!4uVJlF!B%G7Q!4s^e8G7?X)b_~x zxKzAv#SapA4}gJ0RE)&h^@r5sG5F=qt&Q%MCSWCq%C6$##)u`G^7^ZO&66jt)a?l$ zTg|r3{;Zwn<-SrC9y4cp!Eb9A>>5IkweBTZlON}Z7^sQa}i*sAIi+0S* z(oSUehX1Fw>kMjYYu5;(f`Ig9B+|PQlwLv;r1yI07=Z|c-ULGL5D-M93P_QzNGJ5( zQAp$nN^b!nQUZo@<56esoO^$LGvA*5Z_k>&)|&mU_gT;L>~}T86Y&OP64kphBuO37 z3S7ESt(tH7b8em&lA%M%28k_7UvKOh*;2xt1@O?i@>VzvU-j*yS^n4d+k;-wg*T2W{FgJL#dNm@rE_`F%- zF}@=Sn1qSS$`Y7M!PO3$%!88-fueY4SsO^IA4MGn9L4tqU8M01{}TkU7d2ncObekhuMx0oR&(y^(8DoUpzB+&@y|kX=5( z-Z37@*4Mh%iuT>~sVB8VA@T?0_Scsd{=#5m&^k`*c->3{nYMiIR)cUefHvyVnYXUA zIELt@&9j6JF8#py$W*uQw}3M@unxr}HXXDz)l99g+bBYwOeH?HVbYH6Rz{S>7nRie ziVWhQjmm!-mwry?=u{qUvcm z-8<#{J)0_U)`xqdN=8Azf|8hMYxYmR$i|WbR&S!7wp?B=YVxPIsoFctL+(O-n+9k6D6DBK+D3=n^sUu+SczjwBWB33k; zX|Bj0TghU=N@>(wZ^BEvHa^xM&J2W?`p9O-j@xz6Hw~Lx;nTr{>LGM$K5_#?vhFtu zWah!+*IG2d66}RQ+S1j<9qo#}EgtRymN#`w;uzCbLz#fB^>fHMpHMsIaz^!;;JKlX zw<4LpfJ2#A9rW&If1J$DdiRwv75luvFPk_Im%#n?c!7j7%g^}c{&jr}UaH7yrU8W9 z^V>h4JV2@xW2betvS?WGHp_{-u20By3N2KZjbSh3A+1&+9&CqcFB}{FFC@|8WsWe4 zT11nw51-c4KK&9tb)ggQ*_PH77?p>%teM=1x9{5Z=wXqnFL_+LB=3_#Kcna`F zFT{|B#KRw)7_OT*$-qTD@?jBX;==YGzA$s&YRx3G(lPcV<;9#DpP1l;pxG)VY7XVU z8|`FQF`Ge#C`MN8sV z*jRG0b+j{myNK@*q$buxmX2brYBsRtuJO~Ep~0+-AI3*s)#-9r@1&U!wQsu>YbQ^k zrE8$G1Ffs`U_UL*m2zhh&exlGP1EPNWWiAi=`=$69e+LX?l+JBhh`&aZ_+uTn9ND4 zyUC>gn=t#s%KH(t#lt(Wj9>ocKSbM4&T9ce2oWSjPTy}$=?^nTXzBlgASdpAf9(8k zZj7ML5wv3TNT)gA--KF9=zsN9-fF}W6)IN)q6k?F% zfxy$OVt*vxt@6?=lZgYL;j)isvN0Y=FK)Yk%oKzXPZkrzWPWEpihueZYD3%P?S4}{ zN1_0(#~O{hOdQceO#@XmFmjJUqaO?OzxeFe16A+cNR8W=TIBY%z9b${F1Y%|(g7pO zZYKJB{N9Q`c|_YHJ+7as!5%NMqj+_vr+%PuQ!VyZoB93g`zl|n8qM5`P{f+&cTRCH z9BCw8gP{}oYRgddjj!lfr(BnC??zOWIZa zsGEIK#~SI@*(@S);XzV<*W3BtE_ILV)=F1g7NhLixHUuKBXP%e`E&mA`?6pH9D!=U z+?Q^@?tiHlP(`Hq!m*3-gOjm%La|ia59;%G@-~K|c&-~t_GTpIBjkIFEola3!qxGj z*49S1HEN90KKm9?e{NV?idUcM4lf@GFsolKRQ{&PUEIV1h|W>Ql_ug4w%e*7s7jKid!8;G&HLvIWEshZZzZps{|D~_O9GFgW2v*DG8r1BG*@x z!x`jg@sc0Be)%;+FQ3K{A{-9YMiGpfglW7T*1L(YTeJ-h_4uGXvDo~EjHB4;y>>Xa zFW`KK7u-pw{-cQBe^2OiUpFL?f2yUy?*z*43lu^da6caHs?!}+dN)K~aYF2yQ>mgH zk!P18M&7fFWb#934752ryxohzv1_Co955%EwqH;i1gebG(BOv)6l=vo=T(Y>GpdC?yR{cDc_AZt}{O4J>QO0JyhL0gt_k>s{`gCmLBsK5j`}5rK4Rf-K;4oi6T-|-+*L*=5M09 zlFlw!d6I7Q22Zzpus;Nw^e8Fxqpj4aS$hALTVu0IO}=%HQC~)4xqn89a1u?D43Wfy z%fL;zCznD}+T2J(Xwcm7QRTE30v~vED`Bcc z2v8x|AARePk0#-*lrx35&!>8Nwm+r>@J|QKjjBQEpTQc6pspp2r^T>mrc~VSL$uhl1dy{o^Y-e;68H18xw&{3C%|C5Ki^W>nh5BLvub*-LhB_|xwG{2 zT)N)6wo=OuMsEj`W)(`T=F!tvehMojqPsFDaov!%PgF3zh9+Id^}(g{gDL4b^n^T* zh2`zPMs)4Yl4+Zv;o5cgF`fe4&GlUdO{DOXHfDVYFNCeuslQA8A>BO6s;Mc2G~@X} z*H~qpr@7IJ;t(^c6p($#N@)`-_lf=s5OOuSpmsR&*x5Prc3>5`$^2d=Q1UEEbDJ`! zIkG|qZEJ2U3Pr388q=kpilH$FnQj;|t}nUCot3cOsf6hR~NQ^^iJe&J2!b@eFmdmGRUJsXXXc3R_;&3T5up{~eP_IAWz$6?0 z-kRrr?7eb1{PUUn)Db0P0|&AGvLINbi7IwH>Dd2sfU6UBPYPG)hp@{85$X%IXraF6 zim;H7xQNW;^p#flE#PywgCv6XWEVif8E*8F7v4eVoDyH`j&N{|oyoV~n+tz}7QV(5khL*rA%5}_=5Vs9FdA?YpjB&9 zFh?6}y{J7s8*O1^g6igBE98_#T=Nsm=;E{(hOrU~Fq;I@=5I?U?~@j1scpc7&p~Xl zu&ieTf4T|2%nGi@>*ku#{jxoPd2Ol4l;2-n6_J$HGI{SR6)7p~HSM{*2GTnGQM<8a zQLGTdY2kM~CtH}~tmhTkN8*#DLCYF#{mnkEPsTEXt0mW6knP)+xoJjJn-_#5%@Zm{ ziqqdj-rCms6(tMG1mWzkpue!}S0AimL(QB7t7AXhyct!-D9`y9RKQ&wVAJ>DK}-F$ zr$`eHycooa(a~W{Xu_H)n)KN(psFzN)5(ENG$;N)NK}EB_LWzKPUJ-4|AUtE2tYd7 pag>&j_WpHCrxG6h2dz3UuJ+3l>#Iy}2@nx3^#?jC)k?PE{{sMLEXV)= 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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