Skip to content

Commit bd71a93

Browse files
authored
Add support for nested sort.
Original Pull Request #2653 Closes #1783
1 parent 076f261 commit bd71a93

File tree

10 files changed

+706
-303
lines changed

10 files changed

+706
-303
lines changed

Diff for: src/main/asciidoc/reference/elasticsearch-misc.adoc

+31
Original file line numberDiff line numberDiff line change
@@ -419,3 +419,34 @@ public class PersonCustomRepositoryImpl implements PersonCustomRepository {
419419
<.> The parameters are passed in a `Map<String,Object>`
420420
<.> Do the search in the same way as with the other query types.
421421
====
422+
423+
[[elasticsearch.misc.nested-sort]]
424+
== Nested sort
425+
Spring Data Elasticsearch supports sorting within nested objects (https://www.elastic.co/guide/en/elasticsearch/reference/8.9/sort-search-results.html#nested-sorting)
426+
427+
The following example, taken from the `org.springframework.data.elasticsearch.core.query.sort.NestedSortIntegrationTests` class, shows how to define the nested sort.
428+
429+
====
430+
[source,java]
431+
----
432+
var filter = StringQuery.builder("""
433+
{ "term": {"movies.actors.sex": "m"} }
434+
""").build();
435+
var order = new org.springframework.data.elasticsearch.core.query.Order(Sort.Direction.DESC,
436+
"movies.actors.yearOfBirth")
437+
.withNested(
438+
Nested.builder("movies")
439+
.withNested(
440+
Nested.builder("movies.actors")
441+
.withFilter(filter)
442+
.build())
443+
.build());
444+
445+
var query = Query.findAll().addSort(Sort.by(order));
446+
447+
----
448+
====
449+
450+
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.
451+
452+
For the definition of the order path and the nested paths, the Java entity property names should be used.

Diff for: src/main/asciidoc/reference/elasticsearch-new.adoc

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
* Improved AOT runtime hints for Elasticsearch client library classes.
1010
* Add Kotlin extensions and repository coroutine support.
1111
* Introducing `VersionConflictException` class thrown in case thatElasticsearch reports an 409 error with a version conflict.
12+
* Enable MultiField annotation on property getter
13+
* Support nested sort option
1214

1315
[[new-features.5-1-0]]
1416
== New in Spring Data Elasticsearch 5.1

Diff for: src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java

+112-84
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,7 @@
1818
import static org.springframework.data.elasticsearch.client.elc.TypeUtils.*;
1919
import static org.springframework.util.CollectionUtils.*;
2020

21-
import co.elastic.clients.elasticsearch._types.Conflicts;
22-
import co.elastic.clients.elasticsearch._types.ExpandWildcard;
23-
import co.elastic.clients.elasticsearch._types.FieldValue;
24-
import co.elastic.clients.elasticsearch._types.InlineScript;
25-
import co.elastic.clients.elasticsearch._types.OpType;
26-
import co.elastic.clients.elasticsearch._types.SortOptions;
27-
import co.elastic.clients.elasticsearch._types.SortOrder;
28-
import co.elastic.clients.elasticsearch._types.VersionType;
29-
import co.elastic.clients.elasticsearch._types.WaitForActiveShardOptions;
21+
import co.elastic.clients.elasticsearch._types.*;
3022
import co.elastic.clients.elasticsearch._types.mapping.FieldType;
3123
import co.elastic.clients.elasticsearch._types.mapping.RuntimeField;
3224
import co.elastic.clients.elasticsearch._types.mapping.RuntimeFieldType;
@@ -71,6 +63,7 @@
7163

7264
import org.apache.commons.logging.Log;
7365
import org.apache.commons.logging.LogFactory;
66+
import org.jetbrains.annotations.NotNull;
7467
import org.springframework.dao.InvalidDataAccessApiUsageException;
7568
import org.springframework.data.domain.Sort;
7669
import org.springframework.data.elasticsearch.core.RefreshPolicy;
@@ -89,6 +82,7 @@
8982
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
9083
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
9184
import org.springframework.data.elasticsearch.core.query.*;
85+
import org.springframework.data.elasticsearch.core.query.IndicesOptions;
9286
import org.springframework.data.elasticsearch.core.reindex.ReindexRequest;
9387
import org.springframework.data.elasticsearch.core.reindex.Remote;
9488
import org.springframework.data.elasticsearch.core.script.Script;
@@ -269,36 +263,7 @@ public UpdateAliasesRequest indicesUpdateAliasesRequest(AliasActions aliasAction
269263
List<Action> actions = new ArrayList<>();
270264
aliasActions.getActions().forEach(aliasAction -> {
271265

272-
Action.Builder actionBuilder = new Action.Builder();
273-
274-
if (aliasAction instanceof AliasAction.Add add) {
275-
AliasActionParameters parameters = add.getParameters();
276-
actionBuilder.add(addActionBuilder -> {
277-
addActionBuilder //
278-
.indices(Arrays.asList(parameters.getIndices())) //
279-
.isHidden(parameters.getHidden()) //
280-
.isWriteIndex(parameters.getWriteIndex()) //
281-
.routing(parameters.getRouting()) //
282-
.indexRouting(parameters.getIndexRouting()) //
283-
.searchRouting(parameters.getSearchRouting()); //
284-
285-
if (parameters.getAliases() != null) {
286-
addActionBuilder.aliases(Arrays.asList(parameters.getAliases()));
287-
}
288-
289-
Query filterQuery = parameters.getFilterQuery();
290-
291-
if (filterQuery != null) {
292-
elasticsearchConverter.updateQuery(filterQuery, parameters.getFilterQueryClass());
293-
co.elastic.clients.elasticsearch._types.query_dsl.Query esQuery = getQuery(filterQuery, null);
294-
if (esQuery != null) {
295-
addActionBuilder.filter(esQuery);
296-
297-
}
298-
}
299-
return addActionBuilder;
300-
});
301-
}
266+
var actionBuilder = getBuilder(aliasAction);
302267

303268
if (aliasAction instanceof AliasAction.Remove remove) {
304269
AliasActionParameters parameters = remove.getParameters();
@@ -327,6 +292,40 @@ public UpdateAliasesRequest indicesUpdateAliasesRequest(AliasActions aliasAction
327292
return updateAliasRequestBuilder.build();
328293
}
329294

295+
@NotNull
296+
private Action.Builder getBuilder(AliasAction aliasAction) {
297+
Action.Builder actionBuilder = new Action.Builder();
298+
299+
if (aliasAction instanceof AliasAction.Add add) {
300+
AliasActionParameters parameters = add.getParameters();
301+
actionBuilder.add(addActionBuilder -> {
302+
addActionBuilder //
303+
.indices(Arrays.asList(parameters.getIndices())) //
304+
.isHidden(parameters.getHidden()) //
305+
.isWriteIndex(parameters.getWriteIndex()) //
306+
.routing(parameters.getRouting()) //
307+
.indexRouting(parameters.getIndexRouting()) //
308+
.searchRouting(parameters.getSearchRouting()); //
309+
310+
if (parameters.getAliases() != null) {
311+
addActionBuilder.aliases(Arrays.asList(parameters.getAliases()));
312+
}
313+
314+
Query filterQuery = parameters.getFilterQuery();
315+
316+
if (filterQuery != null) {
317+
elasticsearchConverter.updateQuery(filterQuery, parameters.getFilterQueryClass());
318+
co.elastic.clients.elasticsearch._types.query_dsl.Query esQuery = getQuery(filterQuery, null);
319+
if (esQuery != null) {
320+
addActionBuilder.filter(esQuery);
321+
}
322+
}
323+
return addActionBuilder;
324+
});
325+
}
326+
return actionBuilder;
327+
}
328+
330329
public PutMappingRequest indicesPutMappingRequest(IndexCoordinates indexCoordinates, Document mapping) {
331330

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

1505-
Order.Mode mode = Order.DEFAULT_MODE;
1504+
Order.Mode mode = order.getDirection().isAscending() ? Order.Mode.min : Order.Mode.max;
15061505
String unmappedType = null;
1506+
String missing = null;
1507+
NestedSortValue nestedSortValue = null;
1508+
1509+
if (SortOptions.Kind.Score.jsonValue().equals(order.getProperty())) {
1510+
return SortOptions.of(so -> so.score(s -> s.order(sortOrder)));
1511+
}
15071512

15081513
if (order instanceof Order o) {
1509-
mode = o.getMode();
1514+
1515+
if (o.getMode() != null) {
1516+
mode = o.getMode();
1517+
}
15101518
unmappedType = o.getUnmappedType();
1519+
missing = o.getMissing();
1520+
nestedSortValue = getNestedSort(o.getNested(), persistentEntity);
15111521
}
1522+
Order.Mode finalMode = mode;
1523+
String finalUnmappedType = unmappedType;
1524+
var finalNestedSortValue = nestedSortValue;
15121525

1513-
if (SortOptions.Kind.Score.jsonValue().equals(order.getProperty())) {
1514-
return SortOptions.of(so -> so.score(s -> s.order(sortOrder)));
1515-
} else {
1516-
ElasticsearchPersistentProperty property = (persistentEntity != null) //
1517-
? persistentEntity.getPersistentProperty(order.getProperty()) //
1518-
: null;
1519-
String fieldName = property != null ? property.getFieldName() : order.getProperty();
1520-
1521-
Order.Mode finalMode = mode;
1522-
if (order instanceof GeoDistanceOrder geoDistanceOrder) {
1523-
1524-
return SortOptions.of(so -> so //
1525-
.geoDistance(gd -> gd //
1526-
.field(fieldName) //
1527-
.location(loc -> loc.latlon(Queries.latLon(geoDistanceOrder.getGeoPoint()))) //
1528-
.distanceType(geoDistanceType(geoDistanceOrder.getDistanceType())).mode(sortMode(finalMode)) //
1529-
.order(sortOrder(geoDistanceOrder.getDirection())) //
1530-
.unit(distanceUnit(geoDistanceOrder.getUnit())) //
1531-
.ignoreUnmapped(geoDistanceOrder.getIgnoreUnmapped())));
1532-
} else {
1533-
String missing = (order.getNullHandling() == Sort.NullHandling.NULLS_FIRST) ? "_first"
1534-
: ((order.getNullHandling() == Sort.NullHandling.NULLS_LAST) ? "_last" : null);
1535-
String finalUnmappedType = unmappedType;
1536-
return SortOptions.of(so -> so //
1537-
.field(f -> {
1538-
f.field(fieldName) //
1539-
.order(sortOrder) //
1540-
.mode(sortMode(finalMode));
1541-
1542-
if (finalUnmappedType != null) {
1543-
FieldType fieldType = fieldType(finalUnmappedType);
1544-
1545-
if (fieldType != null) {
1546-
f.unmappedType(fieldType);
1547-
}
1548-
}
1526+
ElasticsearchPersistentProperty property = (persistentEntity != null) //
1527+
? persistentEntity.getPersistentProperty(order.getProperty()) //
1528+
: null;
1529+
String fieldName = property != null ? property.getFieldName() : order.getProperty();
15491530

1550-
if (missing != null) {
1551-
f.missing(fv -> fv //
1552-
.stringValue(missing));
1553-
}
1554-
return f;
1555-
}));
1556-
}
1531+
if (order instanceof GeoDistanceOrder geoDistanceOrder) {
1532+
return getSortOptions(geoDistanceOrder, fieldName, finalMode);
15571533
}
1534+
1535+
var finalMissing = missing != null ? missing
1536+
: (order.getNullHandling() == Sort.NullHandling.NULLS_FIRST) ? "_first"
1537+
: ((order.getNullHandling() == Sort.NullHandling.NULLS_LAST) ? "_last" : null);
1538+
1539+
return SortOptions.of(so -> so //
1540+
.field(f -> {
1541+
f.field(fieldName) //
1542+
.order(sortOrder) //
1543+
.mode(sortMode(finalMode));
1544+
1545+
if (finalUnmappedType != null) {
1546+
FieldType fieldType = fieldType(finalUnmappedType);
1547+
1548+
if (fieldType != null) {
1549+
f.unmappedType(fieldType);
1550+
}
1551+
}
1552+
1553+
if (finalMissing != null) {
1554+
f.missing(fv -> fv //
1555+
.stringValue(finalMissing));
1556+
}
1557+
1558+
if (finalNestedSortValue != null) {
1559+
f.nested(finalNestedSortValue);
1560+
}
1561+
1562+
return f;
1563+
}));
1564+
}
1565+
1566+
@Nullable
1567+
private NestedSortValue getNestedSort(@Nullable Order.Nested nested,
1568+
ElasticsearchPersistentEntity<?> persistentEntity) {
1569+
return (nested == null) ? null
1570+
: NestedSortValue.of(b -> b //
1571+
.path(elasticsearchConverter.updateFieldNames(nested.getPath(), persistentEntity)) //
1572+
.maxChildren(nested.getMaxChildren()) //
1573+
.nested(getNestedSort(nested.getNested(), persistentEntity)) //
1574+
.filter(getQuery(nested.getFilter(), persistentEntity.getType())));
1575+
}
1576+
1577+
private static SortOptions getSortOptions(GeoDistanceOrder geoDistanceOrder, String fieldName, Order.Mode finalMode) {
1578+
return SortOptions.of(so -> so //
1579+
.geoDistance(gd -> gd //
1580+
.field(fieldName) //
1581+
.location(loc -> loc.latlon(Queries.latLon(geoDistanceOrder.getGeoPoint()))) //
1582+
.distanceType(geoDistanceType(geoDistanceOrder.getDistanceType())).mode(sortMode(finalMode)) //
1583+
.order(sortOrder(geoDistanceOrder.getDirection())) //
1584+
.unit(distanceUnit(geoDistanceOrder.getUnit())) //
1585+
.ignoreUnmapped(geoDistanceOrder.getIgnoreUnmapped())));
15581586
}
15591587

15601588
@SuppressWarnings("DuplicatedCode")

Diff for: src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchConverter.java

+10
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,15 @@ default Document mapObject(@Nullable Object source) {
101101
*/
102102
void updateQuery(Query query, @Nullable Class<?> domainClass);
103103

104+
/**
105+
* Replaces the parts in a dot separated property path with the field names of the respective properties. If no
106+
* matching property is found, the original parts are rteturned.
107+
*
108+
* @param propertyPath the property path
109+
* @param persistentEntity the replaced values.
110+
* @return a String wihere the property names are replaced with field names
111+
* @since 5.2
112+
*/
113+
public String updateFieldNames(String propertyPath, ElasticsearchPersistentEntity<?> persistentEntity);
104114
// endregion
105115
}

Diff for: src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java

+36-14
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import org.apache.commons.logging.Log;
2525
import org.apache.commons.logging.LogFactory;
26+
import org.jetbrains.annotations.NotNull;
2627
import org.springframework.beans.BeansException;
2728
import org.springframework.beans.factory.InitializingBean;
2829
import org.springframework.context.ApplicationContext;
@@ -55,16 +56,7 @@
5556
import org.springframework.data.mapping.PersistentPropertyAccessor;
5657
import org.springframework.data.mapping.SimplePropertyHandler;
5758
import org.springframework.data.mapping.context.MappingContext;
58-
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
59-
import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator;
60-
import org.springframework.data.mapping.model.EntityInstantiator;
61-
import org.springframework.data.mapping.model.EntityInstantiators;
62-
import org.springframework.data.mapping.model.ParameterValueProvider;
63-
import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider;
64-
import org.springframework.data.mapping.model.PropertyValueProvider;
65-
import org.springframework.data.mapping.model.SpELContext;
66-
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
67-
import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider;
59+
import org.springframework.data.mapping.model.*;
6860
import org.springframework.data.util.TypeInformation;
6961
import org.springframework.format.datetime.DateFormatterRegistrar;
7062
import org.springframework.lang.Nullable;
@@ -1277,10 +1269,14 @@ private void updatePropertiesInFieldsAndSourceFilter(Query query, Class<?> domai
12771269
* @return an updated list of field names
12781270
*/
12791271
private List<String> updateFieldNames(List<String> fieldNames, ElasticsearchPersistentEntity<?> persistentEntity) {
1280-
return fieldNames.stream().map(fieldName -> {
1281-
ElasticsearchPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(fieldName);
1282-
return persistentProperty != null ? persistentProperty.getFieldName() : fieldName;
1283-
}).collect(Collectors.toList());
1272+
return fieldNames.stream().map(fieldName -> updateFieldName(persistentEntity, fieldName))
1273+
.collect(Collectors.toList());
1274+
}
1275+
1276+
@NotNull
1277+
private String updateFieldName(ElasticsearchPersistentEntity<?> persistentEntity, String fieldName) {
1278+
ElasticsearchPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(fieldName);
1279+
return persistentProperty != null ? persistentProperty.getFieldName() : fieldName;
12841280
}
12851281

12861282
private void updatePropertiesInCriteriaQuery(CriteriaQuery criteriaQuery, Class<?> domainClass) {
@@ -1384,6 +1380,32 @@ private void updatePropertiesInCriteria(Criteria criteria, ElasticsearchPersiste
13841380
}
13851381
}
13861382

1383+
@Override
1384+
public String updateFieldNames(String propertyPath, ElasticsearchPersistentEntity<?> persistentEntity) {
1385+
1386+
Assert.notNull(propertyPath, "propertyPath must not be null");
1387+
Assert.notNull(persistentEntity, "persistentEntity must not be null");
1388+
1389+
var properties = propertyPath.split("\\.", 2);
1390+
1391+
if (properties.length > 0) {
1392+
var propertyName = properties[0];
1393+
var fieldName = updateFieldName(persistentEntity, propertyName);
1394+
1395+
if (properties.length > 1) {
1396+
var persistentProperty = persistentEntity.getPersistentProperty(propertyName);
1397+
return (persistentProperty != null)
1398+
? fieldName + "." + updateFieldNames(properties[1], mappingContext.getPersistentEntity(persistentProperty))
1399+
: fieldName;
1400+
} else {
1401+
return fieldName;
1402+
}
1403+
} else {
1404+
return propertyPath;
1405+
}
1406+
1407+
}
1408+
13871409
// endregion
13881410

13891411
static class MapValueAccessor {

Diff for: src/main/java/org/springframework/data/elasticsearch/core/query/GeoDistanceOrder.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public class GeoDistanceOrder extends Order {
3737
private final Boolean ignoreUnmapped;
3838

3939
public GeoDistanceOrder(String property, GeoPoint geoPoint) {
40-
this(property, geoPoint, Sort.Direction.ASC, DEFAULT_DISTANCE_TYPE, DEFAULT_MODE, DEFAULT_UNIT,
40+
this(property, geoPoint, Sort.Direction.ASC, DEFAULT_DISTANCE_TYPE, null, DEFAULT_UNIT,
4141
DEFAULT_IGNORE_UNMAPPED);
4242
}
4343

0 commit comments

Comments
 (0)