Added support for sorting buckets based on sub aggregations

Supports sorting on sub-aggs down the current hierarchy. This is supported as long as the aggregation in the specified order path are of a single-bucket type, where the last aggregation in the path points to either a single-bucket aggregation or a metrics one. If it's a single-bucket aggregation, the sort will be applied on the document count in the bucket (i.e. doc_count), and if it is a metrics type, the sort will be applied on the pointed out metric (in case of a single-metric aggregations, such as avg, the sort will be applied on the single metric value)

 NOTE: this commit adds a constraint on what should be considered a valid aggregation name. Aggregations names must be alpha-numeric and may contain '-' and '_'.

 Closes #5253
This commit is contained in:
uboness 2014-02-27 07:58:28 -08:00
parent b723ee0d20
commit 9d0fc76f54
24 changed files with 1012 additions and 173 deletions

View File

@ -175,6 +175,48 @@ If the histogram aggregation has a direct metrics sub-aggregation, the latter ca
<2> There is no need to configure the `price` field for the `price_stats` aggregation as it will inherit it by default from its parent histogram aggregation.
It is also possible to order the buckets based on a "deeper" aggregation in the hierarchy. This is supported as long
as the aggregations path are of a single-bucket type, where the last aggregation in the path may either by a single-bucket
one or a metrics one. If it's a single-bucket type, the order will be defined by the number of docs in the bucket (i.e. `doc_count`),
in case it's a metrics one, the same rules as above apply (where the path must indicate the metric name to sort by in case of
a multi-value metrics aggregation, and in case of a single-value metrics aggregation the sort will be applied on that value).
The path must be defined in the following form:
--------------------------------------------------
AGG_SEPARATOR := '>'
METRIC_SEPARATOR := '.'
AGG_NAME := <the name of the aggregation>
METRIC := <the name of the metric (in case of multi-value metrics aggregation)>
PATH := <AGG_NAME>[<AGG_SEPARATOR><AGG_NAME>]*[<METRIC_SEPARATOR><METRIC>]
--------------------------------------------------
[source,js]
--------------------------------------------------
{
"aggs" : {
"prices" : {
"histogram" : {
"field" : "price",
"interval" : 50,
"order" : { "promoted_products>rating_stats.avg" : "desc" } <1>
},
"aggs" : {
"promoted_products" : {
"filter" : { "term" : { "promoted" : true }},
"aggs" : {
"rating_stats" : { "stats" : { "field" : "rating" }}
}
}
}
}
}
}
--------------------------------------------------
The above will sort the buckets based on the avg rating among the promoted products
==== Minimum document count
It is possible to only return buckets that have a document count that is greater than or equal to a configured

View File

@ -144,6 +144,46 @@ Ordering the buckets by multi value metrics sub-aggregation (identified by the a
}
--------------------------------------------------
It is also possible to order the buckets based on a "deeper" aggregation in the hierarchy. This is supported as long
as the aggregations path are of a single-bucket type, where the last aggregation in the path may either by a single-bucket
one or a metrics one. If it's a single-bucket type, the order will be defined by the number of docs in the bucket (i.e. `doc_count`),
in case it's a metrics one, the same rules as above apply (where the path must indicate the metric name to sort by in case of
a multi-value metrics aggregation, and in case of a single-value metrics aggregation the sort will be applied on that value).
The path must be defined in the following form:
--------------------------------------------------
AGG_SEPARATOR := '>'
METRIC_SEPARATOR := '.'
AGG_NAME := <the name of the aggregation>
METRIC := <the name of the metric (in case of multi-value metrics aggregation)>
PATH := <AGG_NAME>[<AGG_SEPARATOR><AGG_NAME>]*[<METRIC_SEPARATOR><METRIC>]
--------------------------------------------------
[source,js]
--------------------------------------------------
{
"aggs" : {
"countries" : {
"terms" : {
"field" : "address.country",
"order" : { "females>height_stats.avg" : "desc" }
},
"aggs" : {
"females" : {
"filter" : { "term" : { "gender" : { "female" }}},
"aggs" : {
"height_stats" : { "stats" : { "field" : "height" }}
}
}
}
}
}
}
--------------------------------------------------
The above will sort the countries buckets based on the average height among the female population.
==== Minimum document count
It is possible to only return terms that match more than a configured number of hits using the `min_doc_count` option:

View File

