Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an option to force the numeric type of a field sort #38095

Merged
merged 9 commits into from
Mar 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions docs/reference/search/request/sort.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,73 @@ POST /_search
--------------------------------------------------
// CONSOLE

==== Sorting numeric fields

For numeric fields it is also possible to cast the values from one type
to another using the `numeric_type` option.
This option accepts the following values: [`"double", "long"`] and can be useful
for cross-index search if the sort field is mapped differently on some
indices.

Consider for instance these two indices:

[source,js]
--------------------------------------------------
PUT /index_double
{
"mappings": {
"properties": {
"field": { "type": "double" }
}
}
}
--------------------------------------------------
// CONSOLE

[source,js]
--------------------------------------------------
PUT /index_long
{
"mappings": {
"properties": {
"field": { "type": "long" }
}
}
}
--------------------------------------------------
// CONSOLE
// TEST[continued]

Since `field` is mapped as a `double` in the first index and as a `long`
in the second index, it is not possible to use this field to sort requests
that query both indices by default. However you can force the type to one
or the other with the `numeric_type` option in order to force a specific
type for all indices:

[source,js]
--------------------------------------------------
POST /index_long,index_double/_search
{
"sort" : [
{
"field" : {
"numeric_type" : "double"
}
}
]
}
--------------------------------------------------
// CONSOLE
// TEST[continued]

In the example above, values for the `index_long` index are casted to
a double in order to be compatible with the values produced by the
`index_double` index.
It is also possible to transform a floating point field into a `long`
but note that in this case floating points are replaced by the largest
value that is less than or equal (greater than or equal if the value
is negative) to the argument and is equal to a mathematical integer.

[[nested-sorting]]
==== Sorting within nested objects.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,15 @@ public SortedNumericDVIndexFieldData(Index index, String fieldNames, NumericType
this.numericType = numericType;
}

