Support multiple metrics in `top_metrics` agg (backport of #52965) (#53163)

This adds support for returning multiple metrics to the `top_metrics`
agg. It looks like:
```
POST /test/_search?filter_path=aggregations
{
  "aggs": {
    "tm": {
      "top_metrics": {
        "metrics": [
          {"field": "v"},
          {"field": "m"}
        ],
        "sort": {"s": "desc"}
      }
    }
  }
}
```
This commit is contained in:
Nik Everett 2020-03-05 08:12:01 -05:00 committed by GitHub
parent 01504df876
commit 28df7ae5ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 479 additions and 201 deletions

View File

@ -32,6 +32,8 @@ import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortBuilder; import org.elasticsearch.search.sort.SortBuilder;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
@ -49,20 +51,20 @@ public class TopMetricsAggregationBuilder extends AbstractAggregationBuilder<Top
private final SortBuilder<?> sort; private final SortBuilder<?> sort;
private final int size; private final int size;
private final String metric; private final List<String> metrics;
/** /**
* Build the request. * Build the request.
* @param name the name of the metric * @param name the name of the metric
* @param sort the sort key used to select the top metrics * @param sort the sort key used to select the top metrics
* @param size number of results to return per bucket * @param size number of results to return per bucket
* @param metric the name of the field to select * @param metrics the names of the fields to select
*/ */
public TopMetricsAggregationBuilder(String name, SortBuilder<?> sort, int size, String metric) { public TopMetricsAggregationBuilder(String name, SortBuilder<?> sort, int size, String... metrics) {
super(name); super(name);
this.sort = sort; this.sort = sort;
this.size = size; this.size = size;
this.metric = metric; this.metrics = Arrays.asList(metrics);
} }
@Override @Override
@ -78,7 +80,11 @@ public class TopMetricsAggregationBuilder extends AbstractAggregationBuilder<Top
sort.toXContent(builder, params); sort.toXContent(builder, params);
builder.endArray(); builder.endArray();
builder.field("size", size); builder.field("size", size);
builder.startObject("metric").field("field", metric).endObject(); builder.startArray("metrics");
for (String metric: metrics) {
builder.startObject().field("field", metric).endObject();
}
builder.endArray();
} }
return builder.endObject(); return builder.endObject();
} }

View File