@ -77,7 +77,7 @@ abstract class AbstractHash implements Releasable {
}
/**
* Get the id associated with key at <code>0 &lte; index &lte; capacity()</code> or -1 if this slot is unused.
* Get the id associated with key at <code>0 &lt;= index &lt;= capacity()</code> or -1 if this slot is unused.
*/
public long id(long index) {
return ids.get(index) - 1;

View File

@ -28,7 +28,9 @@ import org.elasticsearch.search.internal.SearchContext;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public abstract class Aggregator implements Releasable, ReaderContextAware {
@ -59,6 +61,8 @@ public abstract class Aggregator implements Releasable, ReaderContextAware {
protected final AggregatorFactories factories;
protected final Aggregator[] subAggregators;
private Map<String, Aggregator> subAggregatorbyName;
/**
* Constructs a new Aggregator.
*
@ -113,6 +117,16 @@ public abstract class Aggregator implements Releasable, ReaderContextAware {
return subAggregators;
}
public Aggregator subAggregator(String aggName) {
if (subAggregatorbyName == null) {
subAggregatorbyName = new HashMap<String, Aggregator>(subAggregators.length);
for (int i = 0; i < subAggregators.length; i++) {
subAggregatorbyName.put(subAggregators[i].name, subAggregators[i]);
}
}
return subAggregatorbyName.get(aggName);
}
/**
* @return The current aggregation context.
*/

View File

@ -27,14 +27,18 @@ import org.elasticsearch.search.internal.SearchContext;
import java.io.IOException;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A registry for all the aggregator parser, also servers as the main parser for the aggregations module
*/
public class AggregatorParsers {
public static final Pattern VALID_AGG_NAME = Pattern.compile("[a-zA-Z0-9\\-_]+");
private final ImmutableMap<String, Aggregator.Parser> parsers;
/**
* Constructs the AggregatorParsers out of all the given parsers
*
@ -78,6 +82,8 @@ public class AggregatorParsers {
XContentParser.Token token = null;
String currentFieldName = null;
Matcher validAggMatcher = VALID_AGG_NAME.matcher("");
AggregatorFactories.Builder factories = new AggregatorFactories.Builder();
String aggregationName = null;
@ -102,6 +108,9 @@ public class AggregatorParsers {
if (aggregatorParser == null) {
throw new SearchParseException(context, "Could not find aggregator type [" + currentFieldName + "]");
}
if (!validAggMatcher.reset(aggregationName).matches()) {
throw new SearchParseException(context, "Invalid aggregation name [" + aggregationName + "]. Aggregation names must be alpha-numeric and can only contain '_' and '-'");
}
factory = aggregatorParser.parse(aggregationName, parser, context);
}
}

View File

@ -0,0 +1,29 @@
/*
* 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;
/**
*
*/
public interface HasAggregations {
Aggregations getAggregations();
}

View File

@ -84,7 +84,7 @@ public abstract class BucketsAggregator extends Aggregator {
/**
* Utility method to return the number of documents that fell in the given bucket (identified by the bucket ordinal)
*/
protected final long bucketDocCount(long bucketOrd) {
public final long bucketDocCount(long bucketOrd) {
if (bucketOrd >= docCounts.size()) {
// This may happen eg. if no document in the highest buckets is accepted by a sub aggregator.
// For example, if there is a long terms agg on 3 terms 1,2,3 with a sub filter aggregator and if no document with 3 as a value

View File

@ -19,12 +19,12 @@
package org.elasticsearch.search.aggregations.bucket;
import org.elasticsearch.ElasticsearchIllegalArgumentException;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.util.Comparators;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.metrics.MetricsAggregation;
import org.elasticsearch.search.aggregations.HasAggregations;
import org.elasticsearch.search.aggregations.support.OrderPath;
import java.util.Collection;
@ -38,7 +38,7 @@ public interface MultiBucketsAggregation extends Aggregation {
* A bucket represents a criteria to which all documents that fall in it adhere to. It is also uniquely identified
* by a key, and can potentially hold sub-aggregations computed over all documents in it.
*/
public interface Bucket {
public interface Bucket extends HasAggregations {
/**
* @return The key associated with the bucket as a string
@ -62,66 +62,28 @@ public interface MultiBucketsAggregation extends Aggregation {
static class SubAggregationComparator<B extends Bucket> implements java.util.Comparator<B> {
private final String aggName;
private final String valueName;
private final OrderPath path;
private final boolean asc;
public SubAggregationComparator(String expression, boolean asc) {
this.asc = asc;
int i = expression.indexOf('.');
if (i < 0) {
this.aggName = expression;
this.valueName = null;
} else {
this.aggName = expression.substring(0, i);
this.valueName = expression.substring(i+1);
}
}
public SubAggregationComparator(String aggName, String valueName, boolean asc) {
this.aggName = aggName;
this.valueName = valueName;
this.asc = asc;
this.path = OrderPath.parse(expression);
}
public boolean asc() {
return asc;
}
public String aggName() {
return aggName;
}
public String valueName() {
return valueName;
public OrderPath path() {
return path;
}
@Override
public int compare(B b1, B b2) {
double v1 = value(b1);
double v2 = value(b2);
double v1 = path.resolveValue(b1);
double v2 = path.resolveValue(b2);
return Comparators.compareDiscardNaN(v1, v2, asc);
}
private double value(B bucket) {
MetricsAggregation aggregation = bucket.getAggregations().get(aggName);
if (aggregation == null) {
throw new ElasticsearchIllegalArgumentException("Unknown aggregation named [" + aggName + "]");
}
if (aggregation instanceof MetricsAggregation.SingleValue) {
//TODO should we throw an exception if the value name is specified?
return ((MetricsAggregation.SingleValue) aggregation).value();
}
if (aggregation instanceof MetricsAggregation.MultiValue) {
if (valueName == null) {
throw new ElasticsearchIllegalArgumentException("Cannot sort on multi valued aggregation [" + aggName + "]. A value name is required");
}
return ((MetricsAggregation.MultiValue) aggregation).value(valueName);
}
throw new ElasticsearchIllegalArgumentException("A mal attempt to sort terms by aggregation [" + aggregation.getName() +
"]. Terms can only be ordered by either standard order or direct calc aggregators of the terms");
}
}
}

View File

@ -21,11 +21,12 @@ package org.elasticsearch.search.aggregations.bucket;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.HasAggregations;
/**
* A single bucket aggregation
*/
public interface SingleBucketAggregation extends Aggregation {
public interface SingleBucketAggregation extends Aggregation, HasAggregations {
/**
* @return The number of documents in this bucket

View File

@ -238,11 +238,7 @@ public class DateHistogramParser implements Aggregator.Parser {
if ("_count".equals(key)) {
return (InternalOrder) (asc ? InternalOrder.COUNT_ASC : InternalOrder.COUNT_DESC);
}
int i = key.indexOf('.');
if (i < 0) {
return new InternalOrder.Aggregation(key, null, asc);
}
return new InternalOrder.Aggregation(key.substring(0, i), key.substring(i + 1), asc);
return new InternalOrder.Aggregation(key, asc);
}
private long parseOffset(String offset) throws IOException {

View File

@ -20,7 +20,6 @@ package org.elasticsearch.search.aggregations.bucket.histogram;
import com.google.common.primitives.Longs;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
import java.util.Collection;
@ -110,11 +109,11 @@ public interface Histogram extends MultiBucketsAggregation {
/**
* Creates a bucket ordering strategy that sorts buckets based on a single-valued calc sug-aggregation
*
* @param aggregationName the name of the aggregation
* @param path the name of the aggregation
* @param asc The direction of the order (ascending or descending)
*/
public static Order aggregation(String aggregationName, boolean asc) {
return new InternalOrder.Aggregation(aggregationName, null, asc);
public static Order aggregation(String path, boolean asc) {
return new InternalOrder.Aggregation(path, asc);
}
/**
@ -125,7 +124,7 @@ public interface Histogram extends MultiBucketsAggregation {
* @param asc The direction of the order (ascending or descending)
*/
public static Order aggregation(String aggregationName, String valueName, boolean asc) {
return new InternalOrder.Aggregation(aggregationName, valueName, asc);
return new InternalOrder.Aggregation(aggregationName + "." + valueName, asc);
}
/**

View File

@ -23,12 +23,12 @@ import org.apache.lucene.util.CollectionUtil;
import org.elasticsearch.common.inject.internal.Nullable;
import org.elasticsearch.common.lease.Releasables;
import org.elasticsearch.common.rounding.Rounding;
import org.elasticsearch.common.util.LongHash;
import org.elasticsearch.index.fielddata.LongValues;
import org.elasticsearch.search.aggregations.Aggregator;
import org.elasticsearch.search.aggregations.AggregatorFactories;
import org.elasticsearch.search.aggregations.InternalAggregation;
import org.elasticsearch.search.aggregations.bucket.BucketsAggregator;
import org.elasticsearch.common.util.LongHash;
import org.elasticsearch.search.aggregations.support.AggregationContext;
import org.elasticsearch.search.aggregations.support.ValueSourceAggregatorFactory;
import org.elasticsearch.search.aggregations.support.ValuesSourceConfig;

View File

@ -159,10 +159,6 @@ public class HistogramParser implements Aggregator.Parser {
if ("_count".equals(key)) {
return (InternalOrder) (asc ? InternalOrder.COUNT_ASC : InternalOrder.COUNT_DESC);
}
int i = key.indexOf('.');
if (i < 0) {
return new InternalOrder.Aggregation(key, null, asc);
}
return new InternalOrder.Aggregation(key.substring(0, i), key.substring(i + 1), asc);
return new InternalOrder.Aggregation(key, asc);
}
}

View File

@ -73,10 +73,6 @@ class InternalOrder extends Histogram.Order {
super(ID, key, asc, new MultiBucketsAggregation.Bucket.SubAggregationComparator<InternalHistogram.Bucket>(key, asc));
}
Aggregation(String aggName, String valueName, boolean asc) {
super(ID, key(aggName, valueName), asc, new MultiBucketsAggregation.Bucket.SubAggregationComparator<InternalHistogram.Bucket>(aggName, valueName, asc));
}
private static String key(String aggName, String valueName) {
return (valueName == null) ? aggName : aggName + "." + valueName;
}

View File

@ -19,14 +19,17 @@
package org.elasticsearch.search.aggregations.bucket.terms;
import com.google.common.primitives.Longs;
import org.elasticsearch.Version;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.util.Comparators;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.search.aggregations.AggregationExecutionException;
import org.elasticsearch.search.aggregations.Aggregator;
import org.elasticsearch.search.aggregations.bucket.BucketsAggregator;
import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
import org.elasticsearch.search.aggregations.bucket.SingleBucketAggregator;
import org.elasticsearch.search.aggregations.metrics.MetricsAggregator;
import org.elasticsearch.search.aggregations.support.OrderPath;
import java.io.IOException;
import java.util.Comparator;
@ -104,14 +107,6 @@ class InternalOrder extends Terms.Order {
return id;
}
String key() {
return key;
}
boolean asc() {
return asc;
}
@Override
protected Comparator<Terms.Bucket> comparator(Aggregator aggregator) {
return comparator;
@ -126,39 +121,9 @@ class InternalOrder extends Terms.Order {
if (!(order instanceof Aggregation)) {
return order;
}
String aggName = ((Aggregation) order).aggName();
Aggregator[] subAggregators = termsAggregator.subAggregators();
for (int i = 0; i < subAggregators.length; i++) {
Aggregator aggregator = subAggregators[i];
if (aggregator.name().equals(aggName)) {
// we can only apply order on metrics sub-aggregators
if (!(aggregator instanceof MetricsAggregator)) {
throw new AggregationExecutionException("terms aggregation [" + termsAggregator.name() + "] is configured to order by sub-aggregation ["
+ aggName + "] which is is not a metrics aggregation. Terms aggregation order can only refer to metrics aggregations");
}
if (aggregator instanceof MetricsAggregator.MultiValue) {
String valueName = ((Aggregation) order).metricName();
if (valueName == null) {
throw new AggregationExecutionException("terms aggregation [" + termsAggregator.name() + "] is configured with a sub-aggregation order ["
+ aggName + "] which is a multi-valued aggregation, yet no metric name was specified");
}
if (!((MetricsAggregator.MultiValue) aggregator).hasMetric(valueName)) {
throw new AggregationExecutionException("terms aggregation [" + termsAggregator.name() + "] is configured with a sub-aggregation order ["
+ aggName + "] and value [" + valueName + "] yet the referred sub aggregator holds no metric that goes by this name");
}
return order;
}
// aggregator must be of a single value type
// todo we can also choose to be really strict and verify that the user didn't specify a value name and if so fail?
return order;
}
}
throw new AggregationExecutionException("terms aggregation [" + termsAggregator.name() + "] is configured with a sub-aggregation order ["
+ aggName + "] but no sub aggregation with this name is configured");
OrderPath path = ((Aggregation) order).path();
path.validate(termsAggregator);
return order;
}
static class Aggregation extends InternalOrder {
@ -169,22 +134,8 @@ class InternalOrder extends Terms.Order {
super(ID, key, asc, new MultiBucketsAggregation.Bucket.SubAggregationComparator<Terms.Bucket>(key, asc));
}
Aggregation(String aggName, String metricName, boolean asc) {
super(ID, key(aggName, metricName), asc, new MultiBucketsAggregation.Bucket.SubAggregationComparator<Terms.Bucket>(aggName, metricName, asc));
}
String aggName() {
int index = key.indexOf('.');
return index < 0 ? key : key.substring(0, index);
}
String metricName() {
int index = key.indexOf('.');
return index < 0 ? null : key.substring(index + 1, key.length());
}
private static String key(String aggName, String valueName) {
return (valueName == null) ? aggName : aggName + "." + valueName;
OrderPath path() {
return ((MultiBucketsAggregation.Bucket.SubAggregationComparator) comparator).path();
}
@Override
@ -201,16 +152,32 @@ class InternalOrder extends Terms.Order {
// sub aggregation values directly from the sub aggregators bypassing bucket creation. Note that the comparator
// attached to the order will still be used in the reduce phase of the Aggregation.
final Aggregator aggregator = subAggregator(aggName(), termsAggregator);
assert aggregator != null && aggregator instanceof MetricsAggregator : "this should be picked up before the aggregation is executed";
if (aggregator instanceof MetricsAggregator.MultiValue) {
final String valueName = metricName();
assert valueName != null : "this should be picked up before the aggregation is executed";
OrderPath path = path();
final Aggregator aggregator = path.resolveAggregator(termsAggregator, false);
final String key = path.tokens[path.tokens.length - 1].key;
if (aggregator instanceof SingleBucketAggregator) {
assert key == null : "this should be picked up before the aggregation is executed - on validate";
return new Comparator<Terms.Bucket>() {
@Override
public int compare(Terms.Bucket o1, Terms.Bucket o2) {
double v1 = ((MetricsAggregator.MultiValue) aggregator).metric(valueName, ((InternalTerms.Bucket) o1).bucketOrd);
double v2 = ((MetricsAggregator.MultiValue) aggregator).metric(valueName, ((InternalTerms.Bucket) o2).bucketOrd);
long v1 = ((SingleBucketAggregator) aggregator).bucketDocCount(((InternalTerms.Bucket) o1).bucketOrd);
long v2 = ((SingleBucketAggregator) aggregator).bucketDocCount(((InternalTerms.Bucket) o2).bucketOrd);
return asc ? Longs.compare(v1, v2) : Longs.compare(v2, v1);
}
};
}
// with only support single-bucket aggregators
assert !(aggregator instanceof BucketsAggregator) : "this should be picked up before the aggregation is executed - on validate";
if (aggregator instanceof MetricsAggregator.MultiValue) {
assert key != null : "this should be picked up before the aggregation is executed - on validate";
return new Comparator<Terms.Bucket>() {
@Override
public int compare(Terms.Bucket o1, Terms.Bucket o2) {
double v1 = ((MetricsAggregator.MultiValue) aggregator).metric(key, ((InternalTerms.Bucket) o1).bucketOrd);
double v2 = ((MetricsAggregator.MultiValue) aggregator).metric(key, ((InternalTerms.Bucket) o2).bucketOrd);
// some metrics may return NaN (eg. avg, variance, etc...) in which case we'd like to push all of those to
// the bottom
return Comparators.compareDiscardNaN(v1, v2, asc);
@ -218,6 +185,7 @@ class InternalOrder extends Terms.Order {
};
}
// single-value metrics agg
return new Comparator<Terms.Bucket>() {
@Override
public int compare(Terms.Bucket o1, Terms.Bucket o2) {
@ -229,16 +197,6 @@ class InternalOrder extends Terms.Order {
}
};
}
private Aggregator subAggregator(String aggName, Aggregator termsAggregator) {
Aggregator[] subAggregators = termsAggregator.subAggregators();
for (int i = 0; i < subAggregators.length; i++) {
if (subAggregators[i].name().equals(aggName)) {
return subAggregators[i];
}
}
return null;
}
}
public static class Streams {
@ -247,11 +205,18 @@ class InternalOrder extends Terms.Order {
out.writeByte(order.id());
if (order instanceof Aggregation) {
out.writeBoolean(((MultiBucketsAggregation.Bucket.SubAggregationComparator) order.comparator).asc());
out.writeString(((MultiBucketsAggregation.Bucket.SubAggregationComparator) order.comparator).aggName());
boolean hasValueName = ((MultiBucketsAggregation.Bucket.SubAggregationComparator) order.comparator).valueName() != null;
out.writeBoolean(hasValueName);
if (hasValueName) {
out.writeString(((MultiBucketsAggregation.Bucket.SubAggregationComparator) order.comparator).valueName());
OrderPath path = ((Aggregation) order).path();
if (out.getVersion().onOrAfter(Version.V_1_1_0)) {
out.writeString(path.toString());
} else {
// prev versions only supported sorting on a single level -> a single token;
OrderPath.Token token = path.lastToken();
out.writeString(token.name);
boolean hasValueName = token.key != null;
out.writeBoolean(hasValueName);
if (hasValueName) {
out.writeString(token.key);
}
}
}
}
@ -266,8 +231,12 @@ class InternalOrder extends Terms.Order {
case 0:
boolean asc = in.readBoolean();
String key = in.readString();
if (in.readBoolean()) {
return new InternalOrder.Aggregation(key, in.readString(), asc);
if (in.getVersion().onOrAfter(Version.V_1_1_0)) {
return new InternalOrder.Aggregation(key, asc);
}
boolean hasValueNmae = in.readBoolean();
if (hasValueNmae) {
return new InternalOrder.Aggregation(key + "." + in.readString(), asc);
}
return new InternalOrder.Aggregation(key, asc);
default:

View File

@ -95,11 +95,11 @@ public interface Terms extends MultiBucketsAggregation {
/**
* Creates a bucket ordering strategy which sorts buckets based on a single-valued calc get
*
* @param aggregationName the name of the get
* @param path the name of the get
* @param asc The direction of the order (ascending or descending)
*/
public static Order aggregation(String aggregationName, boolean asc) {
return new InternalOrder.Aggregation(aggregationName, null, asc);
public static Order aggregation(String path, boolean asc) {
return new InternalOrder.Aggregation(path, asc);
}
/**
@ -110,7 +110,7 @@ public interface Terms extends MultiBucketsAggregation {
* @param asc The direction of the order (ascending or descending)
*/
public static Order aggregation(String aggregationName, String metricName, boolean asc) {
return new InternalOrder.Aggregation(aggregationName, metricName, asc);
return new InternalOrder.Aggregation(aggregationName + "." + metricName, asc);
}
/**

View File

@ -276,11 +276,7 @@ public class TermsParser implements Aggregator.Parser {
if ("_count".equals(key)) {
return asc ? InternalOrder.COUNT_ASC : InternalOrder.COUNT_DESC;
}
int i = key.indexOf('.');
if (i < 0) {
return new InternalOrder.Aggregation(key, asc);
}
return new InternalOrder.Aggregation(key.substring(0, i), key.substring(i+1), asc);
return new InternalOrder.Aggregation(key, asc);
}
}

View File

@ -0,0 +1,319 @@
/*
* 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.support;
import org.elasticsearch.ElasticsearchIllegalArgumentException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationExecutionException;
import org.elasticsearch.search.aggregations.Aggregator;
import org.elasticsearch.search.aggregations.HasAggregations;
import org.elasticsearch.search.aggregations.bucket.SingleBucketAggregation;
import org.elasticsearch.search.aggregations.bucket.SingleBucketAggregator;
import org.elasticsearch.search.aggregations.metrics.MetricsAggregation;
import org.elasticsearch.search.aggregations.metrics.MetricsAggregator;
/**
* A path that can be used to sort/order buckets (in some multi-bucket aggregations, eg terms & histogram) based on
* sub-aggregations. The path may point to either a single-bucket aggregation or a metrics aggregation. If the path
* points to a single-bucket aggregation, the sort will be applied based on the {@code doc_count} of the bucket. If this
* path points to a metrics aggregation, if it's a single-value metrics (eg. avg, max, min, etc..) the sort will be
* applied on that single value. If it points to a multi-value metrics, the path should point out what metric should be
* the sort-by value.
* <p/>
* The path has the following form:
* <p/>
* <center>{@code <aggregation_name>['>'<aggregation_name>*]['.'<metric_name>]}</center>
* <p/>
* <p/>
* Examples:
*
* <ul>
* <li>
* {@code agg1>agg2>agg3} - where agg1, agg2 and agg3 are all single-bucket aggs (eg filter, nested, missing, etc..). In
* this case, the order will be based on the number of documents under {@code agg3}.
* </li>
* <li>
* {@code agg1>agg2>agg3} - where agg1 and agg2 are both single-bucket aggs and agg3 is a single-value metrics agg (eg avg, max, min, etc..).
* In this case, the order will be based on the value of {@code agg3}.
* </li>
* <li>
* {@code agg1>agg2>agg3.avg} - where agg1 and agg2 are both single-bucket aggs and agg3 is a multi-value metrics agg (eg stats, extended_stats, etc...).
* In this case, the order will be based on the avg value of {@code agg3}.
* </li>
* </ul>
*
*/
public class OrderPath {
private final static String AGG_DELIM = ">";
public static OrderPath parse(String path) {
String[] elements = Strings.tokenizeToStringArray(path, AGG_DELIM);
Token[] tokens = new Token[elements.length];
String[] tuple = new String[2];
for (int i = 0; i < elements.length; i++) {
String element = elements[i];
int index = element.lastIndexOf('.');
if (index >= 0) {
if (index == 0 || index > element.length() - 2) {
throw new AggregationExecutionException("Invalid path element [" + element + "] in path [" + path + "]");
}
tuple = split(element, index, tuple);
tokens[i] = new Token(element, tuple[0], tuple[1]);
continue;
}
index = element.lastIndexOf('[');
if (index < 0) {
tokens[i] = new Token(element, element, null);
continue;
}
if (index == 0 || index > element.length() - 3) {
throw new AggregationExecutionException("Invalid path element [" + element + "] in path [" + path + "]");
}
if (element.charAt(element.length() - 1) != ']') {
throw new AggregationExecutionException("Invalid path element [" + element + "] in path [" + path + "]");
}
tokens[i] = new Token(element, element.substring(0, index), element.substring(index + 1, element.length() - 1));
}
return new OrderPath(tokens);
}
public static class Token {
private final String fullName;
public final String name;
public final String key;
public Token(String fullName, String name, String key) {
this.fullName = fullName;
this.name = name;
this.key = key;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Token token = (Token) o;
if (key != null ? !key.equals(token.key) : token.key != null) return false;
if (!name.equals(token.name)) return false;
return true;
}
@Override
public int hashCode() {
int result = name.hashCode();
result = 31 * result + (key != null ? key.hashCode() : 0);
return result;
}
@Override
public String toString() {
return fullName;
}
}
public final Token[] tokens;
public OrderPath(Token[] tokens) {
this.tokens = tokens;
if (tokens == null || tokens.length == 0) {
throw new ElasticsearchIllegalArgumentException("Invalid path [" + this + "]");
}
}
@Override
public String toString() {
return Strings.arrayToDelimitedString(tokens, AGG_DELIM);
}
public Token lastToken() {
return tokens[tokens.length - 1];
}
public OrderPath subPath(int offset, int length) {
Token[] subTokens = new Token[length];
System.arraycopy(tokens, offset, subTokens, 0, length);
return new OrderPath(tokens);
}
/**
* Resolves the value pointed by this path given an aggregations root
*
* @param root The root that serves as a point of reference for this path
* @return The resolved value
*/
public double resolveValue(HasAggregations root) {
HasAggregations parent = root;
double value = Double.NaN;
for (int i = 0; i < tokens.length; i++) {
OrderPath.Token token = tokens[i];
Aggregation agg = parent.getAggregations().get(token.name);
if (agg == null) {
throw new ElasticsearchIllegalArgumentException("Invalid order path [" + this +
"]. Cannot find aggregation named [" + token.name + "]");
}
if (agg instanceof SingleBucketAggregation) {
if (token.key != null && !token.key.equals("doc_count")) {
throw new ElasticsearchIllegalArgumentException("Invalid order path [" + this +
"]. Unknown value key [" + token.key + "] for single-bucket aggregation [" + token.name +
"]. Either use [doc_count] as key or drop the key all together");
}
parent = (SingleBucketAggregation) agg;
value = ((SingleBucketAggregation) agg).getDocCount();
continue;
}
// the agg can only be a metrics agg, and a metrics agg must be at the end of the path
if (i != tokens.length - 1) {
throw new ElasticsearchIllegalArgumentException("Invalid order path [" + this +
"]. Metrics aggregations cannot have sub-aggregations (at [" + token + ">" + tokens[i+1] + "]");
}
if (agg instanceof MetricsAggregation.SingleValue) {
if (token.key != null && !token.key.equals("value")) {
throw new ElasticsearchIllegalArgumentException("Invalid order path [" + this +
"]. Unknown value key [" + token.key + "] for single-value metric aggregation [" + token.name +
"]. Either use [value] as key or drop the key all together");
}
parent = null;
value = ((MetricsAggregation.SingleValue) agg).value();
continue;
}
// we're left with a multi-value metric agg
if (token.key == null) {
throw new ElasticsearchIllegalArgumentException("Invalid order path [" + this +
"]. Missing value key in [" + token + "] which refers to a multi-value metric aggregation");
}
parent = null;
value = ((MetricsAggregation.MultiValue) agg).value(token.key);
}
return value;
}
/**
* Resolves the aggregator pointed by this path using the given root as a point of reference.
*
* @param root The point of reference of this path
* @param validate Indicates whether the path should be validated first over the given root aggregator
* @return The aggregator pointed by this path starting from the given aggregator as a point of reference
*/
public Aggregator resolveAggregator(Aggregator root, boolean validate) {
if (validate) {
validate(root);
}
Aggregator aggregator = root;
for (int i = 0; i < tokens.length; i++) {
OrderPath.Token token = tokens[i];
aggregator = aggregator.subAggregator(token.name);
assert (aggregator instanceof SingleBucketAggregator && i <= tokens.length - 1) ||
(aggregator instanceof MetricsAggregator && i == tokens.length - 1) :
"this should be picked up before aggregation execution - on validate";
}
return aggregator;
}
/**
* Validates this path over the given aggregator as a point of reference.
*
* @param root The point of reference of this path
*/
public void validate(Aggregator root) {
Aggregator aggregator = root;
for (int i = 0; i < tokens.length; i++) {
aggregator = aggregator.subAggregator(tokens[i].name);
if (aggregator == null) {
throw new AggregationExecutionException("Invalid term-aggregator order path [" + this + "]. Unknown aggregation [" + tokens[i].name + "]");
}
if (i < tokens.length - 1) {
// we're in the middle of the path, so the aggregator can only be a single-bucket aggregator
if (!(aggregator instanceof SingleBucketAggregator)) {
throw new AggregationExecutionException("Invalid terms aggregation order path [" + this +
"]. Terms buckets can only be sorted on a sub-aggregator path " +
"that is built out of zero or more single-bucket aggregations within the path and a final " +
"single-bucket or a metrics aggregation at the path end. Sub-path [" +
subPath(0, i + 1) + "] points to non single-bucket aggregation");
}
if (tokens[i].key != null) {
throw new AggregationExecutionException("Invalid terms aggregation order path [" + this +
"]. Terms buckets can only be sorted on a sub-aggregator path " +
"that is built out of zero or more single-bucket aggregations within the path and a " +
"final single-bucket or a metrics aggregation at the path end. Sub-path [" +
subPath(0, i + 1) + "] points to non single-bucket aggregation");
}
}
}
boolean singleBucket = aggregator instanceof SingleBucketAggregator;
if (!singleBucket && !(aggregator instanceof MetricsAggregator)) {
throw new AggregationExecutionException("Invalid terms aggregation order path [" + this +
"]. Terms buckets can only be sorted on a sub-aggregator path " +
"that is built out of zero or more single-bucket aggregations within the path and a final " +
"single-bucket or a metrics aggregation at the path end.");
}
OrderPath.Token lastToken = lastToken();
if (singleBucket) {
if (lastToken.key != null && !"doc_count".equals(lastToken.key)) {
throw new AggregationExecutionException("Invalid terms aggregation order path [" + this +
"]. Ordering on a single-bucket aggregation can only be done on its doc_count. " +
"Either drop the key (a la \"" + lastToken.name + "\") or change it to \"doc_count\" (a la \"" + lastToken.name + ".doc_count\")");
}
return; // perfectly valid to sort on single-bucket aggregation (will be sored on its doc_count)
}
if (aggregator instanceof MetricsAggregator.SingleValue) {
if (lastToken.key != null && !"value".equals(lastToken.key)) {
throw new AggregationExecutionException("Invalid terms aggregation order path [" + this +
"]. Ordering on a single-value metrics aggregation can only be done on its value. " +
"Either drop the key (a la \"" + lastToken.name + "\") or change it to \"value\" (a la \"" + lastToken.name + ".value\")");
}
return; // perfectly valid to sort on single metric aggregation (will be sorted on its associated value)
}
// the aggregator must be of a multi-value metrics type
if (lastToken.key == null) {
throw new AggregationExecutionException("Invalid terms aggregation order path [" + this +
"]. When ordering on a multi-value metrics aggregation a metric name must be specified");
}
if (!((MetricsAggregator.MultiValue) aggregator).hasMetric(lastToken.key)) {
throw new AggregationExecutionException("Invalid terms aggregation order path [" + this +
"]. Unknown metric name [" + lastToken.key + "] on multi-value metrics aggregation [" + lastToken.name + "]");
}
}
private static String[] split(String toSplit, int index, String[] result) {
result[0] = toSplit.substring(0, index);
result[1] = toSplit.substring(index + 1);
return result;
}
}

View File

@ -20,10 +20,15 @@
package org.elasticsearch.search.aggregations;
import org.elasticsearch.action.search.SearchPhaseExecutionException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.test.ElasticsearchIntegrationTest;
import org.junit.Test;
import java.security.SecureRandom;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ParsingTests extends ElasticsearchIntegrationTest {
@Test(expected=SearchPhaseExecutionException.class)
@ -46,4 +51,35 @@ public class ParsingTests extends ElasticsearchIntegrationTest {
.endObject()).execute().actionGet();
}
@Test(expected=SearchPhaseExecutionException.class)
public void testInvalidAggregationName() throws Exception {
Matcher matcher = Pattern.compile("[a-zA-Z0-9\\-_]+").matcher("");
String name;
SecureRandom rand = new SecureRandom();
int len = randomIntBetween(1, 5);
char[] word = new char[len];
while(true) {
for (int i = 0; i < word.length; i++) {
word[i] = (char) rand.nextInt(127);
}
name = String.valueOf(word);
if (!matcher.reset(name).matches()) {
break;
}
}
createIndex("idx");
client().prepareSearch("idx").setAggregations(JsonXContent.contentBuilder()
.startObject()
.startObject(name)
.startObject("filter")
.startObject("range")
.startObject("stock")
.field("gt", 0)
.endObject()
.endObject()
.endObject()
.endObject()).execute().actionGet();
}
}

View File

@ -25,9 +25,11 @@ import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.query.FilterBuilders;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.search.aggregations.bucket.filter.Filter;
import org.elasticsearch.search.aggregations.bucket.histogram.Histogram;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.metrics.avg.Avg;
import org.elasticsearch.search.aggregations.metrics.max.Max;
import org.elasticsearch.search.aggregations.metrics.stats.Stats;
import org.elasticsearch.search.aggregations.metrics.stats.extended.ExtendedStats;
import org.elasticsearch.search.aggregations.metrics.sum.Sum;
@ -38,6 +40,7 @@ import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
@ -75,7 +78,8 @@ public class DoubleTermsTests extends ElasticsearchIntegrationTest {
lowcardBuilders[i] = client().prepareIndex("idx", "type").setSource(jsonBuilder()
.startObject()
.field(SINGLE_VALUED_FIELD_NAME, (double) i)
.startArray(MULTI_VALUED_FIELD_NAME).value((double)i).value(i + 1d).endArray()
.field("num_tag", i < lowcardBuilders.length/2 + 1 ? 1 : 0) // used to test order by single-bucket sub agg
.startArray(MULTI_VALUED_FIELD_NAME).value((double) i).value(i + 1d).endArray()
.endObject());
}
@ -673,6 +677,97 @@ public class DoubleTermsTests extends ElasticsearchIntegrationTest {
}
}
@Test
public void singleValuedField_OrderedBySingleBucketSubAggregationAsc() throws Exception {
boolean asc = randomBoolean();
SearchResponse response = client().prepareSearch("idx").setTypes("type")
.addAggregation(terms("num_tags")
.field("num_tag")
.order(Terms.Order.aggregation("filter", asc))
.subAggregation(filter("filter").filter(FilterBuilders.matchAllFilter()))
).execute().actionGet();
assertSearchResponse(response);
Terms tags = response.getAggregations().get("num_tags");
assertThat(tags, notNullValue());
assertThat(tags.getName(), equalTo("num_tags"));
assertThat(tags.getBuckets().size(), equalTo(2));
Iterator<Terms.Bucket> iters = tags.getBuckets().iterator();
Terms.Bucket tag = iters.next();
assertThat(tag, notNullValue());
assertThat(key(tag), equalTo(asc ? "0" : "1"));
assertThat(tag.getDocCount(), equalTo(asc ? 2l : 3l));
Filter filter = tag.getAggregations().get("filter");
assertThat(filter, notNullValue());
assertThat(filter.getDocCount(), equalTo(asc ? 2l : 3l));
tag = iters.next();
assertThat(tag, notNullValue());
assertThat(key(tag), equalTo(asc ? "1" : "0"));
assertThat(tag.getDocCount(), equalTo(asc ? 3l : 2l));
filter = tag.getAggregations().get("filter");
assertThat(filter, notNullValue());
assertThat(filter.getDocCount(), equalTo(asc ? 3l : 2l));
}
@Test
public void singleValuedField_OrderedBySubAggregationAsc_MultiHierarchyLevels() throws Exception {
boolean asc = randomBoolean();
SearchResponse response = client().prepareSearch("idx").setTypes("type")
.addAggregation(terms("tags")
.field("num_tag")
.order(Terms.Order.aggregation("filter1>filter2>max", asc))
.subAggregation(filter("filter1").filter(FilterBuilders.matchAllFilter())
.subAggregation(filter("filter2").filter(FilterBuilders.matchAllFilter())
.subAggregation(max("max").field(SINGLE_VALUED_FIELD_NAME))))
).execute().actionGet();
assertSearchResponse(response);
Terms tags = response.getAggregations().get("tags");
assertThat(tags, notNullValue());
assertThat(tags.getName(), equalTo("tags"));
assertThat(tags.getBuckets().size(), equalTo(2));
Iterator<Terms.Bucket> iters = tags.getBuckets().iterator();
// the max for "1" is 2
// the max for "0" is 4
Terms.Bucket tag = iters.next();
assertThat(tag, notNullValue());
assertThat(key(tag), equalTo(asc ? "1" : "0"));
assertThat(tag.getDocCount(), equalTo(asc ? 3l : 2l));
Filter filter1 = tag.getAggregations().get("filter1");
assertThat(filter1, notNullValue());
assertThat(filter1.getDocCount(), equalTo(asc ? 3l : 2l));
Filter filter2 = filter1.getAggregations().get("filter2");
assertThat(filter2, notNullValue());
assertThat(filter2.getDocCount(), equalTo(asc ? 3l : 2l));
Max max = filter2.getAggregations().get("max");
assertThat(max, notNullValue());
assertThat(max.getValue(), equalTo(asc ? 2.0 : 4.0));
tag = iters.next();
assertThat(tag, notNullValue());
assertThat(key(tag), equalTo(asc ? "0" : "1"));
assertThat(tag.getDocCount(), equalTo(asc ? 2l : 3l));
filter1 = tag.getAggregations().get("filter1");
assertThat(filter1, notNullValue());
assertThat(filter1.getDocCount(), equalTo(asc ? 2l : 3l));
filter2 = filter1.getAggregations().get("filter2");
assertThat(filter2, notNullValue());
assertThat(filter2.getDocCount(), equalTo(asc ? 2l : 3l));
max = filter2.getAggregations().get("max");
assertThat(max, notNullValue());
assertThat(max.getValue(), equalTo(asc ? 4.0 : 2.0));
}
@Test
public void singleValuedField_OrderedByMissingSubAggregation() throws Exception {
@ -693,7 +788,7 @@ public class DoubleTermsTests extends ElasticsearchIntegrationTest {
}
@Test
public void singleValuedField_OrderedByNonMetricsSubAggregation() throws Exception {
public void singleValuedField_OrderedByNonMetricsOrMultiBucketSubAggregation() throws Exception {
MockBigArrays.discardNextCheck();
try {
@ -701,8 +796,8 @@ public class DoubleTermsTests extends ElasticsearchIntegrationTest {
client().prepareSearch("idx").setTypes("type")
.addAggregation(terms("terms")
.field(SINGLE_VALUED_FIELD_NAME)
.order(Terms.Order.aggregation("filter", true))
.subAggregation(filter("filter").filter(FilterBuilders.termFilter("foo", "bar")))
.order(Terms.Order.aggregation("num_tags", true))
.subAggregation(terms("num_tags").field("num_tags"))
).execute().actionGet();
fail("Expected search to fail when trying to sort terms aggregation by sug-aggregation which is not of a metrics type");

View File

@ -23,8 +23,10 @@ import org.elasticsearch.action.index.IndexRequestBuilder;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.search.aggregations.bucket.filter.Filter;
import org.elasticsearch.search.aggregations.bucket.histogram.Histogram;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.metrics.max.Max;
import org.elasticsearch.search.aggregations.metrics.stats.Stats;
import org.elasticsearch.search.aggregations.metrics.sum.Sum;
import org.elasticsearch.test.ElasticsearchIntegrationTest;
@ -37,6 +39,7 @@ import java.util.Iterator;
import java.util.List;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.elasticsearch.index.query.FilterBuilders.matchAllFilter;
import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
import static org.elasticsearch.search.aggregations.AggregationBuilders.*;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse;
@ -458,6 +461,44 @@ public class HistogramTests extends ElasticsearchIntegrationTest {
}
}
@Test
public void singleValuedField_OrderedBySubAggregationDesc_DeepOrderPath() throws Exception {
boolean asc = randomBoolean();
SearchResponse response = client().prepareSearch("idx")
.addAggregation(histogram("histo").field(SINGLE_VALUED_FIELD_NAME).interval(interval).order(Histogram.Order.aggregation("filter>max", asc))
.subAggregation(filter("filter").filter(matchAllFilter())
.subAggregation(max("max").field(SINGLE_VALUED_FIELD_NAME))))
.execute().actionGet();
assertSearchResponse(response);
Histogram histo = response.getAggregations().get("histo");
assertThat(histo, notNullValue());
assertThat(histo.getName(), equalTo("histo"));
assertThat(histo.getBuckets().size(), equalTo(numValueBuckets));
LongOpenHashSet visited = new LongOpenHashSet();
double prevMax = asc? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY;
List<Histogram.Bucket> buckets = new ArrayList<Histogram.Bucket>(histo.getBuckets());
for (int i = 0; i < numValueBuckets; ++i) {
Histogram.Bucket bucket = buckets.get(i);
assertThat(bucket, notNullValue());
long key = bucket.getKeyAsNumber().longValue();
assertTrue(visited.add(key));
int b = (int) (key / interval);
assertThat(bucket.getDocCount(), equalTo(valueCounts[b]));
assertThat(bucket.getAggregations().asList().isEmpty(), is(false));
Filter filter = bucket.getAggregations().get("filter");
assertThat(filter, notNullValue());
assertThat(bucket.getDocCount(), equalTo(filter.getDocCount()));
Max max = filter.getAggregations().get("max");
assertThat(max, Matchers.notNullValue());
assertThat(max.getValue(), asc ? greaterThanOrEqualTo(prevMax) : lessThanOrEqualTo(prevMax));
prevMax = max.getValue();
}
}
@Test
public void singleValuedField_WithValueScript() throws Exception {
SearchResponse response = client().prepareSearch("idx")

View File

@ -24,9 +24,11 @@ import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.query.FilterBuilders;
import org.elasticsearch.search.aggregations.bucket.filter.Filter;
import org.elasticsearch.search.aggregations.bucket.histogram.Histogram;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.metrics.avg.Avg;
import org.elasticsearch.search.aggregations.metrics.max.Max;
import org.elasticsearch.search.aggregations.metrics.stats.Stats;
import org.elasticsearch.search.aggregations.metrics.stats.extended.ExtendedStats;
import org.elasticsearch.search.aggregations.metrics.sum.Sum;
@ -37,6 +39,7 @@ import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
@ -73,6 +76,7 @@ public class LongTermsTests extends ElasticsearchIntegrationTest {
.startObject()
.field(SINGLE_VALUED_FIELD_NAME, i)
.startArray(MULTI_VALUED_FIELD_NAME).value(i).value(i + 1).endArray()
.field("num_tag", i < lowCardBuilders.length / 2 + 1 ? 1 : 0) // used to test order by single-bucket sub agg
.endObject());
}
indexRandom(randomBoolean(), lowCardBuilders);
@ -665,6 +669,97 @@ public class LongTermsTests extends ElasticsearchIntegrationTest {
}
}
@Test
public void singleValuedField_OrderedBySingleBucketSubAggregationAsc() throws Exception {
boolean asc = randomBoolean();
SearchResponse response = client().prepareSearch("idx").setTypes("type")
.addAggregation(terms("num_tags")
.field("num_tag")
.order(Terms.Order.aggregation("filter", asc))
.subAggregation(filter("filter").filter(FilterBuilders.matchAllFilter()))
).execute().actionGet();
assertSearchResponse(response);
Terms tags = response.getAggregations().get("num_tags");
assertThat(tags, notNullValue());
assertThat(tags.getName(), equalTo("num_tags"));
assertThat(tags.getBuckets().size(), equalTo(2));
Iterator<Terms.Bucket> iters = tags.getBuckets().iterator();
Terms.Bucket tag = iters.next();
assertThat(tag, notNullValue());
assertThat(key(tag), equalTo(asc ? "0" : "1"));
assertThat(tag.getDocCount(), equalTo(asc ? 2l : 3l));
Filter filter = tag.getAggregations().get("filter");
assertThat(filter, notNullValue());
assertThat(filter.getDocCount(), equalTo(asc ? 2l : 3l));
tag = iters.next();
assertThat(tag, notNullValue());
assertThat(key(tag), equalTo(asc ? "1" : "0"));
assertThat(tag.getDocCount(), equalTo(asc ? 3l : 2l));
filter = tag.getAggregations().get("filter");
assertThat(filter, notNullValue());
assertThat(filter.getDocCount(), equalTo(asc ? 3l : 2l));
}
@Test
public void singleValuedField_OrderedBySubAggregationAsc_MultiHierarchyLevels() throws Exception {
boolean asc = randomBoolean();
SearchResponse response = client().prepareSearch("idx").setTypes("type")
.addAggregation(terms("tags")
.field("num_tag")
.order(Terms.Order.aggregation("filter1>filter2>max", asc))
.subAggregation(filter("filter1").filter(FilterBuilders.matchAllFilter())
.subAggregation(filter("filter2").filter(FilterBuilders.matchAllFilter())
.subAggregation(max("max").field(SINGLE_VALUED_FIELD_NAME))))
).execute().actionGet();
assertSearchResponse(response);
Terms tags = response.getAggregations().get("tags");
assertThat(tags, notNullValue());
assertThat(tags.getName(), equalTo("tags"));
assertThat(tags.getBuckets().size(), equalTo(2));
Iterator<Terms.Bucket> iters = tags.getBuckets().iterator();
// the max for "1" is 2
// the max for "0" is 4
Terms.Bucket tag = iters.next();
assertThat(tag, notNullValue());
assertThat(key(tag), equalTo(asc ? "1" : "0"));
assertThat(tag.getDocCount(), equalTo(asc ? 3l : 2l));
Filter filter1 = tag.getAggregations().get("filter1");
assertThat(filter1, notNullValue());
assertThat(filter1.getDocCount(), equalTo(asc ? 3l : 2l));
Filter filter2 = filter1.getAggregations().get("filter2");
assertThat(filter2, notNullValue());
assertThat(filter2.getDocCount(), equalTo(asc ? 3l : 2l));
Max max = filter2.getAggregations().get("max");
assertThat(max, notNullValue());
assertThat(max.getValue(), equalTo(asc ? 2.0 : 4.0));
tag = iters.next();
assertThat(tag, notNullValue());
assertThat(key(tag), equalTo(asc ? "0" : "1"));
assertThat(tag.getDocCount(), equalTo(asc ? 2l : 3l));
filter1 = tag.getAggregations().get("filter1");
assertThat(filter1, notNullValue());
assertThat(filter1.getDocCount(), equalTo(asc ? 2l : 3l));
filter2 = filter1.getAggregations().get("filter2");
assertThat(filter2, notNullValue());
assertThat(filter2.getDocCount(), equalTo(asc ? 2l : 3l));
max = filter2.getAggregations().get("max");
assertThat(max, notNullValue());
assertThat(max.getValue(), equalTo(asc ? 4.0 : 2.0));
}
@Test
public void singleValuedField_OrderedByMissingSubAggregation() throws Exception {
@ -685,7 +780,7 @@ public class LongTermsTests extends ElasticsearchIntegrationTest {
}
@Test
public void singleValuedField_OrderedByNonMetricsSubAggregation() throws Exception {
public void singleValuedField_OrderedByNonMetricsOrMultiBucketSubAggregation() throws Exception {
MockBigArrays.discardNextCheck();
try {
@ -693,8 +788,8 @@ public class LongTermsTests extends ElasticsearchIntegrationTest {
client().prepareSearch("idx").setTypes("type")
.addAggregation(terms("terms")
.field(SINGLE_VALUED_FIELD_NAME)
.order(Terms.Order.aggregation("filter", true))
.subAggregation(filter("filter").filter(FilterBuilders.termFilter("foo", "bar")))
.order(Terms.Order.aggregation("num_tags", true))
.subAggregation(terms("num_tags").field("num_tags"))
).execute().actionGet();
fail("Expected search to fail when trying to sort terms aggregation by sug-aggregation which is not of a metrics type");

View File

@ -41,6 +41,7 @@ import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;
@ -82,6 +83,7 @@ public class StringTermsTests extends ElasticsearchIntegrationTest {
.startObject()
.field(SINGLE_VALUED_FIELD_NAME, "val" + i)
.field("i", i)
.field("tag", i < lowCardBuilders.length/2 + 1 ? "more" : "less")
.startArray(MULTI_VALUED_FIELD_NAME).value("val" + i).value("val" + (i + 1)).endArray()
.endObject());
}
@ -833,6 +835,100 @@ public class StringTermsTests extends ElasticsearchIntegrationTest {
}
}
@Test
public void singleValuedField_OrderedBySingleBucketSubAggregationAsc() throws Exception {
boolean asc = randomBoolean();
SearchResponse response = client().prepareSearch("idx").setTypes("type")
.addAggregation(terms("tags")
.executionHint(randomExecutionHint())
.field("tag")
.order(Terms.Order.aggregation("filter", asc))
.subAggregation(filter("filter").filter(FilterBuilders.matchAllFilter()))
).execute().actionGet();
assertSearchResponse(response);
Terms tags = response.getAggregations().get("tags");
assertThat(tags, notNullValue());
assertThat(tags.getName(), equalTo("tags"));
assertThat(tags.getBuckets().size(), equalTo(2));
Iterator<Terms.Bucket> iters = tags.getBuckets().iterator();
Terms.Bucket tag = iters.next();
assertThat(tag, notNullValue());
assertThat(key(tag), equalTo(asc ? "less" : "more"));
assertThat(tag.getDocCount(), equalTo(asc ? 2l : 3l));
Filter filter = tag.getAggregations().get("filter");
assertThat(filter, notNullValue());
assertThat(filter.getDocCount(), equalTo(asc ? 2l : 3l));
tag = iters.next();
assertThat(tag, notNullValue());
assertThat(key(tag), equalTo(asc ? "more" : "less"));
assertThat(tag.getDocCount(), equalTo(asc ? 3l : 2l));
filter = tag.getAggregations().get("filter");
assertThat(filter, notNullValue());
assertThat(filter.getDocCount(), equalTo(asc ? 3l : 2l));
}
@Test
public void singleValuedField_OrderedBySubAggregationAsc_MultiHierarchyLevels() throws Exception {
boolean asc = randomBoolean();
SearchResponse response = client().prepareSearch("idx").setTypes("type")
.addAggregation(terms("tags")
.executionHint(randomExecutionHint())
.field("tag")
.order(Terms.Order.aggregation("filter1>filter2>stats.max", asc))
.subAggregation(filter("filter1").filter(FilterBuilders.matchAllFilter())
.subAggregation(filter("filter2").filter(FilterBuilders.matchAllFilter())
.subAggregation(stats("stats").field("i"))))
).execute().actionGet();
assertSearchResponse(response);
Terms tags = response.getAggregations().get("tags");
assertThat(tags, notNullValue());
assertThat(tags.getName(), equalTo("tags"));
assertThat(tags.getBuckets().size(), equalTo(2));
Iterator<Terms.Bucket> iters = tags.getBuckets().iterator();
// the max for "more" is 2
// the max for "less" is 4
Terms.Bucket tag = iters.next();
assertThat(tag, notNullValue());
assertThat(key(tag), equalTo(asc ? "more" : "less"));
assertThat(tag.getDocCount(), equalTo(asc ? 3l : 2l));
Filter filter1 = tag.getAggregations().get("filter1");
assertThat(filter1, notNullValue());
assertThat(filter1.getDocCount(), equalTo(asc ? 3l : 2l));
Filter filter2 = filter1.getAggregations().get("filter2");
assertThat(filter2, notNullValue());
assertThat(filter2.getDocCount(), equalTo(asc ? 3l : 2l));
Stats stats = filter2.getAggregations().get("stats");
assertThat(stats, notNullValue());
assertThat(stats.getMax(), equalTo(asc ? 2.0 : 4.0));
tag = iters.next();
assertThat(tag, notNullValue());
assertThat(key(tag), equalTo(asc ? "less" : "more"));
assertThat(tag.getDocCount(), equalTo(asc ? 2l : 3l));
filter1 = tag.getAggregations().get("filter1");
assertThat(filter1, notNullValue());
assertThat(filter1.getDocCount(), equalTo(asc ? 2l : 3l));
filter2 = filter1.getAggregations().get("filter2");
assertThat(filter2, notNullValue());
assertThat(filter2.getDocCount(), equalTo(asc ? 2l : 3l));
stats = filter2.getAggregations().get("stats");
assertThat(stats, notNullValue());
assertThat(stats.getMax(), equalTo(asc ? 4.0 : 2.0));
}
@Test
public void singleValuedField_OrderedByMissingSubAggregation() throws Exception {
@ -854,7 +950,7 @@ public class StringTermsTests extends ElasticsearchIntegrationTest {
}
@Test
public void singleValuedField_OrderedByNonMetricsSubAggregation() throws Exception {
public void singleValuedField_OrderedByNonMetricsOrMultiBucketSubAggregation() throws Exception {
MockBigArrays.discardNextCheck();
try {
@ -863,11 +959,11 @@ public class StringTermsTests extends ElasticsearchIntegrationTest {
.addAggregation(terms("terms")
.executionHint(randomExecutionHint())
.field(SINGLE_VALUED_FIELD_NAME)
.order(Terms.Order.aggregation("filter", true))
.subAggregation(filter("filter").filter(FilterBuilders.termFilter("foo", "bar")))
.order(Terms.Order.aggregation("values", true))
.subAggregation(terms("values").field("i"))
).execute().actionGet();
fail("Expected search to fail when trying to sort terms aggregation by sug-aggregation which is not of a metrics type");
fail("Expected search to fail when trying to sort terms aggregation by sug-aggregation which is not of a metrics or single-bucket type");
} catch (ElasticsearchException e) {
// expected
@ -879,7 +975,6 @@ public class StringTermsTests extends ElasticsearchIntegrationTest {
MockBigArrays.discardNextCheck();
try {
client().prepareSearch("idx").setTypes("type")
.addAggregation(terms("terms")
.executionHint(randomExecutionHint())

View File

@ -0,0 +1,109 @@
/*
* 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.support;
import org.elasticsearch.search.aggregations.AggregationExecutionException;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
/**
*
*/
public class PathTests {
@Test
public void testInvalidPaths() throws Exception {
assertInvalidPath("[foo]", "brackets at the beginning of the token expression");
assertInvalidPath("foo[bar", "open brackets without closing at the token expression");
assertInvalidPath("foo[", "open bracket at the end of the token expression");
assertInvalidPath("foo[]", "empty brackets in the token expression");
assertInvalidPath("foo[bar]baz", "brackets not enclosing at the end of the token expression");
assertInvalidPath(".foo", "dot separator at the beginning of the token expression");
assertInvalidPath("foo.", "dot separator at the end of the token expression");
}
@Test
public void testValidPaths() throws Exception {
assertValidPath("foo>bar", tokens().add("foo").add("bar"));
assertValidPath("foo.bar", tokens().add("foo", "bar"));
assertValidPath("foo[bar]", tokens().add("foo", "bar"));
assertValidPath("foo[bar]>baz", tokens().add("foo", "bar").add("baz"));
assertValidPath("foo[bar]>baz[qux]", tokens().add("foo", "bar").add("baz", "qux"));
assertValidPath("foo[bar]>baz.qux", tokens().add("foo", "bar").add("baz", "qux"));
assertValidPath("foo.bar>baz.qux", tokens().add("foo", "bar").add("baz", "qux"));
assertValidPath("foo.bar>baz[qux]", tokens().add("foo", "bar").add("baz", "qux"));
}
private void assertInvalidPath(String path, String reason) {
try {
OrderPath.parse(path);
fail("Expected parsing path [" + path + "] to fail - " + reason);
} catch (AggregationExecutionException aee) {
// expected
}
}
private void assertValidPath(String path, Tokens tokenz) {
OrderPath.Token[] tokens = tokenz.toArray();
OrderPath p = OrderPath.parse(path);
assertThat(p.tokens.length, equalTo(tokens.length));
for (int i = 0; i < p.tokens.length; i++) {
OrderPath.Token t1 = p.tokens[i];
OrderPath.Token t2 = tokens[i];
assertThat(t1, equalTo(t2));
}
}
private static Tokens tokens() {
return new Tokens();
}
private static class Tokens {
private List<OrderPath.Token> tokens = new ArrayList<OrderPath.Token>();
Tokens add(String name) {
tokens.add(new OrderPath.Token(name, name, null));
return this;
}
Tokens add(String name, String key) {
if (Math.random() > 0.5) {
tokens.add(new OrderPath.Token(name + "." + key, name, key));
} else {
tokens.add(new OrderPath.Token(name + "[" + key + "]", name, key));
}
return this;
}
OrderPath.Token[] toArray() {
return tokens.toArray(new OrderPath.Token[tokens.size()]);
}
}
}