diff --git a/core/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketmetrics/InternalBucketMetricValue.java b/core/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketmetrics/InternalBucketMetricValue.java index 9c9da2f26bd53..95b3782bd54f4 100644 --- a/core/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketmetrics/InternalBucketMetricValue.java +++ b/core/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketmetrics/InternalBucketMetricValue.java @@ -19,6 +19,7 @@ package org.elasticsearch.search.aggregations.pipeline.bucketmetrics; +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; @@ -28,11 +29,14 @@ import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; import java.io.IOException; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; public class InternalBucketMetricValue extends InternalNumericMetricsAggregation.SingleValue implements BucketMetricValue { public static final String NAME = "bucket_metric_value"; + static final ParseField KEYS_FIELD = new ParseField("keys"); private double value; private String[] keys; @@ -88,7 +92,7 @@ public Object getProperty(List path) { return this; } else if (path.size() == 1 && "value".equals(path.get(0))) { return value(); - } else if (path.size() == 1 && "keys".equals(path.get(0))) { + } else if (path.size() == 1 && KEYS_FIELD.getPreferredName().equals(path.get(0))) { return keys(); } else { throw new IllegalArgumentException("path not supported for [" + getName() + "]: " + path); @@ -102,7 +106,7 @@ public XContentBuilder doXContentBody(XContentBuilder builder, Params params) th if (hasValue && format != DocValueFormat.RAW) { builder.field(CommonFields.VALUE_AS_STRING.getPreferredName(), format.format(value)); } - builder.startArray("keys"); + builder.startArray(KEYS_FIELD.getPreferredName()); for (String key : keys) { builder.value(key); } @@ -110,4 +114,15 @@ public XContentBuilder doXContentBody(XContentBuilder builder, Params params) th return builder; } + @Override + protected int doHashCode() { + return Objects.hash(value, Arrays.hashCode(keys)); + } + + @Override + protected boolean doEquals(Object obj) { + InternalBucketMetricValue other = (InternalBucketMetricValue) obj; + return Objects.equals(value, other.value) + && Arrays.equals(keys, other.keys); + } } diff --git a/core/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketmetrics/ParsedBucketMetricValue.java b/core/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketmetrics/ParsedBucketMetricValue.java new file mode 100644 index 0000000000000..9008c56c5caaa --- /dev/null +++ b/core/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketmetrics/ParsedBucketMetricValue.java @@ -0,0 +1,73 @@ +/* + * 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.bucketmetrics; + +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.search.aggregations.metrics.ParsedSingleValueNumericMetricsAggregation; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +public class ParsedBucketMetricValue extends ParsedSingleValueNumericMetricsAggregation implements BucketMetricValue { + + private List keys = Collections.emptyList(); + + @Override + public String[] keys() { + return this.keys.toArray(new String[keys.size()]); + } + + @Override + protected String getType() { + return InternalBucketMetricValue.NAME; + } + + @Override + protected XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { + boolean hasValue = !Double.isInfinite(value); + builder.field(CommonFields.VALUE.getPreferredName(), hasValue ? value : null); + if (hasValue && valueAsString != null) { + builder.field(CommonFields.VALUE_AS_STRING.getPreferredName(), valueAsString); + } + builder.startArray(InternalBucketMetricValue.KEYS_FIELD.getPreferredName()); + for (String key : keys) { + builder.value(key); + } + builder.endArray(); + return builder; + } + + private static final ObjectParser PARSER = new ObjectParser<>( + ParsedBucketMetricValue.class.getSimpleName(), true, ParsedBucketMetricValue::new); + + static { + declareSingleValueFields(PARSER, Double.NEGATIVE_INFINITY); + PARSER.declareStringArray((agg, value) -> agg.keys = value, InternalBucketMetricValue.KEYS_FIELD); + } + + public static ParsedBucketMetricValue fromXContent(XContentParser parser, final String name) { + ParsedBucketMetricValue bucketMetricValue = PARSER.apply(parser, null); + bucketMetricValue.setName(name); + return bucketMetricValue; + } +} \ No newline at end of file diff --git a/core/src/test/java/org/elasticsearch/search/aggregations/InternalAggregationTestCase.java b/core/src/test/java/org/elasticsearch/search/aggregations/InternalAggregationTestCase.java index e3584f5a85c59..1588f59974b23 100644 --- a/core/src/test/java/org/elasticsearch/search/aggregations/InternalAggregationTestCase.java +++ b/core/src/test/java/org/elasticsearch/search/aggregations/InternalAggregationTestCase.java @@ -33,7 +33,6 @@ import org.elasticsearch.rest.action.search.RestSearchAction; import org.elasticsearch.script.ScriptService; import org.elasticsearch.search.DocValueFormat; -import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.SearchModule; import org.elasticsearch.search.aggregations.metrics.avg.AvgAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.avg.ParsedAvg; @@ -54,6 +53,8 @@ import org.elasticsearch.search.aggregations.pipeline.InternalSimpleValue; import org.elasticsearch.search.aggregations.pipeline.ParsedSimpleValue; import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; +import org.elasticsearch.search.aggregations.pipeline.bucketmetrics.InternalBucketMetricValue; +import org.elasticsearch.search.aggregations.pipeline.bucketmetrics.ParsedBucketMetricValue; import org.elasticsearch.search.aggregations.pipeline.derivative.DerivativePipelineAggregationBuilder; import org.elasticsearch.search.aggregations.pipeline.derivative.ParsedDerivative; import org.elasticsearch.test.AbstractWireSerializingTestCase; @@ -91,6 +92,7 @@ static List getNamedXContents() { namedXContents.put(ValueCountAggregationBuilder.NAME, (p, c) -> ParsedValueCount.fromXContent(p, (String) c)); namedXContents.put(InternalSimpleValue.NAME, (p, c) -> ParsedSimpleValue.fromXContent(p, (String) c)); namedXContents.put(DerivativePipelineAggregationBuilder.NAME, (p, c) -> ParsedDerivative.fromXContent(p, (String) c)); + namedXContents.put(InternalBucketMetricValue.NAME, (p, c) -> ParsedBucketMetricValue.fromXContent(p, (String) c)); return namedXContents.entrySet().stream() .map(entry -> new NamedXContentRegistry.Entry(Aggregation.class, new ParseField(entry.getKey()), entry.getValue())) diff --git a/core/src/test/java/org/elasticsearch/search/aggregations/pipeline/bucketmetrics/InternalBucketMetricValueTests.java b/core/src/test/java/org/elasticsearch/search/aggregations/pipeline/bucketmetrics/InternalBucketMetricValueTests.java new file mode 100644 index 0000000000000..8de1700141ba3 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/search/aggregations/pipeline/bucketmetrics/InternalBucketMetricValueTests.java @@ -0,0 +1,75 @@ +/* + * 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.bucketmetrics; + +import org.elasticsearch.common.io.stream.Writeable.Reader; +import org.elasticsearch.search.aggregations.InternalAggregationTestCase; +import org.elasticsearch.search.aggregations.ParsedAggregation; +import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class InternalBucketMetricValueTests extends InternalAggregationTestCase { + + @Override + protected InternalBucketMetricValue createTestInstance(String name, List pipelineAggregators, + Map metaData) { + double value = frequently() ? randomDoubleBetween(-10000, 100000, true) + : randomFrom(new Double[] { Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Double.NaN }); + String[] keys = new String[randomIntBetween(0, 5)]; + for (int i = 0; i < keys.length; i++) { + keys[i] = randomAlphaOfLength(10); + } + return new InternalBucketMetricValue(name, keys, value, randomNumericDocValueFormat(), pipelineAggregators, metaData); + } + + @Override + public void testReduceRandom() { + expectThrows(UnsupportedOperationException.class, + () -> createTestInstance("name", Collections.emptyList(), null).reduce(null, + null)); + } + + @Override + protected void assertReduced(InternalBucketMetricValue reduced, List inputs) { + // no test since reduce operation is unsupported + } + + @Override + protected Reader instanceReader() { + return InternalBucketMetricValue::new; + } + + @Override + protected void assertFromXContent(InternalBucketMetricValue bucketMetricValue, ParsedAggregation parsedAggregation) { + BucketMetricValue parsed = ((BucketMetricValue) parsedAggregation); + assertArrayEquals(bucketMetricValue.keys(), parsed.keys()); + if (Double.isInfinite(bucketMetricValue.value()) == false) { + assertEquals(bucketMetricValue.value(), parsed.value(), 0); + assertEquals(bucketMetricValue.getValueAsString(), parsed.getValueAsString()); + } else { + // we write Double.NEGATIVE_INFINITY and Double.POSITIVE_INFINITY to xContent as 'null', so we + // cannot differentiate between them. Also we cannot recreate the exact String representation + assertEquals(parsed.value(), Double.NEGATIVE_INFINITY, 0); + } + } +}