@Override
public SortField sortField(Object missingValue, MultiValueMode sortMode, Nested nested, boolean reverse) {
/**
* Returns the {@link SortField} to used for sorting.
* Values are casted to the provided <code>targetNumericType</code> type if it doesn't
* match the field's <code>numericType</code>.
*/
public SortField sortField(NumericType targetNumericType, Object missingValue, MultiValueMode sortMode, Nested nested,
boolean reverse) {
final XFieldComparatorSource source;
switch (numericType) {
switch (targetNumericType) {
case HALF_FLOAT:
case FLOAT:
source = new FloatValuesComparatorSource(this, missingValue, sortMode, nested);
Expand All @@ -78,7 +83,7 @@ public SortField sortField(Object missingValue, MultiValueMode sortMode, Nested
break;

default:
assert !numericType.isFloatingPoint();
assert !targetNumericType.isFloatingPoint();
source = new LongValuesComparatorSource(this, missingValue, sortMode, nested);
break;
}
Expand All @@ -88,8 +93,9 @@ public SortField sortField(Object missingValue, MultiValueMode sortMode, Nested
* returns a custom sort field otherwise.
*/
if (nested != null
|| (sortMode != MultiValueMode.MAX && sortMode != MultiValueMode.MIN)
|| numericType == NumericType.HALF_FLOAT) {
|| (sortMode != MultiValueMode.MAX && sortMode != MultiValueMode.MIN)
|| numericType == NumericType.HALF_FLOAT
|| targetNumericType != numericType) {
return new SortField(fieldName, source, reverse);
}

Expand All @@ -114,6 +120,11 @@ public SortField sortField(Object missingValue, MultiValueMode sortMode, Nested
return sortField;
}

@Override
public SortField sortField(Object missingValue, MultiValueMode sortMode, Nested nested, boolean reverse) {
return sortField(numericType, missingValue, sortMode, nested, reverse);
}

@Override
public NumericType getNumericType() {
return numericType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import org.elasticsearch.index.fielddata.IndexFieldData;
import org.elasticsearch.index.fielddata.IndexFieldData.XFieldComparatorSource.Nested;
import org.elasticsearch.index.fielddata.IndexNumericFieldData;
import org.elasticsearch.index.fielddata.IndexNumericFieldData.NumericType;
import org.elasticsearch.index.fielddata.plain.SortedNumericDVIndexFieldData;
import org.elasticsearch.index.mapper.MappedFieldType;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryRewriteContext;
Expand All @@ -42,6 +44,7 @@
import org.elasticsearch.search.MultiValueMode;

import java.io.IOException;
import java.util.Locale;
import java.util.Objects;

import static org.elasticsearch.search.sort.NestedSortBuilder.NESTED_FIELD;
Expand All @@ -56,6 +59,7 @@ public class FieldSortBuilder extends SortBuilder<FieldSortBuilder> {
public static final ParseField MISSING = new ParseField("missing");
public static final ParseField SORT_MODE = new ParseField("mode");
public static final ParseField UNMAPPED_TYPE = new ParseField("unmapped_type");
public static final ParseField NUMERIC_TYPE = new ParseField("numeric_type");

/**
* special field name to sort by index order
Expand All @@ -72,6 +76,8 @@ public class FieldSortBuilder extends SortBuilder<FieldSortBuilder> {

private String unmappedType;

private String numericType;

private SortMode sortMode;

private QueryBuilder nestedFilter;
Expand All @@ -94,6 +100,7 @@ public FieldSortBuilder(FieldSortBuilder template) {
if (template.getNestedSort() != null) {
this.setNestedSort(template.getNestedSort());
}
this.numericType = template.numericType;
}

/**
Expand Down Expand Up @@ -121,6 +128,9 @@ public FieldSortBuilder(StreamInput in) throws IOException {
sortMode = in.readOptionalWriteable(SortMode::readFromStream);
unmappedType = in.readOptionalString();
nestedSort = in.readOptionalWriteable(NestedSortBuilder::new);
if (in.getVersion().onOrAfter(Version.V_8_0_0)) {
numericType = in.readOptionalString();
}
}

@Override
Expand All @@ -133,6 +143,9 @@ public void writeTo(StreamOutput out) throws IOException {
out.writeOptionalWriteable(sortMode);
out.writeOptionalString(unmappedType);
out.writeOptionalWriteable(nestedSort);
if (out.getVersion().onOrAfter(Version.V_8_0_0)) {
out.writeOptionalString(numericType);
}
}

/** Returns the document field this sort should be based on. */
Expand Down Expand Up @@ -270,6 +283,36 @@ public FieldSortBuilder setNestedSort(final NestedSortBuilder nestedSort) {
return this;
}

/**
* Returns the numeric type that values should translated to or null
* if the original numeric type should be preserved.
*/
public String getNumericType() {
return numericType;
}

/**
* Forces the numeric type to use for the field. The query will fail if this option
* is set on a field that is not mapped as a numeric in some indices.
* Specifying a numeric type tells Elasticsearch what type the sort values should
* have, which is important for cross-index search, if a field does not have
* the same type on all indices.
* Allowed values are <code>long</code> and <code>double</code>.
*/
public FieldSortBuilder setNumericType(String numericType) {
String upperCase = numericType.toUpperCase(Locale.ENGLISH);
switch (upperCase) {
case "LONG":
case "DOUBLE":
break;

default:
throw new IllegalArgumentException("invalid value for [numeric_type], must be [LONG, DOUBLE], got " + numericType);
}
this.numericType = upperCase;
return this;
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
Expand All @@ -293,6 +336,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
if (nestedSort != null) {
builder.field(NESTED_FIELD.getPreferredName(), nestedSort);
}
if (numericType != null) {
builder.field(NUMERIC_TYPE.getPreferredName(), numericType);
}
builder.endObject();
builder.endObject();
return builder;
Expand Down Expand Up @@ -347,7 +393,18 @@ public SortFieldAndFormat build(QueryShardContext context) throws IOException {
&& (sortMode == SortMode.SUM || sortMode == SortMode.AVG || sortMode == SortMode.MEDIAN)) {
throw new QueryShardException(context, "we only support AVG, MEDIAN and SUM on number based fields");
}
SortField field = fieldData.sortField(missing, localSortMode, nested, reverse);
final SortField field;
if (numericType != null) {
if (fieldData instanceof IndexNumericFieldData == false) {
throw new QueryShardException(context,
"[numeric_type] option cannot be set on a non-numeric field, got " + fieldType.typeName());
}
SortedNumericDVIndexFieldData numericFieldData = (SortedNumericDVIndexFieldData) fieldData;
NumericType resolvedType = NumericType.valueOf(numericType);
field = numericFieldData.sortField(resolvedType, missing, localSortMode, nested, reverse);
} else {
field = fieldData.sortField(missing, localSortMode, nested, reverse);
}
return new SortFieldAndFormat(field, fieldType.docValueFormat(null, null));
}
}
Expand All @@ -366,13 +423,14 @@ public boolean equals(Object 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) && Objects.equals(this.nestedSort, builder.nestedSort));
&& Objects.equals(this.unmappedType, builder.unmappedType) && Objects.equals(this.nestedSort, builder.nestedSort))
&& Objects.equals(this.numericType, builder.numericType);
}

@Override
public int hashCode() {
return Objects.hash(this.fieldName, this.nestedFilter, this.nestedPath, this.nestedSort, this.missing, this.order, this.sortMode,
this.unmappedType);
this.unmappedType, this.numericType);
}

@Override
Expand Down Expand Up @@ -409,6 +467,7 @@ public static FieldSortBuilder fromXContent(XContentParser parser, String fieldN
return SortBuilder.parseNestedFilter(p);
}, NESTED_FILTER_FIELD);
PARSER.declareObject(FieldSortBuilder::setNestedSort, (p, c) -> NestedSortBuilder.fromXContent(p), NESTED_FIELD);
PARSER.declareString((b, v) -> b.setNumericType(v), NUMERIC_TYPE);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,16 @@ public FieldSortBuilder randomFieldSortBuilder() {
}
}
}
if (randomBoolean()) {
builder.setNumericType(randomFrom(random(), "long", "double"));
}
return builder;
}

