diff --git a/docs/reference/aggregations/pipeline/percentiles-bucket-aggregation.asciidoc b/docs/reference/aggregations/pipeline/percentiles-bucket-aggregation.asciidoc index 6c4329f6f20..032b6ef4e41 100644 --- a/docs/reference/aggregations/pipeline/percentiles-bucket-aggregation.asciidoc +++ b/docs/reference/aggregations/pipeline/percentiles-bucket-aggregation.asciidoc @@ -27,6 +27,7 @@ A `percentiles_bucket` aggregation looks like this in isolation: details)|Optional | `skip` |`format` |format to apply to the output value of this aggregation |Optional | `null` |`percents` |The list of percentiles to calculate |Optional | `[ 1, 5, 25, 50, 75, 95, 99 ]` +|`keyed` |Flag which returns the range as an hash instead of an array of key-value pairs |Optional | `true` |=== The following snippet calculates the percentiles for the total monthly `sales` buckets: diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalPercentilesBucket.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalPercentilesBucket.java index 940511619b1..992c29637a2 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalPercentilesBucket.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalPercentilesBucket.java @@ -19,6 +19,7 @@ package org.elasticsearch.search.aggregations.pipeline; +import org.elasticsearch.Version; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -39,9 +40,10 @@ import java.util.Objects; public class InternalPercentilesBucket extends InternalNumericMetricsAggregation.MultiValue implements PercentilesBucket { private double[] percentiles; private double[] percents; + private boolean keyed = true; private final transient Map percentileLookups = new HashMap<>(); - InternalPercentilesBucket(String name, double[] percents, double[] percentiles, + InternalPercentilesBucket(String name, double[] percents, double[] percentiles, boolean keyed, DocValueFormat formatter, List pipelineAggregators, Map metaData) { super(name, pipelineAggregators, metaData); @@ -52,6 +54,7 @@ public class InternalPercentilesBucket extends InternalNumericMetricsAggregation this.format = formatter; this.percentiles = percentiles; this.percents = percents; + this.keyed = keyed; computeLookup(); } @@ -69,6 +72,11 @@ public class InternalPercentilesBucket extends InternalNumericMetricsAggregation format = in.readNamedWriteable(DocValueFormat.class); percentiles = in.readDoubleArray(); percents = in.readDoubleArray(); + + if (in.getVersion().onOrAfter(Version.V_7_0_0)) { + keyed = in.readBoolean(); + } + computeLookup(); } @@ -77,6 +85,10 @@ public class InternalPercentilesBucket extends InternalNumericMetricsAggregation out.writeNamedWriteable(format); out.writeDoubleArray(percentiles); out.writeDoubleArray(percents); + + if (out.getVersion().onOrAfter(Version.V_7_0_0)) { + out.writeBoolean(keyed); + } } @Override @@ -120,17 +132,33 @@ public class InternalPercentilesBucket extends InternalNumericMetricsAggregation @Override public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { - builder.startObject("values"); - for (double percent : percents) { - double value = percentile(percent); - boolean hasValue = !(Double.isInfinite(value) || Double.isNaN(value)); - String key = String.valueOf(percent); - builder.field(key, hasValue ? value : null); - if (hasValue && format != DocValueFormat.RAW) { - builder.field(key + "_as_string", percentileAsString(percent)); + if (keyed) { + builder.startObject("values"); + for (double percent : percents) { + double value = percentile(percent); + boolean hasValue = !(Double.isInfinite(value) || Double.isNaN(value)); + String key = String.valueOf(percent); + builder.field(key, hasValue ? value : null); + if (hasValue && format != DocValueFormat.RAW) { + builder.field(key + "_as_string", percentileAsString(percent)); + } } + builder.endObject(); + } else { + builder.startArray("values"); + for (double percent : percents) { + double value = percentile(percent); + boolean hasValue = !(Double.isInfinite(value) || Double.isNaN(value)); + builder.startObject(); + builder.field("key", percent); + builder.field("value", hasValue ? value : null); + if (hasValue && format != DocValueFormat.RAW) { + builder.field(String.valueOf(percent) + "_as_string", percentileAsString(percent)); + } + builder.endObject(); + } + builder.endArray(); } - builder.endObject(); return builder; } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/PercentilesBucketPipelineAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/PercentilesBucketPipelineAggregationBuilder.java index 49e065cdeef..a3ac2201777 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/PercentilesBucketPipelineAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/PercentilesBucketPipelineAggregationBuilder.java @@ -21,6 +21,7 @@ package org.elasticsearch.search.aggregations.pipeline; import com.carrotsearch.hppc.DoubleArrayList; +import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -40,8 +41,10 @@ public class PercentilesBucketPipelineAggregationBuilder extends BucketMetricsPipelineAggregationBuilder { public static final String NAME = "percentiles_bucket"; static final ParseField PERCENTS_FIELD = new ParseField("percents"); + static final ParseField KEYED_FIELD = new ParseField("keyed"); private double[] percents = new double[] { 1.0, 5.0, 25.0, 50.0, 75.0, 95.0, 99.0 }; + private boolean keyed = true; public PercentilesBucketPipelineAggregationBuilder(String name, String bucketsPath) { super(name, NAME, new String[] { bucketsPath }); @@ -54,24 +57,32 @@ public class PercentilesBucketPipelineAggregationBuilder throws IOException { super(in, NAME); percents = in.readDoubleArray(); + + if (in.getVersion().onOrAfter(Version.V_7_0_0)) { + keyed = in.readBoolean(); + } } @Override protected void innerWriteTo(StreamOutput out) throws IOException { out.writeDoubleArray(percents); + + if (out.getVersion().onOrAfter(Version.V_7_0_0)) { + out.writeBoolean(keyed); + } } /** * Get the percentages to calculate percentiles for in this aggregation */ - public double[] percents() { + public double[] getPercents() { return percents; } /** * Set the percentages to calculate percentiles for in this aggregation */ - public PercentilesBucketPipelineAggregationBuilder percents(double[] percents) { + public PercentilesBucketPipelineAggregationBuilder setPercents(double[] percents) { if (percents == null) { throw new IllegalArgumentException("[percents] must not be null: [" + name + "]"); } @@ -85,9 +96,24 @@ public class PercentilesBucketPipelineAggregationBuilder return this; } + /** + * Set whether the XContent should be keyed + */ + public PercentilesBucketPipelineAggregationBuilder setKeyed(boolean keyed) { + this.keyed = keyed; + return this; + } + + /** + * Get whether the XContent should be keyed + */ + public boolean getKeyed() { + return keyed; + } + @Override protected PipelineAggregator createInternal(Map metaData) throws IOException { - return new PercentilesBucketPipelineAggregator(name, percents, bucketsPaths, gapPolicy(), formatter(), metaData); + return new PercentilesBucketPipelineAggregator(name, percents, keyed, bucketsPaths, gapPolicy(), formatter(), metaData); } @Override @@ -108,6 +134,7 @@ public class PercentilesBucketPipelineAggregationBuilder if (percents != null) { builder.array(PERCENTS_FIELD.getPreferredName(), percents); } + builder.field(KEYED_FIELD.getPreferredName(), keyed); return builder; } @@ -122,7 +149,11 @@ public class PercentilesBucketPipelineAggregationBuilder double[] percents = (double[]) params.get(PERCENTS_FIELD.getPreferredName()); if (percents != null) { - factory.percents(percents); + factory.setPercents(percents); + } + Boolean keyed = (Boolean) params.get(KEYED_FIELD.getPreferredName()); + if (keyed != null) { + factory.setKeyed(keyed); } return factory; @@ -139,6 +170,10 @@ public class PercentilesBucketPipelineAggregationBuilder params.put(PERCENTS_FIELD.getPreferredName(), percents.toArray()); return true; } + else if (KEYED_FIELD.match(field, parser.getDeprecationHandler()) && token == XContentParser.Token.VALUE_BOOLEAN){ + params.put(KEYED_FIELD.getPreferredName(), parser.booleanValue()); + return true; + } return false; } @@ -146,13 +181,13 @@ public class PercentilesBucketPipelineAggregationBuilder @Override protected int innerHashCode() { - return Arrays.hashCode(percents); + return Objects.hash(Arrays.hashCode(percents), keyed); } @Override protected boolean innerEquals(BucketMetricsPipelineAggregationBuilder obj) { PercentilesBucketPipelineAggregationBuilder other = (PercentilesBucketPipelineAggregationBuilder) obj; - return Objects.deepEquals(percents, other.percents); + return Objects.deepEquals(percents, other.percents) && Objects.equals(keyed, other.keyed); } @Override diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/PercentilesBucketPipelineAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/PercentilesBucketPipelineAggregator.java index 20c38ca05bd..3333e80963e 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/PercentilesBucketPipelineAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/PercentilesBucketPipelineAggregator.java @@ -19,6 +19,7 @@ package org.elasticsearch.search.aggregations.pipeline; +import org.elasticsearch.Version; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.search.DocValueFormat; @@ -34,12 +35,14 @@ import java.util.Map; public class PercentilesBucketPipelineAggregator extends BucketMetricsPipelineAggregator { private final double[] percents; + private boolean keyed = true; private List data; - PercentilesBucketPipelineAggregator(String name, double[] percents, String[] bucketsPaths, GapPolicy gapPolicy, - DocValueFormat formatter, Map metaData) { + PercentilesBucketPipelineAggregator(String name, double[] percents, boolean keyed, String[] bucketsPaths, + GapPolicy gapPolicy, DocValueFormat formatter, Map metaData) { super(name, bucketsPaths, gapPolicy, formatter, metaData); this.percents = percents; + this.keyed = keyed; } /** @@ -48,11 +51,19 @@ public class PercentilesBucketPipelineAggregator extends BucketMetricsPipelineAg public PercentilesBucketPipelineAggregator(StreamInput in) throws IOException { super(in); percents = in.readDoubleArray(); + + if (in.getVersion().onOrAfter(Version.V_7_0_0)) { + keyed = in.readBoolean(); + } } @Override public void innerWriteTo(StreamOutput out) throws IOException { out.writeDoubleArray(percents); + + if (out.getVersion().onOrAfter(Version.V_7_0_0)) { + out.writeBoolean(keyed); + } } @Override @@ -91,6 +102,6 @@ public class PercentilesBucketPipelineAggregator extends BucketMetricsPipelineAg // todo need postCollection() to clean up temp sorted data? - return new InternalPercentilesBucket(name(), percents, percentiles, format, pipelineAggregators, metadata); + return new InternalPercentilesBucket(name(), percents, percentiles, keyed, format, pipelineAggregators, metadata); } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/pipeline/InternalPercentilesBucketTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/pipeline/InternalPercentilesBucketTests.java index 176966174c4..78ef2e1df39 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/pipeline/InternalPercentilesBucketTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/pipeline/InternalPercentilesBucketTests.java @@ -19,7 +19,11 @@ package org.elasticsearch.search.aggregations.pipeline; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.aggregations.Aggregation.CommonFields; import org.elasticsearch.search.aggregations.ParsedAggregation; @@ -40,23 +44,29 @@ import java.util.Map; import java.util.function.Predicate; import static org.elasticsearch.search.aggregations.metrics.InternalPercentilesTestCase.randomPercents; +import static org.hamcrest.Matchers.equalTo; public class InternalPercentilesBucketTests extends InternalAggregationTestCase { @Override protected InternalPercentilesBucket createTestInstance(String name, List pipelineAggregators, Map metaData) { - return createTestInstance(name, pipelineAggregators, metaData, randomPercents()); + return createTestInstance(name, pipelineAggregators, metaData, randomPercents(), true); } private static InternalPercentilesBucket createTestInstance(String name, List pipelineAggregators, - Map metaData, double[] percents) { - DocValueFormat format = randomNumericDocValueFormat(); + Map metaData, double[] percents, boolean keyed) { final double[] percentiles = new double[percents.length]; for (int i = 0; i < percents.length; ++i) { percentiles[i] = frequently() ? randomDouble() : Double.NaN; } - return new InternalPercentilesBucket(name, percents, percentiles, format, pipelineAggregators, metaData); + return createTestInstance(name, pipelineAggregators, metaData, percents, percentiles, keyed); + } + + private static InternalPercentilesBucket createTestInstance(String name, List pipelineAggregators, + Map metaData, double[] percents, double[] percentiles, boolean keyed) { + DocValueFormat format = randomNumericDocValueFormat(); + return new InternalPercentilesBucket(name, percents, percentiles, keyed, format, pipelineAggregators, metaData); } @Override @@ -96,7 +106,8 @@ public class InternalPercentilesBucketTests extends InternalAggregationTestCase< */ public void testPercentOrder() { final double[] percents = new double[]{ 0.50, 0.25, 0.01, 0.99, 0.60 }; - InternalPercentilesBucket aggregation = createTestInstance("test", Collections.emptyList(), Collections.emptyMap(), percents); + InternalPercentilesBucket aggregation = createTestInstance("test", Collections.emptyList(), + Collections.emptyMap(), percents, randomBoolean()); Iterator iterator = aggregation.iterator(); for (double percent : percents) { assertTrue(iterator.hasNext()); @@ -110,7 +121,7 @@ public class InternalPercentilesBucketTests extends InternalAggregationTestCase< final double[] percents = new double[]{ 0.1, 0.2, 0.3}; final double[] percentiles = new double[]{ 0.10, 0.2}; IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new InternalPercentilesBucket("test", percents, - percentiles, DocValueFormat.RAW, Collections.emptyList(), Collections.emptyMap())); + percentiles, randomBoolean(), DocValueFormat.RAW, Collections.emptyList(), Collections.emptyMap())); assertEquals("The number of provided percents and percentiles didn't match. percents: [0.1, 0.2, 0.3], percentiles: [0.1, 0.2]", e.getMessage()); } @@ -125,6 +136,52 @@ public class InternalPercentilesBucketTests extends InternalAggregationTestCase< } } + public void testEmptyRanksXContent() throws IOException { + double[] percents = new double[]{1,2,3}; + double[] percentiles = new double[3]; + for (int i = 0; i < 3; ++i) { + percentiles[i] = randomBoolean() ? Double.NaN : Double.POSITIVE_INFINITY; + } + boolean keyed = randomBoolean(); + + InternalPercentilesBucket agg = createTestInstance("test", Collections.emptyList(), Collections.emptyMap(), + percents, percentiles, keyed); + + XContentBuilder builder = JsonXContent.contentBuilder().prettyPrint(); + builder.startObject(); + agg.doXContentBody(builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + String expected; + if (keyed) { + expected = "{\n" + + " \"values\" : {\n" + + " \"1.0\" : null,\n" + + " \"2.0\" : null,\n" + + " \"3.0\" : null\n" + + " }\n" + + "}"; + } else { + expected = "{\n" + + " \"values\" : [\n" + + " {\n" + + " \"key\" : 1.0,\n" + + " \"value\" : null\n" + + " },\n" + + " {\n" + + " \"key\" : 2.0,\n" + + " \"value\" : null\n" + + " },\n" + + " {\n" + + " \"key\" : 3.0,\n" + + " \"value\" : null\n" + + " }\n" + + " ]\n" + + "}"; + } + + assertThat(Strings.toString(builder), equalTo(expected)); + } + @Override protected Predicate excludePathsFromXContentInsertion() { return path -> path.endsWith(CommonFields.VALUES.getPreferredName()); @@ -162,7 +219,7 @@ public class InternalPercentilesBucketTests extends InternalAggregationTestCase< default: throw new AssertionError("Illegal randomisation branch"); } - return new InternalPercentilesBucket(name, percents, percentiles, formatter, pipelineAggregators, metaData); + return new InternalPercentilesBucket(name, percents, percentiles, randomBoolean(), formatter, pipelineAggregators, metaData); } private double[] extractPercentiles(InternalPercentilesBucket instance) { diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/pipeline/PercentilesBucketIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/pipeline/PercentilesBucketIT.java index 4c67fb533e4..546104b0064 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/pipeline/PercentilesBucketIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/pipeline/PercentilesBucketIT.java @@ -100,7 +100,7 @@ public class PercentilesBucketIT extends ESIntegTestCase { .addAggregation(histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) .extendedBounds(minRandomValue, maxRandomValue)) .addAggregation(percentilesBucket("percentiles_bucket", "histo>_count") - .percents(PERCENTS)).get(); + .setPercents(PERCENTS)).get(); assertSearchResponse(response); @@ -138,7 +138,7 @@ public class PercentilesBucketIT extends ESIntegTestCase { histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) .extendedBounds(minRandomValue, maxRandomValue)) .subAggregation(percentilesBucket("percentiles_bucket", "histo>_count") - .percents(PERCENTS))).get(); + .setPercents(PERCENTS))).get(); assertSearchResponse(response); @@ -180,7 +180,7 @@ public class PercentilesBucketIT extends ESIntegTestCase { .prepareSearch("idx") .addAggregation(terms("terms").field("tag").subAggregation(sum("sum").field(SINGLE_VALUED_FIELD_NAME))) .addAggregation(percentilesBucket("percentiles_bucket", "terms>sum") - .percents(PERCENTS)).get(); + .setPercents(PERCENTS)).get(); assertSearchResponse(response); @@ -254,7 +254,7 @@ public class PercentilesBucketIT extends ESIntegTestCase { .extendedBounds(minRandomValue, maxRandomValue) .subAggregation(sum("sum").field(SINGLE_VALUED_FIELD_NAME))) .subAggregation(percentilesBucket("percentiles_bucket", "histo>sum") - .percents(PERCENTS))).get(); + .setPercents(PERCENTS))).get(); assertSearchResponse(response); @@ -308,7 +308,7 @@ public class PercentilesBucketIT extends ESIntegTestCase { .subAggregation(sum("sum").field(SINGLE_VALUED_FIELD_NAME))) .subAggregation(percentilesBucket("percentiles_bucket", "histo>sum") .gapPolicy(BucketHelpers.GapPolicy.INSERT_ZEROS) - .percents(PERCENTS))) + .setPercents(PERCENTS))) .get(); assertSearchResponse(response); @@ -354,7 +354,7 @@ public class PercentilesBucketIT extends ESIntegTestCase { .addAggregation(terms("terms").field("tag").includeExclude(new IncludeExclude(null, "tag.*")) .subAggregation(sum("sum").field(SINGLE_VALUED_FIELD_NAME))) .addAggregation(percentilesBucket("percentiles_bucket", "terms>sum") - .percents(PERCENTS)).get(); + .setPercents(PERCENTS)).get(); assertSearchResponse(response); @@ -377,7 +377,7 @@ public class PercentilesBucketIT extends ESIntegTestCase { .addAggregation(terms("terms").field("tag").includeExclude(new IncludeExclude(null, "tag.*")) .subAggregation(sum("sum").field(SINGLE_VALUED_FIELD_NAME))) .addAggregation(percentilesBucket("percentiles_bucket", "terms>sum") - .percents(PERCENTS)).get(); + .setPercents(PERCENTS)).get(); assertSearchResponse(response); @@ -406,7 +406,7 @@ public class PercentilesBucketIT extends ESIntegTestCase { client().prepareSearch("idx") .addAggregation(terms("terms").field("tag").subAggregation(sum("sum").field(SINGLE_VALUED_FIELD_NAME))) .addAggregation(percentilesBucket("percentiles_bucket", "terms>sum") - .percents(badPercents)).get(); + .setPercents(badPercents)).get(); fail("Illegal percent's were provided but no exception was thrown."); } catch (Exception e) { @@ -440,7 +440,7 @@ public class PercentilesBucketIT extends ESIntegTestCase { histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) .extendedBounds(minRandomValue, maxRandomValue)) .subAggregation(percentilesBucket("percentiles_bucket", "histo>_count") - .percents(badPercents))).get(); + .setPercents(badPercents))).get(); fail("Illegal percent's were provided but no exception was thrown."); } catch (Exception e) { @@ -470,9 +470,9 @@ public class PercentilesBucketIT extends ESIntegTestCase { .subAggregation( histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) .extendedBounds(minRandomValue, maxRandomValue)) - .subAggregation(percentilesBucket("percentile_histo_bucket", "histo>_count").percents(PERCENTS))) + .subAggregation(percentilesBucket("percentile_histo_bucket", "histo>_count").setPercents(PERCENTS))) .addAggregation(percentilesBucket("percentile_terms_bucket", "terms>percentile_histo_bucket.50") - .percents(PERCENTS)).get(); + .setPercents(PERCENTS)).get(); assertSearchResponse(response); @@ -530,9 +530,9 @@ public class PercentilesBucketIT extends ESIntegTestCase { histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval) .extendedBounds(minRandomValue, maxRandomValue)) .subAggregation(percentilesBucket("percentile_histo_bucket", "histo>_count") - .percents(percent))) + .setPercents(percent))) .addAggregation(percentilesBucket("percentile_terms_bucket", "terms>percentile_histo_bucket[99.9]") - .percents(percent)).get(); + .setPercents(percent)).get(); assertSearchResponse(response); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/pipeline/PercentilesBucketTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/pipeline/PercentilesBucketTests.java index 7ad05059a73..165312a5bde 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/pipeline/PercentilesBucketTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/pipeline/PercentilesBucketTests.java @@ -43,7 +43,7 @@ public class PercentilesBucketTests extends AbstractBucketMetricsTestCase