/* * 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.pipeline.moving.avg; import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.search.SearchPhaseExecutionException; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.common.collect.EvictingQueue; import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; import org.elasticsearch.search.aggregations.bucket.histogram.Histogram.Bucket; import org.elasticsearch.search.aggregations.metrics.avg.Avg; import org.elasticsearch.search.aggregations.pipeline.BucketHelpers; import org.elasticsearch.search.aggregations.pipeline.PipelineAggregationHelperTests; import org.elasticsearch.search.aggregations.pipeline.SimpleValue; import org.elasticsearch.search.aggregations.pipeline.derivative.Derivative; import org.elasticsearch.search.aggregations.pipeline.movavg.models.EwmaModel; import org.elasticsearch.search.aggregations.pipeline.movavg.models.HoltLinearModel; import org.elasticsearch.search.aggregations.pipeline.movavg.models.HoltWintersModel; import org.elasticsearch.search.aggregations.pipeline.movavg.models.LinearModel; import org.elasticsearch.search.aggregations.pipeline.movavg.models.MovAvgModelBuilder; import org.elasticsearch.search.aggregations.pipeline.movavg.models.SimpleModel; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregationBuilder; import org.elasticsearch.test.ESIntegTestCase; import org.hamcrest.Matchers; import java.util.ArrayList; import java.util.Arrays; 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; 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.pipeline.PipelineAggregatorBuilders.derivative; import static org.elasticsearch.search.aggregations.pipeline.PipelineAggregatorBuilders.movingAvg; 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; @ESIntegTestCase.SuiteScopeTestCase public class MovAvgIT extends ESIntegTestCase { private static final String INTERVAL_FIELD = "l_value"; private static final String VALUE_FIELD = "v_value"; static int interval; static int numBuckets; static int windowSize; static double alpha; static double beta; static double gamma; static int period; static HoltWintersModel.SeasonalityType seasonalityType; static BucketHelpers.GapPolicy gapPolicy; static ValuesSourceAggregationBuilder<? extends ValuesSource, ? extends ValuesSourceAggregationBuilder<?, ?>> metric; static List<PipelineAggregationHelperTests.MockBucket> mockHisto; static Map<String, ArrayList<Double>> testValues; enum MovAvgType { SIMPLE ("simple"), LINEAR("linear"), EWMA("ewma"), HOLT("holt"), HOLT_WINTERS("holt_winters"), HOLT_BIG_MINIMIZE("holt"); private final String name; MovAvgType(String s) { name = s; } @Override public String toString(){ return name; } } enum MetricTarget { VALUE ("value"), COUNT("count"), METRIC("metric"); private final String name; MetricTarget(String s) { name = s; } @Override public String toString(){ return name; } } @Override public void setupSuiteScopeCluster() throws Exception { createIndex("idx"); createIndex("idx_unmapped"); List<IndexRequestBuilder> builders = new ArrayList<>(); interval = 5; numBuckets = randomIntBetween(6, 80); period = randomIntBetween(1, 5); windowSize = randomIntBetween(period * 2, 10); // start must be 2*period to play nice with HW alpha = randomDouble(); beta = randomDouble(); gamma = randomDouble(); seasonalityType = randomBoolean() ? HoltWintersModel.SeasonalityType.ADDITIVE : HoltWintersModel.SeasonalityType.MULTIPLICATIVE; gapPolicy = randomBoolean() ? BucketHelpers.GapPolicy.SKIP : BucketHelpers.GapPolicy.INSERT_ZEROS; metric = randomMetric("the_metric", VALUE_FIELD); mockHisto = PipelineAggregationHelperTests.generateHistogram(interval, numBuckets, randomDouble(), randomDouble()); testValues = new HashMap<>(8); for (MovAvgType type : MovAvgType.values()) { for (MetricTarget target : MetricTarget.values()) { if (type.equals(MovAvgType.HOLT_BIG_MINIMIZE)) { setupExpected(type, target, numBuckets); } else { setupExpected(type, target, windowSize); } } } for (PipelineAggregationHelperTests.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())); } } for (int i = -10; i < 10; i++) { builders.add(client().prepareIndex("neg_idx", "type").setSource( jsonBuilder().startObject().field(INTERVAL_FIELD, i).field(VALUE_FIELD, 10).endObject())); } for (int i = 0; i < 12; i++) { builders.add(client().prepareIndex("double_predict", "type").setSource( jsonBuilder().startObject().field(INTERVAL_FIELD, i).field(VALUE_FIELD, 10).endObject())); } indexRandom(true, builders); ensureSearchable(); } /** * 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, int windowSize) { ArrayList<Double> values = new ArrayList<>(numBuckets); EvictingQueue<Double> window = new EvictingQueue<>(windowSize); for (PipelineAggregationHelperTests.MockBucket mockBucket : mockHisto) { double metricValue; double[] docValues = mockBucket.docValues; // 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.SKIP)) { values.add(null); continue; } else if (gapPolicy.equals(BucketHelpers.GapPolicy.INSERT_ZEROS)) { // otherwise insert a zero instead of the true value metricValue = 0.0; } else { metricValue = PipelineAggregationHelperTests.calculateMetric(docValues, metric); } } else { // If this isn't a gap, or is a _count, just insert the value metricValue = target.equals(MetricTarget.VALUE) ? PipelineAggregationHelperTests.calculateMetric(docValues, metric) : mockBucket.count; } if (window.size() > 0) { switch (type) { case SIMPLE: values.add(simple(window)); break; case LINEAR: values.add(linear(window)); break; case EWMA: values.add(ewma(window)); break; case HOLT: values.add(holt(window)); break; case HOLT_BIG_MINIMIZE: values.add(holt(window)); break; case HOLT_WINTERS: // HW needs at least 2 periods of data to start if (window.size() >= period * 2) { values.add(holtWinters(window)); } else { values.add(null); } break; } } else { values.add(null); } window.offer(metricValue); } testValues.put(type.name() + "_" + target.name(), values); } /** * Simple, unweighted moving average * * @param window Window of values to compute movavg for */ private double simple(Collection<Double> window) { double movAvg = 0; for (double value : window) { movAvg += value; } movAvg /= window.size(); return movAvg; } /** * Linearly weighted moving avg * * @param window Window of values to compute movavg for */ private double linear(Collection<Double> window) { double avg = 0; long totalWeight = 1; long current = 1; for (double value : window) { avg += value * current; totalWeight += current; current += 1; } return avg / totalWeight; } /** * Exponentially weighted (EWMA, Single exponential) moving avg * * @param window Window of values to compute movavg for */ private double ewma(Collection<Double> window) { double avg = 0; boolean first = true; for (double value : window) { if (first) { avg = value; first = false; } else { avg = (value * alpha) + (avg * (1 - alpha)); } } return avg; } /** * Holt-Linear (Double exponential) moving avg * @param window Window of values to compute movavg for */ private double holt(Collection<Double> 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) ; } /** * Holt winters (triple exponential) moving avg * @param window Window of values to compute movavg for */ private double holtWinters(Collection<Double> window) { // Smoothed value double s = 0; double last_s = 0; // Trend value double b = 0; double last_b = 0; // Seasonal value double[] seasonal = new double[window.size()]; double padding = seasonalityType.equals(HoltWintersModel.SeasonalityType.MULTIPLICATIVE) ? 0.0000000001 : 0; int counter = 0; double[] vs = new double[window.size()]; for (double v : window) { vs[counter] = v + padding; counter += 1; } // Initial level value is average of first season // Calculate the slopes between first and second season for each period for (int i = 0; i < period; i++) { s += vs[i]; b += (vs[i + period] - vs[i]) / period; } s /= period; b /= period; last_s = s; // Calculate first seasonal if (Double.compare(s, 0.0) == 0 || Double.compare(s, -0.0) == 0) { Arrays.fill(seasonal, 0.0); } else { for (int i = 0; i < period; i++) { seasonal[i] = vs[i] / s; } } for (int i = period; i < vs.length; i++) { if (seasonalityType.equals(HoltWintersModel.SeasonalityType.MULTIPLICATIVE)) { s = alpha * (vs[i] / seasonal[i - period]) + (1.0d - alpha) * (last_s + last_b); } else { s = alpha * (vs[i] - seasonal[i - period]) + (1.0d - alpha) * (last_s + last_b); } b = beta * (s - last_s) + (1 - beta) * last_b; if (seasonalityType.equals(HoltWintersModel.SeasonalityType.MULTIPLICATIVE)) { seasonal[i] = gamma * (vs[i] / (last_s + last_b )) + (1 - gamma) * seasonal[i - period]; } else { seasonal[i] = gamma * (vs[i] - (last_s - last_b )) + (1 - gamma) * seasonal[i - period]; } last_s = s; last_b = b; } int idx = window.size() - period + (0 % period); // TODO perhaps pad out seasonal to a power of 2 and use a mask instead of modulo? if (seasonalityType.equals(HoltWintersModel.SeasonalityType.MULTIPLICATIVE)) { return (s + (1 * b)) * seasonal[idx]; } else { return s + (1 * b) + seasonal[idx]; } } /** * test simple moving average on single value field */ public void testSimpleSingleValuedField() { SearchResponse response = client() .prepareSearch("idx").setTypes("type") .addAggregation( histogram("histo").field(INTERVAL_FIELD).interval(interval) .extendedBounds(0L, (long) (interval * (numBuckets - 1))) .subAggregation(metric) .subAggregation(movingAvg("movavg_counts","_count") .window(windowSize) .modelBuilder(new SimpleModel.SimpleModelBuilder()) .gapPolicy(gapPolicy)) .subAggregation(movingAvg("movavg_values","the_metric") .window(windowSize) .modelBuilder(new SimpleModel.SimpleModelBuilder()) .gapPolicy(gapPolicy)) ).execute().actionGet(); assertSearchResponse(response); Histogram histo = response.getAggregations().get("histo"); assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List<? extends Bucket> buckets = histo.getBuckets(); assertThat("Size of buckets array is not correct.", buckets.size(), equalTo(mockHisto.size())); List<Double> expectedCounts = testValues.get(MovAvgType.SIMPLE.name() + "_" + MetricTarget.COUNT.name()); List<Double> expectedValues = testValues.get(MovAvgType.SIMPLE.name() + "_" + MetricTarget.VALUE.name()); Iterator<? extends Histogram.Bucket> actualIter = buckets.iterator(); Iterator<PipelineAggregationHelperTests.MockBucket> expectedBucketIter = mockHisto.iterator(); Iterator<Double> expectedCountsIter = expectedCounts.iterator(); Iterator<Double> expectedValuesIter = expectedValues.iterator(); while (actualIter.hasNext()) { assertValidIterators(expectedBucketIter, expectedCountsIter, expectedValuesIter); Histogram.Bucket actual = actualIter.next(); PipelineAggregationHelperTests.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); } } public void testLinearSingleValuedField() { SearchResponse response = client() .prepareSearch("idx").setTypes("type") .addAggregation( histogram("histo").field(INTERVAL_FIELD).interval(interval) .extendedBounds(0L, (long) (interval * (numBuckets - 1))) .subAggregation(metric) .subAggregation(movingAvg("movavg_counts", "_count") .window(windowSize) .modelBuilder(new LinearModel.LinearModelBuilder()) .gapPolicy(gapPolicy)) .subAggregation(movingAvg("movavg_values", "the_metric") .window(windowSize) .modelBuilder(new LinearModel.LinearModelBuilder()) .gapPolicy(gapPolicy)) ).execute().actionGet(); assertSearchResponse(response); Histogram histo = response.getAggregations().get("histo"); assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List<? extends Bucket> buckets = histo.getBuckets(); assertThat("Size of buckets array is not correct.", buckets.size(), equalTo(mockHisto.size())); List<Double> expectedCounts = testValues.get(MovAvgType.LINEAR.name() + "_" + MetricTarget.COUNT.name()); List<Double> expectedValues = testValues.get(MovAvgType.LINEAR.name() + "_" + MetricTarget.VALUE.name()); Iterator<? extends Histogram.Bucket> actualIter = buckets.iterator(); Iterator<PipelineAggregationHelperTests.MockBucket> expectedBucketIter = mockHisto.iterator(); Iterator<Double> expectedCountsIter = expectedCounts.iterator(); Iterator<Double> expectedValuesIter = expectedValues.iterator(); while (actualIter.hasNext()) { assertValidIterators(expectedBucketIter, expectedCountsIter, expectedValuesIter); Histogram.Bucket actual = actualIter.next(); PipelineAggregationHelperTests.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); } } public void testEwmaSingleValuedField() { SearchResponse response = client() .prepareSearch("idx").setTypes("type") .addAggregation( histogram("histo").field(INTERVAL_FIELD).interval(interval) .extendedBounds(0L, (long) (interval * (numBuckets - 1))) .subAggregation(metric) .subAggregation(movingAvg("movavg_counts", "_count") .window(windowSize) .modelBuilder(new EwmaModel.EWMAModelBuilder().alpha(alpha)) .gapPolicy(gapPolicy)) .subAggregation(movingAvg("movavg_values", "the_metric") .window(windowSize) .modelBuilder(new EwmaModel.EWMAModelBuilder().alpha(alpha)) .gapPolicy(gapPolicy)) ).execute().actionGet(); assertSearchResponse(response); Histogram histo = response.getAggregations().get("histo"); assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List<? extends Bucket> buckets = histo.getBuckets(); assertThat("Size of buckets array is not correct.", buckets.size(), equalTo(mockHisto.size())); List<Double> expectedCounts = testValues.get(MovAvgType.EWMA.name() + "_" + MetricTarget.COUNT.name()); List<Double> expectedValues = testValues.get(MovAvgType.EWMA.name() + "_" + MetricTarget.VALUE.name()); Iterator<? extends Histogram.Bucket> actualIter = buckets.iterator(); Iterator<PipelineAggregationHelperTests.MockBucket> expectedBucketIter = mockHisto.iterator(); Iterator<Double> expectedCountsIter = expectedCounts.iterator(); Iterator<Double> expectedValuesIter = expectedValues.iterator(); while (actualIter.hasNext()) { assertValidIterators(expectedBucketIter, expectedCountsIter, expectedValuesIter); Histogram.Bucket actual = actualIter.next(); PipelineAggregationHelperTests.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); } } public void testHoltSingleValuedField() { SearchResponse response = client() .prepareSearch("idx").setTypes("type") .addAggregation( histogram("histo").field(INTERVAL_FIELD).interval(interval) .extendedBounds(0L, (long) (interval * (numBuckets - 1))) .subAggregation(metric) .subAggregation(movingAvg("movavg_counts", "_count") .window(windowSize) .modelBuilder(new HoltLinearModel.HoltLinearModelBuilder().alpha(alpha).beta(beta)) .gapPolicy(gapPolicy)) .subAggregation(movingAvg("movavg_values", "the_metric") .window(windowSize) .modelBuilder(new HoltLinearModel.HoltLinearModelBuilder().alpha(alpha).beta(beta)) .gapPolicy(gapPolicy)) ).execute().actionGet(); assertSearchResponse(response); Histogram histo = response.getAggregations().get("histo"); assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List<? extends Bucket> buckets = histo.getBuckets(); assertThat("Size of buckets array is not correct.", buckets.size(), equalTo(mockHisto.size())); List<Double> expectedCounts = testValues.get(MovAvgType.HOLT.name() + "_" + MetricTarget.COUNT.name()); List<Double> expectedValues = testValues.get(MovAvgType.HOLT.name() + "_" + MetricTarget.VALUE.name()); Iterator<? extends Histogram.Bucket> actualIter = buckets.iterator(); Iterator<PipelineAggregationHelperTests.MockBucket> expectedBucketIter = mockHisto.iterator(); Iterator<Double> expectedCountsIter = expectedCounts.iterator(); Iterator<Double> expectedValuesIter = expectedValues.iterator(); while (actualIter.hasNext()) { assertValidIterators(expectedBucketIter, expectedCountsIter, expectedValuesIter); Histogram.Bucket actual = actualIter.next(); PipelineAggregationHelperTests.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); } } public void testHoltWintersValuedField() { SearchResponse response = client() .prepareSearch("idx").setTypes("type") .addAggregation( histogram("histo").field(INTERVAL_FIELD).interval(interval) .extendedBounds(0L, (long) (interval * (numBuckets - 1))) .subAggregation(metric) .subAggregation(movingAvg("movavg_counts", "_count") .window(windowSize) .modelBuilder(new HoltWintersModel.HoltWintersModelBuilder() .alpha(alpha).beta(beta).gamma(gamma).period(period).seasonalityType(seasonalityType)) .gapPolicy(gapPolicy) .minimize(false)) .subAggregation(movingAvg("movavg_values", "the_metric") .window(windowSize) .modelBuilder(new HoltWintersModel.HoltWintersModelBuilder() .alpha(alpha).beta(beta).gamma(gamma).period(period).seasonalityType(seasonalityType)) .gapPolicy(gapPolicy) .minimize(false)) ).execute().actionGet(); assertSearchResponse(response); Histogram histo = response.getAggregations().get("histo"); assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List<? extends Bucket> buckets = histo.getBuckets(); assertThat("Size of buckets array is not correct.", buckets.size(), equalTo(mockHisto.size())); List<Double> expectedCounts = testValues.get(MovAvgType.HOLT_WINTERS.name() + "_" + MetricTarget.COUNT.name()); List<Double> expectedValues = testValues.get(MovAvgType.HOLT_WINTERS.name() + "_" + MetricTarget.VALUE.name()); Iterator<? extends Histogram.Bucket> actualIter = buckets.iterator(); Iterator<PipelineAggregationHelperTests.MockBucket> expectedBucketIter = mockHisto.iterator(); Iterator<Double> expectedCountsIter = expectedCounts.iterator(); Iterator<Double> expectedValuesIter = expectedValues.iterator(); while (actualIter.hasNext()) { assertValidIterators(expectedBucketIter, expectedCountsIter, expectedValuesIter); Histogram.Bucket actual = actualIter.next(); PipelineAggregationHelperTests.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); } } public void testPredictNegativeKeysAtStart() { SearchResponse response = client() .prepareSearch("neg_idx") .setTypes("type") .addAggregation( histogram("histo") .field(INTERVAL_FIELD) .interval(1) .subAggregation(avg("avg").field(VALUE_FIELD)) .subAggregation( movingAvg("movavg_values", "avg").window(windowSize).modelBuilder(new SimpleModel.SimpleModelBuilder()) .gapPolicy(gapPolicy).predict(5))).execute().actionGet(); assertSearchResponse(response); Histogram histo = response.getAggregations().get("histo"); assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List<? extends Bucket> buckets = histo.getBuckets(); assertThat("Size of buckets array is not correct.", buckets.size(), equalTo(25)); SimpleValue current = buckets.get(0).getAggregations().get("movavg_values"); assertThat(current, nullValue()); for (int i = 1; i < 20; i++) { Bucket bucket = buckets.get(i); assertThat(bucket, notNullValue()); assertThat(bucket.getKey(), equalTo(i - 10d)); assertThat(bucket.getDocCount(), equalTo(1L)); Avg avgAgg = bucket.getAggregations().get("avg"); assertThat(avgAgg, notNullValue()); assertThat(avgAgg.value(), equalTo(10d)); SimpleValue movAvgAgg = bucket.getAggregations().get("movavg_values"); assertThat(movAvgAgg, notNullValue()); assertThat(movAvgAgg.value(), equalTo(10d)); } for (int i = 20; i < 25; i++) { Bucket bucket = buckets.get(i); assertThat(bucket, notNullValue()); assertThat(bucket.getKey(), equalTo(i - 10d)); assertThat(bucket.getDocCount(), equalTo(0L)); Avg avgAgg = bucket.getAggregations().get("avg"); assertThat(avgAgg, nullValue()); SimpleValue movAvgAgg = bucket.getAggregations().get("movavg_values"); assertThat(movAvgAgg, notNullValue()); assertThat(movAvgAgg.value(), equalTo(10d)); } } public void testSizeZeroWindow() { try { client() .prepareSearch("idx").setTypes("type") .addAggregation( histogram("histo").field(INTERVAL_FIELD).interval(interval) .extendedBounds(0L, (long) (interval * (numBuckets - 1))) .subAggregation(randomMetric("the_metric", VALUE_FIELD)) .subAggregation(movingAvg("movavg_counts", "the_metric") .window(0) .modelBuilder(new SimpleModel.SimpleModelBuilder()) .gapPolicy(gapPolicy)) ).execute().actionGet(); fail("MovingAvg should not accept a window that is zero"); } catch (IllegalArgumentException e) { assertThat(e.getMessage(), is("[window] must be a positive integer: [movavg_counts]")); } } public void testBadParent() { try { client() .prepareSearch("idx").setTypes("type") .addAggregation( range("histo").field(INTERVAL_FIELD).addRange(0, 10) .subAggregation(randomMetric("the_metric", VALUE_FIELD)) .subAggregation(movingAvg("movavg_counts", "the_metric") .window(windowSize) .modelBuilder(new SimpleModel.SimpleModelBuilder()) .gapPolicy(gapPolicy)) ).execute().actionGet(); fail("MovingAvg should not accept non-histogram as parent"); } catch (SearchPhaseExecutionException exception) { // All good } } public void testNegativeWindow() { try { client() .prepareSearch("idx").setTypes("type") .addAggregation( histogram("histo").field(INTERVAL_FIELD).interval(interval) .extendedBounds(0L, (long) (interval * (numBuckets - 1))) .subAggregation(randomMetric("the_metric", VALUE_FIELD)) .subAggregation(movingAvg("movavg_counts", "_count") .window(-10) .modelBuilder(new SimpleModel.SimpleModelBuilder()) .gapPolicy(gapPolicy)) ).execute().actionGet(); fail("MovingAvg should not accept a window that is negative"); } catch (IllegalArgumentException e) { assertThat(e.getMessage(), is("[window] must be a positive integer: [movavg_counts]")); } } public void testNoBucketsInHistogram() { SearchResponse response = client() .prepareSearch("idx").setTypes("type") .addAggregation( histogram("histo").field("test").interval(interval) .subAggregation(randomMetric("the_metric", VALUE_FIELD)) .subAggregation(movingAvg("movavg_counts", "the_metric") .window(windowSize) .modelBuilder(new SimpleModel.SimpleModelBuilder()) .gapPolicy(gapPolicy)) ).execute().actionGet(); assertSearchResponse(response); Histogram histo = response.getAggregations().get("histo"); assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List<? extends Bucket> buckets = histo.getBuckets(); assertThat(buckets.size(), equalTo(0)); } public void testNoBucketsInHistogramWithPredict() { int numPredictions = randomIntBetween(1,10); SearchResponse response = client() .prepareSearch("idx").setTypes("type") .addAggregation( histogram("histo").field("test").interval(interval) .subAggregation(randomMetric("the_metric", VALUE_FIELD)) .subAggregation(movingAvg("movavg_counts", "the_metric") .window(windowSize) .modelBuilder(new SimpleModel.SimpleModelBuilder()) .gapPolicy(gapPolicy) .predict(numPredictions)) ).execute().actionGet(); assertSearchResponse(response); Histogram histo = response.getAggregations().get("histo"); assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List<? extends Bucket> buckets = histo.getBuckets(); assertThat(buckets.size(), equalTo(0)); } public void testZeroPrediction() { try { client() .prepareSearch("idx").setTypes("type") .addAggregation( histogram("histo").field(INTERVAL_FIELD).interval(interval) .extendedBounds(0L, (long) (interval * (numBuckets - 1))) .subAggregation(randomMetric("the_metric", VALUE_FIELD)) .subAggregation(movingAvg("movavg_counts", "the_metric") .window(windowSize) .modelBuilder(randomModelBuilder()) .gapPolicy(gapPolicy) .predict(0)) ).execute().actionGet(); fail("MovingAvg should not accept a prediction size that is zero"); } catch (IllegalArgumentException exception) { // All Good } } public void testNegativePrediction() { try { client() .prepareSearch("idx").setTypes("type") .addAggregation( histogram("histo").field(INTERVAL_FIELD).interval(interval) .extendedBounds(0L, (long) (interval * (numBuckets - 1))) .subAggregation(randomMetric("the_metric", VALUE_FIELD)) .subAggregation(movingAvg("movavg_counts", "the_metric") .window(windowSize) .modelBuilder(randomModelBuilder()) .gapPolicy(gapPolicy) .predict(-10)) ).execute().actionGet(); fail("MovingAvg should not accept a prediction size that is negative"); } catch (IllegalArgumentException exception) { // All Good } } public void testHoltWintersNotEnoughData() { try { SearchResponse response = client() .prepareSearch("idx").setTypes("type") .addAggregation( histogram("histo").field(INTERVAL_FIELD).interval(interval) .extendedBounds(0L, (long) (interval * (numBuckets - 1))) .subAggregation(metric) .subAggregation(movingAvg("movavg_counts", "_count") .window(10) .modelBuilder(new HoltWintersModel.HoltWintersModelBuilder() .alpha(alpha).beta(beta).gamma(gamma).period(20).seasonalityType(seasonalityType)) .gapPolicy(gapPolicy)) .subAggregation(movingAvg("movavg_values", "the_metric") .window(windowSize) .modelBuilder(new HoltWintersModel.HoltWintersModelBuilder() .alpha(alpha).beta(beta).gamma(gamma).period(20).seasonalityType(seasonalityType)) .gapPolicy(gapPolicy)) ).execute().actionGet(); } catch (SearchPhaseExecutionException e) { // All good } } public void testTwoMovAvgsWithPredictions() { SearchResponse response = client() .prepareSearch("double_predict") .setTypes("type") .addAggregation( histogram("histo") .field(INTERVAL_FIELD) .interval(1) .subAggregation(avg("avg").field(VALUE_FIELD)) .subAggregation(derivative("deriv", "avg").gapPolicy(gapPolicy)) .subAggregation( movingAvg("avg_movavg", "avg").window(windowSize).modelBuilder(new SimpleModel.SimpleModelBuilder()) .gapPolicy(gapPolicy).predict(12)) .subAggregation( movingAvg("deriv_movavg", "deriv").window(windowSize).modelBuilder(new SimpleModel.SimpleModelBuilder()) .gapPolicy(gapPolicy).predict(12)) ).execute().actionGet(); assertSearchResponse(response); Histogram histo = response.getAggregations().get("histo"); assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List<? extends Bucket> buckets = histo.getBuckets(); assertThat("Size of buckets array is not correct.", buckets.size(), equalTo(24)); Bucket bucket = buckets.get(0); assertThat(bucket, notNullValue()); assertThat(bucket.getKey(), equalTo(0d)); assertThat(bucket.getDocCount(), equalTo(1L)); Avg avgAgg = bucket.getAggregations().get("avg"); assertThat(avgAgg, notNullValue()); assertThat(avgAgg.value(), equalTo(10d)); SimpleValue movAvgAgg = bucket.getAggregations().get("avg_movavg"); assertThat(movAvgAgg, nullValue()); Derivative deriv = bucket.getAggregations().get("deriv"); assertThat(deriv, nullValue()); SimpleValue derivMovAvg = bucket.getAggregations().get("deriv_movavg"); assertThat(derivMovAvg, nullValue()); // Second bucket bucket = buckets.get(1); assertThat(bucket, notNullValue()); assertThat(bucket.getKey(), equalTo(1d)); assertThat(bucket.getDocCount(), equalTo(1L)); avgAgg = bucket.getAggregations().get("avg"); assertThat(avgAgg, notNullValue()); assertThat(avgAgg.value(), equalTo(10d)); deriv = bucket.getAggregations().get("deriv"); assertThat(deriv, notNullValue()); assertThat(deriv.value(), equalTo(0d)); movAvgAgg = bucket.getAggregations().get("avg_movavg"); assertThat(movAvgAgg, notNullValue()); assertThat(movAvgAgg.value(), equalTo(10d)); derivMovAvg = bucket.getAggregations().get("deriv_movavg"); assertThat(derivMovAvg, Matchers.nullValue()); // still null because of movavg delay for (int i = 2; i < 12; i++) { bucket = buckets.get(i); assertThat(bucket, notNullValue()); assertThat(bucket.getKey(), equalTo((double) i)); assertThat(bucket.getDocCount(), equalTo(1L)); avgAgg = bucket.getAggregations().get("avg"); assertThat(avgAgg, notNullValue()); assertThat(avgAgg.value(), equalTo(10d)); deriv = bucket.getAggregations().get("deriv"); assertThat(deriv, notNullValue()); assertThat(deriv.value(), equalTo(0d)); movAvgAgg = bucket.getAggregations().get("avg_movavg"); assertThat(movAvgAgg, notNullValue()); assertThat(movAvgAgg.value(), equalTo(10d)); derivMovAvg = bucket.getAggregations().get("deriv_movavg"); assertThat(derivMovAvg, notNullValue()); assertThat(derivMovAvg.value(), equalTo(0d)); } // Predictions for (int i = 12; i < 24; i++) { bucket = buckets.get(i); assertThat(bucket, notNullValue()); assertThat(bucket.getKey(), equalTo((double) i)); assertThat(bucket.getDocCount(), equalTo(0L)); avgAgg = bucket.getAggregations().get("avg"); assertThat(avgAgg, nullValue()); deriv = bucket.getAggregations().get("deriv"); assertThat(deriv, nullValue()); movAvgAgg = bucket.getAggregations().get("avg_movavg"); assertThat(movAvgAgg, notNullValue()); assertThat(movAvgAgg.value(), equalTo(10d)); derivMovAvg = bucket.getAggregations().get("deriv_movavg"); assertThat(derivMovAvg, notNullValue()); assertThat(derivMovAvg.value(), equalTo(0d)); } } public void testBadModelParams() { try { SearchResponse response = client() .prepareSearch("idx").setTypes("type") .addAggregation( histogram("histo").field(INTERVAL_FIELD).interval(interval) .extendedBounds(0L, (long) (interval * (numBuckets - 1))) .subAggregation(metric) .subAggregation(movingAvg("movavg_counts", "_count") .window(10) .modelBuilder(randomModelBuilder(100)) .gapPolicy(gapPolicy)) ).execute().actionGet(); } catch (SearchPhaseExecutionException e) { // All good } } public void testHoltWintersMinimization() { SearchResponse response = client() .prepareSearch("idx").setTypes("type") .addAggregation( histogram("histo").field(INTERVAL_FIELD).interval(interval) .extendedBounds(0L, (long) (interval * (numBuckets - 1))) .subAggregation(metric) .subAggregation(movingAvg("movavg_counts", "_count") .window(windowSize) .modelBuilder(new HoltWintersModel.HoltWintersModelBuilder() .period(period).seasonalityType(seasonalityType)) .gapPolicy(gapPolicy) .minimize(true)) .subAggregation(movingAvg("movavg_values", "the_metric") .window(windowSize) .modelBuilder(new HoltWintersModel.HoltWintersModelBuilder() .period(period).seasonalityType(seasonalityType)) .gapPolicy(gapPolicy) .minimize(true)) ).execute().actionGet(); assertSearchResponse(response); Histogram histo = response.getAggregations().get("histo"); assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List<? extends Bucket> buckets = histo.getBuckets(); assertThat("Size of buckets array is not correct.", buckets.size(), equalTo(mockHisto.size())); List<Double> expectedCounts = testValues.get(MovAvgType.HOLT_WINTERS.name() + "_" + MetricTarget.COUNT.name()); List<Double> expectedValues = testValues.get(MovAvgType.HOLT_WINTERS.name() + "_" + MetricTarget.VALUE.name()); Iterator<? extends Histogram.Bucket> actualIter = buckets.iterator(); Iterator<PipelineAggregationHelperTests.MockBucket> expectedBucketIter = mockHisto.iterator(); Iterator<Double> expectedCountsIter = expectedCounts.iterator(); Iterator<Double> expectedValueIter = expectedValues.iterator(); // The minimizer is stochastic, so just make sure all the values coming back aren't null while (actualIter.hasNext()) { Histogram.Bucket actual = actualIter.next(); PipelineAggregationHelperTests.MockBucket expected = expectedBucketIter.next(); Double expectedCount = expectedCountsIter.next(); Double expectedValue = expectedValueIter.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)); SimpleValue countMovAvg = actual.getAggregations().get("movavg_counts"); SimpleValue valuesMovAvg = actual.getAggregations().get("movavg_values"); if (expectedCount == null) { //this bucket wasn't supposed to have a value (empty, skipped, etc), so //movavg should be null too assertThat(countMovAvg, nullValue()); } else { // Note that we don't compare against the mock values, since those are assuming // a non-minimized set of coefficients. Just check for not-nullness assertThat(countMovAvg, notNullValue()); } if (expectedValue == null) { //this bucket wasn't supposed to have a value (empty, skipped, etc), so //movavg should be null too assertThat(valuesMovAvg, nullValue()); } else { // Note that we don't compare against the mock values, since those are assuming // a non-minimized set of coefficients. Just check for not-nullness assertThat(valuesMovAvg, notNullValue()); } } } /** * If the minimizer is turned on, but there isn't enough data to minimize with, it will simply use * the default settings. Which means our mock histo will match the generated result (which it won't * if the minimizer is actually working, since the coefficients will be different and thus generate different * data) * * We can simulate this by setting the window size == size of histo */ public void testMinimizeNotEnoughData() { SearchResponse response = client() .prepareSearch("idx").setTypes("type") .addAggregation( histogram("histo").field(INTERVAL_FIELD).interval(interval) .extendedBounds(0L, (long) (interval * (numBuckets - 1))) .subAggregation(metric) .subAggregation(movingAvg("movavg_counts", "_count") .window(numBuckets) .modelBuilder(new HoltLinearModel.HoltLinearModelBuilder().alpha(alpha).beta(beta)) .gapPolicy(gapPolicy) .minimize(true)) .subAggregation(movingAvg("movavg_values", "the_metric") .window(numBuckets) .modelBuilder(new HoltLinearModel.HoltLinearModelBuilder().alpha(alpha).beta(beta)) .gapPolicy(gapPolicy) .minimize(true)) ).execute().actionGet(); assertSearchResponse(response); Histogram histo = response.getAggregations().get("histo"); assertThat(histo, notNullValue()); assertThat(histo.getName(), equalTo("histo")); List<? extends Bucket> buckets = histo.getBuckets(); assertThat("Size of buckets array is not correct.", buckets.size(), equalTo(mockHisto.size())); List<Double> expectedCounts = testValues.get(MovAvgType.HOLT_BIG_MINIMIZE.name() + "_" + MetricTarget.COUNT.name()); List<Double> expectedValues = testValues.get(MovAvgType.HOLT_BIG_MINIMIZE.name() + "_" + MetricTarget.VALUE.name()); Iterator<? extends Histogram.Bucket> actualIter = buckets.iterator(); Iterator<PipelineAggregationHelperTests.MockBucket> expectedBucketIter = mockHisto.iterator(); Iterator<Double> expectedCountsIter = expectedCounts.iterator(); Iterator<Double> expectedValuesIter = expectedValues.iterator(); while (actualIter.hasNext()) { assertValidIterators(expectedBucketIter, expectedCountsIter, expectedValuesIter); Histogram.Bucket actual = actualIter.next(); PipelineAggregationHelperTests.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); } } /** * Only some models can be minimized, should throw exception for: simple, linear */ public void testCheckIfNonTunableCanBeMinimized() { try { client() .prepareSearch("idx").setTypes("type") .addAggregation( histogram("histo").field(INTERVAL_FIELD).interval(interval) .extendedBounds(0L, (long) (interval * (numBuckets - 1))) .subAggregation(metric) .subAggregation(movingAvg("movavg_counts", "_count") .window(numBuckets) .modelBuilder(new SimpleModel.SimpleModelBuilder()) .gapPolicy(gapPolicy) .minimize(true)) ).execute().actionGet(); fail("Simple Model cannot be minimized, but an exception was not thrown"); } catch (SearchPhaseExecutionException e) { // All good } try { client() .prepareSearch("idx").setTypes("type") .addAggregation( histogram("histo").field(INTERVAL_FIELD).interval(interval) .extendedBounds(0L, (long) (interval * (numBuckets - 1))) .subAggregation(metric) .subAggregation(movingAvg("movavg_counts", "_count") .window(numBuckets) .modelBuilder(new LinearModel.LinearModelBuilder()) .gapPolicy(gapPolicy) .minimize(true)) ).execute().actionGet(); fail("Linear Model cannot be minimized, but an exception was not thrown"); } catch (SearchPhaseExecutionException e) { // all good } } /** * These models are all minimizable, so they should not throw exceptions */ public void testCheckIfTunableCanBeMinimized() { MovAvgModelBuilder[] builders = new MovAvgModelBuilder[]{ new EwmaModel.EWMAModelBuilder(), new HoltLinearModel.HoltLinearModelBuilder(), new HoltWintersModel.HoltWintersModelBuilder() }; for (MovAvgModelBuilder builder : builders) { try { client() .prepareSearch("idx").setTypes("type") .addAggregation( histogram("histo").field(INTERVAL_FIELD).interval(interval) .extendedBounds(0L, (long) (interval * (numBuckets - 1))) .subAggregation(metric) .subAggregation(movingAvg("movavg_counts", "_count") .window(numBuckets) .modelBuilder(builder) .gapPolicy(gapPolicy) .minimize(true)) ).execute().actionGet(); } catch (SearchPhaseExecutionException e) { fail("Model [" + builder.toString() + "] can be minimized, but an exception was thrown"); } } } private void assertValidIterators(Iterator expectedBucketIter, Iterator expectedCountsIter, Iterator expectedValuesIter) { if (!expectedBucketIter.hasNext()) { fail("`expectedBucketIter` iterator ended before `actual` iterator, size mismatch"); } if (!expectedCountsIter.hasNext()) { fail("`expectedCountsIter` iterator ended before `actual` iterator, size mismatch"); } if (!expectedValuesIter.hasNext()) { fail("`expectedValuesIter` iterator ended before `actual` iterator, size mismatch"); } } private void assertBucketContents(Histogram.Bucket actual, Double expectedCount, Double expectedValue) { // This is a gap bucket SimpleValue countMovAvg = actual.getAggregations().get("movavg_counts"); if (expectedCount == null) { assertThat("[_count] movavg is not null", countMovAvg, nullValue()); } else if (Double.isNaN(expectedCount)) { assertThat("[_count] movavg should be NaN, but is ["+countMovAvg.value()+"] instead", countMovAvg.value(), equalTo(Double.NaN)); } else { assertThat("[_count] movavg is null", countMovAvg, notNullValue()); assertTrue("[_count] movavg does not match expected [" + countMovAvg.value() + " vs " + expectedCount + "]", nearlyEqual(countMovAvg.value(), expectedCount, 0.1)); } // 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 if (Double.isNaN(expectedValue)) { assertThat("[value] movavg should be NaN, but is ["+valuesMovAvg.value()+"] instead", valuesMovAvg.value(), equalTo(Double.NaN)); } else { assertThat("[value] movavg is null", valuesMovAvg, notNullValue()); assertTrue("[value] movavg does not match expected [" + valuesMovAvg.value() + " vs " + expectedValue + "]", nearlyEqual(valuesMovAvg.value(), expectedValue, 0.1)); } } /** * Better floating point comparisons courtesy of https://github.com/brazzy/floating-point-gui.de * * Snippet adapted to use doubles instead of floats */ private static boolean nearlyEqual(double a, double b, double epsilon) { final double absA = Math.abs(a); final double absB = Math.abs(b); final double diff = Math.abs(a - b); if (a == b) { // shortcut, handles infinities return true; } else if (a == 0 || b == 0 || diff < Double.MIN_NORMAL) { // a or b is zero or both are extremely close to it // relative error is less meaningful here return diff < (epsilon * Double.MIN_NORMAL); } else { // use relative error return diff / Math.min((absA + absB), Double.MAX_VALUE) < epsilon; } } private MovAvgModelBuilder randomModelBuilder() { return randomModelBuilder(0); } private MovAvgModelBuilder randomModelBuilder(double padding) { int rand = randomIntBetween(0,3); // HoltWinters is excluded from random generation, because it's "cold start" behavior makes // randomized testing too tricky. Should probably add dedicated, randomized tests just for HoltWinters, // which can compensate for the idiosyncrasies switch (rand) { case 0: return new SimpleModel.SimpleModelBuilder(); case 1: return new LinearModel.LinearModelBuilder(); case 2: return new EwmaModel.EWMAModelBuilder().alpha(alpha + padding); case 3: return new HoltLinearModel.HoltLinearModelBuilder().alpha(alpha + padding).beta(beta + padding); default: return new SimpleModel.SimpleModelBuilder(); } } private ValuesSourceAggregationBuilder<? extends ValuesSource, ? extends ValuesSourceAggregationBuilder<?, ?>> 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); } } }