diff --git a/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java b/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java index 4263e148323..66c4fba43fb 100644 --- a/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java +++ b/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java @@ -62,6 +62,7 @@ import java.util.Objects; */ public class GeoDistanceSortBuilder extends SortBuilder implements SortBuilderParser { public static final String NAME = "_geo_distance"; + public static final String ALTERNATIVE_NAME = "_geoDistance"; public static final boolean DEFAULT_COERCE = false; public static final boolean DEFAULT_IGNORE_MALFORMED = false; public static final ParseField UNIT_FIELD = new ParseField("unit"); diff --git a/core/src/main/java/org/elasticsearch/search/sort/ScoreSortBuilder.java b/core/src/main/java/org/elasticsearch/search/sort/ScoreSortBuilder.java index 422be339788..db2fccf6712 100644 --- a/core/src/main/java/org/elasticsearch/search/sort/ScoreSortBuilder.java +++ b/core/src/main/java/org/elasticsearch/search/sort/ScoreSortBuilder.java @@ -38,7 +38,7 @@ import java.util.Objects; */ public class ScoreSortBuilder extends SortBuilder implements SortBuilderParser { - private static final String NAME = "_score"; + public static final String NAME = "_score"; static final ScoreSortBuilder PROTOTYPE = new ScoreSortBuilder(); public static final ParseField REVERSE_FIELD = new ParseField("reverse"); public static final ParseField ORDER_FIELD = new ParseField("order"); @@ -88,6 +88,7 @@ public class ScoreSortBuilder extends SortBuilder implements S return result; } + @Override public SortField build(QueryShardContext context) { if (order == SortOrder.DESC) { return SORT_SCORE; diff --git a/core/src/main/java/org/elasticsearch/search/sort/ScriptSortBuilder.java b/core/src/main/java/org/elasticsearch/search/sort/ScriptSortBuilder.java index 6005d9354ff..1702c03bc6c 100644 --- a/core/src/main/java/org/elasticsearch/search/sort/ScriptSortBuilder.java +++ b/core/src/main/java/org/elasticsearch/search/sort/ScriptSortBuilder.java @@ -66,7 +66,7 @@ import java.util.Objects; */ public class ScriptSortBuilder extends SortBuilder implements SortBuilderParser { - private static final String NAME = "_script"; + public static final String NAME = "_script"; static final ScriptSortBuilder PROTOTYPE = new ScriptSortBuilder(new Script("_na_"), ScriptSortType.STRING); public static final ParseField TYPE_FIELD = new ParseField("type"); public static final ParseField SCRIPT_FIELD = new ParseField("script"); diff --git a/core/src/main/java/org/elasticsearch/search/sort/SortBuilder.java b/core/src/main/java/org/elasticsearch/search/sort/SortBuilder.java index 35d59de011c..45fd4d45796 100644 --- a/core/src/main/java/org/elasticsearch/search/sort/SortBuilder.java +++ b/core/src/main/java/org/elasticsearch/search/sort/SortBuilder.java @@ -20,6 +20,8 @@ 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.ElasticsearchException; import org.elasticsearch.common.ParseField; @@ -27,47 +29,44 @@ import org.elasticsearch.common.lucene.search.Queries; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.fielddata.IndexFieldData.XFieldComparatorSource.Nested; import org.elasticsearch.index.mapper.object.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.internal.SearchContext; 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> implements ToXContent { - - 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; - } +public abstract class SortBuilder> implements SortBuilderParser, ToXContent { protected SortOrder order = SortOrder.ASC; public static final ParseField ORDER_FIELD = new ParseField("order"); + private static final Map> PARSERS; + + static { + Map> parsers = new HashMap<>(); + parsers.put(ScriptSortBuilder.NAME, ScriptSortBuilder.PROTOTYPE); + parsers.put(GeoDistanceSortBuilder.NAME, new GeoDistanceSortBuilder("_na_", -1, -1)); + parsers.put(GeoDistanceSortBuilder.ALTERNATIVE_NAME, new GeoDistanceSortBuilder("_na_", -1, -1)); + parsers.put(ScoreSortBuilder.NAME, ScoreSortBuilder.PROTOTYPE); + PARSERS = unmodifiableMap(parsers); + } + @Override public String toString() { try { @@ -96,4 +95,122 @@ public abstract class SortBuilder> implements ToXConten public SortOrder order() { return this.order; } + + public static List> fromXContent(QueryParseContext context) throws IOException { + List> 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(parser, context, sortFields); + } else if (token == XContentParser.Token.VALUE_STRING) { + String fieldName = parser.text(); + if (fieldName.equals(ScoreSortBuilder.NAME)) { + sortFields.add(new ScoreSortBuilder()); + } else { + sortFields.add(new FieldSortBuilder(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(); + if (fieldName.equals(ScoreSortBuilder.NAME)) { + sortFields.add(new ScoreSortBuilder()); + } else { + sortFields.add(new FieldSortBuilder(fieldName)); + } + } else if (token == XContentParser.Token.START_OBJECT) { + parseCompoundSortField(parser, context, sortFields); + } else { + throw new IllegalArgumentException("malformed sort format, either start with array, object, or an actual string"); + } + return sortFields; + } + + private static void parseCompoundSortField(XContentParser parser, QueryParseContext context, List> sortFields) + throws IOException { + XContentParser.Token token; + 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()); + if (fieldName.equals(ScoreSortBuilder.NAME)) { + sortFields.add(new ScoreSortBuilder().order(order)); + } else { + sortFields.add(new FieldSortBuilder(fieldName).order(order)); + } + } else { + if (PARSERS.containsKey(fieldName)) { + sortFields.add(PARSERS.get(fieldName).fromXContent(context, fieldName)); + } else { + sortFields.add(FieldSortBuilder.PROTOTYPE.fromXContent(context, fieldName)); + } + } + } + } + } + + public static void parseSort(XContentParser parser, SearchContext context) throws IOException { + QueryParseContext parseContext = context.getQueryShardContext().parseContext(); + parseContext.reset(parser); + Optional sortOptional = buildSort(SortBuilder.fromXContent(parseContext), context.getQueryShardContext()); + if (sortOptional.isPresent()) { + context.sort(sortOptional.get()); + } + } + + public static Optional buildSort(List> sortBuilders, QueryShardContext context) throws IOException { + List sortFields = new ArrayList<>(sortBuilders.size()); + for (SortBuilder builder : sortBuilders) { + sortFields.add(builder.build(context)); + } + 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 Sort(sortFields.toArray(new SortField[sortFields.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; + } } diff --git a/core/src/test/java/org/elasticsearch/search/sort/FieldSortBuilderTests.java b/core/src/test/java/org/elasticsearch/search/sort/FieldSortBuilderTests.java index d00b60e0c83..74b56353a91 100644 --- a/core/src/test/java/org/elasticsearch/search/sort/FieldSortBuilderTests.java +++ b/core/src/test/java/org/elasticsearch/search/sort/FieldSortBuilderTests.java @@ -25,10 +25,14 @@ public class FieldSortBuilderTests extends AbstractSortTestCase> result = parseSort(json); + assertEquals(1, result.size()); + SortBuilder sortBuilder = result.get(0); + assertEquals(new FieldSortBuilder("field1").order(order), sortBuilder); + + json = "{ \"sort\" : \"field1\" }"; + result = parseSort(json); + assertEquals(1, result.size()); + sortBuilder = result.get(0); + assertEquals(new FieldSortBuilder("field1"), sortBuilder); + + json = "{ \"sort\" : { \"_doc\" : \"" + order + "\" }}"; + result = parseSort(json); + assertEquals(1, result.size()); + sortBuilder = result.get(0); + assertEquals(new FieldSortBuilder("_doc").order(order), sortBuilder); + + json = "{ \"sort\" : \"_doc\" }"; + result = parseSort(json); + assertEquals(1, result.size()); + sortBuilder = result.get(0); + assertEquals(new FieldSortBuilder("_doc"), sortBuilder); + + json = "{ \"sort\" : { \"_score\" : \"" + order +"\" }}"; + result = parseSort(json); + assertEquals(1, result.size()); + sortBuilder = result.get(0); + assertEquals(new ScoreSortBuilder().order(order), sortBuilder); + + json = "{ \"sort\" : \"_score\" }"; + result = parseSort(json); + assertEquals(1, result.size()); + sortBuilder = result.get(0); + assertEquals(new ScoreSortBuilder(), sortBuilder); + + // test two spellings for _geo_disctance + json = "{ \"sort\" : [" + + "{\"_geoDistance\" : {" + + "\"pin.location\" : \"40,-70\" } }" + + "] }"; + result = parseSort(json); + assertEquals(1, result.size()); + sortBuilder = result.get(0); + assertEquals(new GeoDistanceSortBuilder("pin.location", 40, -70), sortBuilder); + + json = "{ \"sort\" : [" + + "{\"_geo_distance\" : {" + + "\"pin.location\" : \"40,-70\" } }" + + "] }"; + result = parseSort(json); + assertEquals(1, result.size()); + sortBuilder = result.get(0); + assertEquals(new GeoDistanceSortBuilder("pin.location", 40, -70), sortBuilder); + } + + /** + * test random syntax variations + */ + public void testRandomSortBuilders() throws IOException { + for (int runs = 0; runs < NUMBER_OF_RUNS; runs++) { + List> testBuilders = randomSortBuilderList(); + XContentBuilder xContentBuilder = XContentFactory.jsonBuilder(); + xContentBuilder.startObject(); + if (testBuilders.size() > 1) { + xContentBuilder.startArray("sort"); + } else { + xContentBuilder.field("sort"); + } + for (SortBuilder builder : testBuilders) { + if (builder instanceof ScoreSortBuilder || builder instanceof FieldSortBuilder) { + switch (randomIntBetween(0, 2)) { + case 0: + if (builder instanceof ScoreSortBuilder) { + xContentBuilder.value("_score"); + } else { + xContentBuilder.value(((FieldSortBuilder) builder).getFieldName()); + } + break; + case 1: + xContentBuilder.startObject(); + if (builder instanceof ScoreSortBuilder) { + xContentBuilder.field("_score"); + } else { + xContentBuilder.field(((FieldSortBuilder) builder).getFieldName()); + } + xContentBuilder.value(builder.order()); + xContentBuilder.endObject(); + break; + case 2: + xContentBuilder.startObject(); + builder.toXContent(xContentBuilder, ToXContent.EMPTY_PARAMS); + xContentBuilder.endObject(); + break; + } + } else { + xContentBuilder.startObject(); + builder.toXContent(xContentBuilder, ToXContent.EMPTY_PARAMS); + xContentBuilder.endObject(); + } + } + if (testBuilders.size() > 1) { + xContentBuilder.endArray(); + } + xContentBuilder.endObject(); + List> parsedSort = parseSort(xContentBuilder.string()); + assertEquals(testBuilders.size(), parsedSort.size()); + Iterator> iterator = testBuilders.iterator(); + for (SortBuilder parsedBuilder : parsedSort) { + assertEquals(iterator.next(), parsedBuilder); + } + } + } + + public static List> randomSortBuilderList() { + int size = randomIntBetween(1, 5); + List> list = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + switch (randomIntBetween(0, 3)) { + case 0: + list.add(new ScoreSortBuilder()); + break; + case 1: + String fieldName = rarely() ? SortParseElement.DOC_FIELD_NAME : randomAsciiOfLengthBetween(1, 10); + list.add(new FieldSortBuilder(fieldName)); + break; + case 2: + list.add(GeoDistanceSortBuilderTests.randomGeoDistanceSortBuilder()); + break; + case 3: + list.add(ScriptSortBuilderTests.randomScriptSortBuilder()); + break; + default: + throw new IllegalStateException("unexpected randomization in tests"); + } + } + return list; + } + + /** + * test array syntax variations: + * - "sort" : [ "fieldname", { "fieldname2" : "asc" }, ...] + */ + public void testMultiFieldSort() throws IOException { + String json = "{ \"sort\" : [" + + "{ \"post_date\" : {\"order\" : \"asc\"}}," + + "\"user\"," + + "{ \"name\" : \"desc\" }," + + "{ \"age\" : \"desc\" }," + + "{" + + "\"_geo_distance\" : {" + + "\"pin.location\" : \"40,-70\" } }," + + "\"_score\"" + + "] }"; + List> result = parseSort(json); + assertEquals(6, result.size()); + assertEquals(new FieldSortBuilder("post_date").order(SortOrder.ASC), result.get(0)); + assertEquals(new FieldSortBuilder("user").order(SortOrder.ASC), result.get(1)); + assertEquals(new FieldSortBuilder("name").order(SortOrder.DESC), result.get(2)); + assertEquals(new FieldSortBuilder("age").order(SortOrder.DESC), result.get(3)); + assertEquals(new GeoDistanceSortBuilder("pin.location", new GeoPoint(40, -70)), result.get(4)); + assertEquals(new ScoreSortBuilder(), result.get(5)); + } + + private static List> parseSort(String jsonString) throws IOException { + XContentParser itemParser = XContentHelper.createParser(new BytesArray(jsonString)); + QueryParseContext context = new QueryParseContext(indicesQueriesRegistry); + context.reset(itemParser); + + assertEquals(XContentParser.Token.START_OBJECT, itemParser.nextToken()); + assertEquals(XContentParser.Token.FIELD_NAME, itemParser.nextToken()); + assertEquals("sort", itemParser.currentName()); + itemParser.nextToken(); + return SortBuilder.fromXContent(context); + } +}