Add support for nested sort.

Original Pull Request #2653
Closes #1783
This commit is contained in:
Peter-Josef Meisch 2023-07-30 16:29:52 +00:00 committed by GitHub
parent 076f261a7d
commit bd71a9311c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 709 additions and 306 deletions

View File

@ -419,3 +419,34 @@ public class PersonCustomRepositoryImpl implements PersonCustomRepository {
<.> The parameters are passed in a `Map<String,Object>` <.> The parameters are passed in a `Map<String,Object>`
<.> Do the search in the same way as with the other query types. <.> 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.

View File

@ -9,6 +9,8 @@
* Improved AOT runtime hints for Elasticsearch client library classes. * Improved AOT runtime hints for Elasticsearch client library classes.
* Add Kotlin extensions and repository coroutine support. * Add Kotlin extensions and repository coroutine support.
* Introducing `VersionConflictException` class thrown in case thatElasticsearch reports an 409 error with a version conflict. * 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-features.5-1-0]]
== New in Spring Data Elasticsearch 5.1 == New in Spring Data Elasticsearch 5.1

View File

@ -18,15 +18,7 @@ package org.springframework.data.elasticsearch.client.elc;
import static org.springframework.data.elasticsearch.client.elc.TypeUtils.*; import static org.springframework.data.elasticsearch.client.elc.TypeUtils.*;
import static org.springframework.util.CollectionUtils.*; import static org.springframework.util.CollectionUtils.*;
import co.elastic.clients.elasticsearch._types.Conflicts; import co.elastic.clients.elasticsearch._types.*;
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.mapping.FieldType; import co.elastic.clients.elasticsearch._types.mapping.FieldType;
import co.elastic.clients.elasticsearch._types.mapping.RuntimeField; import co.elastic.clients.elasticsearch._types.mapping.RuntimeField;
import co.elastic.clients.elasticsearch._types.mapping.RuntimeFieldType; import co.elastic.clients.elasticsearch._types.mapping.RuntimeFieldType;
@ -71,6 +63,7 @@ import java.util.stream.Collectors;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NotNull;
import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.RefreshPolicy; import org.springframework.data.elasticsearch.core.RefreshPolicy;
@ -89,6 +82,7 @@ import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersiste
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.*; 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.ReindexRequest;
import org.springframework.data.elasticsearch.core.reindex.Remote; import org.springframework.data.elasticsearch.core.reindex.Remote;
import org.springframework.data.elasticsearch.core.script.Script; import org.springframework.data.elasticsearch.core.script.Script;
@ -269,36 +263,7 @@ class RequestConverter {
List<Action> actions = new ArrayList<>(); List<Action> actions = new ArrayList<>();
aliasActions.getActions().forEach(aliasAction -> { aliasActions.getActions().forEach(aliasAction -> {
Action.Builder actionBuilder = new Action.Builder(); var actionBuilder = getBuilder(aliasAction);
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;
});
}
if (aliasAction instanceof AliasAction.Remove remove) { if (aliasAction instanceof AliasAction.Remove remove) {
AliasActionParameters parameters = remove.getParameters(); AliasActionParameters parameters = remove.getParameters();
@ -327,6 +292,40 @@ class RequestConverter {
return updateAliasRequestBuilder.build(); 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) { public PutMappingRequest indicesPutMappingRequest(IndexCoordinates indexCoordinates, Document mapping) {
Assert.notNull(indexCoordinates, "indexCoordinates must not be null"); Assert.notNull(indexCoordinates, "indexCoordinates must not be null");
@ -1502,37 +1501,41 @@ class RequestConverter {
private SortOptions getSortOptions(Sort.Order order, @Nullable ElasticsearchPersistentEntity<?> persistentEntity) { private SortOptions getSortOptions(Sort.Order order, @Nullable ElasticsearchPersistentEntity<?> persistentEntity) {
SortOrder sortOrder = order.getDirection().isDescending() ? SortOrder.Desc : SortOrder.Asc; 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 unmappedType = null;
String missing = null;
if (order instanceof Order o) { NestedSortValue nestedSortValue = null;
mode = o.getMode();
unmappedType = o.getUnmappedType();
}
if (SortOptions.Kind.Score.jsonValue().equals(order.getProperty())) { if (SortOptions.Kind.Score.jsonValue().equals(order.getProperty())) {
return SortOptions.of(so -> so.score(s -> s.order(sortOrder))); return SortOptions.of(so -> so.score(s -> s.order(sortOrder)));
} else { }
if (order instanceof Order o) {
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;
ElasticsearchPersistentProperty property = (persistentEntity != null) // ElasticsearchPersistentProperty property = (persistentEntity != null) //
? persistentEntity.getPersistentProperty(order.getProperty()) // ? persistentEntity.getPersistentProperty(order.getProperty()) //
: null; : null;
String fieldName = property != null ? property.getFieldName() : order.getProperty(); String fieldName = property != null ? property.getFieldName() : order.getProperty();
Order.Mode finalMode = mode;
if (order instanceof GeoDistanceOrder geoDistanceOrder) { if (order instanceof GeoDistanceOrder geoDistanceOrder) {
return getSortOptions(geoDistanceOrder, fieldName, finalMode);
}
return SortOptions.of(so -> so // var finalMissing = missing != null ? missing
.geoDistance(gd -> gd // : (order.getNullHandling() == Sort.NullHandling.NULLS_FIRST) ? "_first"
.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); : ((order.getNullHandling() == Sort.NullHandling.NULLS_LAST) ? "_last" : null);
String finalUnmappedType = unmappedType;
return SortOptions.of(so -> so // return SortOptions.of(so -> so //
.field(f -> { .field(f -> {
f.field(fieldName) // f.field(fieldName) //
@ -1547,14 +1550,39 @@ class RequestConverter {
} }
} }
if (missing != null) { if (finalMissing != null) {
f.missing(fv -> fv // f.missing(fv -> fv //
.stringValue(missing)); .stringValue(finalMissing));
} }
if (finalNestedSortValue != null) {
f.nested(finalNestedSortValue);
}
return f; 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") @SuppressWarnings("DuplicatedCode")

View File

@ -101,5 +101,15 @@ public interface ElasticsearchConverter
*/ */
void updateQuery(Query query, @Nullable Class<?> domainClass); 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 // endregion
} }

View File

@ -23,6 +23,7 @@ import java.util.stream.Collectors;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
@ -55,16 +56,7 @@ import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.SimplePropertyHandler; import org.springframework.data.mapping.SimplePropertyHandler;
import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.*;
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.util.TypeInformation; import org.springframework.data.util.TypeInformation;
import org.springframework.format.datetime.DateFormatterRegistrar; import org.springframework.format.datetime.DateFormatterRegistrar;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@ -1277,10 +1269,14 @@ public class MappingElasticsearchConverter
* @return an updated list of field names * @return an updated list of field names
*/ */
private List<String> updateFieldNames(List<String> fieldNames, ElasticsearchPersistentEntity<?> persistentEntity) { private List<String> updateFieldNames(List<String> fieldNames, ElasticsearchPersistentEntity<?> persistentEntity) {
return fieldNames.stream().map(fieldName -> { return fieldNames.stream().map(fieldName -> updateFieldName(persistentEntity, fieldName))
.collect(Collectors.toList());
}
@NotNull
private String updateFieldName(ElasticsearchPersistentEntity<?> persistentEntity, String fieldName) {
ElasticsearchPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(fieldName); ElasticsearchPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(fieldName);
return persistentProperty != null ? persistentProperty.getFieldName() : fieldName; return persistentProperty != null ? persistentProperty.getFieldName() : fieldName;
}).collect(Collectors.toList());
} }
private void updatePropertiesInCriteriaQuery(CriteriaQuery criteriaQuery, Class<?> domainClass) { private void updatePropertiesInCriteriaQuery(CriteriaQuery criteriaQuery, Class<?> domainClass) {
@ -1384,6 +1380,32 @@ public class MappingElasticsearchConverter
} }
} }
@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 // endregion
static class MapValueAccessor { static class MapValueAccessor {

View File

@ -37,7 +37,7 @@ public class GeoDistanceOrder extends Order {
private final Boolean ignoreUnmapped; private final Boolean ignoreUnmapped;
public GeoDistanceOrder(String property, GeoPoint geoPoint) { 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); DEFAULT_IGNORE_UNMAPPED);
} }

View File

@ -17,6 +17,10 @@ package org.springframework.data.elasticsearch.core.query;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import java.util.function.BiFunction;
import java.util.function.Function;
/** /**
* Extends the {@link Sort.Order} with properties that can be set on Elasticsearch order options. * Extends the {@link Sort.Order} with properties that can be set on Elasticsearch order options.
@ -26,46 +30,61 @@ import org.springframework.lang.Nullable;
*/ */
public class Order extends Sort.Order { public class Order extends Sort.Order {
public static final Mode DEFAULT_MODE = Mode.min;
public static final Sort.NullHandling DEFAULT_NULL_HANDLING = Sort.NullHandling.NATIVE; public static final Sort.NullHandling DEFAULT_NULL_HANDLING = Sort.NullHandling.NATIVE;
protected final Mode mode; @Nullable protected final Mode mode;
@Nullable protected final String unmappedType; @Nullable protected final String unmappedType;
@Nullable protected final String missing;
@Nullable protected final Nested nested;
public Order(Sort.Direction direction, String property) { public Order(Sort.Direction direction, String property) {
this(direction, property, DEFAULT_MODE, null); this(direction, property, (Mode) null, null);
} }
public Order(Sort.Direction direction, String property, Mode mode) { public Order(Sort.Direction direction, String property, @Nullable Mode mode) {
this(direction, property, DEFAULT_NULL_HANDLING, mode, null); this(direction, property, DEFAULT_NULL_HANDLING, mode, null);
} }
public Order(Sort.Direction direction, String property, @Nullable String unmappedType) { public Order(Sort.Direction direction, String property, @Nullable String unmappedType) {
this(direction, property, DEFAULT_NULL_HANDLING, DEFAULT_MODE, unmappedType); this(direction, property, DEFAULT_NULL_HANDLING, null, unmappedType);
} }
public Order(Sort.Direction direction, String property, Mode mode, @Nullable String unmappedType) { public Order(Sort.Direction direction, String property, @Nullable Mode mode, @Nullable String unmappedType) {
this(direction, property, DEFAULT_NULL_HANDLING, mode, unmappedType); this(direction, property, DEFAULT_NULL_HANDLING, mode, unmappedType);
} }
public Order(Sort.Direction direction, String property, Sort.NullHandling nullHandlingHint) { public Order(Sort.Direction direction, String property, Sort.NullHandling nullHandlingHint) {
this(direction, property, nullHandlingHint, DEFAULT_MODE, null); this(direction, property, nullHandlingHint, null, null);
} }
public Order(Sort.Direction direction, String property, Sort.NullHandling nullHandlingHint, Mode mode) { public Order(Sort.Direction direction, String property, Sort.NullHandling nullHandlingHint, @Nullable Mode mode) {
this(direction, property, nullHandlingHint, mode, null); this(direction, property, nullHandlingHint, mode, null);
} }
public Order(Sort.Direction direction, String property, Sort.NullHandling nullHandlingHint, public Order(Sort.Direction direction, String property, Sort.NullHandling nullHandlingHint,
@Nullable String unmappedType) { @Nullable String unmappedType) {
this(direction, property, nullHandlingHint, DEFAULT_MODE, unmappedType); this(direction, property, nullHandlingHint, null, unmappedType);
} }
public Order(Sort.Direction direction, String property, Sort.NullHandling nullHandlingHint, Mode mode, public Order(Sort.Direction direction, String property, Sort.NullHandling nullHandlingHint, @Nullable Mode mode,
@Nullable String unmappedType) { @Nullable String unmappedType) {
this(direction, property, nullHandlingHint, null, unmappedType, null);
}
public Order(Sort.Direction direction, String property, Sort.NullHandling nullHandlingHint, @Nullable Mode mode,
@Nullable String unmappedType, @Nullable String missing) {
this(direction, property, nullHandlingHint, mode, unmappedType, missing, null);
}
public Order(Sort.Direction direction, String property, Sort.NullHandling nullHandlingHint, @Nullable Mode mode,
@Nullable String unmappedType, @Nullable String missing, @Nullable Nested nested) {
super(direction, property, nullHandlingHint); super(direction, property, nullHandlingHint);
this.mode = mode; this.mode = mode;
this.unmappedType = unmappedType; this.unmappedType = unmappedType;
this.missing = missing;
this.nested = nested;
} }
@Nullable @Nullable
@ -75,33 +94,143 @@ public class Order extends Sort.Order {
@Override @Override
public Sort.Order with(Sort.Direction direction) { public Sort.Order with(Sort.Direction direction) {
return new Order(direction, getProperty(), getNullHandling(), mode, unmappedType); return new Order(direction, getProperty(), getNullHandling(), mode, unmappedType, missing, nested);
} }
@Override @Override
public Sort.Order withProperty(String property) { public Sort.Order withProperty(String property) {
return new Order(getDirection(), property, getNullHandling(), mode, unmappedType); return new Order(getDirection(), property, getNullHandling(), mode, unmappedType, missing, nested);
} }
@Override @Override
public Sort.Order with(Sort.NullHandling nullHandling) { public Sort.Order with(Sort.NullHandling nullHandling) {
return new Order(getDirection(), getProperty(), nullHandling, getMode(), unmappedType); return new Order(getDirection(), getProperty(), nullHandling, getMode(), unmappedType, missing, nested);
} }
public Order withUnmappedType(@Nullable String unmappedType) { public Order withUnmappedType(@Nullable String unmappedType) {
return new Order(getDirection(), getProperty(), getNullHandling(), getMode(), unmappedType); return new Order(getDirection(), getProperty(), getNullHandling(), getMode(), unmappedType, missing, nested);
} }
public Order with(Mode mode) { public Order with(@Nullable Mode mode) {
return new Order(getDirection(), getProperty(), getNullHandling(), mode, unmappedType); return new Order(getDirection(), getProperty(), getNullHandling(), mode, unmappedType, missing, nested);
} }
public Order withMissing(@Nullable String missing) {
return new Order(getDirection(), getProperty(), getNullHandling(), mode, unmappedType, missing, nested);
}
public Order withNested(@Nullable Nested nested) {
return new Order(getDirection(), getProperty(), getNullHandling(), mode, unmappedType, missing, nested);
}
@Nullable
public Mode getMode() { public Mode getMode() {
return mode; return mode;
} }
@Nullable
public String getMissing() {
return missing;
}
@Nullable
public Nested getNested() {
return nested;
}
public enum Mode { public enum Mode {
min, max, median, avg min, max, median, avg
} }
public static class Nested {
private String path;
@Nullable private Query filter;
@Nullable private Integer maxChildren = null;
@Nullable private Nested nested;
public static Nested of(String path, Function<Nested.Builder, Nested.Builder> builderFunction) {
Assert.notNull(path, "path must not be null");
Assert.notNull(builderFunction, "builderFunction must not be null");
return builderFunction.apply(builder(path)).build();
}
public Nested(String path, @Nullable Query filter, @Nullable Integer maxChildren, @Nullable Nested nested) {
Assert.notNull(path, "path must not be null");
this.path = path;
this.filter = filter;
this.maxChildren = maxChildren;
this.nested = nested;
}
public String getPath() {
return path;
}
@Nullable
public Query getFilter() {
return filter;
}
@Nullable
public Integer getMaxChildren() {
return maxChildren;
}
@Nullable
public Nested getNested() {
return nested;
}
public static Builder builder(String path) {
return new Builder(path);
}
public static class Builder {
private String path;
@Nullable private Query filter = null;
@Nullable private Integer maxChildren = null;
@Nullable private Nested nested = null;
public Builder(String path) {
Assert.notNull(path, "path must not be null");
this.path = path;
}
/**
* Sets the filter query for a nested sort.<br/>
* Note: This cannot be a {@link CriteriaQuery}, as that would be sent as a nested query within the filter,
* use a {@link org.springframework.data.elasticsearch.client.elc.NativeQuery} or {@link StringQuery} instead.
* @param filter the filter to set
* @return this builder
* @throws IllegalArgumentException when a {@link CriteriaQuery} is passed.
*/
public Builder withFilter(@Nullable Query filter) {
if (filter instanceof CriteriaQuery) {
throw new IllegalArgumentException("Cannot use a CriteriaQuery in a nested sort filter.");
}
this.filter = filter;
return this;
}
public Builder withMaxChildren(@Nullable Integer maxChildren) {
this.maxChildren = maxChildren;
return this;
}
public Builder withNested(@Nullable Nested nested) {
this.nested = nested;
return this;
}
public Nested build() {
return new Nested(path, filter, maxChildren, nested);
}
}
}
} }

View File

@ -25,16 +25,7 @@ import java.time.LocalTime;
import java.time.OffsetTime; import java.time.OffsetTime;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.intellij.lang.annotations.Language; import org.intellij.lang.annotations.Language;
@ -69,6 +60,7 @@ import org.springframework.data.elasticsearch.core.geo.GeoJsonMultiPolygon;
import org.springframework.data.elasticsearch.core.geo.GeoJsonPoint; import org.springframework.data.elasticsearch.core.geo.GeoJsonPoint;
import org.springframework.data.elasticsearch.core.geo.GeoJsonPolygon; import org.springframework.data.elasticsearch.core.geo.GeoJsonPolygon;
import org.springframework.data.elasticsearch.core.geo.GeoPoint; import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.PropertyValueConverter; import org.springframework.data.elasticsearch.core.mapping.PropertyValueConverter;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.elasticsearch.core.query.Criteria; import org.springframework.data.elasticsearch.core.query.Criteria;
@ -94,7 +86,13 @@ import org.springframework.util.Assert;
*/ */
public class MappingElasticsearchConverterUnitTests { public class MappingElasticsearchConverterUnitTests {
static final String JSON_STRING = "{\"_class\":\"org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverterUnitTests$Car\",\"name\":\"Grat\",\"model\":\"Ford\"}"; static final String JSON_STRING = """
{
"_class": "org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverterUnitTests$Car",
"name": "Grat",
"model": "Ford"
}
""";
static final String CAR_MODEL = "Ford"; static final String CAR_MODEL = "Ford";
static final String CAR_NAME = "Grat"; static final String CAR_NAME = "Grat";
MappingElasticsearchConverter mappingElasticsearchConverter; MappingElasticsearchConverter mappingElasticsearchConverter;
@ -233,18 +231,15 @@ public class MappingElasticsearchConverterUnitTests {
@Test @Test
public void shouldFailToInitializeGivenMappingContextIsNull() { public void shouldFailToInitializeGivenMappingContextIsNull() {
// given
assertThatThrownBy(() -> new MappingElasticsearchConverter(null)).isInstanceOf(IllegalArgumentException.class); assertThatThrownBy(() -> new MappingElasticsearchConverter(null)).isInstanceOf(IllegalArgumentException.class);
} }
@Test @Test
public void shouldReturnMappingContextWithWhichItWasInitialized() { public void shouldReturnMappingContextWithWhichItWasInitialized() {
// given
SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext(); SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext();
MappingElasticsearchConverter converter = new MappingElasticsearchConverter(mappingContext); MappingElasticsearchConverter converter = new MappingElasticsearchConverter(mappingContext);
// then
assertThat(converter.getMappingContext()).isNotNull(); assertThat(converter.getMappingContext()).isNotNull();
assertThat(converter.getMappingContext()).isSameAs(mappingContext); assertThat(converter.getMappingContext()).isSameAs(mappingContext);
} }
@ -252,35 +247,29 @@ public class MappingElasticsearchConverterUnitTests {
@Test @Test
public void shouldReturnDefaultConversionService() { public void shouldReturnDefaultConversionService() {
// given
MappingElasticsearchConverter converter = new MappingElasticsearchConverter( MappingElasticsearchConverter converter = new MappingElasticsearchConverter(
new SimpleElasticsearchMappingContext()); new SimpleElasticsearchMappingContext());
// when
ConversionService conversionService = converter.getConversionService(); ConversionService conversionService = converter.getConversionService();
// then
assertThat(conversionService).isNotNull(); assertThat(conversionService).isNotNull();
} }
@Test // DATAES-530 @Test // DATAES-530
public void shouldMapObjectToJsonString() { public void shouldMapObjectToJsonString() throws JSONException {
Car car = new Car(); Car car = new Car();
car.setModel(CAR_MODEL); car.setModel(CAR_MODEL);
car.setName(CAR_NAME); car.setName(CAR_NAME);
String jsonResult = mappingElasticsearchConverter.mapObject(car).toJson(); String jsonResult = mappingElasticsearchConverter.mapObject(car).toJson();
assertThat(jsonResult).isEqualTo(JSON_STRING); assertEquals(jsonResult, JSON_STRING, false);
} }
@Test // DATAES-530 @Test // DATAES-530
public void shouldReadJsonStringToObject() { public void shouldReadJsonStringToObject() {
// Given
// When
Car result = mappingElasticsearchConverter.read(Car.class, Document.parse(JSON_STRING)); Car result = mappingElasticsearchConverter.read(Car.class, Document.parse(JSON_STRING));
// Then
assertThat(result).isNotNull(); assertThat(result).isNotNull();
assertThat(result.getName()).isEqualTo(CAR_NAME); assertThat(result.getName()).isEqualTo(CAR_NAME);
assertThat(result.getModel()).isEqualTo(CAR_MODEL); assertThat(result.getModel()).isEqualTo(CAR_MODEL);
@ -288,7 +277,6 @@ public class MappingElasticsearchConverterUnitTests {
@Test // DATAES-530 @Test // DATAES-530
public void shouldMapGeoPointElasticsearchNames() throws JSONException { public void shouldMapGeoPointElasticsearchNames() throws JSONException {
// given
double lon = 5; double lon = 5;
double lat = 48; double lat = 48;
Point point = new Point(lon, lat); Point point = new Point(lon, lat);
@ -327,17 +315,14 @@ public class MappingElasticsearchConverterUnitTests {
@Test // DATAES-530 @Test // DATAES-530
public void ignoresReadOnlyProperties() { public void ignoresReadOnlyProperties() {
// given
Sample sample = new Sample(); Sample sample = new Sample();
sample.setReadOnly("readOnly"); sample.setReadOnly("readOnly");
sample.setProperty("property"); sample.setProperty("property");
sample.setJavaTransientProperty("javaTransient"); sample.setJavaTransientProperty("javaTransient");
sample.setAnnotatedTransientProperty("transient"); sample.setAnnotatedTransientProperty("transient");
// when
String result = mappingElasticsearchConverter.mapObject(sample).toJson(); String result = mappingElasticsearchConverter.mapObject(sample).toJson();
// then
assertThat(result).contains("\"property\""); assertThat(result).contains("\"property\"");
assertThat(result).contains("\"javaTransient\""); assertThat(result).contains("\"javaTransient\"");
@ -638,13 +623,15 @@ public class MappingElasticsearchConverterUnitTests {
person.birthDate = LocalDate.of(2000, 8, 22); person.birthDate = LocalDate.of(2000, 8, 22);
person.gender = Gender.MAN; person.gender = Gender.MAN;
String expected = '{' + // String expected = """
" \"id\": \"4711\"," + // {
" \"first-name\": \"John\"," + // "id": "4711",
" \"last-name\": \"Doe\"," + // "first-name": "John",
" \"birth-date\": \"22.08.2000\"," + // "last-name": "Doe",
" \"gender\": \"MAN\"" + // "birth-date": "22.08.2000",
'}'; "gender": "MAN"
}
""";
Document document = Document.create(); Document document = Document.create();
mappingElasticsearchConverter.write(person, document); mappingElasticsearchConverter.write(person, document);
String json = document.toJson(); String json = document.toJson();
@ -961,30 +948,59 @@ public class MappingElasticsearchConverterUnitTests {
@Nested @Nested
class RangeTests { class RangeTests {
static final String JSON = "{" static final String JSON = """
+ "\"_class\":\"org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverterUnitTests$RangeTests$RangeEntity\"," {
+ "\"integerRange\":{\"gt\":\"1\",\"lt\":\"10\"}," // "_class": "org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverterUnitTests$RangeTests$RangeEntity",
+ "\"floatRange\":{\"gte\":\"1.2\",\"lte\":\"2.5\"}," // "integerRange": {
+ "\"longRange\":{\"gt\":\"2\",\"lte\":\"5\"}," // "gt": "1",
+ "\"doubleRange\":{\"gte\":\"3.2\",\"lt\":\"7.4\"}," // "lt": "10"
+ "\"dateRange\":{\"gte\":\"1970-01-01T00:00:00.000Z\",\"lte\":\"1970-01-01T01:00:00.000Z\"}," // },
+ "\"localDateRange\":{\"gte\":\"2021-07-06\"}," // "floatRange": {
+ "\"localTimeRange\":{\"gte\":\"00:30:00.000\",\"lt\":\"02:30:00.000\"}," // "gte": "1.2",
+ "\"localDateTimeRange\":{\"gt\":\"2021-01-01T00:30:00.000\",\"lt\":\"2021-01-01T02:30:00.000\"}," // "lte": "2.5"
+ "\"offsetTimeRange\":{\"gte\":\"00:30:00.000+02:00\",\"lt\":\"02:30:00.000+02:00\"}," // },
+ "\"zonedDateTimeRange\":{\"gte\":\"2021-01-01T00:30:00.000+02:00\",\"lte\":\"2021-01-01T00:30:00.000+02:00\"}," // "longRange": {
+ "\"nullRange\":null}"; "gt": "2",
"lte": "5"
},
"doubleRange": {
"gte": "3.2",
"lt": "7.4"
},
"dateRange": {
"gte": "1970-01-01T00:00:00.000Z",
"lte": "1970-01-01T01:00:00.000Z"
},
"localDateRange": {
"gte": "2021-07-06"
},
"localTimeRange": {
"gte": "00:30:00.000",
"lt": "02:30:00.000"
},
"localDateTimeRange": {
"gt": "2021-01-01T00:30:00.000",
"lt": "2021-01-01T02:30:00.000"
},
"offsetTimeRange": {
"gte": "00:30:00.000+02:00",
"lt": "02:30:00.000+02:00"
},
"zonedDateTimeRange": {
"gte": "2021-01-01T00:30:00.000+02:00",
"lte": "2021-01-01T00:30:00.000+02:00"
},
"nullRange": null
}
""";
@Test @Test
public void shouldReadRanges() throws JSONException { public void shouldReadRanges() throws JSONException {
// given
Document source = Document.parse(JSON); Document source = Document.parse(JSON);
// when
RangeEntity entity = mappingElasticsearchConverter.read(RangeEntity.class, source); RangeEntity entity = mappingElasticsearchConverter.read(RangeEntity.class, source);
// then
assertThat(entity) // assertThat(entity) //
.isNotNull() // .isNotNull() //
.satisfies(e -> { .satisfies(e -> {
@ -1010,7 +1026,6 @@ public class MappingElasticsearchConverterUnitTests {
@Test @Test
public void shouldWriteRanges() throws JSONException { public void shouldWriteRanges() throws JSONException {
// given
Document source = Document.parse(JSON); Document source = Document.parse(JSON);
RangeEntity entity = new RangeEntity(); RangeEntity entity = new RangeEntity();
entity.setIntegerRange(Range.open(1, 10)); entity.setIntegerRange(Range.open(1, 10));
@ -1028,10 +1043,8 @@ public class MappingElasticsearchConverterUnitTests {
Range.just(ZonedDateTime.of(LocalDate.of(2021, 1, 1), LocalTime.of(0, 30), ZoneOffset.ofHours(2)))); Range.just(ZonedDateTime.of(LocalDate.of(2021, 1, 1), LocalTime.of(0, 30), ZoneOffset.ofHours(2))));
entity.setNullRange(null); entity.setNullRange(null);
// when
Document document = mappingElasticsearchConverter.mapObject(entity); Document document = mappingElasticsearchConverter.mapObject(entity);
// then
assertThat(document).isEqualTo(source); assertThat(document).isEqualTo(source);
} }
@ -1564,7 +1577,6 @@ public class MappingElasticsearchConverterUnitTests {
Document source = Document.parse(json); Document source = Document.parse(json);
// when
EntityWithCustomValueConverters entity = mappingElasticsearchConverter.read(EntityWithCustomValueConverters.class, EntityWithCustomValueConverters entity = mappingElasticsearchConverter.read(EntityWithCustomValueConverters.class,
source); source);
@ -2038,6 +2050,17 @@ public class MappingElasticsearchConverterUnitTests {
assertThat(entity.getDottedField()).isEqualTo("dotted field"); assertThat(entity.getDottedField()).isEqualTo("dotted field");
} }
@Test // #1784
@DisplayName("should map property path to field names")
void shouldMapPropertyPathToFieldNames() {
var propertyPath = "level1Entries.level2Entries.keyWord";
ElasticsearchPersistentEntity<?> persistentEntity = mappingElasticsearchConverter.getMappingContext().getPersistentEntity(NestedEntity.class);
var mappedNames = mappingElasticsearchConverter.updateFieldNames(propertyPath, persistentEntity);
assertThat(mappedNames).isEqualTo("level-one.level-two.key-word");
}
// region entities // region entities
public static class Sample { public static class Sample {
@Nullable public @ReadOnlyProperty String readOnly; @Nullable public @ReadOnlyProperty String readOnly;
@ -2288,149 +2311,20 @@ public class MappingElasticsearchConverterUnitTests {
} }
interface Inventory { interface Inventory {
String label();
String getLabel();
} }
static class Gun implements Inventory { record Gun(String label, int shotsPerMagazine) implements Inventory {
final String label;
final int shotsPerMagazine;
public Gun(@Nullable String label, int shotsPerMagazine) {
this.label = label;
this.shotsPerMagazine = shotsPerMagazine;
} }
@Override record Grenade(String label) implements Inventory {
public String getLabel() {
return label;
}
public int getShotsPerMagazine() {
return shotsPerMagazine;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Gun gun = (Gun) o;
if (shotsPerMagazine != gun.shotsPerMagazine)
return false;
return label.equals(gun.label);
}
@Override
public int hashCode() {
int result = label.hashCode();
result = 31 * result + shotsPerMagazine;
return result;
}
}
static class Grenade implements Inventory {
final String label;
public Grenade(String label) {
this.label = label;
}
@Override
public String getLabel() {
return label;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof Grenade grenade))
return false;
return label.equals(grenade.label);
}
@Override
public int hashCode() {
return label.hashCode();
}
} }
@TypeAlias("rifle") @TypeAlias("rifle")
static class Rifle implements Inventory { record Rifle(String label, double weight, int maxShotsPerMagazine) implements Inventory {
final String label;
final double weight;
final int maxShotsPerMagazine;
public Rifle(String label, double weight, int maxShotsPerMagazine) {
this.label = label;
this.weight = weight;
this.maxShotsPerMagazine = maxShotsPerMagazine;
} }
@Override record ShotGun(String label) implements Inventory {
public String getLabel() {
return label;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof Rifle rifle))
return false;
if (Double.compare(rifle.weight, weight) != 0)
return false;
if (maxShotsPerMagazine != rifle.maxShotsPerMagazine)
return false;
return label.equals(rifle.label);
}
@Override
public int hashCode() {
int result;
long temp;
result = label.hashCode();
temp = Double.doubleToLongBits(weight);
result = 31 * result + (int) (temp ^ (temp >>> 32));
result = 31 * result + maxShotsPerMagazine;
return result;
}
}
static class ShotGun implements Inventory {
private final String label;
public ShotGun(String label) {
this.label = label;
}
@Override
public String getLabel() {
return label;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof ShotGun shotGun))
return false;
return label.equals(shotGun.label);
}
@Override
public int hashCode() {
return label.hashCode();
}
} }
static class Address { static class Address {
@ -2599,7 +2493,7 @@ public class MappingElasticsearchConverterUnitTests {
public Map<String, Object> convert(ShotGun source) { public Map<String, Object> convert(ShotGun source) {
LinkedHashMap<String, Object> target = new LinkedHashMap<>(); LinkedHashMap<String, Object> target = new LinkedHashMap<>();
target.put("model", source.getLabel()); target.put("model", source.label());
target.put("_class", ShotGun.class.getName()); target.put("_class", ShotGun.class.getName());
return target; return target;
} }
@ -3267,6 +3161,54 @@ public class MappingElasticsearchConverterUnitTests {
} }
} }
static class NestedEntity {
@Id
@Nullable private String id;
@Field(type = FieldType.Nested, name = "level-one") private List<Level1> level1Entries;
@Nullable
public String getId() {
return id;
}
public void setId(@Nullable String id) {
this.id = id;
}
public List<Level1> getLevel1Entries() {
return level1Entries;
}
public void setLevel1Entries(List<Level1> level1Entries) {
this.level1Entries = level1Entries;
}
static class Level1 {
@Field(type = FieldType.Nested, name = "level-two") private List<Level2> level2Entries;
public List<Level2> getLevel2Entries() {
return level2Entries;
}
public void setLevel2Entries(List<Level2> level2Entries) {
this.level2Entries = level2Entries;
}
}
static class Level2 {
@Field(type = FieldType.Keyword, name = "key-word") private String keyWord;
public String getKeyWord() {
return keyWord;
}
public void setKeyWord(String keyWord) {
this.keyWord = keyWord;
}
}
}
// endregion // endregion
private static String reverse(Object o) { private static String reverse(Object o) {

View File

@ -0,0 +1,39 @@
/*
* Copyright 023 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.data.elasticsearch.core.query.sort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.test.context.ContextConfiguration;
/**
* @author Peter-Josef Meisch
*/
@ContextConfiguration(classes = { NestedSortELCIntegrationTests.Config.class })
public class NestedSortELCIntegrationTests extends NestedSortIntegrationTests {
@Configuration
@Import({ ElasticsearchTemplateConfiguration.class })
static class Config {
@Bean
IndexNameProvider indexNameProvider() {
return new IndexNameProvider("nested-sort");
}
}
}

View File

@ -0,0 +1,200 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.data.elasticsearch.core.query.sort;
import static org.assertj.core.api.Assertions.*;
import java.util.List;
import java.util.function.Function;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.IndexOperations;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.Order.Nested;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.lang.Nullable;
/**
* Integration tests for nested sorts.
*
* @author Peter-Josef Meisch
*/
@SpringIntegrationTest
public abstract class NestedSortIntegrationTests {
@Autowired ElasticsearchOperations operations;
@Autowired IndexNameProvider indexNameProvider;
@Nullable IndexOperations indexOperations;
private final Actor marlonBrando = new Actor("Marlon Brando", "m", 1924);
private final Actor robertDuvall = new Actor("RobertDuvall", "m", 1931);
private final Actor jackNicholson = new Actor("Jack Nicholson", "m", 1937);
private final Actor alPacino = new Actor("Al Pacino", "m", 1940);
private final Actor ronalLeeErmey = new Actor("Ronal Lee Ermey", "m", 1944);
private final Actor dianeKeaton = new Actor("Diane Keaton", "f", 1946);
private final Actor shelleyDuval = new Actor("Shelley Duval", "f", 1949);
private final Actor matthewModine = new Actor("Matthew Modine", "m", 1959);
private final Movie theGodfather = new Movie("The Godfather", 1972, List.of(alPacino, dianeKeaton));
private final Movie apocalypseNow = new Movie("Apocalypse Now", 1979, List.of(marlonBrando, robertDuvall));
private final Movie theShining = new Movie("The Shining", 1980, List.of(jackNicholson, shelleyDuval));
private final Movie fullMetalJacket = new Movie("Full Metal Jacket", 1987, List.of(matthewModine, ronalLeeErmey));
private final Director stanleyKubrik = new Director("1", "Stanley Kubrik", 1928,
List.of(fullMetalJacket, theShining));
private final Director francisFordCoppola = new Director("2", "Francis Ford Coppola", 1939,
List.of(apocalypseNow, theGodfather));
@BeforeEach
void setUp() {
indexNameProvider.increment();
indexOperations = operations.indexOps(Director.class);
indexOperations.createWithMapping();
operations.save(francisFordCoppola, stanleyKubrik);
}
@Test
@Order(Integer.MAX_VALUE)
void cleanup() {
operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + '*')).delete();
}
@Test // #1784
@DisplayName("should sort directors by year of birth of actor in their movies ascending")
void shouldSortDirectorsByYearOfBirthOfActorInTheirMoviesAscending() {
var order = new org.springframework.data.elasticsearch.core.query.Order(Sort.Direction.ASC,
"movies.actors.yearOfBirth") //
.withNested(Nested.of("movies", //
b -> b.withNested(Nested.of("movies.actors", Function.identity()))));
var query = Query.findAll().addSort(Sort.by(order));
var searchHits = operations.search(query, Director.class);
assertThat(searchHits.getTotalHits()).isEqualTo(2);
assertThat(searchHits.getSearchHit(0).getContent().id).isEqualTo(francisFordCoppola.id);
var sortValues = searchHits.getSearchHit(0).getSortValues();
assertThat(sortValues).hasSize(1);
assertThat(sortValues.get(0)).isEqualTo("1924");
assertThat(searchHits.getSearchHit(1).getContent().id).isEqualTo(stanleyKubrik.id);
sortValues = searchHits.getSearchHit(1).getSortValues();
assertThat(sortValues).hasSize(1);
assertThat(sortValues.get(0)).isEqualTo("1937");
}
@Test // #1784
@DisplayName("should sort directors by year of birth of actor in their movies descending")
void shouldSortDirectorsByYearOfBirthOfActorInTheirMoviesDescending() {
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") //
.build()) //
.build());
var query = Query.findAll().addSort(Sort.by(order));
var searchHits = operations.search(query, Director.class);
assertThat(searchHits.getTotalHits()).isEqualTo(2);
assertThat(searchHits.getSearchHit(0).getContent().id).isEqualTo(stanleyKubrik.id);
var sortValues = searchHits.getSearchHit(0).getSortValues();
assertThat(sortValues).hasSize(1);
assertThat(sortValues.get(0)).isEqualTo("1959");
assertThat(searchHits.getSearchHit(1).getContent().id).isEqualTo(francisFordCoppola.id);
sortValues = searchHits.getSearchHit(1).getSortValues();
assertThat(sortValues).hasSize(1);
assertThat(sortValues.get(0)).isEqualTo("1946");
}
@Test // #1784
@DisplayName("should sort directors by year of birth of male actor in their movies descending")
void shouldSortDirectorsByYearOfBirthOfMaleActorInTheirMoviesDescending() {
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));
var searchHits = operations.search(query, Director.class);
assertThat(searchHits.getTotalHits()).isEqualTo(2);
assertThat(searchHits.getSearchHit(0).getContent().id).isEqualTo(stanleyKubrik.id);
var sortValues = searchHits.getSearchHit(0).getSortValues();
assertThat(sortValues).hasSize(1);
assertThat(sortValues.get(0)).isEqualTo("1959");
assertThat(searchHits.getSearchHit(1).getContent().id).isEqualTo(francisFordCoppola.id);
sortValues = searchHits.getSearchHit(1).getSortValues();
assertThat(sortValues).hasSize(1);
assertThat(sortValues.get(0)).isEqualTo("1940");
}
@Document(indexName = "#{@indexNameProvider.indexName()}")
record Director( //
@Nullable @Id String id, //
@Field(type = FieldType.Text) String name, //
@Field(type = FieldType.Integer) Integer yearOfBirth, //
@Field(type = FieldType.Nested) List<Movie> movies //
) {
}
record Movie( //
@Field(type = FieldType.Text) String title, //
@Field(type = FieldType.Integer) Integer year, //
@Field(type = FieldType.Nested) List<Actor> actors //
) {
}
record Actor( //
@Field(type = FieldType.Text) String name, //
@Field(type = FieldType.Keyword) String sex, //
@Field(type = FieldType.Integer) Integer yearOfBirth //
) {
}
}