Skip to content

Add support for nested sort. #2653

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

Merged
merged 1 commit into from
Jul 30, 2023
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
31 changes: 31 additions & 0 deletions src/main/asciidoc/reference/elasticsearch-misc.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -419,3 +419,34 @@ public class PersonCustomRepositoryImpl implements PersonCustomRepository {
<.> The parameters are passed in a `Map<String,Object>`
<.> Do the search in the same way as with the other query types.
====

[[elasticsearch.misc.nested-sort]]
== Nested sort
Spring Data Elasticsearch supports sorting within nested objects (https://www.elastic.co/guide/en/elasticsearch/reference/8.9/sort-search-results.html#nested-sorting)

The following example, taken from the `org.springframework.data.elasticsearch.core.query.sort.NestedSortIntegrationTests` class, shows how to define the nested sort.

====
[source,java]
----
var filter = StringQuery.builder("""
{ "term": {"movies.actors.sex": "m"} }
""").build();
var order = new org.springframework.data.elasticsearch.core.query.Order(Sort.Direction.DESC,
"movies.actors.yearOfBirth")
.withNested(
Nested.builder("movies")
.withNested(
Nested.builder("movies.actors")
.withFilter(filter)
.build())
.build());

var query = Query.findAll().addSort(Sort.by(order));

----
====

About the filter query: It is not possible to use a `CriteriaQuery` here, as this query would be converted into a Elasticsearch nested query which does not work in the filter context. So only `StringQuery` or `NativeQuery` can be used here. When using one of these, like the term query above, the Elasticsearch field names must be used, so take care, when these are redefined with the `@Field(name="...")` definition.

For the definition of the order path and the nested paths, the Java entity property names should be used.
2 changes: 2 additions & 0 deletions src/main/asciidoc/reference/elasticsearch-new.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
* Improved AOT runtime hints for Elasticsearch client library classes.
* Add Kotlin extensions and repository coroutine support.
* Introducing `VersionConflictException` class thrown in case thatElasticsearch reports an 409 error with a version conflict.
* Enable MultiField annotation on property getter
* Support nested sort option

[[new-features.5-1-0]]
== New in Spring Data Elasticsearch 5.1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,7 @@
import static org.springframework.data.elasticsearch.client.elc.TypeUtils.*;
import static org.springframework.util.CollectionUtils.*;

import co.elastic.clients.elasticsearch._types.Conflicts;
import co.elastic.clients.elasticsearch._types.ExpandWildcard;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.InlineScript;
import co.elastic.clients.elasticsearch._types.OpType;
import co.elastic.clients.elasticsearch._types.SortOptions;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.VersionType;
import co.elastic.clients.elasticsearch._types.WaitForActiveShardOptions;
import co.elastic.clients.elasticsearch._types.*;
import co.elastic.clients.elasticsearch._types.mapping.FieldType;
import co.elastic.clients.elasticsearch._types.mapping.RuntimeField;
import co.elastic.clients.elasticsearch._types.mapping.RuntimeFieldType;
Expand Down Expand Up @@ -71,6 +63,7 @@

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NotNull;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.RefreshPolicy;
Expand All @@ -89,6 +82,7 @@
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.data.elasticsearch.core.query.IndicesOptions;
import org.springframework.data.elasticsearch.core.reindex.ReindexRequest;
import org.springframework.data.elasticsearch.core.reindex.Remote;
import org.springframework.data.elasticsearch.core.script.Script;
Expand Down Expand Up @@ -269,36 +263,7 @@ public UpdateAliasesRequest indicesUpdateAliasesRequest(AliasActions aliasAction
List<Action> actions = new ArrayList<>();
aliasActions.getActions().forEach(aliasAction -> {

Action.Builder actionBuilder = new Action.Builder();

if (aliasAction instanceof AliasAction.Add add) {
AliasActionParameters parameters = add.getParameters();
actionBuilder.add(addActionBuilder -> {
addActionBuilder //
.indices(Arrays.asList(parameters.getIndices())) //
.isHidden(parameters.getHidden()) //
.isWriteIndex(parameters.getWriteIndex()) //
.routing(parameters.getRouting()) //
.indexRouting(parameters.getIndexRouting()) //
.searchRouting(parameters.getSearchRouting()); //

if (parameters.getAliases() != null) {
addActionBuilder.aliases(Arrays.asList(parameters.getAliases()));
}

Query filterQuery = parameters.getFilterQuery();

if (filterQuery != null) {
elasticsearchConverter.updateQuery(filterQuery, parameters.getFilterQueryClass());
co.elastic.clients.elasticsearch._types.query_dsl.Query esQuery = getQuery(filterQuery, null);
if (esQuery != null) {
addActionBuilder.filter(esQuery);

}
}
return addActionBuilder;
});
}
var actionBuilder = getBuilder(aliasAction);

if (aliasAction instanceof AliasAction.Remove remove) {
AliasActionParameters parameters = remove.getParameters();
Expand Down Expand Up @@ -327,6 +292,40 @@ public UpdateAliasesRequest indicesUpdateAliasesRequest(AliasActions aliasAction
return updateAliasRequestBuilder.build();
}

@NotNull
private Action.Builder getBuilder(AliasAction aliasAction) {
Action.Builder actionBuilder = new Action.Builder();

if (aliasAction instanceof AliasAction.Add add) {
AliasActionParameters parameters = add.getParameters();
actionBuilder.add(addActionBuilder -> {
addActionBuilder //
.indices(Arrays.asList(parameters.getIndices())) //
.isHidden(parameters.getHidden()) //
.isWriteIndex(parameters.getWriteIndex()) //
.routing(parameters.getRouting()) //
.indexRouting(parameters.getIndexRouting()) //
.searchRouting(parameters.getSearchRouting()); //

if (parameters.getAliases() != null) {
addActionBuilder.aliases(Arrays.asList(parameters.getAliases()));
}

Query filterQuery = parameters.getFilterQuery();

if (filterQuery != null) {
elasticsearchConverter.updateQuery(filterQuery, parameters.getFilterQueryClass());
co.elastic.clients.elasticsearch._types.query_dsl.Query esQuery = getQuery(filterQuery, null);
if (esQuery != null) {
addActionBuilder.filter(esQuery);
}
}
return addActionBuilder;
});
}
return actionBuilder;
}

public PutMappingRequest indicesPutMappingRequest(IndexCoordinates indexCoordinates, Document mapping) {

Assert.notNull(indexCoordinates, "indexCoordinates must not be null");
Expand Down Expand Up @@ -1502,59 +1501,88 @@ private List<SortOptions> getSortOptions(Sort sort, @Nullable ElasticsearchPersi
private SortOptions getSortOptions(Sort.Order order, @Nullable ElasticsearchPersistentEntity<?> persistentEntity) {
SortOrder sortOrder = order.getDirection().isDescending() ? SortOrder.Desc : SortOrder.Asc;

Order.Mode mode = Order.DEFAULT_MODE;
Order.Mode mode = order.getDirection().isAscending() ? Order.Mode.min : Order.Mode.max;
String unmappedType = null;
String missing = null;
NestedSortValue nestedSortValue = null;

if (SortOptions.Kind.Score.jsonValue().equals(order.getProperty())) {
return SortOptions.of(so -> so.score(s -> s.order(sortOrder)));
}

if (order instanceof Order o) {
mode = o.getMode();

if (o.getMode() != null) {
mode = o.getMode();
}
unmappedType = o.getUnmappedType();
missing = o.getMissing();
nestedSortValue = getNestedSort(o.getNested(), persistentEntity);
}
Order.Mode finalMode = mode;
String finalUnmappedType = unmappedType;
var finalNestedSortValue = nestedSortValue;

if (SortOptions.Kind.Score.jsonValue().equals(order.getProperty())) {
return SortOptions.of(so -> so.score(s -> s.order(sortOrder)));
} else {
ElasticsearchPersistentProperty property = (persistentEntity != null) //
? persistentEntity.getPersistentProperty(order.getProperty()) //
: null;
String fieldName = property != null ? property.getFieldName() : order.getProperty();

Order.Mode finalMode = mode;
if (order instanceof GeoDistanceOrder geoDistanceOrder) {

return SortOptions.of(so -> so //
.geoDistance(gd -> gd //
.field(fieldName) //
.location(loc -> loc.latlon(Queries.latLon(geoDistanceOrder.getGeoPoint()))) //
.distanceType(geoDistanceType(geoDistanceOrder.getDistanceType())).mode(sortMode(finalMode)) //
.order(sortOrder(geoDistanceOrder.getDirection())) //
.unit(distanceUnit(geoDistanceOrder.getUnit())) //
.ignoreUnmapped(geoDistanceOrder.getIgnoreUnmapped())));
} else {
String missing = (order.getNullHandling() == Sort.NullHandling.NULLS_FIRST) ? "_first"
: ((order.getNullHandling() == Sort.NullHandling.NULLS_LAST) ? "_last" : null);
String finalUnmappedType = unmappedType;
return SortOptions.of(so -> so //
.field(f -> {
f.field(fieldName) //
.order(sortOrder) //
.mode(sortMode(finalMode));

if (finalUnmappedType != null) {
FieldType fieldType = fieldType(finalUnmappedType);

if (fieldType != null) {
f.unmappedType(fieldType);
}
}
ElasticsearchPersistentProperty property = (persistentEntity != null) //
? persistentEntity.getPersistentProperty(order.getProperty()) //
: null;
String fieldName = property != null ? property.getFieldName() : order.getProperty();

if (missing != null) {
f.missing(fv -> fv //
.stringValue(missing));
}
return f;
}));
}
if (order instanceof GeoDistanceOrder geoDistanceOrder) {
return getSortOptions(geoDistanceOrder, fieldName, finalMode);
}

var finalMissing = missing != null ? missing
: (order.getNullHandling() == Sort.NullHandling.NULLS_FIRST) ? "_first"
: ((order.getNullHandling() == Sort.NullHandling.NULLS_LAST) ? "_last" : null);

return SortOptions.of(so -> so //
.field(f -> {
f.field(fieldName) //
.order(sortOrder) //
.mode(sortMode(finalMode));

if (finalUnmappedType != null) {
FieldType fieldType = fieldType(finalUnmappedType);

if (fieldType != null) {
f.unmappedType(fieldType);
}
}

if (finalMissing != null) {
f.missing(fv -> fv //
.stringValue(finalMissing));
}

if (finalNestedSortValue != null) {
f.nested(finalNestedSortValue);
}

return f;
}));
}

@Nullable
private NestedSortValue getNestedSort(@Nullable Order.Nested nested,
ElasticsearchPersistentEntity<?> persistentEntity) {
return (nested == null) ? null
: NestedSortValue.of(b -> b //
.path(elasticsearchConverter.updateFieldNames(nested.getPath(), persistentEntity)) //
.maxChildren(nested.getMaxChildren()) //
.nested(getNestedSort(nested.getNested(), persistentEntity)) //
.filter(getQuery(nested.getFilter(), persistentEntity.getType())));
}

private static SortOptions getSortOptions(GeoDistanceOrder geoDistanceOrder, String fieldName, Order.Mode finalMode) {
return SortOptions.of(so -> so //
.geoDistance(gd -> gd //
.field(fieldName) //
.location(loc -> loc.latlon(Queries.latLon(geoDistanceOrder.getGeoPoint()))) //
.distanceType(geoDistanceType(geoDistanceOrder.getDistanceType())).mode(sortMode(finalMode)) //
.order(sortOrder(geoDistanceOrder.getDirection())) //
.unit(distanceUnit(geoDistanceOrder.getUnit())) //
.ignoreUnmapped(geoDistanceOrder.getIgnoreUnmapped())));
}

@SuppressWarnings("DuplicatedCode")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,15 @@ default Document mapObject(@Nullable Object source) {
*/
void updateQuery(Query query, @Nullable Class<?> domainClass);

/**
* Replaces the parts in a dot separated property path with the field names of the respective properties. If no
* matching property is found, the original parts are rteturned.
*
* @param propertyPath the property path
* @param persistentEntity the replaced values.
* @return a String wihere the property names are replaced with field names
* @since 5.2
*/
public String updateFieldNames(String propertyPath, ElasticsearchPersistentEntity<?> persistentEntity);
// endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
Expand Down Expand Up @@ -55,16 +56,7 @@
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.SimplePropertyHandler;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator;
import org.springframework.data.mapping.model.EntityInstantiator;
import org.springframework.data.mapping.model.EntityInstantiators;
import org.springframework.data.mapping.model.ParameterValueProvider;
import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider;
import org.springframework.data.mapping.model.PropertyValueProvider;
import org.springframework.data.mapping.model.SpELContext;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider;
import org.springframework.data.mapping.model.*;
import org.springframework.data.util.TypeInformation;
import org.springframework.format.datetime.DateFormatterRegistrar;
import org.springframework.lang.Nullable;
Expand Down Expand Up @@ -1277,10 +1269,14 @@ private void updatePropertiesInFieldsAndSourceFilter(Query query, Class<?> domai
* @return an updated list of field names
*/
private List<String> updateFieldNames(List<String> fieldNames, ElasticsearchPersistentEntity<?> persistentEntity) {
return fieldNames.stream().map(fieldName -> {
ElasticsearchPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(fieldName);
return persistentProperty != null ? persistentProperty.getFieldName() : fieldName;
}).collect(Collectors.toList());
return fieldNames.stream().map(fieldName -> updateFieldName(persistentEntity, fieldName))
.collect(Collectors.toList());
}

@NotNull
private String updateFieldName(ElasticsearchPersistentEntity<?> persistentEntity, String fieldName) {
ElasticsearchPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(fieldName);
return persistentProperty != null ? persistentProperty.getFieldName() : fieldName;
}

private void updatePropertiesInCriteriaQuery(CriteriaQuery criteriaQuery, Class<?> domainClass) {
Expand Down Expand Up @@ -1384,6 +1380,32 @@ private void updatePropertiesInCriteria(Criteria criteria, ElasticsearchPersiste
}
}

@Override
public String updateFieldNames(String propertyPath, ElasticsearchPersistentEntity<?> persistentEntity) {

Assert.notNull(propertyPath, "propertyPath must not be null");
Assert.notNull(persistentEntity, "persistentEntity must not be null");

var properties = propertyPath.split("\\.", 2);

if (properties.length > 0) {
var propertyName = properties[0];
var fieldName = updateFieldName(persistentEntity, propertyName);

if (properties.length > 1) {
var persistentProperty = persistentEntity.getPersistentProperty(propertyName);
return (persistentProperty != null)
? fieldName + "." + updateFieldNames(properties[1], mappingContext.getPersistentEntity(persistentProperty))
: fieldName;
} else {
return fieldName;
}
} else {
return propertyPath;
}

}

// endregion

static class MapValueAccessor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public class GeoDistanceOrder extends Order {
private final Boolean ignoreUnmapped;

public GeoDistanceOrder(String property, GeoPoint geoPoint) {
this(property, geoPoint, Sort.Direction.ASC, DEFAULT_DISTANCE_TYPE, DEFAULT_MODE, DEFAULT_UNIT,
this(property, geoPoint, Sort.Direction.ASC, DEFAULT_DISTANCE_TYPE, null, DEFAULT_UNIT,
DEFAULT_IGNORE_UNMAPPED);
}

Expand Down
Loading