diff --git a/core/src/main/java/org/elasticsearch/search/sort/FieldSortBuilder.java b/core/src/main/java/org/elasticsearch/search/sort/FieldSortBuilder.java index e805e21eff5..1157457afb9 100644 --- a/core/src/main/java/org/elasticsearch/search/sort/FieldSortBuilder.java +++ b/core/src/main/java/org/elasticsearch/search/sort/FieldSortBuilder.java @@ -19,15 +19,33 @@ package org.elasticsearch.search.sort; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.lucene.BytesRefs; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryParseContext; import java.io.IOException; +import java.util.Objects; /** * A sort builder to sort based on a document field. */ -public class FieldSortBuilder extends SortBuilder { +public class FieldSortBuilder extends SortBuilder implements SortBuilderParser { + static final FieldSortBuilder PROTOTYPE = new FieldSortBuilder(""); + public static final String NAME = "field_sort"; + public static final ParseField NESTED_PATH = new ParseField("nested_path"); + public static final ParseField NESTED_FILTER = new ParseField("nested_filter"); + public static final ParseField MISSING = new ParseField("missing"); + public static final ParseField ORDER = new ParseField("order"); + public static final ParseField REVERSE = new ParseField("reverse"); + public static final ParseField SORT_MODE = new ParseField("mode"); + public static final ParseField UNMAPPED_TYPE = new ParseField("unmapped_type"); private final String fieldName; @@ -41,10 +59,22 @@ public class FieldSortBuilder extends SortBuilder { private String nestedPath; + /** Copy constructor. */ + public FieldSortBuilder(FieldSortBuilder template) { + this(template.fieldName); + this.order(template.order()); + this.missing(template.missing()); + this.unmappedType(template.unmappedType()); + this.sortMode(template.sortMode()); + this.setNestedFilter(template.getNestedFilter()); + this.setNestedPath(template.getNestedPath()); + } + /** * Constructs a new sort based on a document field. * - * @param fieldName The field name. + * @param fieldName + * The field name. */ public FieldSortBuilder(String fieldName) { if (fieldName == null) { @@ -53,21 +83,39 @@ public class FieldSortBuilder extends SortBuilder { this.fieldName = fieldName; } + /** Returns the document field this sort should be based on. */ + public String getFieldName() { + return this.fieldName; + } + /** * Sets the value when a field is missing in a doc. Can also be set to _last or * _first to sort missing last or first respectively. */ public FieldSortBuilder missing(Object missing) { - this.missing = missing; + if (missing instanceof String) { + this.missing = BytesRefs.toBytesRef(missing); + } else { + this.missing = missing; + } return this; } + /** Returns the value used when a field is missing in a doc. */ + public Object missing() { + if (missing instanceof BytesRef) { + return ((BytesRef) missing).utf8ToString(); + } + return missing; + } + /** * Set the type to use in case the current field is not mapped in an index. - * Specifying a type tells Elasticsearch what type the sort values should have, which is important - * for cross-index search, if there are sort fields that exist on some indices only. - * If the unmapped type is null then query execution will fail if one or more indices - * don't have a mapping for the current field. + * Specifying a type tells Elasticsearch what type the sort values should + * have, which is important for cross-index search, if there are sort fields + * that exist on some indices only. If the unmapped type is null + * then query execution will fail if one or more indices don't have a + * mapping for the current field. */ public FieldSortBuilder unmappedType(String type) { this.unmappedType = type; @@ -75,8 +123,19 @@ public class FieldSortBuilder extends SortBuilder { } /** - * Defines what values to pick in the case a document contains multiple values for the targeted sort field. - * Possible values: min, max, sum and avg + * Returns the type to use in case the current field is not mapped in an + * index. + */ + public String unmappedType() { + return this.unmappedType; + } + + /** + * Defines what values to pick in the case a document contains multiple + * values for the targeted sort field. Possible values: min, max, sum and + * avg + * + * TODO would love to see an enum here *

* The last two values are only applicable for number based fields. */ @@ -86,44 +145,217 @@ public class FieldSortBuilder extends SortBuilder { } /** - * Sets the nested filter that the nested objects should match with in order to be taken into account - * for sorting. + * Returns what values to pick in the case a document contains multiple + * values for the targeted sort field. + */ + public String sortMode() { + return this.sortMode; + } + + /** + * Sets the nested filter that the nested objects should match with in order + * to be taken into account for sorting. + * + * TODO should the above getters and setters be deprecated/ changed in + * favour of real getters and setters? */ public FieldSortBuilder setNestedFilter(QueryBuilder nestedFilter) { this.nestedFilter = nestedFilter; return this; } + /** + * Returns the nested filter that the nested objects should match with in + * order to be taken into account for sorting. + */ + public QueryBuilder getNestedFilter() { + return this.nestedFilter; + } /** - * Sets the nested path if sorting occurs on a field that is inside a nested object. By default when sorting on a - * field inside a nested object, the nearest upper nested object is selected as nested path. + * Sets the nested path if sorting occurs on a field that is inside a nested + * object. By default when sorting on a field inside a nested object, the + * nearest upper nested object is selected as nested path. */ public FieldSortBuilder setNestedPath(String nestedPath) { this.nestedPath = nestedPath; return this; } + /** + * Returns the nested path if sorting occurs in a field that is inside a + * nested object. + */ + public String getNestedPath() { + return this.nestedPath; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(fieldName); builder.field(ORDER_FIELD.getPreferredName(), order); if (missing != null) { - builder.field("missing", missing); + if (missing instanceof BytesRef) { + builder.field(MISSING.getPreferredName(), ((BytesRef) missing).utf8ToString()); + } else { + builder.field(MISSING.getPreferredName(), missing); + } } if (unmappedType != null) { - builder.field(SortParseElement.UNMAPPED_TYPE.getPreferredName(), unmappedType); + builder.field(UNMAPPED_TYPE.getPreferredName(), unmappedType); } if (sortMode != null) { - builder.field("mode", sortMode); + builder.field(SORT_MODE.getPreferredName(), sortMode); } if (nestedFilter != null) { - builder.field("nested_filter", nestedFilter, params); + builder.field(NESTED_FILTER.getPreferredName(), nestedFilter, params); } if (nestedPath != null) { - builder.field("nested_path", nestedPath); + builder.field(NESTED_PATH.getPreferredName(), nestedPath); } builder.endObject(); return builder; } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (other == null || getClass() != other.getClass()) { + return false; + } + + FieldSortBuilder builder = (FieldSortBuilder) other; + return (Objects.equals(this.fieldName, builder.fieldName) && Objects.equals(this.nestedFilter, builder.nestedFilter) + && Objects.equals(this.nestedPath, builder.nestedPath) && Objects.equals(this.missing, builder.missing) + && Objects.equals(this.order, builder.order) && Objects.equals(this.sortMode, builder.sortMode) + && Objects.equals(this.unmappedType, builder.unmappedType)); + } + + @Override + public int hashCode() { + return Objects.hash(this.fieldName, this.nestedFilter, this.nestedPath, this.missing, this.order, this.sortMode, this.unmappedType); + } + + @Override + public String getWriteableName() { + return NAME; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(this.fieldName); + if (this.nestedFilter != null) { + out.writeBoolean(true); + out.writeQuery(this.nestedFilter); + } else { + out.writeBoolean(false); + } + out.writeOptionalString(this.nestedPath); + out.writeGenericValue(this.missing); + + if (this.order != null) { + out.writeBoolean(true); + this.order.writeTo(out); + } else { + out.writeBoolean(false); + } + + out.writeOptionalString(this.sortMode); + out.writeOptionalString(this.unmappedType); + } + + @Override + public FieldSortBuilder readFrom(StreamInput in) throws IOException { + String fieldName = in.readString(); + FieldSortBuilder result = new FieldSortBuilder(fieldName); + if (in.readBoolean()) { + QueryBuilder query = in.readQuery(); + result.setNestedFilter(query); + } + result.setNestedPath(in.readOptionalString()); + result.missing(in.readGenericValue()); + + if (in.readBoolean()) { + result.order(SortOrder.readOrderFrom(in)); + } + result.sortMode(in.readOptionalString()); + result.unmappedType(in.readOptionalString()); + return result; + } + + @Override + public FieldSortBuilder fromXContent(QueryParseContext context, String fieldName) throws IOException { + XContentParser parser = context.parser(); + + QueryBuilder nestedFilter = null; + String nestedPath = null; + Object missing = null; + SortOrder order = null; + String sortMode = null; + String unmappedType = null; + + String currentFieldName = null; + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.START_OBJECT) { + if (context.parseFieldMatcher().match(currentFieldName, NESTED_FILTER)) { + nestedFilter = context.parseInnerQueryBuilder(); + } else { + throw new ParsingException(parser.getTokenLocation(), "Expected " + NESTED_FILTER.getPreferredName() + " element."); + } + } else if (token.isValue()) { + if (context.parseFieldMatcher().match(currentFieldName, NESTED_PATH)) { + nestedPath = parser.text(); + } else if (context.parseFieldMatcher().match(currentFieldName, MISSING)) { + missing = parser.objectBytes(); + } else if (context.parseFieldMatcher().match(currentFieldName, REVERSE)) { + if (parser.booleanValue()) { + order = SortOrder.DESC; + } + // else we keep the default ASC + } else if (context.parseFieldMatcher().match(currentFieldName, ORDER)) { + String sortOrder = parser.text(); + if ("asc".equals(sortOrder)) { + order = SortOrder.ASC; + } else if ("desc".equals(sortOrder)) { + order = SortOrder.DESC; + } else { + throw new IllegalStateException("Sort order " + sortOrder + " not supported."); + } + } else if (context.parseFieldMatcher().match(currentFieldName, SORT_MODE)) { + sortMode = parser.text(); + } else if (context.parseFieldMatcher().match(currentFieldName, UNMAPPED_TYPE)) { + unmappedType = parser.text(); + } else { + throw new IllegalArgumentException("Option " + currentFieldName + " not supported."); + } + } + } + + FieldSortBuilder builder = new FieldSortBuilder(fieldName); + if (nestedFilter != null) { + builder.setNestedFilter(nestedFilter); + } + if (nestedPath != null) { + builder.setNestedPath(nestedPath); + } + if (missing != null) { + builder.missing(missing); + } + if (order != null) { + builder.order(order); + } + if (sortMode != null) { + builder.sortMode(sortMode); + } + if (unmappedType != null) { + builder.unmappedType(unmappedType); + } + return builder; + } } 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 b5a10e238b7..630ff635afa 100644 --- a/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java +++ b/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java @@ -23,11 +23,9 @@ import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.geo.GeoDistance; import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.GeoUtils; -import org.elasticsearch.common.io.stream.NamedWriteable; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.unit.DistanceUnit; -import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.query.QueryBuilder; @@ -44,8 +42,7 @@ import java.util.Objects; /** * A geo distance based sorting on a geo point like field. */ -public class GeoDistanceSortBuilder extends SortBuilder - implements ToXContent, NamedWriteable, SortElementParserTemp { +public class GeoDistanceSortBuilder extends SortBuilder implements SortBuilderParser { public static final String NAME = "_geo_distance"; public static final boolean DEFAULT_COERCE = false; public static final boolean DEFAULT_IGNORE_MALFORMED = false; 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 6b1bc054ee7..c416965f38a 100644 --- a/core/src/main/java/org/elasticsearch/search/sort/ScoreSortBuilder.java +++ b/core/src/main/java/org/elasticsearch/search/sort/ScoreSortBuilder.java @@ -22,7 +22,6 @@ package org.elasticsearch.search.sort; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.ParseFieldMatcher; import org.elasticsearch.common.ParsingException; -import org.elasticsearch.common.io.stream.NamedWriteable; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -35,7 +34,7 @@ import java.util.Objects; /** * A sort builder allowing to sort by score. */ -public class ScoreSortBuilder extends SortBuilder implements NamedWriteable, +public class ScoreSortBuilder extends SortBuilder implements SortBuilderParser, SortElementParserTemp { private static final String NAME = "_score"; diff --git a/core/src/main/java/org/elasticsearch/search/sort/SortBuilderParser.java b/core/src/main/java/org/elasticsearch/search/sort/SortBuilderParser.java new file mode 100644 index 00000000000..90d54a50121 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/search/sort/SortBuilderParser.java @@ -0,0 +1,40 @@ +/* + * 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.elasticsearch.common.io.stream.NamedWriteable; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.index.query.QueryParseContext; + +import java.io.IOException; + +public interface SortBuilderParser extends NamedWriteable, ToXContent { + /** + * Creates a new item from the json held by the {@link SortBuilderParser} + * in {@link org.elasticsearch.common.xcontent.XContent} format + * + * @param context + * the input parse context. The state on the parser contained in + * this context will be changed as a side effect of this method + * call + * @return the new item + */ + SortBuilder fromXContent(QueryParseContext context, String elementName) throws IOException; +} diff --git a/core/src/test/java/org/elasticsearch/search/sort/AbstractSortTestCase.java b/core/src/test/java/org/elasticsearch/search/sort/AbstractSortTestCase.java index dc61f0ef34c..6c5800c97cb 100644 --- a/core/src/test/java/org/elasticsearch/search/sort/AbstractSortTestCase.java +++ b/core/src/test/java/org/elasticsearch/search/sort/AbstractSortTestCase.java @@ -19,8 +19,8 @@ package org.elasticsearch.search.sort; +import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.common.io.stream.NamedWriteable; import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; @@ -43,7 +43,7 @@ import java.io.IOException; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; -public abstract class AbstractSortTestCase & SortElementParserTemp> extends ESTestCase { +public abstract class AbstractSortTestCase> extends ESTestCase { protected static NamedWriteableRegistry namedWriteableRegistry; @@ -55,6 +55,7 @@ public abstract class AbstractSortTestCase { + + @Override + protected FieldSortBuilder createTestItem() { + String fieldName = randomAsciiOfLengthBetween(1, 10); + FieldSortBuilder builder = new FieldSortBuilder(fieldName); + if (randomBoolean()) { + builder.order(RandomSortDataGenerator.order(builder.order())); + } + + if (randomBoolean()) { + builder.missing(RandomSortDataGenerator.missing(builder.missing())); + } + + if (randomBoolean()) { + builder.unmappedType(RandomSortDataGenerator.randomAscii(builder.unmappedType())); + } + + if (randomBoolean()) { + builder.sortMode(RandomSortDataGenerator.mode(builder.sortMode())); + } + + if (randomBoolean()) { + builder.setNestedFilter(RandomSortDataGenerator.nestedFilter(builder.getNestedFilter())); + } + + if (randomBoolean()) { + builder.setNestedPath(RandomSortDataGenerator.randomAscii(builder.getNestedPath())); + } + + return builder; + } + + @Override + protected FieldSortBuilder mutate(FieldSortBuilder original) throws IOException { + FieldSortBuilder mutated = new FieldSortBuilder(original); + int parameter = randomIntBetween(0, 5); + switch (parameter) { + case 0: + mutated.setNestedPath(RandomSortDataGenerator.randomAscii(mutated.getNestedPath())); + break; + case 1: + mutated.setNestedFilter(RandomSortDataGenerator.nestedFilter(mutated.getNestedFilter())); + break; + case 2: + mutated.sortMode(RandomSortDataGenerator.mode(mutated.sortMode())); + break; + case 3: + mutated.unmappedType(RandomSortDataGenerator.randomAscii(mutated.unmappedType())); + break; + case 4: + mutated.missing(RandomSortDataGenerator.missing(mutated.missing())); + break; + case 5: + mutated.order(RandomSortDataGenerator.order(mutated.order())); + break; + default: + throw new IllegalStateException("Unsupported mutation."); + } + return mutated; + } +}