Multi-level Nested Sort with Filters (#26395)

Multi-level Nested Sort with Filters

Allow multiple levels of nested sorting where each level can have it's own filter.
Backward compatible with previous single-level nested sort.
This commit is contained in:
Matt Weber 2017-08-30 09:52:56 -07:00 committed by Martijn van Groningen
parent 42e8940a3d
commit 140395c83f
13 changed files with 829 additions and 163 deletions

View File

@ -36,6 +36,7 @@ import org.apache.lucene.util.BitDocIdSet;
import org.apache.lucene.util.BitSet;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.lucene.search.Queries;
import org.elasticsearch.index.IndexComponent;
import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.index.fielddata.IndexFieldData.XFieldComparatorSource.Nested;
@ -116,6 +117,14 @@ public interface IndexFieldData<FD extends AtomicFieldData> extends IndexCompone
this.innerQuery = innerQuery;
}
public Query getInnerQuery() {
return innerQuery;
}
public BitSetProducer getRootFilter() {
return rootFilter;
}
/**
* Get a {@link BitDocIdSet} that matches the root documents.
*/

View File

@ -514,42 +514,41 @@ public enum MultiValueMode implements Writeable {
* NOTE: Calling the returned instance on docs that are not root docs is illegal
* The returned instance can only be evaluate the current and upcoming docs
*/
public NumericDocValues select(final SortedNumericDocValues values, final long missingValue, final BitSet rootDocs, final DocIdSetIterator innerDocs, int maxDoc) throws IOException {
if (rootDocs == null || innerDocs == null) {
public NumericDocValues select(final SortedNumericDocValues values, final long missingValue, final BitSet parentDocs, final DocIdSetIterator childDocs, int maxDoc) throws IOException {
if (parentDocs == null || childDocs == null) {
return select(DocValues.emptySortedNumeric(maxDoc), missingValue);
}
return new AbstractNumericDocValues() {
int lastSeenRootDoc = -1;
int lastSeenParentDoc = -1;
long lastEmittedValue = missingValue;
@Override
public boolean advanceExact(int rootDoc) throws IOException {
assert rootDocs.get(rootDoc) : "can only sort root documents";
assert rootDoc >= lastSeenRootDoc : "can only evaluate current and upcoming root docs";
if (rootDoc == lastSeenRootDoc) {
public boolean advanceExact(int parentDoc) throws IOException {
assert parentDoc >= lastSeenParentDoc : "can only evaluate current and upcoming parent docs";
if (parentDoc == lastSeenParentDoc) {
return true;
} else if (rootDoc == 0) {
} else if (parentDoc == 0) {
lastEmittedValue = missingValue;
return true;
}
final int prevRootDoc = rootDocs.prevSetBit(rootDoc - 1);
final int firstNestedDoc;
if (innerDocs.docID() > prevRootDoc) {
firstNestedDoc = innerDocs.docID();
final int prevParentDoc = parentDocs.prevSetBit(parentDoc - 1);
final int firstChildDoc;
if (childDocs.docID() > prevParentDoc) {
firstChildDoc = childDocs.docID();
} else {
firstNestedDoc = innerDocs.advance(prevRootDoc + 1);
firstChildDoc = childDocs.advance(prevParentDoc + 1);
}
lastSeenRootDoc = rootDoc;
lastEmittedValue = pick(values, missingValue, innerDocs, firstNestedDoc, rootDoc);
lastSeenParentDoc = parentDoc;
lastEmittedValue = pick(values, missingValue, childDocs, firstChildDoc, parentDoc);
return true;
}
@Override
public int docID() {
return lastSeenRootDoc;
return lastSeenParentDoc;
}
@Override
@ -624,33 +623,32 @@ public enum MultiValueMode implements Writeable {
* NOTE: Calling the returned instance on docs that are not root docs is illegal
* The returned instance can only be evaluate the current and upcoming docs
*/
public NumericDoubleValues select(final SortedNumericDoubleValues values, final double missingValue, final BitSet rootDocs, final DocIdSetIterator innerDocs, int maxDoc) throws IOException {
if (rootDocs == null || innerDocs == null) {
public NumericDoubleValues select(final SortedNumericDoubleValues values, final double missingValue, final BitSet parentDocs, final DocIdSetIterator childDocs, int maxDoc) throws IOException {
if (parentDocs == null || childDocs == null) {
return select(FieldData.emptySortedNumericDoubles(), missingValue);
}
return new NumericDoubleValues() {
int lastSeenRootDoc = 0;
int lastSeenParentDoc = 0;
double lastEmittedValue = missingValue;
@Override
public boolean advanceExact(int rootDoc) throws IOException {
assert rootDocs.get(rootDoc) : "can only sort root documents";
assert rootDoc >= lastSeenRootDoc : "can only evaluate current and upcoming root docs";
if (rootDoc == lastSeenRootDoc) {
public boolean advanceExact(int parentDoc) throws IOException {
assert parentDoc >= lastSeenParentDoc : "can only evaluate current and upcoming parent docs";
if (parentDoc == lastSeenParentDoc) {
return true;
}
final int prevRootDoc = rootDocs.prevSetBit(rootDoc - 1);
final int firstNestedDoc;
if (innerDocs.docID() > prevRootDoc) {
firstNestedDoc = innerDocs.docID();
final int prevParentDoc = parentDocs.prevSetBit(parentDoc - 1);
final int firstChildDoc;
if (childDocs.docID() > prevParentDoc) {
firstChildDoc = childDocs.docID();
} else {
firstNestedDoc = innerDocs.advance(prevRootDoc + 1);
firstChildDoc = childDocs.advance(prevParentDoc + 1);
}
lastSeenRootDoc = rootDoc;
lastEmittedValue = pick(values, missingValue, innerDocs, firstNestedDoc, rootDoc);
lastSeenParentDoc = parentDoc;
lastEmittedValue = pick(values, missingValue, childDocs, firstChildDoc, parentDoc);
return true;
}
@ -733,8 +731,8 @@ public enum MultiValueMode implements Writeable {
* NOTE: Calling the returned instance on docs that are not root docs is illegal
* The returned instance can only be evaluate the current and upcoming docs
*/
public BinaryDocValues select(final SortedBinaryDocValues values, final BytesRef missingValue, final BitSet rootDocs, final DocIdSetIterator innerDocs, int maxDoc) throws IOException {
if (rootDocs == null || innerDocs == null) {
public BinaryDocValues select(final SortedBinaryDocValues values, final BytesRef missingValue, final BitSet parentDocs, final DocIdSetIterator childDocs, int maxDoc) throws IOException {
if (parentDocs == null || childDocs == null) {
return select(FieldData.emptySortedBinary(), missingValue);
}
final BinaryDocValues selectedValues = select(values, null);
@ -743,27 +741,26 @@ public enum MultiValueMode implements Writeable {
final BytesRefBuilder builder = new BytesRefBuilder();
int lastSeenRootDoc = 0;
int lastSeenParentDoc = 0;
BytesRef lastEmittedValue = missingValue;
@Override
public boolean advanceExact(int rootDoc) throws IOException {
assert rootDocs.get(rootDoc) : "can only sort root documents";
assert rootDoc >= lastSeenRootDoc : "can only evaluate current and upcoming root docs";
if (rootDoc == lastSeenRootDoc) {
public boolean advanceExact(int parentDoc) throws IOException {
assert parentDoc >= lastSeenParentDoc : "can only evaluate current and upcoming root docs";
if (parentDoc == lastSeenParentDoc) {
return true;
}
final int prevRootDoc = rootDocs.prevSetBit(rootDoc - 1);
final int firstNestedDoc;
if (innerDocs.docID() > prevRootDoc) {
firstNestedDoc = innerDocs.docID();
final int prevParentDoc = parentDocs.prevSetBit(parentDoc - 1);
final int firstChildDoc;
if (childDocs.docID() > prevParentDoc) {
firstChildDoc = childDocs.docID();
} else {
firstNestedDoc = innerDocs.advance(prevRootDoc + 1);
firstChildDoc = childDocs.advance(prevParentDoc + 1);
}
lastSeenRootDoc = rootDoc;
lastEmittedValue = pick(selectedValues, builder, innerDocs, firstNestedDoc, rootDoc);
lastSeenParentDoc = parentDoc;
lastEmittedValue = pick(selectedValues, builder, childDocs, firstChildDoc, parentDoc);
if (lastEmittedValue == null) {
lastEmittedValue = missingValue;
}
@ -850,8 +847,8 @@ public enum MultiValueMode implements Writeable {
* NOTE: Calling the returned instance on docs that are not root docs is illegal
* The returned instance can only be evaluate the current and upcoming docs
*/
public SortedDocValues select(final SortedSetDocValues values, final BitSet rootDocs, final DocIdSetIterator innerDocs) throws IOException {
if (rootDocs == null || innerDocs == null) {
public SortedDocValues select(final SortedSetDocValues values, final BitSet parentDocs, final DocIdSetIterator childDocs) throws IOException {
if (parentDocs == null || childDocs == null) {
return select(DocValues.emptySortedSet());
}
final SortedDocValues selectedValues = select(values);
@ -859,7 +856,7 @@ public enum MultiValueMode implements Writeable {
return new AbstractSortedDocValues() {
int docID = -1;
int lastSeenRootDoc = 0;
int lastSeenParentDoc = 0;
int lastEmittedOrd = -1;
@Override
@ -873,23 +870,22 @@ public enum MultiValueMode implements Writeable {
}
@Override
public boolean advanceExact(int rootDoc) throws IOException {
assert rootDocs.get(rootDoc) : "can only sort root documents";
assert rootDoc >= lastSeenRootDoc : "can only evaluate current and upcoming root docs";
if (rootDoc == lastSeenRootDoc) {
public boolean advanceExact(int parentDoc) throws IOException {
assert parentDoc >= lastSeenParentDoc : "can only evaluate current and upcoming root docs";
if (parentDoc == lastSeenParentDoc) {
return lastEmittedOrd != -1;
}
final int prevRootDoc = rootDocs.prevSetBit(rootDoc - 1);
final int firstNestedDoc;
if (innerDocs.docID() > prevRootDoc) {
firstNestedDoc = innerDocs.docID();
final int prevParentDoc = parentDocs.prevSetBit(parentDoc - 1);
final int firstChildDoc;
if (childDocs.docID() > prevParentDoc) {
firstChildDoc = childDocs.docID();
} else {
firstNestedDoc = innerDocs.advance(prevRootDoc + 1);
firstChildDoc = childDocs.advance(prevParentDoc + 1);
}
docID = lastSeenRootDoc = rootDoc;
lastEmittedOrd = pick(selectedValues, innerDocs, firstNestedDoc, rootDoc);
docID = lastSeenParentDoc = parentDoc;
lastEmittedOrd = pick(selectedValues, childDocs, firstChildDoc, parentDoc);
return lastEmittedOrd != -1;
}

View File

@ -19,10 +19,15 @@
package org.elasticsearch.search.sort;
import static org.elasticsearch.search.sort.NestedSortBuilder.NESTED_FIELD;
import org.apache.lucene.search.SortField;
import org.elasticsearch.Version;
import org.elasticsearch.common.ParseField;
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.xcontent.ObjectParser;
import org.elasticsearch.common.xcontent.ObjectParser.ValueType;
import org.elasticsearch.common.xcontent.XContentBuilder;
@ -45,6 +50,8 @@ import java.util.Objects;
* A sort builder to sort based on a document field.
*/
public class FieldSortBuilder extends SortBuilder<FieldSortBuilder> {
private static final DeprecationLogger DEPRECATION_LOGGER = new DeprecationLogger(Loggers.getLogger(FieldSortBuilder.class));
public static final String NAME = "field_sort";
public static final ParseField MISSING = new ParseField("missing");
public static final ParseField SORT_MODE = new ParseField("mode");
@ -71,6 +78,8 @@ public class FieldSortBuilder extends SortBuilder<FieldSortBuilder> {
private String nestedPath;
private NestedSortBuilder nestedSort;
/** Copy constructor. */
public FieldSortBuilder(FieldSortBuilder template) {
this(template.fieldName);
@ -82,6 +91,7 @@ public class FieldSortBuilder extends SortBuilder<FieldSortBuilder> {
}
this.setNestedFilter(template.getNestedFilter());
this.setNestedPath(template.getNestedPath());
this.setNestedSort(template.getNestedSort());
}
/**
@ -108,6 +118,9 @@ public class FieldSortBuilder extends SortBuilder<FieldSortBuilder> {
order = in.readOptionalWriteable(SortOrder::readFromStream);
sortMode = in.readOptionalWriteable(SortMode::readFromStream);
unmappedType = in.readOptionalString();
if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
nestedSort = in.readOptionalWriteable(NestedSortBuilder::new);
}
}
@Override
@ -119,6 +132,9 @@ public class FieldSortBuilder extends SortBuilder<FieldSortBuilder> {
out.writeOptionalWriteable(order);
out.writeOptionalWriteable(sortMode);
out.writeOptionalString(unmappedType);
if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
out.writeOptionalWriteable(nestedSort);
}
}
/** Returns the document field this sort should be based on. */
@ -221,6 +237,15 @@ public class FieldSortBuilder extends SortBuilder<FieldSortBuilder> {
return this.nestedPath;
}
public NestedSortBuilder getNestedSort() {
return this.nestedSort;
}
public FieldSortBuilder setNestedSort(final NestedSortBuilder nestedSort) {
this.nestedSort = nestedSort;
return this;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
@ -241,6 +266,9 @@ public class FieldSortBuilder extends SortBuilder<FieldSortBuilder> {
if (nestedPath != null) {
builder.field(NESTED_PATH_FIELD.getPreferredName(), nestedPath);
}
if (nestedSort != null) {
builder.field(NESTED_FIELD.getPreferredName(), nestedSort);
}
builder.endObject();
builder.endObject();
return builder;
@ -274,7 +302,14 @@ public class FieldSortBuilder extends SortBuilder<FieldSortBuilder> {
localSortMode = reverse ? MultiValueMode.MAX : MultiValueMode.MIN;
}
final Nested nested = resolveNested(context, nestedPath, nestedFilter);
final Nested nested;
if (nestedSort != null) {
// new nested sorts takes priority
nested = resolveNested(context, nestedSort);
} else {
nested = resolveNested(context, nestedPath, nestedFilter);
}
IndexFieldData<?> fieldData = context.getForField(fieldType);
if (fieldData instanceof IndexNumericFieldData == false
&& (sortMode == SortMode.SUM || sortMode == SortMode.AVG || sortMode == SortMode.MEDIAN)) {
@ -299,12 +334,13 @@ public class FieldSortBuilder extends SortBuilder<FieldSortBuilder> {
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.unmappedType, builder.unmappedType) && Objects.equals(this.nestedSort, builder.nestedSort));
}
@Override
public int hashCode() {
return Objects.hash(this.fieldName, this.nestedFilter, this.nestedPath, this.missing, this.order, this.sortMode, this.unmappedType);
return Objects.hash(this.fieldName, this.nestedFilter, this.nestedPath, this.nestedSort, this.missing, this.order, this.sortMode,
this.unmappedType);
}
@Override
@ -329,11 +365,18 @@ public class FieldSortBuilder extends SortBuilder<FieldSortBuilder> {
static {
PARSER.declareField(FieldSortBuilder::missing, p -> p.objectText(), MISSING, ValueType.VALUE);
PARSER.declareString(FieldSortBuilder::setNestedPath , NESTED_PATH_FIELD);
PARSER.declareString((fieldSortBuilder, nestedPath) -> {
DEPRECATION_LOGGER.deprecated("[nested_path] has been deprecated in favor of the [nested] parameter");
fieldSortBuilder.setNestedPath(nestedPath);
}, NESTED_PATH_FIELD);
PARSER.declareString(FieldSortBuilder::unmappedType , UNMAPPED_TYPE);
PARSER.declareString((b, v) -> b.order(SortOrder.fromString(v)) , ORDER_FIELD);
PARSER.declareString((b, v) -> b.sortMode(SortMode.fromString(v)), SORT_MODE);
PARSER.declareObject(FieldSortBuilder::setNestedFilter, (p, c) -> SortBuilder.parseNestedFilter(p), NESTED_FILTER_FIELD);
PARSER.declareObject(FieldSortBuilder::setNestedFilter, (p, c) -> {
DEPRECATION_LOGGER.deprecated("[nested_filter] has been deprecated in favour for the [nested] parameter");
return SortBuilder.parseNestedFilter(p);
}, NESTED_FILTER_FIELD);
PARSER.declareObject(FieldSortBuilder::setNestedSort, (p, c) -> NestedSortBuilder.fromXContent(p), NESTED_FIELD);
}
@Override

View File

@ -27,6 +27,7 @@ import org.apache.lucene.search.FieldComparator;
import org.apache.lucene.search.SortField;
import org.apache.lucene.util.BitSet;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.Version;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.geo.GeoDistance;
@ -34,6 +35,8 @@ import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.geo.GeoUtils;
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.unit.DistanceUnit;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
@ -61,10 +64,14 @@ import java.util.Locale;
import java.util.Objects;
import static org.elasticsearch.index.query.AbstractQueryBuilder.parseInnerQueryBuilder;
import static org.elasticsearch.search.sort.NestedSortBuilder.NESTED_FIELD;
/**
* A geo distance based sorting on a geo point like field.
*/
public class GeoDistanceSortBuilder extends SortBuilder<GeoDistanceSortBuilder> {
private static final DeprecationLogger DEPRECATION_LOGGER = new DeprecationLogger(Loggers.getLogger(GeoDistanceSortBuilder.class));
public static final String NAME = "_geo_distance";
public static final String ALTERNATIVE_NAME = "_geoDistance";
public static final GeoValidationMethod DEFAULT_VALIDATION = GeoValidationMethod.DEFAULT;
@ -85,6 +92,8 @@ public class GeoDistanceSortBuilder extends SortBuilder<GeoDistanceSortBuilder>
private QueryBuilder nestedFilter;
private String nestedPath;
private NestedSortBuilder nestedSort;
private GeoValidationMethod validation = DEFAULT_VALIDATION;
/**
@ -141,6 +150,7 @@ public class GeoDistanceSortBuilder extends SortBuilder<GeoDistanceSortBuilder>
this.nestedFilter = original.nestedFilter;
this.nestedPath = original.nestedPath;
this.validation = original.validation;
this.nestedSort = original.nestedSort;
}
/**
@ -156,6 +166,9 @@ public class GeoDistanceSortBuilder extends SortBuilder<GeoDistanceSortBuilder>
sortMode = in.readOptionalWriteable(SortMode::readFromStream);
nestedFilter = in.readOptionalNamedWriteable(QueryBuilder.class);
nestedPath = in.readOptionalString();
if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
nestedSort = in.readOptionalWriteable(NestedSortBuilder::new);
}
validation = GeoValidationMethod.readFromStream(in);
}
@ -169,6 +182,9 @@ public class GeoDistanceSortBuilder extends SortBuilder<GeoDistanceSortBuilder>
out.writeOptionalWriteable(sortMode);
out.writeOptionalNamedWriteable(nestedFilter);
out.writeOptionalString(nestedPath);
if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
out.writeOptionalWriteable(nestedSort);
}
validation.writeTo(out);
}
@ -317,6 +333,15 @@ public class GeoDistanceSortBuilder extends SortBuilder<GeoDistanceSortBuilder>
return this.nestedPath;
}
public NestedSortBuilder getNestedSort() {
return this.nestedSort;
}
public GeoDistanceSortBuilder setNestedSort(final NestedSortBuilder nestedSort) {
this.nestedSort = nestedSort;
return this;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
@ -342,6 +367,9 @@ public class GeoDistanceSortBuilder extends SortBuilder<GeoDistanceSortBuilder>
if (nestedFilter != null) {
builder.field(NESTED_FILTER_FIELD.getPreferredName(), nestedFilter, params);
}
if (nestedSort != null) {
builder.field(NESTED_FIELD.getPreferredName(), nestedSort);
}
builder.field(VALIDATION_METHOD_FIELD.getPreferredName(), validation);
builder.endObject();
@ -373,14 +401,15 @@ public class GeoDistanceSortBuilder extends SortBuilder<GeoDistanceSortBuilder>
Objects.equals(order, other.order) &&
Objects.equals(nestedFilter, other.nestedFilter) &&
Objects.equals(nestedPath, other.nestedPath) &&
Objects.equals(validation, other.validation);
Objects.equals(validation, other.validation) &&
Objects.equals(nestedSort, other.nestedSort);
}
@Override
public int hashCode() {
return Objects.hash(this.fieldName, this.points, this.geoDistance,
this.unit, this.sortMode, this.order, this.nestedFilter,
this.nestedPath, this.validation);
this.nestedPath, this.validation, this.nestedSort);
}
/**
@ -402,6 +431,7 @@ public class GeoDistanceSortBuilder extends SortBuilder<GeoDistanceSortBuilder>
SortMode sortMode = null;
QueryBuilder nestedFilter = null;
String nestedPath = null;
NestedSortBuilder nestedSort = null;
GeoValidationMethod validation = null;
XContentParser.Token token;
@ -415,7 +445,10 @@ public class GeoDistanceSortBuilder extends SortBuilder<GeoDistanceSortBuilder>
fieldName = currentName;
} else if (token == XContentParser.Token.START_OBJECT) {
if (NESTED_FILTER_FIELD.match(currentName)) {
DEPRECATION_LOGGER.deprecated("[nested_filter] has been deprecated in favour for the [nested] parameter");
nestedFilter = parseInnerQueryBuilder(parser);
} else if (NESTED_FIELD.match(currentName)) {
nestedSort = NestedSortBuilder.fromXContent(parser);
} else {
// the json in the format of -> field : { lat : 30, lon : 12 }
if (fieldName != null && fieldName.equals(currentName) == false) {
@ -442,6 +475,7 @@ public class GeoDistanceSortBuilder extends SortBuilder<GeoDistanceSortBuilder>
} else if (SORTMODE_FIELD.match(currentName)) {
sortMode = SortMode.fromString(parser.text());
} else if (NESTED_PATH_FIELD.match(currentName)) {
DEPRECATION_LOGGER.deprecated("[nested_path] has been deprecated in favor of the [nested] parameter");
nestedPath = parser.text();
} else if (token == Token.VALUE_STRING){
if (fieldName != null && fieldName.equals(currentName) == false) {
@ -482,6 +516,9 @@ public class GeoDistanceSortBuilder extends SortBuilder<GeoDistanceSortBuilder>
result.setNestedFilter(nestedFilter);
}
result.setNestedPath(nestedPath);
if (nestedSort != null) {
result.setNestedSort(nestedSort);
}
if (validation != null) {
result.validation(validation);
}
@ -531,7 +568,14 @@ public class GeoDistanceSortBuilder extends SortBuilder<GeoDistanceSortBuilder>
+ "] for geo distance based sort");
}
final IndexGeoPointFieldData geoIndexFieldData = context.getForField(fieldType);
final Nested nested = resolveNested(context, nestedPath, nestedFilter);
final Nested nested;
if (nestedSort != null) {
// new nested sorts takes priority
nested = resolveNested(context, nestedSort);
} else {
nested = resolveNested(context, nestedPath, nestedFilter);
}
if (geoIndexFieldData.getClass() == LatLonPointDVIndexFieldData.class // only works with 5.x geo_point
&& nested == null

View File

@ -0,0 +1,176 @@
/*
* 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 static org.elasticsearch.search.sort.SortBuilder.parseNestedFilter;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.query.QueryBuilder;
import java.io.IOException;
import java.util.Objects;
public class NestedSortBuilder implements Writeable, Writeable.Reader<NestedSortBuilder>, ToXContentObject {
public static final ParseField NESTED_FIELD = new ParseField("nested");
public static final ParseField PATH_FIELD = new ParseField("path");
public static final ParseField FILTER_FIELD = new ParseField("filter");
private String path;
private QueryBuilder filter;
private NestedSortBuilder nestedSort;
public NestedSortBuilder(String path) {
this.path = path;
}
public NestedSortBuilder(StreamInput in) throws IOException {
read(in);
}
public NestedSortBuilder(NestedSortBuilder other) {
if (other != null) {
this.path = other.path;
this.filter = other.filter;
this.nestedSort = other.nestedSort;
}
}
public String getPath() {
return path;
}
public NestedSortBuilder setPath(final String path) {
this.path = path;
return this;
}
public QueryBuilder getFilter() {
return filter;
}
public NestedSortBuilder setFilter(final QueryBuilder filter) {
this.filter = filter;
return this;
}
public NestedSortBuilder getNestedSort() {
return nestedSort;
}
public NestedSortBuilder setNestedSort(final NestedSortBuilder nestedSortBuilder) {
this.nestedSort = nestedSortBuilder;
return this;
}
/**
* Write this object's fields to a {@linkplain StreamOutput}.
*/
@Override
public void writeTo(final StreamOutput out) throws IOException {
out.writeOptionalString(path);
out.writeOptionalNamedWriteable(filter);
out.writeOptionalWriteable(nestedSort);
}
/**
* Read {@code V}-type value from a stream.
*
* @param in Input to read the value from
*/
@Override
public NestedSortBuilder read(final StreamInput in) throws IOException {
path = in.readOptionalString();
filter = in.readOptionalNamedWriteable(QueryBuilder.class);
nestedSort = in.readOptionalWriteable(NestedSortBuilder::new);
return this;
}
@Override
public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException {
builder.startObject();
if (path != null) {
builder.field(PATH_FIELD.getPreferredName(), path);
}
if (filter != null) {
builder.field(FILTER_FIELD.getPreferredName(), filter);
}
if (nestedSort != null) {
builder.field(NESTED_FIELD.getPreferredName(), nestedSort);
}
builder.endObject();
return builder;
}
public static NestedSortBuilder fromXContent(XContentParser parser) throws IOException {
String path = null;
QueryBuilder filter = null;
NestedSortBuilder nestedSort = null;
XContentParser.Token token = parser.currentToken();
if (token == XContentParser.Token.START_OBJECT) {
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
String currentName = parser.currentName();
parser.nextToken();
if (currentName.equals(PATH_FIELD.getPreferredName())) {
path = parser.text();
} else if (currentName.equals(FILTER_FIELD.getPreferredName())) {
filter = parseNestedFilter(parser);
} else if (currentName.equals(NESTED_FIELD.getPreferredName())) {
nestedSort = NestedSortBuilder.fromXContent(parser);
} else {
throw new IllegalArgumentException("malformed nested sort format, unknown field name [" + currentName + "]");
}
} else {
throw new IllegalArgumentException("malformed nested sort format, only field names are allowed");
}
}
} else {
throw new IllegalArgumentException("malformed nested sort format, must start with an object");
}
return new NestedSortBuilder(path).setFilter(filter).setNestedSort(nestedSort);
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
NestedSortBuilder that = (NestedSortBuilder) obj;
return Objects.equals(path, that.path)
&& Objects.equals(filter, that.filter)
&& Objects.equals(nestedSort, that.nestedSort);
}
@Override
public int hashCode() {
return Objects.hash(path, filter, nestedSort);
}
}

View File

@ -25,10 +25,13 @@ import org.apache.lucene.search.Scorer;
import org.apache.lucene.search.SortField;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.BytesRefBuilder;
import org.elasticsearch.Version;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.ObjectParser.ValueType;
import org.elasticsearch.common.xcontent.XContentBuilder;
@ -56,11 +59,13 @@ import java.util.Locale;
import java.util.Objects;
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
import static org.elasticsearch.search.sort.NestedSortBuilder.NESTED_FIELD;
/**
* Script sort builder allows to sort based on a custom script expression.
*/
public class ScriptSortBuilder extends SortBuilder<ScriptSortBuilder> {
private static final DeprecationLogger DEPRECATION_LOGGER = new DeprecationLogger(Loggers.getLogger(ScriptSortBuilder.class));
public static final String NAME = "_script";
public static final ParseField TYPE_FIELD = new ParseField("type");
@ -77,6 +82,8 @@ public class ScriptSortBuilder extends SortBuilder<ScriptSortBuilder> {
private String nestedPath;
private NestedSortBuilder nestedSort;
/**
* Constructs a script sort builder with the given script.
*
@ -100,6 +107,7 @@ public class ScriptSortBuilder extends SortBuilder<ScriptSortBuilder> {
this.sortMode = original.sortMode;
this.nestedFilter = original.nestedFilter;
this.nestedPath = original.nestedPath;
this.nestedSort = original.nestedSort;
}
/**
@ -112,6 +120,9 @@ public class ScriptSortBuilder extends SortBuilder<ScriptSortBuilder> {
sortMode = in.readOptionalWriteable(SortMode::readFromStream);
nestedPath = in.readOptionalString();
nestedFilter = in.readOptionalNamedWriteable(QueryBuilder.class);
if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
nestedSort = in.readOptionalWriteable(NestedSortBuilder::new);
}
}
@Override
@ -122,6 +133,9 @@ public class ScriptSortBuilder extends SortBuilder<ScriptSortBuilder> {
out.writeOptionalWriteable(sortMode);
out.writeOptionalString(nestedPath);
out.writeOptionalNamedWriteable(nestedFilter);
if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
out.writeOptionalWriteable(nestedSort);
}
}
/**
@ -191,6 +205,15 @@ public class ScriptSortBuilder extends SortBuilder<ScriptSortBuilder> {
return this.nestedPath;
}
public NestedSortBuilder getNestedSort() {
return this.nestedSort;
}
public ScriptSortBuilder setNestedSort(final NestedSortBuilder nestedSort) {
this.nestedSort = nestedSort;
return this;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params builderParams) throws IOException {
builder.startObject();
@ -207,6 +230,9 @@ public class ScriptSortBuilder extends SortBuilder<ScriptSortBuilder> {
if (nestedFilter != null) {
builder.field(NESTED_FILTER_FIELD.getPreferredName(), nestedFilter, builderParams);
}
if (nestedSort != null) {
builder.field(NESTED_FIELD.getPreferredName(), nestedSort);
}
builder.endObject();
builder.endObject();
return builder;
@ -221,8 +247,15 @@ public class ScriptSortBuilder extends SortBuilder<ScriptSortBuilder> {
PARSER.declareField(constructorArg(), p -> ScriptSortType.fromString(p.text()), TYPE_FIELD, ValueType.STRING);
PARSER.declareString((b, v) -> b.order(SortOrder.fromString(v)), ORDER_FIELD);
PARSER.declareString((b, v) -> b.sortMode(SortMode.fromString(v)), SORTMODE_FIELD);
PARSER.declareString(ScriptSortBuilder::setNestedPath , NESTED_PATH_FIELD);
PARSER.declareObject(ScriptSortBuilder::setNestedFilter, (p,c) -> SortBuilder.parseNestedFilter(p), NESTED_FILTER_FIELD);
PARSER.declareString((fieldSortBuilder, nestedPath) -> {
DEPRECATION_LOGGER.deprecated("[nested_path] has been deprecated in favor of the [nested] parameter");
fieldSortBuilder.setNestedPath(nestedPath);
}, NESTED_PATH_FIELD);
PARSER.declareObject(ScriptSortBuilder::setNestedFilter, (p, c) -> {
DEPRECATION_LOGGER.deprecated("[nested_filter] has been deprecated in favour for the [nested] parameter");
return SortBuilder.parseNestedFilter(p);
}, NESTED_FILTER_FIELD);
PARSER.declareObject(ScriptSortBuilder::setNestedSort, (p, c) -> NestedSortBuilder.fromXContent(p), NESTED_FIELD);
}
/**
@ -253,7 +286,14 @@ public class ScriptSortBuilder extends SortBuilder<ScriptSortBuilder> {
valueMode = reverse ? MultiValueMode.MAX : MultiValueMode.MIN;
}
final Nested nested = resolveNested(context, nestedPath, nestedFilter);
final Nested nested;
if (nestedSort != null) {
// new nested sorts takes priority
nested = resolveNested(context, nestedSort);
} else {
nested = resolveNested(context, nestedPath, nestedFilter);
}
final IndexFieldData.XFieldComparatorSource fieldComparatorSource;
switch (type) {
case STRING:
@ -329,12 +369,13 @@ public class ScriptSortBuilder extends SortBuilder<ScriptSortBuilder> {
Objects.equals(order, other.order) &&
Objects.equals(sortMode, other.sortMode) &&
Objects.equals(nestedFilter, other.nestedFilter) &&
Objects.equals(nestedPath, other.nestedPath);
Objects.equals(nestedPath, other.nestedPath) &&
Objects.equals(nestedSort, other.nestedSort);
}
@Override
public int hashCode() {
return Objects.hash(script, type, order, sortMode, nestedFilter, nestedPath);
return Objects.hash(script, type, order, sortMode, nestedFilter, nestedPath, nestedSort);
}
@Override

View File

@ -22,7 +22,9 @@ 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.apache.lucene.search.join.ScoreMode;
import org.apache.lucene.search.join.ToChildBlockJoinQuery;
import org.apache.lucene.search.join.ToParentBlockJoinQuery;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.Strings;
@ -178,28 +180,86 @@ public abstract class SortBuilder<T extends SortBuilder<T>> implements NamedWrit
}
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);
assert nestedFilter == Rewriteable.rewrite(nestedFilter, context) : "nested filter is not rewritten";
innerDocumentsQuery = nestedFilter.toFilter(context);
context.nestedScope().previousLevel();
} else {
innerDocumentsQuery = nestedObjectMapper.nestedTypeFilter();
}
nested = new Nested(rootDocumentsFilter, innerDocumentsQuery);
NestedSortBuilder nestedSortBuilder = new NestedSortBuilder(nestedPath);
nestedSortBuilder.setFilter(nestedFilter);
return resolveNested(context, nestedSortBuilder);
}
protected static Nested resolveNested(QueryShardContext context, NestedSortBuilder nestedSort) throws IOException {
return resolveNested(context, nestedSort, null);
}
protected static Nested resolveNested(QueryShardContext context, NestedSortBuilder nestedSort, Nested nested) throws IOException {
if (nestedSort == null || nestedSort.getPath() == null) {
return null;
}
String nestedPath = nestedSort.getPath();
QueryBuilder nestedFilter = nestedSort.getFilter();
NestedSortBuilder nestedNestedSort = nestedSort.getNestedSort();
// verify our nested path
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");
}
// get our parent query which will determines our parent documents
Query parentQuery;
ObjectMapper objectMapper = context.nestedScope().getObjectMapper();
if (objectMapper == null) {
parentQuery = Queries.newNonNestedFilter();
} else {
parentQuery = objectMapper.nestedTypeFilter();
}
// get our child query, potentially applying a users filter
Query childQuery;
try {
context.nestedScope().nextLevel(nestedObjectMapper);
if (nestedFilter != null) {
assert nestedFilter == Rewriteable.rewrite(nestedFilter, context) : "nested filter is not rewritten";
if (nested == null) {
// this is for back-compat, original single level nested sorting never applied a nested type filter
childQuery = nestedFilter.toFilter(context);
} else {
childQuery = Queries.filtered(nestedObjectMapper.nestedTypeFilter(), nestedFilter.toFilter(context));
}
} else {
childQuery = nestedObjectMapper.nestedTypeFilter();
}
} finally {
context.nestedScope().previousLevel();
}
// apply filters from the previous nested level
if (nested != null) {
parentQuery = Queries.filtered(parentQuery,
new ToParentBlockJoinQuery(nested.getInnerQuery(), nested.getRootFilter(), ScoreMode.None));
if (objectMapper != null) {
childQuery = Queries.filtered(childQuery,
new ToChildBlockJoinQuery(nested.getInnerQuery(), context.bitsetFilter(objectMapper.nestedTypeFilter())));
}
}
// wrap up our parent and child and either process the next level of nesting or return
final Nested innerNested = new Nested(context.bitsetFilter(parentQuery), childQuery);
if (nestedNestedSort != null) {
try {
context.nestedScope().nextLevel(nestedObjectMapper);
return resolveNested(context, nestedNestedSort, innerNested);
} finally {
context.nestedScope().previousLevel();
}
} else {
return innerNested;
}
return nested;
}
protected static QueryBuilder parseNestedFilter(XContentParser parser) {

View File

@ -56,6 +56,14 @@ public class SortBuilders {
return new ScriptSortBuilder(script, type);
}
/**
* Constructs a new nested sort builder.
*
*/
public static NestedSortBuilder nestedSort(String path) {
return new NestedSortBuilder(path);
}
/**
* A geo distance based sort.
*

View File

@ -33,6 +33,7 @@ import org.elasticsearch.cluster.health.ClusterHealthStatus;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortMode;
@ -498,6 +499,220 @@ public class SimpleNestedIT extends ESIntegTestCase {
client().prepareClearScroll().addScrollId("_all").get();
}
public void testNestedSortWithMultiLevelFiltering() throws Exception {
assertAcked(prepareCreate("test")
.addMapping("type1", "{\n"
+ " \"type1\": {\n"
+ " \"properties\": {\n"
+ " \"acl\": {\n"
+ " \"type\": \"nested\",\n"
+ " \"properties\": {\n"
+ " \"access_id\": {\"type\": \"keyword\"},\n"
+ " \"operation\": {\n"
+ " \"type\": \"nested\",\n"
+ " \"properties\": {\n"
+ " \"name\": {\"type\": \"keyword\"},\n"
+ " \"user\": {\n"
+ " \"type\": \"nested\",\n"
+ " \"properties\": {\n"
+ " \"username\": {\"type\": \"keyword\"},\n"
+ " \"id\": {\"type\": \"integer\"}\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ "}", XContentType.JSON));
ensureGreen();
client().prepareIndex("test", "type1", "1").setSource("{\n"
+ " \"acl\": [\n"
+ " {\n"
+ " \"access_id\": 1,\n"
+ " \"operation\": [\n"
+ " {\n"
+ " \"name\": \"read\",\n"
+ " \"user\": [\n"
+ " {\"username\": \"matt\", \"id\": 1},\n"
+ " {\"username\": \"shay\", \"id\": 2},\n"
+ " {\"username\": \"adrien\", \"id\": 3}\n"
+ " ]\n"
+ " },\n"
+ " {\n"
+ " \"name\": \"write\",\n"
+ " \"user\": [\n"
+ " {\"username\": \"shay\", \"id\": 2},\n"
+ " {\"username\": \"adrien\", \"id\": 3}\n"
+ " ]\n"
+ " }\n"
+ " ]\n"
+ " },\n"
+ " {\n"
+ " \"access_id\": 2,\n"
+ " \"operation\": [\n"
+ " {\n"
+ " \"name\": \"read\",\n"
+ " \"user\": [\n"
+ " {\"username\": \"jim\", \"id\": 4},\n"
+ " {\"username\": \"shay\", \"id\": 2}\n"
+ " ]\n"
+ " },\n"
+ " {\n"
+ " \"name\": \"write\",\n"
+ " \"user\": [\n"
+ " {\"username\": \"shay\", \"id\": 2}\n"
+ " ]\n"
+ " },\n"
+ " {\n"
+ " \"name\": \"execute\",\n"
+ " \"user\": [\n"
+ " {\"username\": \"shay\", \"id\": 2}\n"
+ " ]\n"
+ " }\n"
+ " ]\n"
+ " }\n"
+ " ]\n"
+ "}", XContentType.JSON).execute().actionGet();
client().prepareIndex("test", "type1", "2").setSource("{\n"
+ " \"acl\": [\n"
+ " {\n"
+ " \"access_id\": 1,\n"
+ " \"operation\": [\n"
+ " {\n"
+ " \"name\": \"read\",\n"
+ " \"user\": [\n"
+ " {\"username\": \"matt\", \"id\": 1},\n"
+ " {\"username\": \"luca\", \"id\": 5}\n"
+ " ]\n"
+ " },\n"
+ " {\n"
+ " \"name\": \"execute\",\n"
+ " \"user\": [\n"
+ " {\"username\": \"luca\", \"id\": 5}\n"
+ " ]\n"
+ " }\n"
+ " ]\n"
+ " },\n"
+ " {\n"
+ " \"access_id\": 3,\n"
+ " \"operation\": [\n"
+ " {\n"
+ " \"name\": \"read\",\n"
+ " \"user\": [\n"
+ " {\"username\": \"matt\", \"id\": 1}\n"
+ " ]\n"
+ " },\n"
+ " {\n"
+ " \"name\": \"write\",\n"
+ " \"user\": [\n"
+ " {\"username\": \"matt\", \"id\": 1}\n"
+ " ]\n"
+ " },\n"
+ " {\n"
+ " \"name\": \"execute\",\n"
+ " \"user\": [\n"
+ " {\"username\": \"matt\", \"id\": 1}\n"
+ " ]\n"
+ " }\n"
+ " ]\n"
+ " }\n"
+ " ]\n"
+ "}", XContentType.JSON).execute().actionGet();
refresh();
// access id = 1, read, max value, asc, should use matt and shay
SearchResponse searchResponse = client().prepareSearch()
.setQuery(matchAllQuery())
.addSort(
SortBuilders.fieldSort("acl.operation.user.username")
.setNestedSort(SortBuilders.nestedSort("acl")
.setFilter(QueryBuilders.termQuery("acl.access_id", "1"))
.setNestedSort(SortBuilders.nestedSort("acl.operation")
.setFilter(QueryBuilders.termQuery("acl.operation.name", "read"))
.setNestedSort(SortBuilders.nestedSort("acl.operation.user"))))
.sortMode(SortMode.MAX)
.order(SortOrder.ASC)
)
.execute().actionGet();
assertHitCount(searchResponse, 2);
assertThat(searchResponse.getHits().getHits().length, equalTo(2));
assertThat(searchResponse.getHits().getHits()[0].getId(), equalTo("2"));
assertThat(searchResponse.getHits().getHits()[0].getSortValues()[0].toString(), equalTo("matt"));
assertThat(searchResponse.getHits().getHits()[1].getId(), equalTo("1"));
assertThat(searchResponse.getHits().getHits()[1].getSortValues()[0].toString(), equalTo("shay"));
// access id = 1, read, min value, asc, should now use adrien and luca
searchResponse = client().prepareSearch()
.setQuery(matchAllQuery())
.addSort(
SortBuilders.fieldSort("acl.operation.user.username")
.setNestedSort(SortBuilders.nestedSort("acl")
.setFilter(QueryBuilders.termQuery("acl.access_id", "1"))
.setNestedSort(SortBuilders.nestedSort("acl.operation")
.setFilter(QueryBuilders.termQuery("acl.operation.name", "read"))
.setNestedSort(SortBuilders.nestedSort("acl.operation.user"))))
.sortMode(SortMode.MIN)
.order(SortOrder.ASC)
)
.execute().actionGet();
assertHitCount(searchResponse, 2);
assertThat(searchResponse.getHits().getHits().length, equalTo(2));
assertThat(searchResponse.getHits().getHits()[0].getId(), equalTo("1"));
assertThat(searchResponse.getHits().getHits()[0].getSortValues()[0].toString(), equalTo("adrien"));
assertThat(searchResponse.getHits().getHits()[1].getId(), equalTo("2"));
assertThat(searchResponse.getHits().getHits()[1].getSortValues()[0].toString(), equalTo("luca"));
// execute, by matt or luca, by user id, sort missing first
searchResponse = client().prepareSearch()
.setQuery(matchAllQuery())
.addSort(
SortBuilders.fieldSort("acl.operation.user.id")
.setNestedSort(SortBuilders.nestedSort("acl")
.setNestedSort(SortBuilders.nestedSort("acl.operation")
.setFilter(QueryBuilders.termQuery("acl.operation.name", "execute"))
.setNestedSort(SortBuilders.nestedSort("acl.operation.user")
.setFilter(QueryBuilders.termsQuery("acl.operation.user.username", "matt", "luca")))))
.missing("_first")
.sortMode(SortMode.MIN)
.order(SortOrder.DESC)
)
.execute().actionGet();
assertHitCount(searchResponse, 2);
assertThat(searchResponse.getHits().getHits().length, equalTo(2));
assertThat(searchResponse.getHits().getHits()[0].getId(), equalTo("1")); // missing first
assertThat(searchResponse.getHits().getHits()[1].getId(), equalTo("2"));
assertThat(searchResponse.getHits().getHits()[1].getSortValues()[0].toString(), equalTo("1"));
// execute, by matt or luca, by username, sort missing last (default)
searchResponse = client().prepareSearch()
.setQuery(matchAllQuery())
.addSort(
SortBuilders.fieldSort("acl.operation.user.username")
.setNestedSort(SortBuilders.nestedSort("acl")
.setNestedSort(SortBuilders.nestedSort("acl.operation")
.setFilter(QueryBuilders.termQuery("acl.operation.name", "execute"))
.setNestedSort(SortBuilders.nestedSort("acl.operation.user")
.setFilter(QueryBuilders.termsQuery("acl.operation.user.username", "matt", "luca")))))
.sortMode(SortMode.MIN)
.order(SortOrder.DESC)
)
.execute().actionGet();
assertHitCount(searchResponse, 2);
assertThat(searchResponse.getHits().getHits().length, equalTo(2));
assertThat(searchResponse.getHits().getHits()[0].getId(), equalTo("2"));
assertThat(searchResponse.getHits().getHits()[0].getSortValues()[0].toString(), equalTo("luca"));
assertThat(searchResponse.getHits().getHits()[1].getId(), equalTo("1")); // missing last
}
public void testSortNestedWithNestedFilter() throws Exception {
assertAcked(prepareCreate("test")
.addMapping("type1", XContentFactory.jsonBuilder()
@ -529,7 +744,7 @@ public class SimpleNestedIT extends ESIntegTestCase {
ensureGreen();
// sum: 11
client().prepareIndex("test", "type1", Integer.toString(1)).setSource(jsonBuilder()
client().prepareIndex("test", "type1", "1").setSource(jsonBuilder()
.startObject()
.field("grand_parent_values", 1L)
.startArray("parent")
@ -568,7 +783,7 @@ public class SimpleNestedIT extends ESIntegTestCase {
.endObject()).execute().actionGet();
// sum: 7
client().prepareIndex("test", "type1", Integer.toString(2)).setSource(jsonBuilder()
client().prepareIndex("test", "type1", "2").setSource(jsonBuilder()
.startObject()
.field("grand_parent_values", 2L)
.startArray("parent")
@ -607,7 +822,7 @@ public class SimpleNestedIT extends ESIntegTestCase {
.endObject()).execute().actionGet();
// sum: 2
client().prepareIndex("test", "type1", Integer.toString(3)).setSource(jsonBuilder()
client().prepareIndex("test", "type1", "3").setSource(jsonBuilder()
.startObject()
.field("grand_parent_values", 3L)
.startArray("parent")
@ -722,25 +937,27 @@ public class SimpleNestedIT extends ESIntegTestCase {
assertThat(searchResponse.getHits().getHits()[2].getId(), equalTo("3"));
assertThat(searchResponse.getHits().getHits()[2].getSortValues()[0].toString(), equalTo("3"));
searchResponse = client().prepareSearch()
.setQuery(matchAllQuery())
.addSort(
SortBuilders.fieldSort("parent.child.child_values")
.setNestedPath("parent.child")
.setNestedFilter(QueryBuilders.termQuery("parent.filter", false))
.setNestedSort(SortBuilders.nestedSort("parent")
.setFilter(QueryBuilders.termQuery("parent.filter", false))
.setNestedSort(SortBuilders.nestedSort("parent.child")))
.sortMode(SortMode.MAX)
.order(SortOrder.ASC)
)
.execute().actionGet();
assertHitCount(searchResponse, 3);
assertThat(searchResponse.getHits().getHits().length, equalTo(3));
// TODO: If we expose ToChildBlockJoinQuery we can filter sort values based on a higher level nested objects
// assertThat(searchResponse.getHits().getHits()[0].getId(), equalTo("3"));
// assertThat(searchResponse.getHits().getHits()[0].sortValues()[0].toString(), equalTo("-3"));
// assertThat(searchResponse.getHits().getHits()[1].getId(), equalTo("2"));
// assertThat(searchResponse.getHits().getHits()[1].sortValues()[0].toString(), equalTo("-2"));
// assertThat(searchResponse.getHits().getHits()[2].getId(), equalTo("1"));
// assertThat(searchResponse.getHits().getHits()[2].sortValues()[0].toString(), equalTo("-1"));
assertThat(searchResponse.getHits().getHits()[0].getId(), equalTo("3"));
assertThat(searchResponse.getHits().getHits()[0].getSortValues()[0].toString(), equalTo("3"));
assertThat(searchResponse.getHits().getHits()[1].getId(), equalTo("2"));
assertThat(searchResponse.getHits().getHits()[1].getSortValues()[0].toString(), equalTo("4"));
assertThat(searchResponse.getHits().getHits()[2].getId(), equalTo("1"));
assertThat(searchResponse.getHits().getHits()[2].getSortValues()[0].toString(), equalTo("6"));
// Check if closest nested type is resolved
searchResponse = client().prepareSearch()

View File

@ -62,11 +62,11 @@ public class FieldSortBuilderTests extends AbstractSortTestCase<FieldSortBuilder
}
if (randomBoolean()) {
builder.setNestedFilter(randomNestedFilter());
}
if (randomBoolean()) {
builder.setNestedPath(randomAlphaOfLengthBetween(1, 10));
NestedSortBuilder nestedSort = SortBuilders.nestedSort(randomAlphaOfLengthBetween(1, 10));
if (randomBoolean()) {
nestedSort.setFilter(randomNestedFilter());
}
builder.setNestedSort(nestedSort);
}
return builder;
@ -77,16 +77,18 @@ public class FieldSortBuilderTests extends AbstractSortTestCase<FieldSortBuilder
FieldSortBuilder mutated = new FieldSortBuilder(original);
int parameter = randomIntBetween(0, 5);
switch (parameter) {
case 0:
mutated.setNestedPath(randomValueOtherThan(
original.getNestedPath(),
() -> randomAlphaOfLengthBetween(1, 10)));
case 0: {
NestedSortBuilder nestedSort = new NestedSortBuilder(mutated.getNestedSort());
nestedSort.setPath(randomValueOtherThan(nestedSort.getPath(), () -> randomAlphaOfLengthBetween(1, 10)));
mutated.setNestedSort(nestedSort);
break;
case 1:
mutated.setNestedFilter(randomValueOtherThan(
original.getNestedFilter(),
() -> randomNestedFilter()));
}
case 1: {
NestedSortBuilder nestedSort = new NestedSortBuilder(mutated.getNestedSort());
nestedSort.setFilter(randomValueOtherThan(nestedSort.getFilter(), AbstractSortTestCase::randomNestedFilter));
mutated.setNestedSort(nestedSort);
break;
}
case 2:
mutated.sortMode(randomValueOtherThan(original.sortMode(), () -> randomFrom(SortMode.values())));
break;

View File

@ -87,13 +87,16 @@ public class GeoDistanceSortBuilderTests extends AbstractSortTestCase<GeoDistanc
result.sortMode(randomValueOtherThan(SortMode.SUM, () -> randomFrom(SortMode.values())));
}
if (randomBoolean()) {
result.setNestedFilter(new MatchAllQueryBuilder());
}
if (randomBoolean()) {
result.setNestedPath(
randomValueOtherThan(
result.getNestedPath(),
() -> randomAlphaOfLengthBetween(1, 10)));
NestedSortBuilder nestedSort = SortBuilders.nestedSort(
randomValueOtherThan(
result.getNestedPath(),
() -> randomAlphaOfLengthBetween(1, 10)));
if (randomBoolean()) {
nestedSort.setFilter(new MatchAllQueryBuilder());
}
result.setNestedSort(nestedSort);
}
if (randomBoolean()) {
result.validation(randomValueOtherThan(result.validation(), () -> randomFrom(GeoValidationMethod.values())));
@ -157,16 +160,18 @@ public class GeoDistanceSortBuilderTests extends AbstractSortTestCase<GeoDistanc
Arrays.asList(SortMode.SUM, result.sortMode())::contains,
() -> randomFrom(SortMode.values())));
break;
case 6:
result.setNestedFilter(randomValueOtherThan(
original.getNestedFilter(),
() -> randomNestedFilter()));
case 6: {
NestedSortBuilder nestedSort = new NestedSortBuilder(result.getNestedSort());
nestedSort.setFilter(randomValueOtherThan(nestedSort.getFilter(), AbstractSortTestCase::randomNestedFilter));
result.setNestedSort(nestedSort);
break;
case 7:
result.setNestedPath(randomValueOtherThan(
result.getNestedPath(),
() -> randomAlphaOfLengthBetween(1, 10)));
}
case 7: {
NestedSortBuilder nestedSort = new NestedSortBuilder(result.getNestedSort());
nestedSort.setPath(randomValueOtherThan(nestedSort.getPath(), () -> randomAlphaOfLengthBetween(1, 10)));
result.setNestedSort(nestedSort);
break;
}
case 8:
result.validation(randomValueOtherThan(result.validation(), () -> randomFrom(GeoValidationMethod.values())));
break;
@ -217,11 +222,13 @@ public class GeoDistanceSortBuilderTests extends AbstractSortTestCase<GeoDistanc
" \"unit\" : \"m\",\n" +
" \"distance_type\" : \"arc\",\n" +
" \"mode\" : \"MAX\",\n" +
" \"nested_filter\" : {\n" +
" \"ids\" : {\n" +
" \"type\" : [ ],\n" +
" \"values\" : [ ],\n" +
" \"boost\" : 5.711116\n" +
" \"nested\" : {\n" +
" \"filter\" : {\n" +
" \"ids\" : {\n" +
" \"type\" : [ ],\n" +
" \"values\" : [ ],\n" +
" \"boost\" : 5.711116\n" +
" }\n" +
" }\n" +
" },\n" +
" \"validation_method\" : \"STRICT\"\n" +

View File

@ -59,10 +59,12 @@ public class ScriptSortBuilderTests extends AbstractSortTestCase<ScriptSortBuild
}
}
if (randomBoolean()) {
builder.setNestedFilter(randomNestedFilter());
}
if (randomBoolean()) {
builder.setNestedPath(randomAlphaOfLengthBetween(1, 10));
NestedSortBuilder nestedSort = SortBuilders.nestedSort(randomAlphaOfLengthBetween(1, 10));
if (randomBoolean()) {
nestedSort.setFilter(randomNestedFilter());
}
builder.setNestedSort(nestedSort);
}
return builder;
}
@ -83,8 +85,7 @@ public class ScriptSortBuilderTests extends AbstractSortTestCase<ScriptSortBuild
if (original.sortMode() != null && result.type() == ScriptSortType.NUMBER) {
result.sortMode(original.sortMode());
}
result.setNestedFilter(original.getNestedFilter());
result.setNestedPath(original.getNestedPath());
result.setNestedSort(original.getNestedSort());
return result;
}
result = new ScriptSortBuilder(original);
@ -108,14 +109,18 @@ public class ScriptSortBuilderTests extends AbstractSortTestCase<ScriptSortBuild
}
}
break;
case 2:
result.setNestedFilter(randomValueOtherThan(
original.getNestedFilter(),
() -> randomNestedFilter()));
case 2: {
NestedSortBuilder nestedSort = new NestedSortBuilder(original.getNestedSort());
nestedSort.setFilter(randomValueOtherThan(nestedSort.getFilter(), AbstractSortTestCase::randomNestedFilter));
result.setNestedSort(nestedSort);
break;
case 3:
result.setNestedPath(original.getNestedPath() + "_some_suffix");
}
case 3: {
NestedSortBuilder nestedSort = new NestedSortBuilder(original.getNestedSort());
nestedSort.setPath(nestedSort.getPath() + "_some_suffix");
result.setNestedSort(nestedSort);
break;
}
}
return result;
}
@ -178,8 +183,7 @@ public class ScriptSortBuilderTests extends AbstractSortTestCase<ScriptSortBuild
assertEquals(ScriptSortType.NUMBER, builder.type());
assertEquals(SortOrder.ASC, builder.order());
assertEquals(SortMode.MAX, builder.sortMode());
assertNull(builder.getNestedFilter());
assertNull(builder.getNestedPath());
assertNull(builder.getNestedSort());
}
public void testParseJson_simple() throws IOException {
@ -203,8 +207,7 @@ public class ScriptSortBuilderTests extends AbstractSortTestCase<ScriptSortBuild
assertEquals(ScriptSortType.NUMBER, builder.type());
assertEquals(SortOrder.ASC, builder.order());
assertEquals(SortMode.MAX, builder.sortMode());
assertNull(builder.getNestedFilter());
assertNull(builder.getNestedPath());
assertNull(builder.getNestedSort());
}
public void testParseBadFieldNameExceptions() throws IOException {

View File

@ -115,24 +115,35 @@ POST /_search
Elasticsearch also supports sorting by
fields that are inside one or more nested objects. The sorting by nested
field support has the following parameters on top of the already
existing sort options:
field support has a `nested` sort option with the following properties:
`nested_path`::
`path`::
Defines on which nested object to sort. The actual
sort field must be a direct field inside this nested object.
When sorting by nested field, this field is mandatory.
`nested_filter`::
`filter`::
A filter that the inner objects inside the nested path
should match with in order for its field values to be taken into account
by sorting. Common case is to repeat the query / filter inside the
nested filter or query. By default no `nested_filter` is active.
`nested`::
Same as top-level `nested` but applies to another nested path within the
current nested object.
===== Nested sorting example
[WARNING]
.Nested sort options before Elasticseach 6.1
============================================
The `nested_path` and `nested_filter` options have been deprecated in
favor of the options documented above.
============================================
===== Nested sorting examples
In the below example `offer` is a field of type `nested`.
The `nested_path` needs to be specified; otherwise, elasticsearch doesn't know on what nested level sort values need to be captured.
The nested `path` needs to be specified; otherwise, elasticsearch doesn't know on what nested level sort values need to be captured.
[source,js]
--------------------------------------------------
@ -146,9 +157,11 @@ POST /_search
"offer.price" : {
"mode" : "avg",
"order" : "asc",
"nested_path" : "offer",
"nested_filter" : {
"term" : { "offer.color" : "blue" }
"nested": {
"path": "offer",
"filter": {
"term" : { "offer.color" : "blue" }
}
}
}
}
@ -157,6 +170,53 @@ POST /_search
--------------------------------------------------
// CONSOLE
In the below example `parent` and `child` fields are of type `nested`.
The `nested_path` needs to be specified at each level; otherwise, elasticsearch doesn't know on what nested level sort values need to be captured.
[source,js]
--------------------------------------------------
POST /_search
{
"query": {
"nested": {
"path": "parent",
"query": {
"bool": {
"must": {"range": {"parent.age": {"gte": 21}}},
"filter": {
"nested": {
"path": "parent.child",
"query": {"match": {"parent.child.name": "matt"}}
}
}
}
}
}
},
"sort" : [
{
"parent.child.age" : {
"mode" : "min",
"order" : "asc",
"nested": {
"path": "parent",
"filter": {
"range": {"parent.age": {"gte": 21}}
},
"nested": {
"path": "parent.child",
"filter": {
"match": {"parent.child.name": "matt"}
}
}
}
}
}
]
}
--------------------------------------------------
// CONSOLE
Nested sorting is also supported when sorting by
scripts and sorting by geo distance.