/*
* 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.sort;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.join.BitSetProducer;
import org.elasticsearch.action.support.ToXContentToBytes;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.io.stream.NamedWriteable;
import org.elasticsearch.common.lucene.search.Queries;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.fielddata.IndexFieldData.XFieldComparatorSource.Nested;
import org.elasticsearch.index.mapper.ObjectMapper;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryParseContext;
import org.elasticsearch.index.query.QueryShardContext;
import org.elasticsearch.index.query.QueryShardException;
import org.elasticsearch.search.DocValueFormat;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import static java.util.Collections.unmodifiableMap;
public abstract class SortBuilder<T extends SortBuilder<T>> extends ToXContentToBytes implements NamedWriteable {
protected SortOrder order = SortOrder.ASC;
// parse fields common to more than one SortBuilder
public static final ParseField ORDER_FIELD = new ParseField("order");
public static final ParseField NESTED_FILTER_FIELD = new ParseField("nested_filter");
public static final ParseField NESTED_PATH_FIELD = new ParseField("nested_path");
private static final Map<String, Parser<?>> PARSERS;
static {
Map<String, Parser<?>> parsers = new HashMap<>();
parsers.put(ScriptSortBuilder.NAME, ScriptSortBuilder::fromXContent);
parsers.put(GeoDistanceSortBuilder.NAME, GeoDistanceSortBuilder::fromXContent);
parsers.put(GeoDistanceSortBuilder.ALTERNATIVE_NAME, GeoDistanceSortBuilder::fromXContent);
parsers.put(ScoreSortBuilder.NAME, ScoreSortBuilder::fromXContent);
// FieldSortBuilder gets involved if the user specifies a name that isn't one of these.
PARSERS = unmodifiableMap(parsers);
}
/**
* Create a @link {@link SortFieldAndFormat} from this builder.
*/
protected abstract SortFieldAndFormat build(QueryShardContext context) throws IOException;
/**
* Set the order of sorting.
*/
@SuppressWarnings("unchecked")
public T order(SortOrder order) {
Objects.requireNonNull(order, "sort order cannot be null.");
this.order = order;
return (T) this;
}
/**
* Return the {@link SortOrder} used for this {@link SortBuilder}.
*/
public SortOrder order() {
return this.order;
}
public static List<SortBuilder<?>> fromXContent(QueryParseContext context) throws IOException {
List<SortBuilder<?>> sortFields = new ArrayList<>(2);
XContentParser parser = context.parser();
XContentParser.Token token = parser.currentToken();
if (token == XContentParser.Token.START_ARRAY) {
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
if (token == XContentParser.Token.START_OBJECT) {
parseCompoundSortField(context, sortFields);
} else if (token == XContentParser.Token.VALUE_STRING) {
String fieldName = parser.text();
sortFields.add(fieldOrScoreSort(fieldName));
} else {
throw new IllegalArgumentException("malformed sort format, "
+ "within the sort array, an object, or an actual string are allowed");
}
}
} else if (token == XContentParser.Token.VALUE_STRING) {
String fieldName = parser.text();
sortFields.add(fieldOrScoreSort(fieldName));
} else if (token == XContentParser.Token.START_OBJECT) {
parseCompoundSortField(context, sortFields);
} else {
throw new IllegalArgumentException("malformed sort format, either start with array, object, or an actual string");
}
return sortFields;
}
private static SortBuilder<?> fieldOrScoreSort(String fieldName) {
if (fieldName.equals(ScoreSortBuilder.NAME)) {
return new ScoreSortBuilder();
} else {
return new FieldSortBuilder(fieldName);
}
}
private static void parseCompoundSortField(QueryParseContext context, List<SortBuilder<?>> sortFields)
throws IOException {
XContentParser.Token token;
XContentParser parser = context.parser();
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
String fieldName = parser.currentName();
token = parser.nextToken();
if (token == XContentParser.Token.VALUE_STRING) {
SortOrder order = SortOrder.fromString(parser.text());
sortFields.add(fieldOrScoreSort(fieldName).order(order));
} else {
if (PARSERS.containsKey(fieldName)) {
sortFields.add(PARSERS.get(fieldName).fromXContent(context, fieldName));
} else {
sortFields.add(FieldSortBuilder.fromXContent(context, fieldName));
}
}
}
}
}
public static Optional<SortAndFormats> buildSort(List<SortBuilder<?>> sortBuilders, QueryShardContext context) throws IOException {
List<SortField> sortFields = new ArrayList<>(sortBuilders.size());
List<DocValueFormat> sortFormats = new ArrayList<>(sortBuilders.size());
for (SortBuilder<?> builder : sortBuilders) {
SortFieldAndFormat sf = builder.build(context);
sortFields.add(sf.field);
sortFormats.add(sf.format);
}
if (!sortFields.isEmpty()) {
// optimize if we just sort on score non reversed, we don't really
// need sorting
boolean sort;
if (sortFields.size() > 1) {
sort = true;
} else {
SortField sortField = sortFields.get(0);
if (sortField.getType() == SortField.Type.SCORE && !sortField.getReverse()) {
sort = false;
} else {
sort = true;
}
}
if (sort) {
return Optional.of(new SortAndFormats(
new Sort(sortFields.toArray(new SortField[sortFields.size()])),
sortFormats.toArray(new DocValueFormat[sortFormats.size()])));
}
}
return Optional.empty();
}
protected static Nested resolveNested(QueryShardContext context, String nestedPath, QueryBuilder nestedFilter) throws IOException {
Nested nested = null;
if (nestedPath != null) {
BitSetProducer rootDocumentsFilter = context.bitsetFilter(Queries.newNonNestedFilter());
ObjectMapper nestedObjectMapper = context.getObjectMapper(nestedPath);
if (nestedObjectMapper == null) {
throw new QueryShardException(context, "[nested] failed to find nested object under path [" + nestedPath + "]");
}
if (!nestedObjectMapper.nested().isNested()) {
throw new QueryShardException(context, "[nested] nested object under path [" + nestedPath + "] is not of nested type");
}
Query innerDocumentsQuery;
if (nestedFilter != null) {
context.nestedScope().nextLevel(nestedObjectMapper);
innerDocumentsQuery = QueryBuilder.rewriteQuery(nestedFilter, context).toFilter(context);
context.nestedScope().previousLevel();
} else {
innerDocumentsQuery = nestedObjectMapper.nestedTypeFilter();
}
nested = new Nested(rootDocumentsFilter, innerDocumentsQuery);
}
return nested;
}
protected static QueryBuilder parseNestedFilter(XContentParser parser, QueryParseContext context) {
try {
return context.parseInnerQueryBuilder();
} catch (Exception e) {
throw new ParsingException(parser.getTokenLocation(), "Expected " + NESTED_FILTER_FIELD.getPreferredName() + " element.", e);
}
}
@FunctionalInterface
private interface Parser<T extends SortBuilder<?>> {
T fromXContent(QueryParseContext context, String elementName) throws IOException;
}
}