/*
* 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;
import org.elasticsearch.Version;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.util.Comparators;
import org.elasticsearch.common.xcontent.XContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.query.QueryParseContext;
import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation.Bucket;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregator;
import org.elasticsearch.search.aggregations.support.AggregationPath;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
/**
* Implementations for {@link Bucket} ordering strategies.
*/
public class InternalOrder extends BucketOrder {
private final byte id;
private final String key;
protected final boolean asc;
protected final Comparator<Bucket> comparator;
/**
* Creates an ordering strategy that sorts {@link Bucket}s by some property.
*
* @param id unique ID for this ordering strategy.
* @param key key of the property to sort on.
* @param asc direction to sort by: {@code true} for ascending, {@code false} for descending.
* @param comparator determines how buckets will be ordered.
*/
public InternalOrder(byte id, String key, boolean asc, Comparator<Bucket> comparator) {
this.id = id;
this.key = key;
this.asc = asc;
this.comparator = comparator;
}
@Override
byte id() {
return id;
}
@Override
public Comparator<Bucket> comparator(Aggregator aggregator) {
return comparator;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.startObject().field(key, asc ? "asc" : "desc").endObject();
}
/**
* Validate a bucket ordering strategy for an {@link Aggregator}.
*
* @param order bucket ordering strategy to sort on.
* @param aggregator aggregator to sort.
* @return unmodified bucket ordering strategy.
* @throws AggregationExecutionException if validation fails
*/
public static BucketOrder validate(BucketOrder order, Aggregator aggregator) throws AggregationExecutionException {
if (order instanceof CompoundOrder) {
for (BucketOrder innerOrder : ((CompoundOrder) order).orderElements) {
validate(innerOrder, aggregator);
}
} else if (order instanceof Aggregation) {
((Aggregation) order).path().validate(aggregator);
}
return order;
}
/**
* {@link Bucket} ordering strategy to sort by a sub-aggregation.
*/
public static class Aggregation extends InternalOrder {
static final byte ID = 0;
/**
* Create a new ordering strategy to sort by a sub-aggregation.
*
* @param path path to the sub-aggregation to sort on.
* @param asc direction to sort by: {@code true} for ascending, {@code false} for descending.
* @see AggregationPath
*/
Aggregation(String path, boolean asc) {
super(ID, path, asc, new AggregationComparator(path, asc));
}
/**
* @return parsed path to the sub-aggregation to sort on.
*/
public AggregationPath path() {
return ((AggregationComparator) comparator).path;
}
@Override
public Comparator<Bucket> comparator(Aggregator aggregator) {
if (aggregator instanceof TermsAggregator) {
// Internal Optimization for terms aggregation to avoid constructing buckets for ordering purposes
return ((TermsAggregator) aggregator).bucketComparator(path(), asc);
}
return comparator;
}
/**
* {@link Bucket} ordering strategy to sort by a sub-aggregation.
*/
static class AggregationComparator implements Comparator<Bucket> {
private final AggregationPath path;
private final boolean asc;
/**
* Create a new {@link Bucket} ordering strategy to sort by a sub-aggregation.
*
* @param path path to the sub-aggregation to sort on.
* @param asc direction to sort by: {@code true} for ascending, {@code false} for descending.
* @see AggregationPath
*/
AggregationComparator(String path, boolean asc) {
this.asc = asc;
this.path = AggregationPath.parse(path);
}
@Override
public int compare(Bucket b1, Bucket b2) {
double v1 = path.resolveValue(b1);
double v2 = path.resolveValue(b2);
return Comparators.compareDiscardNaN(v1, v2, asc);
}
}
}
/**
* {@link Bucket} ordering strategy to sort by multiple criteria.
*/
public static class CompoundOrder extends BucketOrder {
static final byte ID = -1;
final List<BucketOrder> orderElements;
/**
* Create a new ordering strategy to sort by multiple criteria. A tie-breaker may be added to avoid
* non-deterministic ordering.
*
* @param compoundOrder a list of {@link BucketOrder}s to sort on, in order of priority.
*/
CompoundOrder(List<BucketOrder> compoundOrder) {
this(compoundOrder, true);
}
/**
* Create a new ordering strategy to sort by multiple criteria.
*
* @param compoundOrder a list of {@link BucketOrder}s to sort on, in order of priority.
* @param absoluteOrdering {@code true} to add a tie-breaker to avoid non-deterministic ordering if needed,
* {@code false} otherwise.
*/
CompoundOrder(List<BucketOrder> compoundOrder, boolean absoluteOrdering) {
this.orderElements = new LinkedList<>(compoundOrder);
BucketOrder lastElement = null;
for (BucketOrder order : orderElements) {
if (order instanceof CompoundOrder) {
throw new IllegalArgumentException("nested compound order not supported");
}
lastElement = order;
}
if (absoluteOrdering && isKeyOrder(lastElement) == false) {
// add key order ascending as a tie-breaker to avoid non-deterministic ordering
// if all user provided comparators return 0.
this.orderElements.add(KEY_ASC);
}
}
@Override
byte id() {
return ID;
}
/**
* @return unmodifiable list of {@link BucketOrder}s to sort on.
*/
public List<BucketOrder> orderElements() {
return Collections.unmodifiableList(orderElements);
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startArray();
for (BucketOrder order : orderElements) {
order.toXContent(builder, params);
}
return builder.endArray();
}
@Override
public Comparator<Bucket> comparator(Aggregator aggregator) {
return new CompoundOrderComparator(orderElements, aggregator);
}
@Override
public int hashCode() {
return Objects.hash(orderElements);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
CompoundOrder other = (CompoundOrder) obj;
return Objects.equals(orderElements, other.orderElements);
}
/**
* {@code Comparator} for sorting buckets by multiple criteria.
*/
static class CompoundOrderComparator implements Comparator<Bucket> {
private List<BucketOrder> compoundOrder;
private Aggregator aggregator;
/**
* Create a new {@code Comparator} for sorting buckets by multiple criteria.
*
* @param compoundOrder a list of {@link BucketOrder}s to sort on, in order of priority.
* @param aggregator {@link BucketOrder#comparator(Aggregator)}
*/
CompoundOrderComparator(List<BucketOrder> compoundOrder, Aggregator aggregator) {
this.compoundOrder = compoundOrder;
this.aggregator = aggregator;
}
@Override
public int compare(Bucket b1, Bucket b2) {
int result = 0;
for (Iterator<BucketOrder> itr = compoundOrder.iterator(); itr.hasNext() && result == 0; ) {
result = itr.next().comparator(aggregator).compare(b1, b2);
}
return result;
}
}
}
private static final byte COUNT_DESC_ID = 1;
private static final byte COUNT_ASC_ID = 2;
private static final byte KEY_DESC_ID = 3;
private static final byte KEY_ASC_ID = 4;
/**
* Order by the (higher) count of each bucket.
*/
static final InternalOrder COUNT_DESC = new InternalOrder(COUNT_DESC_ID, "_count", false, comparingCounts().reversed());
/**
* Order by the (lower) count of each bucket.
*/
static final InternalOrder COUNT_ASC = new InternalOrder(COUNT_ASC_ID, "_count", true, comparingCounts());
/**
* Order by the key of each bucket descending.
*/
static final InternalOrder KEY_DESC = new InternalOrder(KEY_DESC_ID, "_key", false, comparingKeys().reversed());
/**
* Order by the key of each bucket ascending.
*/
static final InternalOrder KEY_ASC = new InternalOrder(KEY_ASC_ID, "_key", true, comparingKeys());
/**
* @return compare by {@link Bucket#getDocCount()}.
*/
private static Comparator<Bucket> comparingCounts() {
return Comparator.comparingLong(Bucket::getDocCount);
}
/**
* @return compare by {@link Bucket#getKey()} from the appropriate implementation.
*/
@SuppressWarnings("unchecked")
private static Comparator<Bucket> comparingKeys() {
return (b1, b2) -> {
if (b1 instanceof KeyComparable) {
return ((KeyComparable) b1).compareKey(b2);
}
throw new IllegalStateException("Unexpected order bucket class [" + b1.getClass() + "]");
};
}
/**
* Determine if the ordering strategy is sorting on bucket count descending.
*
* @param order bucket ordering strategy to check.
* @return {@code true} if the ordering strategy is sorting on bucket count descending, {@code false} otherwise.
*/
public static boolean isCountDesc(BucketOrder order) {
return isOrder(order, COUNT_DESC);
}
/**
* Determine if the ordering strategy is sorting on bucket key (ascending or descending).
*
* @param order bucket ordering strategy to check.
* @return {@code true} if the ordering strategy is sorting on bucket key, {@code false} otherwise.
*/
public static boolean isKeyOrder(BucketOrder order) {
return isOrder(order, KEY_ASC) || isOrder(order, KEY_DESC);
}
/**
* Determine if the ordering strategy is sorting on bucket key ascending.
*
* @param order bucket ordering strategy to check.
* @return {@code true} if the ordering strategy is sorting on bucket key ascending, {@code false} otherwise.
*/
public static boolean isKeyAsc(BucketOrder order) {
return isOrder(order, KEY_ASC);
}
/**
* Determine if the ordering strategy is sorting on bucket key descending.
*
* @param order bucket ordering strategy to check.
* @return {@code true} if the ordering strategy is sorting on bucket key descending, {@code false} otherwise.
*/
public static boolean isKeyDesc(BucketOrder order) {
return isOrder(order, KEY_DESC);
}
/**
* Determine if the ordering strategy matches the expected one.
*
* @param order bucket ordering strategy to check. If this is a {@link CompoundOrder} the first element will be
* check instead.
* @param expected expected bucket ordering strategy.
* @return {@code true} if the order matches, {@code false} otherwise.
*/
private static boolean isOrder(BucketOrder order, BucketOrder expected) {
if (order == expected) {
return true;
} else if (order instanceof CompoundOrder) {
// check if its a compound order with the first element that matches
List<BucketOrder> orders = ((CompoundOrder) order).orderElements;
if (orders.size() >= 1) {
return isOrder(orders.get(0), expected);
}
}
return false;
}
/**
* Contains logic for reading/writing {@link BucketOrder} from/to streams.
*/
public static class Streams {
/**
* Read a {@link BucketOrder} from a {@link StreamInput}.
*
* @param in stream with order data to read.
* @return order read from the stream
* @throws IOException on error reading from the stream.
*/
public static BucketOrder readOrder(StreamInput in) throws IOException {
byte id = in.readByte();
switch (id) {
case COUNT_DESC_ID: return COUNT_DESC;
case COUNT_ASC_ID: return COUNT_ASC;
case KEY_DESC_ID: return KEY_DESC;
case KEY_ASC_ID: return KEY_ASC;
case Aggregation.ID:
boolean asc = in.readBoolean();
String key = in.readString();
return new Aggregation(key, asc);
case CompoundOrder.ID:
int size = in.readVInt();
List<BucketOrder> compoundOrder = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
compoundOrder.add(Streams.readOrder(in));
}
return new CompoundOrder(compoundOrder, false);
default:
throw new RuntimeException("unknown order id [" + id + "]");
}
}
/**
* ONLY FOR HISTOGRAM ORDER: Backwards compatibility logic to read a {@link BucketOrder} from a {@link StreamInput}.
*
* @param in stream with order data to read.
* @param bwcOrderFlag {@code true} to check {@code in.readBoolean()} in the backwards compat logic before reading
* the order. {@code false} to skip this flag (order always present).
* @return order read from the stream
* @throws IOException on error reading from the stream.
*/
public static BucketOrder readHistogramOrder(StreamInput in, boolean bwcOrderFlag) throws IOException {
if (in.getVersion().onOrAfter(Version.V_6_0_0_alpha2_UNRELEASED)) {
return Streams.readOrder(in);
} else { // backwards compat logic
if (bwcOrderFlag == false || in.readBoolean()) {
// translate the old histogram order IDs to the new order objects
byte id = in.readByte();
switch (id) {
case 1: return KEY_ASC;
case 2: return KEY_DESC;
case 3: return COUNT_ASC;
case 4: return COUNT_DESC;
case 0: // aggregation order stream logic is backwards compatible
boolean asc = in.readBoolean();
String key = in.readString();
return new Aggregation(key, asc);
default: // not expecting compound order ID
throw new RuntimeException("unknown histogram order id [" + id + "]");
}
} else { // default to _key asc if no order specified
return KEY_ASC;
}
}
}
/**
* Write a {@link BucketOrder} to a {@link StreamOutput}.
*
* @param order order to write to the stream.
* @param out stream to write the order to.
* @throws IOException on error writing to the stream.
*/
public static void writeOrder(BucketOrder order, StreamOutput out) throws IOException {
out.writeByte(order.id());
if (order instanceof Aggregation) {
Aggregation aggregationOrder = (Aggregation) order;
out.writeBoolean(aggregationOrder.asc);
out.writeString(aggregationOrder.path().toString());
} else if (order instanceof CompoundOrder) {
CompoundOrder compoundOrder = (CompoundOrder) order;
out.writeVInt(compoundOrder.orderElements.size());
for (BucketOrder innerOrder : compoundOrder.orderElements) {
innerOrder.writeTo(out);
}
}
}
/**
* ONLY FOR HISTOGRAM ORDER: Backwards compatibility logic to write a {@link BucketOrder} to a stream.
*
* @param order order to write to the stream.
* @param out stream to write the order to.
* @param bwcOrderFlag {@code true} to always {@code out.writeBoolean(true)} for the backwards compat logic before
* writing the order. {@code false} to skip this flag.
* @throws IOException on error writing to the stream.
*/
public static void writeHistogramOrder(BucketOrder order, StreamOutput out, boolean bwcOrderFlag) throws IOException {
if (out.getVersion().onOrAfter(Version.V_6_0_0_alpha2_UNRELEASED)) {
order.writeTo(out);
} else { // backwards compat logic
if(bwcOrderFlag) { // need to add flag that determines if order exists
out.writeBoolean(true); // order always exists
}
if (order instanceof CompoundOrder) {
// older versions do not support histogram compound order; the best we can do here is use the first order.
order = ((CompoundOrder) order).orderElements.get(0);
}
if (order instanceof Aggregation) {
// aggregation order stream logic is backwards compatible
order.writeTo(out);
} else {
// convert the new order IDs to the old histogram order IDs.
byte id;
switch (order.id()) {
case COUNT_DESC_ID: id = 4; break;
case COUNT_ASC_ID: id = 3; break;
case KEY_DESC_ID: id = 2; break;
case KEY_ASC_ID: id = 1; break;
default: throw new RuntimeException("unknown order id [" + order.id() + "]");
}
out.writeByte(id);
}
}
}
}
/**
* Contains logic for parsing a {@link BucketOrder} from a {@link XContentParser}.
*/
public static class Parser {
private static final DeprecationLogger DEPRECATION_LOGGER =
new DeprecationLogger(Loggers.getLogger(Parser.class));
/**
* Parse a {@link BucketOrder} from {@link XContent}.
*
* @param parser for parsing {@link XContent} that contains the order.
* @param context parsing context.
* @return bucket ordering strategy
* @throws IOException on error a {@link XContent} parsing error.
*/
public static BucketOrder parseOrderParam(XContentParser parser, QueryParseContext context) throws IOException {
XContentParser.Token token;
String orderKey = null;
boolean orderAsc = false;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
orderKey = parser.currentName();
} else if (token == XContentParser.Token.VALUE_STRING) {
String dir = parser.text();
if ("asc".equalsIgnoreCase(dir)) {
orderAsc = true;
} else if ("desc".equalsIgnoreCase(dir)) {
orderAsc = false;
} else {
throw new ParsingException(parser.getTokenLocation(),
"Unknown order direction [" + dir + "]");
}
} else {
throw new ParsingException(parser.getTokenLocation(),
"Unexpected token [" + token + "] for [order]");
}
}
if (orderKey == null) {
throw new ParsingException(parser.getTokenLocation(),
"Must specify at least one field for [order]");
}
// _term and _time order deprecated in 6.0; replaced by _key
if ("_term".equals(orderKey) || "_time".equals(orderKey)) {
DEPRECATION_LOGGER.deprecated("Deprecated aggregation order key [{}] used, replaced by [_key]", orderKey);
}
switch (orderKey) {
case "_term":
case "_time":
case "_key":
return orderAsc ? KEY_ASC : KEY_DESC;
case "_count":
return orderAsc ? COUNT_ASC : COUNT_DESC;
default: // assume all other orders are sorting on a sub-aggregation. Validation occurs later.
return aggregation(orderKey, orderAsc);
}
}
}
@Override
public int hashCode() {
return Objects.hash(id, key, asc);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
InternalOrder other = (InternalOrder) obj;
return Objects.equals(id, other.id)
&& Objects.equals(key, other.key)
&& Objects.equals(asc, other.asc);
}
}