@ -74,6 +74,20 @@ public class AnalyticsAggsIT extends ESRestHighLevelClientTestCase {
assertThat(metric.getMetrics(), equalTo(singletonMap("v", 3.0))); assertThat(metric.getMetrics(), equalTo(singletonMap("v", 3.0)));
} }
public void testTopMetricsManyMetrics() throws IOException {
indexTopMetricsData();
SearchRequest search = new SearchRequest("test");
search.source().aggregation(new TopMetricsAggregationBuilder(
"test", new FieldSortBuilder("s").order(SortOrder.DESC), 1, "v", "m"));
SearchResponse response = highLevelClient().search(search, RequestOptions.DEFAULT);
ParsedTopMetrics top = response.getAggregations().get("test");
assertThat(top.getTopMetrics(), hasSize(1));
ParsedTopMetrics.TopMetrics metric = top.getTopMetrics().get(0);
assertThat(metric.getSort(), equalTo(singletonList(2)));
assertThat(metric.getMetrics(), hasEntry("v", 3.0));
assertThat(metric.getMetrics(), hasEntry("m", 13.0));
}
public void testTopMetricsSizeTwo() throws IOException { public void testTopMetricsSizeTwo() throws IOException {
indexTopMetricsData(); indexTopMetricsData();
SearchRequest search = new SearchRequest("test"); SearchRequest search = new SearchRequest("test");
@ -92,8 +106,8 @@ public class AnalyticsAggsIT extends ESRestHighLevelClientTestCase {
private void indexTopMetricsData() throws IOException { private void indexTopMetricsData() throws IOException {
BulkRequest bulk = new BulkRequest("test").setRefreshPolicy(RefreshPolicy.IMMEDIATE); BulkRequest bulk = new BulkRequest("test").setRefreshPolicy(RefreshPolicy.IMMEDIATE);
bulk.add(new IndexRequest().source(XContentType.JSON, "s", 1, "v", 2)); bulk.add(new IndexRequest().source(XContentType.JSON, "s", 1, "v", 2.0, "m", 12.0));
bulk.add(new IndexRequest().source(XContentType.JSON, "s", 2, "v", 3)); bulk.add(new IndexRequest().source(XContentType.JSON, "s", 2, "v", 3.0, "m", 13.0));
highLevelClient().bulk(bulk, RequestOptions.DEFAULT); highLevelClient().bulk(bulk, RequestOptions.DEFAULT);
} }
} }

View File

@ -22,7 +22,7 @@ POST /test/_search?filter_path=aggregations
"aggs": { "aggs": {
"tm": { "tm": {
"top_metrics": { "top_metrics": {
"metric": {"field": "v"}, "metrics": {"field": "v"},
"sort": {"s": "desc"} "sort": {"s": "desc"}
} }
} }
@ -68,11 +68,61 @@ request. So,
NOTE: This aggregation doesn't support any sort of "tie breaking". If two documents have NOTE: This aggregation doesn't support any sort of "tie breaking". If two documents have
the same sort values then this aggregation could return either document's fields. the same sort values then this aggregation could return either document's fields.
==== `metric` ==== `metrics`
`metrics` selects the fields to of the "top" document to return. Like most other
aggregations, `top_metrics` casts these values cast to `double` precision
floating point numbers. So they have to be numeric. Dates *work*, but they
come back as a `double` precision floating point containing milliseconds since
epoch. `keyword` fields aren't allowed.
You can return multiple metrics by providing a list:
[source,console,id=search-aggregations-metrics-top-metrics-list-of-metrics]
----
POST /test/_bulk?refresh
{"index": {}}
{"s": 1, "v": 3.1415, "m": 1.9}
{"index": {}}
{"s": 2, "v": 1.0, "m": 6.7}
{"index": {}}
{"s": 3, "v": 2.71828, "m": -12.2}
POST /test/_search?filter_path=aggregations
{
"aggs": {
"tm": {
"top_metrics": {
"metrics": [
{"field": "v"},
{"field": "m"}
],
"sort": {"s": "desc"}
}
}
}
}
----
Which returns:
[source,js]
----
{
"aggregations": {
"tm": {
"top": [ {
"sort": [3],
"metrics": {
"v": 2.718280076980591,
"m": -12.199999809265137
}
} ]
}
}
}
----
// TESTRESPONSE
At this point `metric` supports only `{"field": "field_name"}` and all metrics
are returned as double precision floating point numbers. Expect more to
come here.
==== `size` ==== `size`
@ -92,7 +142,7 @@ POST /test/_search?filter_path=aggregations
"aggs": { "aggs": {
"tm": { "tm": {
"top_metrics": { "top_metrics": {
"metric": {"field": "v"}, "metrics": {"field": "v"},
"sort": {"s": "desc"}, "sort": {"s": "desc"},
"size": 2 "size": 2
} }
@ -174,7 +224,7 @@ POST /node/_search?filter_path=aggregations
"aggs": { "aggs": {
"tm": { "tm": {
"top_metrics": { "top_metrics": {
"metric": {"field": "v"}, "metrics": {"field": "v"},
"sort": {"date": "desc"} "sort": {"date": "desc"}
} }
} }
@ -230,7 +280,7 @@ POST /node/_search?filter_path=aggregations
"aggs": { "aggs": {
"tm": { "tm": {
"top_metrics": { "top_metrics": {
"metric": {"field": "v"}, "metrics": {"field": "v"},
"sort": {"date": "desc"} "sort": {"date": "desc"}
} }
} }
@ -292,7 +342,7 @@ POST /test*/_search?filter_path=aggregations
"aggs": { "aggs": {
"tm": { "tm": {
"top_metrics": { "top_metrics": {
"metric": {"field": "v"}, "metrics": {"field": "v"},
"sort": {"s": "asc"} "sort": {"s": "asc"}
} }
} }
@ -325,7 +375,7 @@ POST /test*/_search?filter_path=aggregations
"aggs": { "aggs": {
"tm": { "tm": {
"top_metrics": { "top_metrics": {
"metric": {"field": "v"}, "metrics": {"field": "v"},
"sort": {"s": {"order": "asc", "numeric_type": "double"}} "sort": {"s": {"order": "asc", "numeric_type": "double"}}
} }
} }

View File

@ -51,6 +51,7 @@ import org.elasticsearch.cluster.metadata.IndexMetaData;
import org.elasticsearch.common.CheckedConsumer; import org.elasticsearch.common.CheckedConsumer;
import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.BigArrays;
import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.index.mapper.ContentPath; import org.elasticsearch.index.mapper.ContentPath;
import org.elasticsearch.index.mapper.DateFieldMapper; import org.elasticsearch.index.mapper.DateFieldMapper;
@ -180,8 +181,9 @@ public class MinAggregatorTests extends AggregatorTestCase {
@Override @Override
protected QueryShardContext queryShardContextMock(IndexSearcher searcher, MapperService mapperService, protected QueryShardContext queryShardContextMock(IndexSearcher searcher, MapperService mapperService,
IndexSettings indexSettings, CircuitBreakerService circuitBreakerService) { IndexSettings indexSettings, CircuitBreakerService circuitBreakerService,
this.queryShardContext = super.queryShardContextMock(searcher, mapperService, indexSettings, circuitBreakerService); BigArrays bigArrays) {
this.queryShardContext = super.queryShardContextMock(searcher, mapperService, indexSettings, circuitBreakerService, bigArrays);
return queryShardContext; return queryShardContext;
} }

View File

@ -421,11 +421,12 @@ public class ScriptedMetricAggregatorTests extends AggregatorTestCase {
protected QueryShardContext queryShardContextMock(IndexSearcher searcher, protected QueryShardContext queryShardContextMock(IndexSearcher searcher,
MapperService mapperService, MapperService mapperService,
IndexSettings indexSettings, IndexSettings indexSettings,
CircuitBreakerService circuitBreakerService) { CircuitBreakerService circuitBreakerService,
BigArrays bigArrays) {
MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME, SCRIPTS, Collections.emptyMap()); MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME, SCRIPTS, Collections.emptyMap());
Map<String, ScriptEngine> engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine); Map<String, ScriptEngine> engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine);
ScriptService scriptService = new ScriptService(Settings.EMPTY, engines, ScriptModule.CORE_CONTEXTS); ScriptService scriptService = new ScriptService(Settings.EMPTY, engines, ScriptModule.CORE_CONTEXTS);
return new QueryShardContext(0, indexSettings, BigArrays.NON_RECYCLING_INSTANCE, null, null, mapperService, null, scriptService, return new QueryShardContext(0, indexSettings, bigArrays, null, null, mapperService, null, scriptService,
xContentRegistry(), writableRegistry(), null, null, System::currentTimeMillis, null, null, () -> true); xContentRegistry(), writableRegistry(), null, null, System::currentTimeMillis, null, null, () -> true);
} }
} }

View File

@ -243,7 +243,7 @@ public abstract class AggregatorTestCase extends ESTestCase {
when(searchContext.lookup()).thenReturn(searchLookup); when(searchContext.lookup()).thenReturn(searchLookup);
QueryShardContext queryShardContext = QueryShardContext queryShardContext =
queryShardContextMock(contextIndexSearcher, mapperService, indexSettings, circuitBreakerService); queryShardContextMock(contextIndexSearcher, mapperService, indexSettings, circuitBreakerService, bigArrays);
when(searchContext.getQueryShardContext()).thenReturn(queryShardContext); when(searchContext.getQueryShardContext()).thenReturn(queryShardContext);
when(queryShardContext.getObjectMapper(anyString())).thenAnswer(invocation -> { when(queryShardContext.getObjectMapper(anyString())).thenAnswer(invocation -> {
String fieldName = (String) invocation.getArguments()[0]; String fieldName = (String) invocation.getArguments()[0];
@ -293,9 +293,10 @@ public abstract class AggregatorTestCase extends ESTestCase {
protected QueryShardContext queryShardContextMock(IndexSearcher searcher, protected QueryShardContext queryShardContextMock(IndexSearcher searcher,
MapperService mapperService, MapperService mapperService,
IndexSettings indexSettings, IndexSettings indexSettings,
CircuitBreakerService circuitBreakerService) { CircuitBreakerService circuitBreakerService,
BigArrays bigArrays) {
return new QueryShardContext(0, indexSettings, BigArrays.NON_RECYCLING_INSTANCE, null, return new QueryShardContext(0, indexSettings, bigArrays, null,
getIndexFieldDataLookup(mapperService, circuitBreakerService), getIndexFieldDataLookup(mapperService, circuitBreakerService),
mapperService, null, getMockScriptService(), xContentRegistry(), mapperService, null, getMockScriptService(), xContentRegistry(),
writableRegistry(), null, searcher, System::currentTimeMillis, null, null, () -> true); writableRegistry(), null, searcher, System::currentTimeMillis, null, null, () -> true);

View File

@ -20,23 +20,27 @@ import org.elasticsearch.search.sort.SortValue;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static org.elasticsearch.search.builder.SearchSourceBuilder.SORT_FIELD;
import static org.elasticsearch.xpack.analytics.topmetrics.TopMetricsAggregationBuilder.METRIC_FIELD;
public class InternalTopMetrics extends InternalNumericMetricsAggregation.MultiValue { public class InternalTopMetrics extends InternalNumericMetricsAggregation.MultiValue {
private final SortOrder sortOrder; private final SortOrder sortOrder;
private final int size; private final int size;
private final String metricName; private final List<String> metricNames;
private final List<TopMetric> topMetrics; private final List<TopMetric> topMetrics;
public InternalTopMetrics(String name, @Nullable SortOrder sortOrder, String metricName, public InternalTopMetrics(String name, @Nullable SortOrder sortOrder, List<String> metricNames,
int size, List<TopMetric> topMetrics, List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) { int size, List<TopMetric> topMetrics, List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) {
super(name, pipelineAggregators, metaData); super(name, pipelineAggregators, metaData);
this.sortOrder = sortOrder; this.sortOrder = sortOrder;
this.metricName = metricName; this.metricNames = metricNames;
/* /*
* topMetrics.size won't be size when the bucket doesn't have size docs! * topMetrics.size won't be size when the bucket doesn't have size docs!
*/ */
@ -44,9 +48,9 @@ public class InternalTopMetrics extends InternalNumericMetricsAggregation.MultiV
this.topMetrics = topMetrics; this.topMetrics = topMetrics;
} }
static InternalTopMetrics buildEmptyAggregation(String name, String metricField, static InternalTopMetrics buildEmptyAggregation(String name, List<String> metricNames,
List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) { List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) {
return new InternalTopMetrics(name, SortOrder.ASC, metricField, 0, emptyList(), pipelineAggregators, metaData); return new InternalTopMetrics(name, SortOrder.ASC, metricNames, 0, emptyList(), pipelineAggregators, metaData);
} }
/** /**
@ -55,7 +59,7 @@ public class InternalTopMetrics extends InternalNumericMetricsAggregation.MultiV
public InternalTopMetrics(StreamInput in) throws IOException { public InternalTopMetrics(StreamInput in) throws IOException {
super(in); super(in);
sortOrder = SortOrder.readFromStream(in); sortOrder = SortOrder.readFromStream(in);
metricName = in.readString(); metricNames = in.readStringList();
size = in.readVInt(); size = in.readVInt();
topMetrics = in.readList(TopMetric::new); topMetrics = in.readList(TopMetric::new);
} }
@ -63,7 +67,7 @@ public class InternalTopMetrics extends InternalNumericMetricsAggregation.MultiV
@Override @Override
protected void doWriteTo(StreamOutput out) throws IOException { protected void doWriteTo(StreamOutput out) throws IOException {
sortOrder.writeTo(out); sortOrder.writeTo(out);
out.writeString(metricName); out.writeStringCollection(metricNames);
out.writeVInt(size); out.writeVInt(size);
out.writeList(topMetrics); out.writeList(topMetrics);
} }
@ -78,15 +82,19 @@ public class InternalTopMetrics extends InternalNumericMetricsAggregation.MultiV
if (path.isEmpty()) { if (path.isEmpty()) {
return this; return this;
} }
if (path.size() == 1 && metricName.contentEquals(path.get(1))) { if (path.size() != 1) {
if (topMetrics.isEmpty()) { throw new IllegalArgumentException("path not supported for [" + getName() + "]: " + path);
// Unmapped.
return null;
}
assert topMetrics.size() == 1 : "property paths should only resolve against top metrics with size == 1.";
return topMetrics.get(0).metricValue;
} }
throw new IllegalArgumentException("path not supported for [" + getName() + "]: " + path); int index = metricNames.indexOf(path.get(0));
if (index < 0) {
throw new IllegalArgumentException("path not supported for [" + getName() + "]: " + path);
}
if (topMetrics.isEmpty()) {
// Unmapped.
return null;
}
assert topMetrics.size() == 1 : "property paths should only resolve against top metrics with size == 1.";
return topMetrics.get(0).metricValues[index];
} }
@Override @Override
@ -116,7 +124,7 @@ public class InternalTopMetrics extends InternalNumericMetricsAggregation.MultiV
queue.updateTop(); queue.updateTop();
} }
} }
return new InternalTopMetrics(getName(), sortOrder, metricName, size, merged, pipelineAggregators(), getMetaData()); return new InternalTopMetrics(getName(), sortOrder, metricNames, size, merged, pipelineAggregators(), getMetaData());
} }
@Override @Override
@ -128,7 +136,7 @@ public class InternalTopMetrics extends InternalNumericMetricsAggregation.MultiV
public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException {
builder.startArray("top"); builder.startArray("top");
for (TopMetric top : topMetrics) { for (TopMetric top : topMetrics) {
top.toXContent(builder, metricName); top.toXContent(builder, metricNames);
} }
builder.endArray(); builder.endArray();
return builder; return builder;
@ -136,7 +144,7 @@ public class InternalTopMetrics extends InternalNumericMetricsAggregation.MultiV
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(super.hashCode(), sortOrder, metricName, size, topMetrics); return Objects.hash(super.hashCode(), sortOrder, metricNames, size, topMetrics);
} }
@Override @Override
@ -144,21 +152,22 @@ public class InternalTopMetrics extends InternalNumericMetricsAggregation.MultiV
if (super.equals(obj) == false) return false; if (super.equals(obj) == false) return false;
InternalTopMetrics other = (InternalTopMetrics) obj; InternalTopMetrics other = (InternalTopMetrics) obj;
return sortOrder.equals(other.sortOrder) && return sortOrder.equals(other.sortOrder) &&
metricName.equals(other.metricName) && metricNames.equals(other.metricNames) &&
size == other.size && size == other.size &&
topMetrics.equals(other.topMetrics); topMetrics.equals(other.topMetrics);
} }
@Override @Override
public double value(String name) { public double value(String name) {
if (metricName.equals(name)) { int index = metricNames.indexOf(name);
if (topMetrics.isEmpty()) { if (index < 0) {
return Double.NaN; throw new IllegalArgumentException("unknown metric [" + name + "]");
}
assert topMetrics.size() == 1 : "property paths should only resolve against top metrics with size == 1.";
return topMetrics.get(0).metricValue;
} }
throw new IllegalArgumentException("known metric [" + name + "]"); if (topMetrics.isEmpty()) {
return Double.NaN;
}
assert topMetrics.size() == 1 : "property paths should only resolve against top metrics with size == 1.";
return topMetrics.get(0).metricValues[index];
} }
SortOrder getSortOrder() { SortOrder getSortOrder() {
@ -169,8 +178,8 @@ public class InternalTopMetrics extends InternalNumericMetricsAggregation.MultiV
return size; return size;
} }
String getMetricName() { List<String> getMetricNames() {
return metricName; return metricNames;
} }
List<TopMetric> getTopMetrics() { List<TopMetric> getTopMetrics() {
@ -197,18 +206,25 @@ public class InternalTopMetrics extends InternalNumericMetricsAggregation.MultiV
static class TopMetric implements Writeable, Comparable<TopMetric> { static class TopMetric implements Writeable, Comparable<TopMetric> {
private final DocValueFormat sortFormat; private final DocValueFormat sortFormat;
private final SortValue sortValue; private final SortValue sortValue;
private final double metricValue; private final double[] metricValues;
TopMetric(DocValueFormat sortFormat, SortValue sortValue, double metricValue) { TopMetric(DocValueFormat sortFormat, SortValue sortValue, double[] metricValues) {
this.sortFormat = sortFormat; this.sortFormat = sortFormat;
this.sortValue = sortValue; this.sortValue = sortValue;
this.metricValue = metricValue; this.metricValues = metricValues;
} }
TopMetric(StreamInput in) throws IOException { TopMetric(StreamInput in) throws IOException {
sortFormat = in.readNamedWriteable(DocValueFormat.class); sortFormat = in.readNamedWriteable(DocValueFormat.class);
sortValue = in.readNamedWriteable(SortValue.class); sortValue = in.readNamedWriteable(SortValue.class);
metricValue = in.readDouble(); metricValues = in.readDoubleArray();
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeNamedWriteable(sortFormat);
out.writeNamedWriteable(sortValue);
out.writeDoubleArray(metricValues);
} }
DocValueFormat getSortFormat() { DocValueFormat getSortFormat() {
@ -219,26 +235,21 @@ public class InternalTopMetrics extends InternalNumericMetricsAggregation.MultiV
return sortValue; return sortValue;
} }
double getMetricValue() { double[] getMetricValues() {
return metricValue; return metricValues;
} }
@Override public XContentBuilder toXContent(XContentBuilder builder, List<String> metricNames) throws IOException {
public void writeTo(StreamOutput out) throws IOException {
out.writeNamedWriteable(sortFormat);
out.writeNamedWriteable(sortValue);
out.writeDouble(metricValue);
}
public XContentBuilder toXContent(XContentBuilder builder, String metricName) throws IOException {
builder.startObject(); builder.startObject();
{ {
builder.startArray("sort"); builder.startArray(SORT_FIELD.getPreferredName());
sortValue.toXContent(builder, sortFormat); sortValue.toXContent(builder, sortFormat);
builder.endArray(); builder.endArray();
builder.startObject("metrics"); builder.startObject(METRIC_FIELD.getPreferredName());
{ {
builder.field(metricName, Double.isNaN(metricValue) ? null : metricValue); for (int i = 0; i < metricValues.length; i++) {
builder.field(metricNames.get(i), Double.isNaN(metricValues[i]) ? null : metricValues[i]);
}
} }
builder.endObject(); builder.endObject();
} }
@ -258,17 +269,17 @@ public class InternalTopMetrics extends InternalNumericMetricsAggregation.MultiV
TopMetric other = (TopMetric) obj; TopMetric other = (TopMetric) obj;
return sortFormat.equals(other.sortFormat) return sortFormat.equals(other.sortFormat)
&& sortValue.equals(other.sortValue) && sortValue.equals(other.sortValue)
&& metricValue == other.metricValue; && Arrays.equals(metricValues, other.metricValues);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(sortFormat, sortValue, metricValue); return Objects.hash(sortFormat, sortValue, Arrays.hashCode(metricValues));
} }
@Override @Override
public String toString() { public String toString() {
return "TopMetric[" + sortFormat + "," + sortValue + "," + metricValue + "]"; return "TopMetric[" + sortFormat + "," + sortValue + "," + Arrays.toString(metricValues) + "]";
} }
} }
} }

View File

@ -32,7 +32,7 @@ import static org.elasticsearch.search.builder.SearchSourceBuilder.SORT_FIELD;
public class TopMetricsAggregationBuilder extends AbstractAggregationBuilder<TopMetricsAggregationBuilder> { public class TopMetricsAggregationBuilder extends AbstractAggregationBuilder<TopMetricsAggregationBuilder> {
public static final String NAME = "top_metrics"; public static final String NAME = "top_metrics";
public static final ParseField METRIC_FIELD = new ParseField("metric"); public static final ParseField METRIC_FIELD = new ParseField("metrics");
/** /**
* Default to returning only a single top metric. * Default to returning only a single top metric.
@ -47,34 +47,35 @@ public class TopMetricsAggregationBuilder extends AbstractAggregationBuilder<Top
if (size < 1) { if (size < 1) {
throw new IllegalArgumentException("[size] must be more than 0 but was [" + size + "]"); throw new IllegalArgumentException("[size] must be more than 0 but was [" + size + "]");
} }
MultiValuesSourceFieldConfig metricField = (MultiValuesSourceFieldConfig) args[2]; @SuppressWarnings("unchecked")
return new TopMetricsAggregationBuilder(name, sorts, size, metricField); List<MultiValuesSourceFieldConfig> metricFields = (List<MultiValuesSourceFieldConfig>) args[2];
return new TopMetricsAggregationBuilder(name, sorts, size, metricFields);
}); });
static { static {
PARSER.declareField(constructorArg(), (p, n) -> SortBuilder.fromXContent(p), SORT_FIELD, PARSER.declareField(constructorArg(), (p, n) -> SortBuilder.fromXContent(p), SORT_FIELD,
ObjectParser.ValueType.OBJECT_ARRAY_OR_STRING); ObjectParser.ValueType.OBJECT_ARRAY_OR_STRING);
PARSER.declareInt(optionalConstructorArg(), SIZE_FIELD); PARSER.declareInt(optionalConstructorArg(), SIZE_FIELD);
ContextParser<Void, MultiValuesSourceFieldConfig.Builder> metricParser = MultiValuesSourceFieldConfig.PARSER.apply(true, false); ContextParser<Void, MultiValuesSourceFieldConfig.Builder> metricParser = MultiValuesSourceFieldConfig.PARSER.apply(true, false);
PARSER.declareObject(constructorArg(), (p, n) -> metricParser.parse(p, null).build(), METRIC_FIELD); PARSER.declareObjectArray(constructorArg(), (p, n) -> metricParser.parse(p, null).build(), METRIC_FIELD);
} }
private final List<SortBuilder<?>> sortBuilders; private final List<SortBuilder<?>> sortBuilders;
// TODO MultiValuesSourceFieldConfig has more things than we support and less things than we want to support
private final int size; private final int size;
private final MultiValuesSourceFieldConfig metricField; private final List<MultiValuesSourceFieldConfig> metricFields;
// TODO replace with ValuesSourceConfig once the value source refactor has landed
/** /**
* Build a {@code top_metrics} aggregation request. * Build a {@code top_metrics} aggregation request.
*/ */
public TopMetricsAggregationBuilder(String name, List<SortBuilder<?>> sortBuilders, int size, public TopMetricsAggregationBuilder(String name, List<SortBuilder<?>> sortBuilders, int size,
MultiValuesSourceFieldConfig metricField) { List<MultiValuesSourceFieldConfig> metricFields) {
super(name); super(name);
if (sortBuilders.size() != 1) { if (sortBuilders.size() != 1) {
throw new IllegalArgumentException("[sort] must contain exactly one sort"); throw new IllegalArgumentException("[sort] must contain exactly one sort");
} }
this.sortBuilders = sortBuilders; this.sortBuilders = sortBuilders;
this.size = size; this.size = size;
this.metricField = metricField; this.metricFields = metricFields;
} }
/** /**
@ -85,7 +86,7 @@ public class TopMetricsAggregationBuilder extends AbstractAggregationBuilder<Top
super(clone, factoriesBuilder, metaData); super(clone, factoriesBuilder, metaData);
this.sortBuilders = clone.sortBuilders; this.sortBuilders = clone.sortBuilders;
this.size = clone.size; this.size = clone.size;
this.metricField = clone.metricField; this.metricFields = clone.metricFields;
} }
/** /**
@ -97,14 +98,14 @@ public class TopMetricsAggregationBuilder extends AbstractAggregationBuilder<Top
List<SortBuilder<?>> sortBuilders = (List<SortBuilder<?>>) (List<?>) in.readNamedWriteableList(SortBuilder.class); List<SortBuilder<?>> sortBuilders = (List<SortBuilder<?>>) (List<?>) in.readNamedWriteableList(SortBuilder.class);
this.sortBuilders = sortBuilders; this.sortBuilders = sortBuilders;
this.size = in.readVInt(); this.size = in.readVInt();
this.metricField = new MultiValuesSourceFieldConfig(in); this.metricFields = in.readList(MultiValuesSourceFieldConfig::new);
} }
@Override @Override
protected void doWriteTo(StreamOutput out) throws IOException { protected void doWriteTo(StreamOutput out) throws IOException {
out.writeNamedWriteableList(sortBuilders); out.writeNamedWriteableList(sortBuilders);
out.writeVInt(size); out.writeVInt(size);
metricField.writeTo(out); out.writeList(metricFields);
} }
@Override @Override
@ -116,7 +117,7 @@ public class TopMetricsAggregationBuilder extends AbstractAggregationBuilder<Top
protected AggregatorFactory doBuild(QueryShardContext queryShardContext, AggregatorFactory parent, Builder subFactoriesBuilder) protected AggregatorFactory doBuild(QueryShardContext queryShardContext, AggregatorFactory parent, Builder subFactoriesBuilder)
throws IOException { throws IOException {
return new TopMetricsAggregatorFactory(name, queryShardContext, parent, subFactoriesBuilder, metaData, sortBuilders, return new TopMetricsAggregatorFactory(name, queryShardContext, parent, subFactoriesBuilder, metaData, sortBuilders,
size, metricField); size, metricFields);
} }
@Override @Override
@ -129,7 +130,11 @@ public class TopMetricsAggregationBuilder extends AbstractAggregationBuilder<Top
} }
builder.endArray(); builder.endArray();
builder.field(SIZE_FIELD.getPreferredName(), size); builder.field(SIZE_FIELD.getPreferredName(), size);
builder.field(METRIC_FIELD.getPreferredName(), metricField); builder.startArray(METRIC_FIELD.getPreferredName());
for (MultiValuesSourceFieldConfig metricField: metricFields) {
metricField.toXContent(builder, params);
}
builder.endArray();
} }
builder.endObject(); builder.endObject();
return builder; return builder;
@ -148,7 +153,7 @@ public class TopMetricsAggregationBuilder extends AbstractAggregationBuilder<Top
return size; return size;
} }
MultiValuesSourceFieldConfig getMetricField() { List<MultiValuesSourceFieldConfig> getMetricFields() {
return metricField; return metricFields;
} }
} }

View File

@ -14,6 +14,7 @@ import org.elasticsearch.common.lease.Releasables;
import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.BigArrays;
import org.elasticsearch.common.util.DoubleArray; import org.elasticsearch.common.util.DoubleArray;
import org.elasticsearch.index.fielddata.NumericDoubleValues; import org.elasticsearch.index.fielddata.NumericDoubleValues;
import org.elasticsearch.index.query.QueryShardContext;
import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.DocValueFormat;
import org.elasticsearch.search.MultiValueMode; import org.elasticsearch.search.MultiValueMode;
import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.Aggregator;
@ -46,25 +47,23 @@ import java.util.Map;
*/ */
class TopMetricsAggregator extends NumericMetricsAggregator.MultiValue { class TopMetricsAggregator extends NumericMetricsAggregator.MultiValue {
private final int size; private final int size;
private final String metricName;
private final BucketedSort sort; private final BucketedSort sort;
private final Values values; private final Metrics metrics;
private final ValuesSource.Numeric metricValueSource;
TopMetricsAggregator(String name, SearchContext context, Aggregator parent, List<PipelineAggregator> pipelineAggregators, TopMetricsAggregator(String name, SearchContext context, Aggregator parent, List<PipelineAggregator> pipelineAggregators,
Map<String, Object> metaData, int size, String metricName, Map<String, Object> metaData, int size,
SortBuilder<?> sort, ValuesSource.Numeric metricValueSource) throws IOException { SortBuilder<?> sort, List<String> metricNames, List<ValuesSource.Numeric> metricValuesSources) throws IOException {
super(name, context, parent, pipelineAggregators, metaData); super(name, context, parent, pipelineAggregators, metaData);
this.size = size; this.size = size;
this.metricName = metricName; assert metricNames.size() == metricValuesSources.size();
this.metricValueSource = metricValueSource; metrics = new Metrics(size, context.getQueryShardContext(), metricNames, metricValuesSources);
if (metricValueSource != null) { /*
values = new Values(size, context.bigArrays(), metricValueSource); * If we're only collecting a single value then only provided *that*
this.sort = sort.buildBucketedSort(context.getQueryShardContext(), size, values); * value to the sort so that swaps and loads are just a little faster
} else { * in that *very* common case.
values = null; */
this.sort = null; BucketedSort.ExtraData values = metricValuesSources.size() == 1 ? metrics.values[0] : metrics;
} this.sort = sort.buildBucketedSort(context.getQueryShardContext(), size, values);
} }
@Override @Override
@ -72,7 +71,7 @@ class TopMetricsAggregator extends NumericMetricsAggregator.MultiValue {
if (size != 1) { if (size != 1) {
throw new IllegalArgumentException("[top_metrics] can only the be target if [size] is [1] but was [" + size + "]"); throw new IllegalArgumentException("[top_metrics] can only the be target if [size] is [1] but was [" + size + "]");
} }
return metricName.equals(name); return metrics.names.contains(name);
} }
@Override @Override
@ -84,12 +83,12 @@ class TopMetricsAggregator extends NumericMetricsAggregator.MultiValue {
* be called after we've collected a bucket, so it won't just fetch * be called after we've collected a bucket, so it won't just fetch
* garbage. * garbage.
*/ */
return values.values.get(owningBucketOrd); return metrics.metric(name, owningBucketOrd);
} }
@Override @Override
public ScoreMode scoreMode() { public ScoreMode scoreMode() {
boolean needs = (sort != null && sort.needsScores()) || (metricValueSource != null && metricValueSource.needsScores()); boolean needs = sort.needsScores() || metrics.needsScores();
return needs ? ScoreMode.COMPLETE : ScoreMode.COMPLETE_NO_SCORES; return needs ? ScoreMode.COMPLETE : ScoreMode.COMPLETE_NO_SCORES;
} }
@ -97,9 +96,6 @@ class TopMetricsAggregator extends NumericMetricsAggregator.MultiValue {
public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, LeafBucketCollector sub) throws IOException { public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, LeafBucketCollector sub) throws IOException {
assert sub == LeafBucketCollector.NO_OP_COLLECTOR : "Expected noop but was " + sub.toString(); assert sub == LeafBucketCollector.NO_OP_COLLECTOR : "Expected noop but was " + sub.toString();
if (metricValueSource == null) {
return LeafBucketCollector.NO_OP_COLLECTOR;
}
BucketedSort.Leaf leafSort = sort.forLeaf(ctx); BucketedSort.Leaf leafSort = sort.forLeaf(ctx);
return new LeafBucketCollector() { return new LeafBucketCollector() {
@ -117,41 +113,115 @@ class TopMetricsAggregator extends NumericMetricsAggregator.MultiValue {
@Override @Override
public InternalAggregation buildAggregation(long bucket) throws IOException { public InternalAggregation buildAggregation(long bucket) throws IOException {
if (metricValueSource == null) { List<InternalTopMetrics.TopMetric> topMetrics = sort.getValues(bucket, metrics.resultBuilder(sort.getFormat()));
return buildEmptyAggregation();
}
List<InternalTopMetrics.TopMetric> topMetrics = sort.getValues(bucket, values.resultBuilder(sort.getFormat()));
assert topMetrics.size() <= size; assert topMetrics.size() <= size;
return new InternalTopMetrics(name, sort.getOrder(), metricName, size, topMetrics, pipelineAggregators(), metaData()); return new InternalTopMetrics(name, sort.getOrder(), metrics.names, size, topMetrics, pipelineAggregators(), metaData());
} }
@Override @Override
public InternalTopMetrics buildEmptyAggregation() { public InternalTopMetrics buildEmptyAggregation() {
// The sort format and sort order aren't used in reduction so we pass the simplest thing. return InternalTopMetrics.buildEmptyAggregation(name, metrics.names, pipelineAggregators(), metaData());
return InternalTopMetrics.buildEmptyAggregation(name, metricName, pipelineAggregators(),
metaData());
} }
@Override @Override
public void doClose() { public void doClose() {
Releasables.close(sort, values); Releasables.close(sort, metrics);
} }
private static class Values implements BucketedSort.ExtraData, Releasable { private static class Metrics implements BucketedSort.ExtraData, Releasable {
private final List<String> names;
private final MetricValues[] values;
Metrics(int size, QueryShardContext ctx, List<String> names, List<ValuesSource.Numeric> valuesSources) {
this.names = names;
values = new MetricValues[valuesSources.size()];
int i = 0;
for (ValuesSource.Numeric valuesSource : valuesSources) {
if (valuesSource == null) {
values[i++] = new MissingMetricValues();
continue;
}
values[i++] = new CollectMetricValues(size, ctx.bigArrays(), valuesSource);
}
}
boolean needsScores() {
for (int i = 0; i < values.length; i++) {
if (values[i].needsScores()) {
return true;
}
}
return false;
}
double metric(String name, long index) {
int valueIndex = names.indexOf(name);
if (valueIndex < 0) {
throw new IllegalArgumentException("[" + name + "] not found");
}
return values[valueIndex].value(index);
}
BucketedSort.ResultBuilder<InternalTopMetrics.TopMetric> resultBuilder(DocValueFormat sortFormat) {
return (index, sortValue) -> {
double[] result = new double[values.length];
for (int i = 0; i < values.length; i++) {
result[i] = values[i].value(index);
}
return new InternalTopMetrics.TopMetric(sortFormat, sortValue, result);
};
}
@Override
public void swap(long lhs, long rhs) {
for (int i = 0; i < values.length; i++) {
values[i].swap(lhs, rhs);
}
}
@Override
public Loader loader(LeafReaderContext ctx) throws IOException {
Loader[] loaders = new Loader[values.length];
for (int i = 0; i < values.length; i++) {
loaders[i] = values[i].loader(ctx);
}
return (index, doc) -> {
for (int i = 0; i < loaders.length; i++) {
loaders[i].loadFromDoc(index, doc);
}
};
}
@Override
public void close() {
Releasables.close(values);
}
}
private interface MetricValues extends BucketedSort.ExtraData, Releasable {
boolean needsScores();
double value(long index);
}
private static class CollectMetricValues implements MetricValues {
private final BigArrays bigArrays; private final BigArrays bigArrays;
private final ValuesSource.Numeric metricValueSource; private final ValuesSource.Numeric metricValueSource;
private DoubleArray values; private DoubleArray values;
Values(int size, BigArrays bigArrays, ValuesSource.Numeric metricValueSource) { CollectMetricValues(int size, BigArrays bigArrays, ValuesSource.Numeric metricValueSource) {
this.bigArrays = bigArrays; this.bigArrays = bigArrays;
this.metricValueSource = metricValueSource; this.metricValueSource = metricValueSource;
values = bigArrays.newDoubleArray(size, false); values = bigArrays.newDoubleArray(size, false);
} }
BucketedSort.ResultBuilder<InternalTopMetrics.TopMetric> resultBuilder(DocValueFormat sortFormat) { @Override
return (index, sortValue) -> public boolean needsScores() {
new InternalTopMetrics.TopMetric(sortFormat, sortValue, values.get(index)); return metricValueSource.needsScores();
}
@Override
public double value(long index) {
return values.get(index);
} }
@Override @Override
@ -179,4 +249,27 @@ class TopMetricsAggregator extends NumericMetricsAggregator.MultiValue {
values.close(); values.close();
} }
} }
private static class MissingMetricValues implements MetricValues {
@Override
public double value(long index) {
return Double.NaN;
}
@Override
public boolean needsScores() {
return false;
}
@Override
public void swap(long lhs, long rhs) {}
@Override
public Loader loader(LeafReaderContext ctx) throws IOException {
return (index, doc) -> {};
}
@Override
public void close() {
}
}
} }

View File

@ -24,6 +24,8 @@ import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static java.util.stream.Collectors.toList;
public class TopMetricsAggregatorFactory extends AggregatorFactory { public class TopMetricsAggregatorFactory extends AggregatorFactory {
/** /**
* Index setting describing the maximum number of top metrics that * Index setting describing the maximum number of top metrics that
@ -35,40 +37,34 @@ public class TopMetricsAggregatorFactory extends AggregatorFactory {
private final List<SortBuilder<?>> sortBuilders; private final List<SortBuilder<?>> sortBuilders;
private final int size; private final int size;
private final MultiValuesSourceFieldConfig metricField; private final List<MultiValuesSourceFieldConfig> metricFields;
public TopMetricsAggregatorFactory(String name, QueryShardContext queryShardContext, AggregatorFactory parent, public TopMetricsAggregatorFactory(String name, QueryShardContext queryShardContext, AggregatorFactory parent,
Builder subFactoriesBuilder, Map<String, Object> metaData, List<SortBuilder<?>> sortBuilders, Builder subFactoriesBuilder, Map<String, Object> metaData, List<SortBuilder<?>> sortBuilders,
int size, MultiValuesSourceFieldConfig metricField) throws IOException { int size, List<MultiValuesSourceFieldConfig> metricFields) throws IOException {
super(name, queryShardContext, parent, subFactoriesBuilder, metaData); super(name, queryShardContext, parent, subFactoriesBuilder, metaData);
this.sortBuilders = sortBuilders; this.sortBuilders = sortBuilders;
this.size = size; this.size = size;
this.metricField = metricField; this.metricFields = metricFields;
} }
@Override @Override
protected TopMetricsAggregator createInternal(SearchContext searchContext, Aggregator parent, boolean collectsFromSingleBucket, protected TopMetricsAggregator createInternal(SearchContext searchContext, Aggregator parent, boolean collectsFromSingleBucket,
List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) throws IOException { List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) throws IOException {
ValuesSourceConfig<ValuesSource.Numeric> metricFieldSource = ValuesSourceConfig.resolve(queryShardContext, ValueType.NUMERIC,
metricField.getFieldName(), metricField.getScript(), metricField.getMissing(), metricField.getTimeZone(), null);
ValuesSource.Numeric metricValueSource = metricFieldSource.toValuesSource(queryShardContext);
int maxBucketSize = MAX_BUCKET_SIZE.get(searchContext.getQueryShardContext().getIndexSettings().getSettings()); int maxBucketSize = MAX_BUCKET_SIZE.get(searchContext.getQueryShardContext().getIndexSettings().getSettings());
if (size > maxBucketSize) { if (size > maxBucketSize) {
throw new IllegalArgumentException("[top_metrics.size] must not be more than [" + maxBucketSize + "] but was [" + size throw new IllegalArgumentException("[top_metrics.size] must not be more than [" + maxBucketSize + "] but was [" + size
+ "]. This limit can be set by changing the [" + MAX_BUCKET_SIZE.getKey() + "]. This limit can be set by changing the [" + MAX_BUCKET_SIZE.getKey()
+ "] index level setting."); + "] index level setting.");
} }
if (metricValueSource == null) { List<String> metricNames = metricFields.stream().map(MultiValuesSourceFieldConfig::getFieldName).collect(toList());
return createUnmapped(searchContext, parent, pipelineAggregators, metaData); List<ValuesSource.Numeric> metricValuesSources = metricFields.stream().map(config -> {
} ValuesSourceConfig<ValuesSource.Numeric> resolved = ValuesSourceConfig.resolve(
return new TopMetricsAggregator(name, searchContext, parent, pipelineAggregators, metaData, size, metricField.getFieldName(), searchContext.getQueryShardContext(), ValueType.NUMERIC,
sortBuilders.get(0), metricValueSource); config.getFieldName(), config.getScript(), config.getMissing(), config.getTimeZone(), null);
return resolved.toValuesSource(searchContext.getQueryShardContext());
}).collect(toList());
return new TopMetricsAggregator(name, searchContext, parent, pipelineAggregators, metaData, size,
sortBuilders.get(0), metricNames, metricValuesSources);
} }
private TopMetricsAggregator createUnmapped(SearchContext searchContext, Aggregator parent,
List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) throws IOException {
return new TopMetricsAggregator(name, searchContext, parent, pipelineAggregators, metaData, size, metricField.getFieldName(),
null, null);
}
} }

View File

@ -14,6 +14,7 @@ import org.elasticsearch.test.ESTestCase;
import java.util.Arrays; import java.util.Arrays;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.sameInstance; import static org.hamcrest.Matchers.sameInstance;
@ -45,7 +46,7 @@ public class InternalTopMetricsReduceTests extends ESTestCase {
InternalTopMetrics winner = first.getSortOrder() == SortOrder.ASC ? min : max; InternalTopMetrics winner = first.getSortOrder() == SortOrder.ASC ? min : max;
InternalTopMetrics reduced = reduce(metrics); InternalTopMetrics reduced = reduce(metrics);
assertThat(reduced.getName(), equalTo("test")); assertThat(reduced.getName(), equalTo("test"));
assertThat(reduced.getMetricName(), equalTo("test")); assertThat(reduced.getMetricNames(), equalTo(singletonList("test")));
assertThat(reduced.getSortOrder(), equalTo(first.getSortOrder())); assertThat(reduced.getSortOrder(), equalTo(first.getSortOrder()));
assertThat(reduced.getSize(), equalTo(first.getSize())); assertThat(reduced.getSize(), equalTo(first.getSize()));
assertThat(reduced.getTopMetrics(), equalTo(winner.getTopMetrics())); assertThat(reduced.getTopMetrics(), equalTo(winner.getTopMetrics()));
@ -60,7 +61,7 @@ public class InternalTopMetricsReduceTests extends ESTestCase {
}; };
InternalTopMetrics reduced = reduce(metrics); InternalTopMetrics reduced = reduce(metrics);
assertThat(reduced.getName(), equalTo("test")); assertThat(reduced.getName(), equalTo("test"));
assertThat(reduced.getMetricName(), equalTo("test")); assertThat(reduced.getMetricNames(), equalTo(singletonList("test")));
assertThat(reduced.getSortOrder(), equalTo(first.getSortOrder())); assertThat(reduced.getSortOrder(), equalTo(first.getSortOrder()));
assertThat(reduced.getSize(), equalTo(first.getSize())); assertThat(reduced.getSize(), equalTo(first.getSize()));
assertThat(reduced.getTopMetrics(), equalTo(Arrays.asList( assertThat(reduced.getTopMetrics(), equalTo(Arrays.asList(
@ -74,14 +75,14 @@ public class InternalTopMetricsReduceTests extends ESTestCase {
// Doubles sort first. // Doubles sort first.
InternalTopMetrics winner = doubleMetrics.getSortOrder() == SortOrder.ASC ? doubleMetrics : longMetrics; InternalTopMetrics winner = doubleMetrics.getSortOrder() == SortOrder.ASC ? doubleMetrics : longMetrics;
assertThat(reduced.getName(), equalTo("test")); assertThat(reduced.getName(), equalTo("test"));
assertThat(reduced.getMetricName(), equalTo("test")); assertThat(reduced.getMetricNames(), equalTo(singletonList("test")));
assertThat(reduced.getSortOrder(), equalTo(doubleMetrics.getSortOrder())); assertThat(reduced.getSortOrder(), equalTo(doubleMetrics.getSortOrder()));
assertThat(reduced.getSize(), equalTo(doubleMetrics.getSize())); assertThat(reduced.getSize(), equalTo(doubleMetrics.getSize()));
assertThat(reduced.getTopMetrics(), equalTo(winner.getTopMetrics())); assertThat(reduced.getTopMetrics(), equalTo(winner.getTopMetrics()));
} }
private InternalTopMetrics buildEmpty() { private InternalTopMetrics buildEmpty() {
return InternalTopMetrics.buildEmptyAggregation("test", "test", emptyList(), null); return InternalTopMetrics.buildEmptyAggregation("test", singletonList("test"), emptyList(), null);
} }
private InternalTopMetrics buildFilled(int size, InternalTopMetrics.TopMetric... metrics) { private InternalTopMetrics buildFilled(int size, InternalTopMetrics.TopMetric... metrics) {
@ -89,12 +90,12 @@ public class InternalTopMetricsReduceTests extends ESTestCase {
} }
private InternalTopMetrics buildFilled(SortOrder sortOrder, int size, InternalTopMetrics.TopMetric... metrics) { private InternalTopMetrics buildFilled(SortOrder sortOrder, int size, InternalTopMetrics.TopMetric... metrics) {
return new InternalTopMetrics("test", sortOrder, "test", size, Arrays.asList(metrics), emptyList(), null); return new InternalTopMetrics("test", sortOrder, singletonList("test"), size, Arrays.asList(metrics), emptyList(), null);
} }
private InternalTopMetrics.TopMetric top(SortValue sortValue, double metricValue) { private InternalTopMetrics.TopMetric top(SortValue sortValue, double metricValue) {
DocValueFormat sortFormat = randomFrom(DocValueFormat.RAW, DocValueFormat.BINARY, DocValueFormat.BOOLEAN, DocValueFormat.IP); DocValueFormat sortFormat = randomFrom(DocValueFormat.RAW, DocValueFormat.BINARY, DocValueFormat.BOOLEAN, DocValueFormat.IP);
return new InternalTopMetrics.TopMetric(sortFormat, sortValue, metricValue); return new InternalTopMetrics.TopMetric(sortFormat, sortValue, new double[] {metricValue});
} }
private InternalTopMetrics reduce(InternalTopMetrics... results) { private InternalTopMetrics reduce(InternalTopMetrics... results) {

View File

@ -27,16 +27,18 @@ import java.time.ZonedDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toList;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
public class InternalTopMetricsTests extends InternalAggregationTestCase<InternalTopMetrics> { public class InternalTopMetricsTests extends InternalAggregationTestCase<InternalTopMetrics> {
@ -51,7 +53,7 @@ public class InternalTopMetricsTests extends InternalAggregationTestCase<Interna
public void testEmptyIsNotMapped() { public void testEmptyIsNotMapped() {
InternalTopMetrics empty = InternalTopMetrics.buildEmptyAggregation( InternalTopMetrics empty = InternalTopMetrics.buildEmptyAggregation(
randomAlphaOfLength(5), randomAlphaOfLength(2), emptyList(), null); randomAlphaOfLength(5), randomMetricNames(between(1, 5)), emptyList(), null);
assertFalse(empty.isMapped()); assertFalse(empty.isMapped());
} }
@ -61,8 +63,9 @@ public class InternalTopMetricsTests extends InternalAggregationTestCase<Interna
} }
public void testToXContentDoubleSortValue() throws IOException { public void testToXContentDoubleSortValue() throws IOException {
InternalTopMetrics tm = new InternalTopMetrics("test", sortOrder, "test", 1, InternalTopMetrics tm = new InternalTopMetrics("test", sortOrder, singletonList("test"), 1,
Arrays.asList(new InternalTopMetrics.TopMetric(DocValueFormat.RAW, SortValue.from(1.0), 1.0)), emptyList(), null); singletonList(new InternalTopMetrics.TopMetric(DocValueFormat.RAW, SortValue.from(1.0), new double[] {1.0})),
emptyList(), null);
assertThat(Strings.toString(tm, true, true), equalTo( assertThat(Strings.toString(tm, true, true), equalTo(
"{\n" + "{\n" +
" \"test\" : {\n" + " \"test\" : {\n" +
@ -84,8 +87,8 @@ public class InternalTopMetricsTests extends InternalAggregationTestCase<Interna
DocValueFormat sortFormat = new DocValueFormat.DateTime(DateFormatter.forPattern("strict_date_time"), ZoneId.of("UTC"), DocValueFormat sortFormat = new DocValueFormat.DateTime(DateFormatter.forPattern("strict_date_time"), ZoneId.of("UTC"),
DateFieldMapper.Resolution.MILLISECONDS); DateFieldMapper.Resolution.MILLISECONDS);
SortValue sortValue = SortValue.from(ZonedDateTime.parse("2007-12-03T10:15:30Z").toInstant().toEpochMilli()); SortValue sortValue = SortValue.from(ZonedDateTime.parse("2007-12-03T10:15:30Z").toInstant().toEpochMilli());
InternalTopMetrics tm = new InternalTopMetrics("test", sortOrder, "test", 1, InternalTopMetrics tm = new InternalTopMetrics("test", sortOrder, singletonList("test"), 1,
Arrays.asList(new InternalTopMetrics.TopMetric(sortFormat, sortValue, 1.0)), emptyList(), null); singletonList(new InternalTopMetrics.TopMetric(sortFormat, sortValue, new double[] {1.0})), emptyList(), null);
assertThat(Strings.toString(tm, true, true), equalTo( assertThat(Strings.toString(tm, true, true), equalTo(
"{\n" + "{\n" +
" \"test\" : {\n" + " \"test\" : {\n" +
@ -103,11 +106,34 @@ public class InternalTopMetricsTests extends InternalAggregationTestCase<Interna
"}")); "}"));
} }
public void testToXContentManyValues() throws IOException { public void testToXContentManyMetrics() throws IOException {
InternalTopMetrics tm = new InternalTopMetrics("test", sortOrder, "test", 2, InternalTopMetrics tm = new InternalTopMetrics("test", sortOrder, Arrays.asList("foo", "bar", "baz"), 1,
singletonList(new InternalTopMetrics.TopMetric(DocValueFormat.RAW, SortValue.from(1.0), new double[] {1.0, 2.0, 3.0})),
emptyList(), null);
assertThat(Strings.toString(tm, true, true), equalTo(
"{\n" +
" \"test\" : {\n" +
" \"top\" : [\n" +
" {\n" +
" \"sort\" : [\n" +
" 1.0\n" +
" ],\n" +
" \"metrics\" : {\n" +
" \"foo\" : 1.0,\n" +
" \"bar\" : 2.0,\n" +
" \"baz\" : 3.0\n" +
" }\n" +
" }\n" +
" ]\n" +
" }\n" +
"}"));
}
public void testToXContentManyTopMetrics() throws IOException {
InternalTopMetrics tm = new InternalTopMetrics("test", sortOrder, singletonList("test"), 2,
Arrays.asList( Arrays.asList(
new InternalTopMetrics.TopMetric(DocValueFormat.RAW, SortValue.from(1.0), 1.0), new InternalTopMetrics.TopMetric(DocValueFormat.RAW, SortValue.from(1.0), new double[] {1.0}),
new InternalTopMetrics.TopMetric(DocValueFormat.RAW, SortValue.from(2.0), 2.0)), new InternalTopMetrics.TopMetric(DocValueFormat.RAW, SortValue.from(2.0), new double[] {2.0})),
emptyList(), null); emptyList(), null);
assertThat(Strings.toString(tm, true, true), equalTo( assertThat(Strings.toString(tm, true, true), equalTo(
"{\n" + "{\n" +
@ -145,17 +171,18 @@ public class InternalTopMetricsTests extends InternalAggregationTestCase<Interna
@Override @Override
protected InternalTopMetrics createTestInstance(String name, List<PipelineAggregator> pipelineAggregators, protected InternalTopMetrics createTestInstance(String name, List<PipelineAggregator> pipelineAggregators,
Map<String, Object> metaData) { Map<String, Object> metaData) {
String metricName = randomAlphaOfLength(5); int metricCount = between(1, 5);
List<String> metricNames = randomMetricNames(metricCount);
int size = between(1, 100); int size = between(1, 100);
List<InternalTopMetrics.TopMetric> topMetrics = randomTopMetrics(between(0, size)); List<InternalTopMetrics.TopMetric> topMetrics = randomTopMetrics(between(0, size), metricCount);
return new InternalTopMetrics(name, sortOrder, metricName, size, topMetrics, pipelineAggregators, metaData); return new InternalTopMetrics(name, sortOrder, metricNames, size, topMetrics, pipelineAggregators, metaData);
} }
@Override @Override
protected InternalTopMetrics mutateInstance(InternalTopMetrics instance) throws IOException { protected InternalTopMetrics mutateInstance(InternalTopMetrics instance) throws IOException {
String name = instance.getName(); String name = instance.getName();
SortOrder sortOrder = instance.getSortOrder(); SortOrder sortOrder = instance.getSortOrder();
String metricName = instance.getMetricName(); List<String> metricNames = instance.getMetricNames();
int size = instance.getSize(); int size = instance.getSize();
List<InternalTopMetrics.TopMetric> topMetrics = instance.getTopMetrics(); List<InternalTopMetrics.TopMetric> topMetrics = instance.getTopMetrics();
switch (randomInt(4)) { switch (randomInt(4)) {
@ -167,19 +194,21 @@ public class InternalTopMetricsTests extends InternalAggregationTestCase<Interna
Collections.reverse(topMetrics); Collections.reverse(topMetrics);
break; break;
case 2: case 2:
metricName = randomAlphaOfLength(6); metricNames = new ArrayList<>(metricNames);
metricNames.set(randomInt(metricNames.size() - 1), randomAlphaOfLength(6));
break; break;
case 3: case 3:
size = randomValueOtherThan(size, () -> between(1, 100)); size = randomValueOtherThan(size, () -> between(1, 100));
break; break;
case 4: case 4:
int fixedSize = size; int fixedSize = size;
topMetrics = randomValueOtherThan(topMetrics, () -> randomTopMetrics(between(1, fixedSize))); int fixedMetricsSize = metricNames.size();
topMetrics = randomValueOtherThan(topMetrics, () -> randomTopMetrics(between(1, fixedSize), fixedMetricsSize));
break; break;
default: default:
throw new IllegalArgumentException("bad mutation"); throw new IllegalArgumentException("bad mutation");
} }
return new InternalTopMetrics(name, sortOrder, metricName, size, topMetrics, return new InternalTopMetrics(name, sortOrder, metricNames, size, topMetrics,
instance.pipelineAggregators(), instance.getMetaData()); instance.pipelineAggregators(), instance.getMetaData());
} }
@ -199,7 +228,11 @@ public class InternalTopMetricsTests extends InternalAggregationTestCase<Interna
Object expectedSort = internalTop.getSortFormat() == DocValueFormat.RAW ? Object expectedSort = internalTop.getSortFormat() == DocValueFormat.RAW ?
internalTop.getSortValue().getKey() : internalTop.getSortValue().format(internalTop.getSortFormat()); internalTop.getSortValue().getKey() : internalTop.getSortValue().format(internalTop.getSortFormat());
assertThat(parsedTop.getSort(), equalTo(singletonList(expectedSort))); assertThat(parsedTop.getSort(), equalTo(singletonList(expectedSort)));
assertThat(parsedTop.getMetrics(), equalTo(singletonMap(aggregation.getMetricName(), internalTop.getMetricValue()))); assertThat(parsedTop.getMetrics().keySet(), hasSize(aggregation.getMetricNames().size()));
for (int m = 0; m < aggregation.getMetricNames().size(); m++) {
assertThat(parsedTop.getMetrics(),
hasEntry(aggregation.getMetricNames().get(m), internalTop.getMetricValues()[m]));
}
} }
} }
@ -214,17 +247,31 @@ public class InternalTopMetricsTests extends InternalAggregationTestCase<Interna
List<InternalTopMetrics.TopMetric> winners = metrics.size() > first.getSize() ? metrics.subList(0, first.getSize()) : metrics; List<InternalTopMetrics.TopMetric> winners = metrics.size() > first.getSize() ? metrics.subList(0, first.getSize()) : metrics;
assertThat(reduced.getName(), equalTo(first.getName())); assertThat(reduced.getName(), equalTo(first.getName()));
assertThat(reduced.getSortOrder(), equalTo(first.getSortOrder())); assertThat(reduced.getSortOrder(), equalTo(first.getSortOrder()));
assertThat(reduced.getMetricName(), equalTo(first.getMetricName())); assertThat(reduced.getMetricNames(), equalTo(first.getMetricNames()));
assertThat(reduced.getTopMetrics(), equalTo(winners)); assertThat(reduced.getTopMetrics(), equalTo(winners));
} }
private List<InternalTopMetrics.TopMetric> randomTopMetrics(int length) { private List<InternalTopMetrics.TopMetric> randomTopMetrics(int length, int metricCount) {
return IntStream.range(0, length) return IntStream.range(0, length)
.mapToObj(i -> new InternalTopMetrics.TopMetric(randomNumericDocValueFormat(), randomSortValue(), randomDouble())) .mapToObj(i -> new InternalTopMetrics.TopMetric(
randomNumericDocValueFormat(), randomSortValue(), randomMetricValues(metricCount)
))
.sorted((lhs, rhs) -> sortOrder.reverseMul() * lhs.getSortValue().compareTo(rhs.getSortValue())) .sorted((lhs, rhs) -> sortOrder.reverseMul() * lhs.getSortValue().compareTo(rhs.getSortValue()))
.collect(toList()); .collect(toList());
} }
static List<String> randomMetricNames(int metricCount) {
Set<String> names = new HashSet<>(metricCount);
while (names.size() < metricCount) {
names.add(randomAlphaOfLength(5));
}
return new ArrayList<>(names);
}
private double[] randomMetricValues(int metricCount) {
return IntStream.range(0, metricCount).mapToDouble(i -> randomDouble()).toArray();
}
private static SortValue randomSortValue() { private static SortValue randomSortValue() {
if (randomBoolean()) { if (randomBoolean()) {
return SortValue.from(randomLong()); return SortValue.from(randomLong());

View File

@ -27,6 +27,7 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.toList;
import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
@ -68,9 +69,14 @@ public class TopMetricsAggregationBuilderTests extends AbstractSerializingTestCa
protected TopMetricsAggregationBuilder createTestInstance() { protected TopMetricsAggregationBuilder createTestInstance() {
List<SortBuilder<?>> sortBuilders = singletonList( List<SortBuilder<?>> sortBuilders = singletonList(
new FieldSortBuilder(randomAlphaOfLength(5)).order(randomFrom(SortOrder.values()))); new FieldSortBuilder(randomAlphaOfLength(5)).order(randomFrom(SortOrder.values())));
MultiValuesSourceFieldConfig.Builder metricField = new MultiValuesSourceFieldConfig.Builder(); List<MultiValuesSourceFieldConfig> metricFields = InternalTopMetricsTests.randomMetricNames(between(1, 5)).stream()
metricField.setFieldName(randomAlphaOfLength(5)).setMissing(1.0); .map(name -> {
return new TopMetricsAggregationBuilder(randomAlphaOfLength(5), sortBuilders, between(1, 100), metricField.build()); MultiValuesSourceFieldConfig.Builder metricField = new MultiValuesSourceFieldConfig.Builder();
metricField.setFieldName(randomAlphaOfLength(5)).setMissing(1.0);
return metricField.build();
})
.collect(toList());
return new TopMetricsAggregationBuilder(randomAlphaOfLength(5), sortBuilders, between(1, 100), metricFields);
} }
public void testClientBuilder() throws IOException { public void testClientBuilder() throws IOException {
@ -98,6 +104,6 @@ public class TopMetricsAggregationBuilderTests extends AbstractSerializingTestCa
serverBuilder.getName(), serverBuilder.getName(),
serverBuilder.getSortBuilders().get(0), serverBuilder.getSortBuilders().get(0),
serverBuilder.getSize(), serverBuilder.getSize(),
serverBuilder.getMetricField().getFieldName()); serverBuilder.getMetricFields().stream().map(MultiValuesSourceFieldConfig::getFieldName).toArray(String[]::new));
} }
} }

View File

@ -68,12 +68,14 @@ import org.elasticsearch.search.sort.SortValue;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.Map; import java.util.Map;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap; import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap; import static java.util.Collections.singletonMap;
import static java.util.stream.Collectors.toList;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.notANumber; import static org.hamcrest.Matchers.notANumber;
@ -95,7 +97,7 @@ public class TopMetricsAggregatorTests extends AggregatorTestCase {
}, },
numberFieldType(NumberType.DOUBLE, "s")); numberFieldType(NumberType.DOUBLE, "s"));
assertThat(result.getSortOrder(), equalTo(SortOrder.ASC)); assertThat(result.getSortOrder(), equalTo(SortOrder.ASC));
assertThat(result.getTopMetrics(), equalTo(emptyList())); assertThat(result.getTopMetrics(), equalTo(singletonList(top(1.0, Double.NaN))));
} }
public void testMissingValueForMetric() throws IOException { public void testMissingValueForMetric() throws IOException {
@ -106,7 +108,8 @@ public class TopMetricsAggregatorTests extends AggregatorTestCase {
assertThat(result.getSortOrder(), equalTo(SortOrder.ASC)); assertThat(result.getSortOrder(), equalTo(SortOrder.ASC));
assertThat(result.getTopMetrics(), hasSize(1)); assertThat(result.getTopMetrics(), hasSize(1));
assertThat(result.getTopMetrics().get(0).getSortValue(), equalTo(SortValue.from(1.0))); assertThat(result.getTopMetrics().get(0).getSortValue(), equalTo(SortValue.from(1.0)));
assertThat(result.getTopMetrics().get(0).getMetricValue(), notANumber()); assertThat(result.getTopMetrics().get(0).getMetricValues().length, equalTo(1));
assertThat(result.getTopMetrics().get(0).getMetricValues()[0], notANumber());
} }
public void testActualValueForMetric() throws IOException { public void testActualValueForMetric() throws IOException {
@ -130,7 +133,7 @@ public class TopMetricsAggregatorTests extends AggregatorTestCase {
InternalTopMetrics result = collectFromDoubles(simpleBuilder(new FieldSortBuilder("s").order(SortOrder.ASC))); InternalTopMetrics result = collectFromDoubles(simpleBuilder(new FieldSortBuilder("s").order(SortOrder.ASC)));
assertThat(result.getSortOrder(), equalTo(SortOrder.ASC)); assertThat(result.getSortOrder(), equalTo(SortOrder.ASC));
assertThat(result.getTopMetrics(), equalTo(singletonList( assertThat(result.getTopMetrics(), equalTo(singletonList(
new InternalTopMetrics.TopMetric(DocValueFormat.RAW, SortValue.from(1.0), 2.0)))); new InternalTopMetrics.TopMetric(DocValueFormat.RAW, SortValue.from(1.0), new double[] {2.0}))));
} }
public void testSortByDoubleDescending() throws IOException { public void testSortByDoubleDescending() throws IOException {
@ -349,21 +352,50 @@ public class TopMetricsAggregatorTests extends AggregatorTestCase {
* breaker. The number of buckets feels fairly arbitrary but * breaker. The number of buckets feels fairly arbitrary but
* it comes from: * it comes from:
* budget = 15k = 20k - 5k for the "default weight" of ever agg * budget = 15k = 20k - 5k for the "default weight" of ever agg
* The 922th bucket causes a resize which requests puts the total * The 646th bucket causes a resize which requests puts the total
* just over 15k.O * just over 15k. This works out to more like 190 bits per bucket
* when we're fairly sure this should take about 129 bits per
* bucket. The difference is because, for arrays in of this size,
* BigArrays allocates the new array before freeing the old one.
* That causes us to trip when we're about 2/3 of the way to the
* limit. And 2/3 of 190 is 126. Which is pretty much what we
* expect. Sort of.
*/ */
int bucketThatBreaks = 922; int bucketThatBreaks = 646;
for (int b = 0; b < bucketThatBreaks; b++) { for (int b = 0; b < bucketThatBreaks; b++) {
leaf.collect(0, b); try {
leaf.collect(0, b);
} catch (CircuitBreakingException e) {
throw new AssertionError("Unexpected circuit break at [" + b + "]. Expected at [" + bucketThatBreaks + "]", e);
}
} }
CircuitBreakingException e = expectThrows(CircuitBreakingException.class, () -> leaf.collect(0, bucketThatBreaks)); CircuitBreakingException e = expectThrows(CircuitBreakingException.class, () -> leaf.collect(0, bucketThatBreaks));
assertThat(e.getMessage(), equalTo("test error")); assertThat(e.getMessage(), equalTo("test error"));
assertThat(e.getByteLimit(), equalTo(max.getBytes())); assertThat(e.getByteLimit(), equalTo(max.getBytes()));
assertThat(e.getBytesWanted(), equalTo(16440L)); assertThat(e.getBytesWanted(), equalTo(5872L));
} }
} }
} }
public void testManyMetrics() throws IOException {
List<SortBuilder<?>> sorts = singletonList(new FieldSortBuilder("s").order(SortOrder.ASC));
TopMetricsAggregationBuilder builder = new TopMetricsAggregationBuilder("test", sorts, 1,
Arrays.asList(
new MultiValuesSourceFieldConfig.Builder().setFieldName("m1").build(),
new MultiValuesSourceFieldConfig.Builder().setFieldName("m2").build(),
new MultiValuesSourceFieldConfig.Builder().setFieldName("m3").build()
));
InternalTopMetrics result = collect(builder, new MatchAllDocsQuery(), writer -> {
writer.addDocument(Arrays.asList(doubleField("s", 1.0),
doubleField("m1", 12.0), doubleField("m2", 22.0), doubleField("m3", 32.0)));
writer.addDocument(Arrays.asList(doubleField("s", 2.0),
doubleField("m1", 13.0), doubleField("m2", 23.0), doubleField("m3", 33.0)));
}, manyMetricsFields());
assertThat(result.getSortOrder(), equalTo(SortOrder.ASC));
assertThat(result.getTopMetrics(), equalTo(singletonList(
new InternalTopMetrics.TopMetric(DocValueFormat.RAW, SortValue.from(1.0), new double[] {12.0, 22.0, 32.0}))));
}
private TopMetricsAggregationBuilder simpleBuilder() { private TopMetricsAggregationBuilder simpleBuilder() {
return simpleBuilder(new FieldSortBuilder("s")); return simpleBuilder(new FieldSortBuilder("s"));
} }
@ -374,7 +406,7 @@ public class TopMetricsAggregatorTests extends AggregatorTestCase {
private TopMetricsAggregationBuilder simpleBuilder(SortBuilder<?> sort, int size) { private TopMetricsAggregationBuilder simpleBuilder(SortBuilder<?> sort, int size) {
return new TopMetricsAggregationBuilder("test", singletonList(sort), size, return new TopMetricsAggregationBuilder("test", singletonList(sort), size,
new MultiValuesSourceFieldConfig.Builder().setFieldName("m").build()); singletonList(new MultiValuesSourceFieldConfig.Builder().setFieldName("m").build()));
} }
/** /**
@ -394,6 +426,16 @@ public class TopMetricsAggregatorTests extends AggregatorTestCase {
return new MappedFieldType[] {numberFieldType(NumberType.DOUBLE, "s"), numberFieldType(NumberType.DOUBLE, "m")}; return new MappedFieldType[] {numberFieldType(NumberType.DOUBLE, "s"), numberFieldType(NumberType.DOUBLE, "m")};
} }
private MappedFieldType[] manyMetricsFields() {
return new MappedFieldType[] {
numberFieldType(NumberType.DOUBLE, "s"),
numberFieldType(NumberType.DOUBLE, "m1"),
numberFieldType(NumberType.DOUBLE, "m2"),
numberFieldType(NumberType.DOUBLE, "m3"),
};
}
private MappedFieldType[] floatAndDoubleField() { private MappedFieldType[] floatAndDoubleField() {
return new MappedFieldType[] {numberFieldType(NumberType.FLOAT, "s"), numberFieldType(NumberType.DOUBLE, "m")}; return new MappedFieldType[] {numberFieldType(NumberType.FLOAT, "s"), numberFieldType(NumberType.DOUBLE, "m")};
} }
@ -452,7 +494,10 @@ public class TopMetricsAggregatorTests extends AggregatorTestCase {
private InternalTopMetrics collect(TopMetricsAggregationBuilder builder, Query query, private InternalTopMetrics collect(TopMetricsAggregationBuilder builder, Query query,
CheckedConsumer<RandomIndexWriter, IOException> buildIndex, MappedFieldType... fields) throws IOException { CheckedConsumer<RandomIndexWriter, IOException> buildIndex, MappedFieldType... fields) throws IOException {
InternalTopMetrics result = (InternalTopMetrics) collect((AggregationBuilder) builder, query, buildIndex, fields); InternalTopMetrics result = (InternalTopMetrics) collect((AggregationBuilder) builder, query, buildIndex, fields);
assertThat(result.getMetricName(), equalTo(builder.getMetricField().getFieldName())); List<String> expectedFieldNames = builder.getMetricFields().stream()
.map(MultiValuesSourceFieldConfig::getFieldName)
.collect(toList());
assertThat(result.getMetricNames(), equalTo(expectedFieldNames));
return result; return result;
} }
@ -470,12 +515,12 @@ public class TopMetricsAggregatorTests extends AggregatorTestCase {
} }
} }
private InternalTopMetrics.TopMetric top(long sortValue, double metricValue) { private InternalTopMetrics.TopMetric top(long sortValue, double... metricValues) {
return new InternalTopMetrics.TopMetric(DocValueFormat.RAW, SortValue.from(sortValue), metricValue); return new InternalTopMetrics.TopMetric(DocValueFormat.RAW, SortValue.from(sortValue), metricValues);
} }
private InternalTopMetrics.TopMetric top(double sortValue, double metricValue) { private InternalTopMetrics.TopMetric top(double sortValue, double... metricValues) {
return new InternalTopMetrics.TopMetric(DocValueFormat.RAW, SortValue.from(sortValue), metricValue); return new InternalTopMetrics.TopMetric(DocValueFormat.RAW, SortValue.from(sortValue), metricValues);
} }
/** /**

View File

@ -23,7 +23,7 @@
aggs: aggs:
tm: tm:
top_metrics: top_metrics:
metric: metrics:
field: v field: v
sort: sort:
s: desc s: desc
@ -36,7 +36,7 @@
aggs: aggs:
tm: tm:
top_metrics: top_metrics:
metric: metrics:
field: v field: v
sort: sort:
s: asc s: asc
@ -49,7 +49,7 @@
aggs: aggs:
tm: tm:
top_metrics: top_metrics:
metric: metrics:
field: v field: v
sort: sort:
s: s:
@ -92,7 +92,7 @@
aggs: aggs:
tm: tm:
top_metrics: top_metrics:
metric: metrics:
field: v field: v
sort: sort:
s: desc s: desc
@ -105,7 +105,7 @@
aggs: aggs:
tm: tm:
top_metrics: top_metrics:
metric: metrics:
field: v field: v
sort: sort:
s: asc s: asc
@ -146,7 +146,7 @@
aggs: aggs:
tm: tm:
top_metrics: top_metrics:
metric: metrics:
field: v field: v
sort: sort:
s: desc s: desc
@ -159,7 +159,7 @@
aggs: aggs:
tm: tm:
top_metrics: top_metrics:
metric: metrics:
field: v field: v
sort: sort:
s: asc s: asc
@ -196,7 +196,7 @@
aggs: aggs:
tm: tm:
top_metrics: top_metrics:
metric: metrics:
field: v field: v
sort: s.keyword sort: s.keyword
- match: { error.root_cause.0.reason: "error building sort for field [s.keyword] of type [keyword] in index [test]: only supported on numeric fields" } - match: { error.root_cause.0.reason: "error building sort for field [s.keyword] of type [keyword] in index [test]: only supported on numeric fields" }
@ -236,7 +236,7 @@
aggs: aggs:
tm: tm:
top_metrics: top_metrics:
metric: metrics:
field: v field: v
sort: _score sort: _score
- match: { aggregations.tm.top.0.metrics.v: 3.1414999961853027 } - match: { aggregations.tm.top.0.metrics.v: 3.1414999961853027 }
@ -263,7 +263,7 @@
aggs: aggs:
tm: tm:
top_metrics: top_metrics:
metric: metrics:
field: v field: v
sort: sort:
_script: _script:
@ -303,7 +303,7 @@
aggs: aggs:
tm: tm:
top_metrics: top_metrics:
metric: metrics:
field: v field: v
sort: sort:
_script: _script:
@ -345,7 +345,7 @@
aggs: aggs:
pop: pop:
top_metrics: top_metrics:
metric: metrics:
field: population field: population
sort: sort:
_geo_distance: _geo_distance:
@ -361,7 +361,7 @@
pop: pop:
top_metrics: top_metrics:
size: 3 size: 3
metric: metrics:
field: population field: population
sort: sort:
_geo_distance: _geo_distance:
@ -412,7 +412,7 @@
aggs: aggs:
tm: tm:
top_metrics: top_metrics:
metric: metrics:
field: v field: v
sort: sort:
date: desc date: desc
@ -437,7 +437,7 @@
aggs: aggs:
tm: tm:
top_metrics: top_metrics:
metric: metrics:
field: v field: v
sort: sort:
date: desc date: desc
@ -483,7 +483,7 @@
tm: tm:
top_metrics: top_metrics:
size: 100 size: 100
metric: metrics:
field: v field: v
sort: sort:
s: desc s: desc
@ -503,7 +503,7 @@
tm: tm:
top_metrics: top_metrics:
size: 100 size: 100
metric: metrics:
field: v field: v
sort: sort:
s: desc s: desc