@Override
protected FieldSortBuilder mutate(FieldSortBuilder original) throws IOException {
FieldSortBuilder mutated = new FieldSortBuilder(original);
int parameter = randomIntBetween(0, 4);
int parameter = randomIntBetween(0, 5);
switch (parameter) {
case 0:
if (original.getNestedPath() == null && original.getNestedFilter() == null) {
Expand Down Expand Up @@ -136,6 +139,9 @@ protected FieldSortBuilder mutate(FieldSortBuilder original) throws IOException
case 4:
mutated.order(randomValueOtherThan(original.order(), () -> randomFrom(SortOrder.values())));
break;
case 5:
mutated.setNumericType(randomValueOtherThan(original.getNumericType(), () -> randomFrom("LONG", "DOUBLE")));
break;
default:
throw new IllegalStateException("Unsupported mutation.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.TestUtil;
import org.apache.lucene.util.UnicodeUtil;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.admin.indices.alias.Alias;
import org.elasticsearch.action.index.IndexRequestBuilder;
import org.elasticsearch.action.search.SearchPhaseExecutionException;
Expand All @@ -36,6 +37,7 @@
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.script.MockScriptPlugin;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptType;
Expand Down Expand Up @@ -1638,4 +1640,70 @@ public void testFieldAliasesWithMissingValues() throws Exception {
assertEquals(100.2, hits.getAt(1).getSortValues()[0]);
assertEquals(120.3, hits.getAt(2).getSortValues()[0]);
}

public void testCastNumericType() throws Exception {
assertAcked(prepareCreate("index_double")
.addMapping("_doc", "field", "type=double"));
assertAcked(prepareCreate("index_long")
.addMapping("_doc", "field", "type=long"));
assertAcked(prepareCreate("index_float")
.addMapping("_doc", "field", "type=float"));
ensureGreen("index_double", "index_long", "index_float");

List<IndexRequestBuilder> builders = new ArrayList<>();
builders.add(client().prepareIndex("index_double", "_doc").setSource("field", 12.6));
builders.add(client().prepareIndex("index_long", "_doc").setSource("field", 12));
builders.add(client().prepareIndex("index_float", "_doc").setSource("field", 12.1));
indexRandom(true, true, builders);

{
SearchResponse response = client().prepareSearch()
.setQuery(matchAllQuery())
.setSize(builders.size())
.addSort(SortBuilders.fieldSort("field").setNumericType("long"))
.get();
SearchHits hits = response.getHits();

assertEquals(3, hits.getHits().length);
for (int i = 0; i < 3; i++) {
assertThat(hits.getAt(i).getSortValues()[0].getClass(), equalTo(Long.class));
}
assertEquals(12L, hits.getAt(0).getSortValues()[0]);
assertEquals(12L, hits.getAt(1).getSortValues()[0]);
assertEquals(12L, hits.getAt(2).getSortValues()[0]);
}

{
SearchResponse response = client().prepareSearch()
.setQuery(matchAllQuery())
.setSize(builders.size())
.addSort(SortBuilders.fieldSort("field").setNumericType("double"))
.get();
SearchHits hits = response.getHits();
assertEquals(3, hits.getHits().length);
for (int i = 0; i < 3; i++) {
assertThat(hits.getAt(i).getSortValues()[0].getClass(), equalTo(Double.class));
}
assertEquals(12D, hits.getAt(0).getSortValues()[0]);
assertEquals(12.1D, (double) hits.getAt(1).getSortValues()[0], 0.001f);
assertEquals(12.6D, hits.getAt(2).getSortValues()[0]);
}
}

public void testCastNumericTypeExceptions() throws Exception {
assertAcked(prepareCreate("index")
.addMapping("_doc", "keyword", "type=keyword", "ip", "type=ip"));
ensureGreen("index");
for (String invalidField : new String[] {"keyword", "ip"}) {
for (String numericType : new String[]{"long", "double"}) {
ElasticsearchException exc = expectThrows(ElasticsearchException.class, () -> client().prepareSearch()
.setQuery(matchAllQuery())
.addSort(SortBuilders.fieldSort(invalidField).setNumericType(numericType))
.get()
);
assertThat(exc.status(), equalTo(RestStatus.BAD_REQUEST));
assertThat(exc.getDetailedMessage(), containsString("[numeric_type] option cannot be set on a non-numeric field"));
}
}
}
}