From b9d2ecc3eab93a2cf909e0c2a2540b668e3d207e Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Fri, 12 May 2017 16:52:47 +0200 Subject: [PATCH] Add parsing methods to Range aggregations (#24583) --- .../ParsedMultiBucketAggregation.java | 4 + .../bucket/range/ParsedRange.java | 193 ++++++++++++++++++ .../bucket/range/date/ParsedDateRange.java | 74 +++++++ .../range/geodistance/ParsedGeoDistance.java | 55 +++++ .../aggregations/AggregationsTests.java | 6 + ...nternalMultiBucketAggregationTestCase.java | 2 +- .../bucket/range/InternalRangeTestCase.java | 30 ++- .../bucket/range/InternalRangeTests.java | 26 ++- .../range/date/InternalDateRangeTests.java | 9 +- .../geodistance/InternalGeoDistanceTests.java | 10 +- .../test/InternalAggregationTestCase.java | 9 + 11 files changed, 407 insertions(+), 11 deletions(-) create mode 100644 core/src/main/java/org/elasticsearch/search/aggregations/bucket/range/ParsedRange.java create mode 100644 core/src/main/java/org/elasticsearch/search/aggregations/bucket/range/date/ParsedDateRange.java create mode 100644 core/src/main/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/ParsedGeoDistance.java diff --git a/core/src/main/java/org/elasticsearch/search/aggregations/ParsedMultiBucketAggregation.java b/core/src/main/java/org/elasticsearch/search/aggregations/ParsedMultiBucketAggregation.java index 52b5fad5379..df80ada8ddd 100644 --- a/core/src/main/java/org/elasticsearch/search/aggregations/ParsedMultiBucketAggregation.java +++ b/core/src/main/java/org/elasticsearch/search/aggregations/ParsedMultiBucketAggregation.java @@ -107,6 +107,10 @@ public abstract class ParsedMultiBucketAggregation implements Range { + + @Override + public String getType() { + return RangeAggregationBuilder.NAME; + } + + @Override + public List getBuckets() { + return buckets; + } + + protected static void declareParsedRangeFields(final ObjectParser objectParser, + final CheckedFunction bucketParser, + final CheckedFunction keyedBucketParser) { + declareMultiBucketAggregationFields(objectParser, bucketParser::apply, keyedBucketParser::apply); + } + + private static ObjectParser PARSER = + new ObjectParser<>(ParsedRange.class.getSimpleName(), true, ParsedRange::new); + static { + declareParsedRangeFields(PARSER, + parser -> ParsedBucket.fromXContent(parser, false), + parser -> ParsedBucket.fromXContent(parser, true)); + } + + public static ParsedRange fromXContent(XContentParser parser, String name) throws IOException { + ParsedRange aggregation = PARSER.parse(parser, null); + aggregation.setName(name); + return aggregation; + } + + public static class ParsedBucket extends ParsedMultiBucketAggregation.ParsedBucket implements Range.Bucket { + + protected String key; + protected double from = Double.NEGATIVE_INFINITY; + protected String fromAsString; + protected double to = Double.POSITIVE_INFINITY; + protected String toAsString; + + @Override + public String getKey() { + return getKeyAsString(); + } + + @Override + public String getKeyAsString() { + String keyAsString = super.getKeyAsString(); + if (keyAsString != null) { + return keyAsString; + } + return key; + } + + @Override + public Object getFrom() { + return from; + } + + @Override + public String getFromAsString() { + if (fromAsString != null) { + return fromAsString; + } + return doubleAsString(from); + } + + @Override + public Object getTo() { + return to; + } + + @Override + public String getToAsString() { + if (toAsString != null) { + return toAsString; + } + return doubleAsString(to); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + if (isKeyed()) { + builder.startObject(key); + } else { + builder.startObject(); + builder.field(CommonFields.KEY.getPreferredName(), key); + } + if (Double.isInfinite(from) == false) { + builder.field(CommonFields.FROM.getPreferredName(), from); + if (fromAsString != null) { + builder.field(CommonFields.FROM_AS_STRING.getPreferredName(), fromAsString); + } + } + if (Double.isInfinite(to) == false) { + builder.field(CommonFields.TO.getPreferredName(), to); + if (toAsString != null) { + builder.field(CommonFields.TO_AS_STRING.getPreferredName(), toAsString); + } + } + builder.field(CommonFields.DOC_COUNT.getPreferredName(), getDocCount()); + getAggregations().toXContentInternal(builder, params); + builder.endObject(); + return builder; + } + + private static String doubleAsString(double d) { + return Double.isInfinite(d) ? null : Double.toString(d); + } + + protected static B parseRangeBucketXContent(final XContentParser parser, + final Supplier bucketSupplier, + final boolean keyed) throws IOException { + final B bucket = bucketSupplier.get(); + bucket.setKeyed(keyed); + XContentParser.Token token = parser.currentToken(); + String currentFieldName = parser.currentName(); + if (keyed) { + ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser::getTokenLocation); + bucket.key = currentFieldName; + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation); + } + + List aggregations = new ArrayList<>(); + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token.isValue()) { + if (CommonFields.KEY_AS_STRING.getPreferredName().equals(currentFieldName)) { + bucket.setKeyAsString(parser.text()); + } else if (CommonFields.KEY.getPreferredName().equals(currentFieldName)) { + bucket.key = parser.text(); + } else if (CommonFields.DOC_COUNT.getPreferredName().equals(currentFieldName)) { + bucket.setDocCount(parser.longValue()); + } else if (CommonFields.FROM.getPreferredName().equals(currentFieldName)) { + bucket.from = parser.doubleValue(); + } else if (CommonFields.FROM_AS_STRING.getPreferredName().equals(currentFieldName)) { + bucket.fromAsString = parser.text(); + } else if (CommonFields.TO.getPreferredName().equals(currentFieldName)) { + bucket.to = parser.doubleValue(); + } else if (CommonFields.TO_AS_STRING.getPreferredName().equals(currentFieldName)) { + bucket.toAsString = parser.text(); + } + } else if (token == XContentParser.Token.START_OBJECT) { + aggregations.add(XContentParserUtils.parseTypedKeysObject(parser, Aggregation.TYPED_KEYS_DELIMITER, Aggregation.class)); + } + } + bucket.setAggregations(new Aggregations(aggregations)); + return bucket; + } + + static ParsedBucket fromXContent(final XContentParser parser, final boolean keyed) throws IOException { + return parseRangeBucketXContent(parser, ParsedBucket::new, keyed); + } + } +} diff --git a/core/src/main/java/org/elasticsearch/search/aggregations/bucket/range/date/ParsedDateRange.java b/core/src/main/java/org/elasticsearch/search/aggregations/bucket/range/date/ParsedDateRange.java new file mode 100644 index 00000000000..b8f2f008cec --- /dev/null +++ b/core/src/main/java/org/elasticsearch/search/aggregations/bucket/range/date/ParsedDateRange.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.bucket.range.date; + +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.search.aggregations.bucket.range.ParsedRange; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import java.io.IOException; + +public class ParsedDateRange extends ParsedRange { + + @Override + public String getType() { + return DateRangeAggregationBuilder.NAME; + } + + private static ObjectParser PARSER = + new ObjectParser<>(ParsedDateRange.class.getSimpleName(), true, ParsedDateRange::new); + static { + declareParsedRangeFields(PARSER, + parser -> ParsedBucket.fromXContent(parser, false), + parser -> ParsedBucket.fromXContent(parser, true)); + } + + public static ParsedDateRange fromXContent(XContentParser parser, String name) throws IOException { + ParsedDateRange aggregation = PARSER.parse(parser, null); + aggregation.setName(name); + return aggregation; + } + + public static class ParsedBucket extends ParsedRange.ParsedBucket { + + @Override + public Object getFrom() { + return doubleAsDateTime(from); + } + + @Override + public Object getTo() { + return doubleAsDateTime(to); + } + + private static DateTime doubleAsDateTime(Double d) { + if (d == null || Double.isInfinite(d)) { + return null; + } + return new DateTime(d.longValue(), DateTimeZone.UTC); + } + + static ParsedBucket fromXContent(final XContentParser parser, final boolean keyed) throws IOException { + return parseRangeBucketXContent(parser, ParsedBucket::new, keyed); + } + } +} diff --git a/core/src/main/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/ParsedGeoDistance.java b/core/src/main/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/ParsedGeoDistance.java new file mode 100644 index 00000000000..a926499e924 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/ParsedGeoDistance.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.bucket.range.geodistance; + +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.search.aggregations.bucket.range.ParsedRange; + +import java.io.IOException; + +public class ParsedGeoDistance extends ParsedRange { + + @Override + public String getType() { + return GeoDistanceAggregationBuilder.NAME; + } + + private static ObjectParser PARSER = + new ObjectParser<>(ParsedGeoDistance.class.getSimpleName(), true, ParsedGeoDistance::new); + static { + declareParsedRangeFields(PARSER, + parser -> ParsedBucket.fromXContent(parser, false), + parser -> ParsedBucket.fromXContent(parser, true)); + } + + public static ParsedGeoDistance fromXContent(XContentParser parser, String name) throws IOException { + ParsedGeoDistance aggregation = PARSER.parse(parser, null); + aggregation.setName(name); + return aggregation; + } + + public static class ParsedBucket extends ParsedRange.ParsedBucket { + + static ParsedBucket fromXContent(final XContentParser parser, final boolean keyed) throws IOException { + return parseRangeBucketXContent(parser, ParsedBucket::new, keyed); + } + } +} diff --git a/core/src/test/java/org/elasticsearch/search/aggregations/AggregationsTests.java b/core/src/test/java/org/elasticsearch/search/aggregations/AggregationsTests.java index d741240e158..4cfc87c171c 100644 --- a/core/src/test/java/org/elasticsearch/search/aggregations/AggregationsTests.java +++ b/core/src/test/java/org/elasticsearch/search/aggregations/AggregationsTests.java @@ -37,6 +37,9 @@ import org.elasticsearch.search.aggregations.bucket.missing.InternalMissingTests import org.elasticsearch.search.aggregations.bucket.nested.InternalNestedTests; import org.elasticsearch.search.aggregations.bucket.nested.InternalReverseNestedTests; import org.elasticsearch.search.aggregations.bucket.sampler.InternalSamplerTests; +import org.elasticsearch.search.aggregations.bucket.range.InternalRangeTests; +import org.elasticsearch.search.aggregations.bucket.range.date.InternalDateRangeTests; +import org.elasticsearch.search.aggregations.bucket.range.geodistance.InternalGeoDistanceTests; import org.elasticsearch.search.aggregations.bucket.terms.DoubleTermsTests; import org.elasticsearch.search.aggregations.bucket.terms.LongTermsTests; import org.elasticsearch.search.aggregations.bucket.terms.StringTermsTests; @@ -119,6 +122,9 @@ public class AggregationsTests extends ESTestCase { aggsTests.add(new InternalFilterTests()); aggsTests.add(new InternalSamplerTests()); aggsTests.add(new InternalGeoHashGridTests()); + aggsTests.add(new InternalRangeTests()); + aggsTests.add(new InternalDateRangeTests()); + aggsTests.add(new InternalGeoDistanceTests()); return Collections.unmodifiableList(aggsTests); } diff --git a/core/src/test/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregationTestCase.java b/core/src/test/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregationTestCase.java index b16b7f49d7e..df977abbe81 100644 --- a/core/src/test/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregationTestCase.java +++ b/core/src/test/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregationTestCase.java @@ -117,7 +117,7 @@ public abstract class InternalMultiBucketAggregationTestCase extends InternalAggregationTestCase { +public abstract class InternalRangeTestCase extends InternalMultiBucketAggregationTestCase { private boolean keyed; @@ -40,13 +42,17 @@ public abstract class InternalRangeTestCase pipelineAggregators, Map metaData) { - return createTestInstance(name, pipelineAggregators, metaData, keyed); + protected T createTestInstance(String name, + List pipelineAggregators, + Map metaData, + InternalAggregations aggregations) { + return createTestInstance(name, pipelineAggregators, metaData, aggregations, keyed); } protected abstract T createTestInstance(String name, List pipelineAggregators, Map metaData, + InternalAggregations aggregations, boolean keyed); @Override protected void assertReduced(T reduced, List inputs) { @@ -65,4 +71,20 @@ public abstract class InternalRangeTestCase { format = randomNumericDocValueFormat(); final int interval = randomFrom(1, 5, 10, 25, 50, 100); - final int numRanges = 1;//randomIntBetween(1, 10); + final int numRanges = randomIntBetween(1, 10); List> listOfRanges = new ArrayList<>(numRanges); for (int i = 0; i < numRanges; i++) { @@ -58,11 +59,23 @@ public class InternalRangeTests extends InternalRangeTestCase { listOfRanges.add(Tuple.tuple(0.0, max / 2)); listOfRanges.add(Tuple.tuple(max / 3, max / 3 * 2)); } + if (rarely()) { + listOfRanges.add(Tuple.tuple(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY)); + } + if (rarely()) { + listOfRanges.add(Tuple.tuple(Double.NEGATIVE_INFINITY, randomDouble())); + } + if (rarely()) { + listOfRanges.add(Tuple.tuple(randomDouble(), Double.POSITIVE_INFINITY)); + } ranges = Collections.unmodifiableList(listOfRanges); } @Override - protected InternalRange createTestInstance(String name, List pipelineAggregators, Map metaData, + protected InternalRange createTestInstance(String name, + List pipelineAggregators, + Map metaData, + InternalAggregations aggregations, boolean keyed) { final List buckets = new ArrayList<>(); for (int i = 0; i < ranges.size(); ++i) { @@ -70,13 +83,18 @@ public class InternalRangeTests extends InternalRangeTestCase { int docCount = randomIntBetween(0, 1000); double from = range.v1(); double to = range.v2(); - buckets.add( new InternalRange.Bucket("range_" + i, from, to, docCount, InternalAggregations.EMPTY, keyed, format)); + buckets.add(new InternalRange.Bucket("range_" + i, from, to, docCount, aggregations, keyed, format)); } - return new InternalRange<>(name, buckets, format, keyed, pipelineAggregators, Collections.emptyMap()); + return new InternalRange<>(name, buckets, format, keyed, pipelineAggregators, metaData); } @Override protected Writeable.Reader instanceReader() { return InternalRange::new; } + + @Override + protected Class implementationClass() { + return ParsedRange.class; + } } diff --git a/core/src/test/java/org/elasticsearch/search/aggregations/bucket/range/date/InternalDateRangeTests.java b/core/src/test/java/org/elasticsearch/search/aggregations/bucket/range/date/InternalDateRangeTests.java index bbfcdf7463a..318e5f6b5ad 100644 --- a/core/src/test/java/org/elasticsearch/search/aggregations/bucket/range/date/InternalDateRangeTests.java +++ b/core/src/test/java/org/elasticsearch/search/aggregations/bucket/range/date/InternalDateRangeTests.java @@ -23,6 +23,7 @@ import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.aggregations.InternalAggregations; +import org.elasticsearch.search.aggregations.ParsedMultiBucketAggregation; import org.elasticsearch.search.aggregations.bucket.range.InternalRangeTestCase; import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; import org.joda.time.DateTime; @@ -78,6 +79,7 @@ public class InternalDateRangeTests extends InternalRangeTestCase pipelineAggregators, Map metaData, + InternalAggregations aggregations, boolean keyed) { final List buckets = new ArrayList<>(); for (int i = 0; i < dateRanges.size(); ++i) { @@ -85,7 +87,7 @@ public class InternalDateRangeTests extends InternalRangeTestCase instanceReader() { return InternalDateRange::new; } + + @Override + protected Class implementationClass() { + return ParsedDateRange.class; + } } diff --git a/core/src/test/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/InternalGeoDistanceTests.java b/core/src/test/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/InternalGeoDistanceTests.java index 9dd2a7a67c7..25c0a9ae3e1 100644 --- a/core/src/test/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/InternalGeoDistanceTests.java +++ b/core/src/test/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/InternalGeoDistanceTests.java @@ -22,6 +22,7 @@ package org.elasticsearch.search.aggregations.bucket.range.geodistance; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.search.aggregations.InternalAggregations; +import org.elasticsearch.search.aggregations.ParsedMultiBucketAggregation; import org.elasticsearch.search.aggregations.bucket.range.InternalRangeTestCase; import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; import org.junit.Before; @@ -58,6 +59,7 @@ public class InternalGeoDistanceTests extends InternalRangeTestCase instanceReader() { return InternalGeoDistance::new; @@ -67,6 +69,7 @@ public class InternalGeoDistanceTests extends InternalRangeTestCase pipelineAggregators, Map metaData, + InternalAggregations aggregations, boolean keyed) { final List buckets = new ArrayList<>(); for (int i = 0; i < geoDistanceRanges.size(); ++i) { @@ -74,8 +77,13 @@ public class InternalGeoDistanceTests extends InternalRangeTestCase implementationClass() { + return ParsedGeoDistance.class; + } } diff --git a/test/framework/src/main/java/org/elasticsearch/test/InternalAggregationTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/InternalAggregationTestCase.java index da7ac647c5c..66dd194e018 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/InternalAggregationTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/InternalAggregationTestCase.java @@ -59,6 +59,12 @@ import org.elasticsearch.search.aggregations.bucket.nested.ParsedReverseNested; import org.elasticsearch.search.aggregations.bucket.nested.ReverseNestedAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.sampler.InternalSampler; import org.elasticsearch.search.aggregations.bucket.sampler.ParsedSampler; +import org.elasticsearch.search.aggregations.bucket.range.ParsedRange; +import org.elasticsearch.search.aggregations.bucket.range.RangeAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.range.date.DateRangeAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.range.date.ParsedDateRange; +import org.elasticsearch.search.aggregations.bucket.range.geodistance.GeoDistanceAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.range.geodistance.ParsedGeoDistance; import org.elasticsearch.search.aggregations.bucket.terms.DoubleTerms; import org.elasticsearch.search.aggregations.bucket.terms.LongTerms; import org.elasticsearch.search.aggregations.bucket.terms.ParsedDoubleTerms; @@ -164,6 +170,9 @@ public abstract class InternalAggregationTestCase namedXContents.put(FilterAggregationBuilder.NAME, (p, c) -> ParsedFilter.fromXContent(p, (String) c)); namedXContents.put(InternalSampler.NAME, (p, c) -> ParsedSampler.fromXContent(p, (String) c)); namedXContents.put(GeoGridAggregationBuilder.NAME, (p, c) -> ParsedGeoHashGrid.fromXContent(p, (String) c)); + namedXContents.put(RangeAggregationBuilder.NAME, (p, c) -> ParsedRange.fromXContent(p, (String) c)); + namedXContents.put(DateRangeAggregationBuilder.NAME, (p, c) -> ParsedDateRange.fromXContent(p, (String) c)); + namedXContents.put(GeoDistanceAggregationBuilder.NAME, (p, c) -> ParsedGeoDistance.fromXContent(p, (String) c)); return namedXContents.entrySet().stream() .map(entry -> new NamedXContentRegistry.Entry(Aggregation.class, new ParseField(entry.getKey()), entry.getValue()))