mirror of
https://github.com/spring-projects/spring-data-elasticsearch.git
synced 2025-06-13 07:32:11 +00:00
parent
076f261a7d
commit
bd71a9311c
@ -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.
|
||||
|
@ -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
|
||||
|
@ -18,15 +18,7 @@ package org.springframework.data.elasticsearch.client.elc;
|
||||
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;
|
||||
@ -71,6 +63,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
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;
|
||||
@ -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.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;
|
||||
@ -269,36 +263,7 @@ class RequestConverter {
|
||||
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();
|
||||
@ -327,6 +292,40 @@ class RequestConverter {
|
||||
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");
|
||||
@ -1502,59 +1501,88 @@ class RequestConverter {
|
||||
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;
|
||||
|
||||
if (order instanceof Order o) {
|
||||
mode = o.getMode();
|
||||
unmappedType = o.getUnmappedType();
|
||||
}
|
||||
String missing = null;
|
||||
NestedSortValue nestedSortValue = null;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing != null) {
|
||||
f.missing(fv -> fv //
|
||||
.stringValue(missing));
|
||||
}
|
||||
return f;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
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) //
|
||||
? persistentEntity.getPersistentProperty(order.getProperty()) //
|
||||
: null;
|
||||
String fieldName = property != null ? property.getFieldName() : order.getProperty();
|
||||
|
||||
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")
|
||||
|
@ -101,5 +101,15 @@ public interface ElasticsearchConverter
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
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;
|
||||
@ -55,16 +56,7 @@ import org.springframework.data.mapping.Parameter;
|
||||
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;
|
||||
@ -1277,10 +1269,14 @@ public class MappingElasticsearchConverter
|
||||
* @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) {
|
||||
@ -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
|
||||
|
||||
static class MapValueAccessor {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,10 @@ package org.springframework.data.elasticsearch.core.query;
|
||||
|
||||
import org.springframework.data.domain.Sort;
|
||||
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.
|
||||
@ -26,46 +30,61 @@ import org.springframework.lang.Nullable;
|
||||
*/
|
||||
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;
|
||||
|
||||
protected final Mode mode;
|
||||
@Nullable protected final Mode mode;
|
||||
@Nullable protected final String unmappedType;
|
||||
|
||||
@Nullable protected final String missing;
|
||||
|
||||
@Nullable protected final Nested nested;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public Order(Sort.Direction direction, String property, Sort.NullHandling nullHandlingHint,
|
||||
@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) {
|
||||
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);
|
||||
this.mode = mode;
|
||||
this.unmappedType = unmappedType;
|
||||
this.missing = missing;
|
||||
this.nested = nested;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@ -75,33 +94,143 @@ public class Order extends Sort.Order {
|
||||
|
||||
@Override
|
||||
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
|
||||
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
|
||||
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) {
|
||||
return new Order(getDirection(), getProperty(), getNullHandling(), getMode(), unmappedType);
|
||||
return new Order(getDirection(), getProperty(), getNullHandling(), getMode(), unmappedType, missing, nested);
|
||||
}
|
||||
|
||||
public Order with(Mode mode) {
|
||||
return new Order(getDirection(), getProperty(), getNullHandling(), mode, unmappedType);
|
||||
public Order with(@Nullable Mode mode) {
|
||||
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() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getMissing() {
|
||||
return missing;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Nested getNested() {
|
||||
return nested;
|
||||
}
|
||||
|
||||
public enum Mode {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,16 +25,7 @@ import java.time.LocalTime;
|
||||
import java.time.OffsetTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
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.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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.GeoJsonPolygon;
|
||||
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.SimpleElasticsearchMappingContext;
|
||||
import org.springframework.data.elasticsearch.core.query.Criteria;
|
||||
@ -94,7 +86,13 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
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_NAME = "Grat";
|
||||
MappingElasticsearchConverter mappingElasticsearchConverter;
|
||||
@ -233,18 +231,15 @@ public class MappingElasticsearchConverterUnitTests {
|
||||
@Test
|
||||
public void shouldFailToInitializeGivenMappingContextIsNull() {
|
||||
|
||||
// given
|
||||
assertThatThrownBy(() -> new MappingElasticsearchConverter(null)).isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnMappingContextWithWhichItWasInitialized() {
|
||||
|
||||
// given
|
||||
SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext();
|
||||
MappingElasticsearchConverter converter = new MappingElasticsearchConverter(mappingContext);
|
||||
|
||||
// then
|
||||
assertThat(converter.getMappingContext()).isNotNull();
|
||||
assertThat(converter.getMappingContext()).isSameAs(mappingContext);
|
||||
}
|
||||
@ -252,35 +247,29 @@ public class MappingElasticsearchConverterUnitTests {
|
||||
@Test
|
||||
public void shouldReturnDefaultConversionService() {
|
||||
|
||||
// given
|
||||
MappingElasticsearchConverter converter = new MappingElasticsearchConverter(
|
||||
new SimpleElasticsearchMappingContext());
|
||||
|
||||
// when
|
||||
ConversionService conversionService = converter.getConversionService();
|
||||
|
||||
// then
|
||||
assertThat(conversionService).isNotNull();
|
||||
}
|
||||
|
||||
@Test // DATAES-530
|
||||
public void shouldMapObjectToJsonString() {
|
||||
public void shouldMapObjectToJsonString() throws JSONException {
|
||||
Car car = new Car();
|
||||
car.setModel(CAR_MODEL);
|
||||
car.setName(CAR_NAME);
|
||||
String jsonResult = mappingElasticsearchConverter.mapObject(car).toJson();
|
||||
|
||||
assertThat(jsonResult).isEqualTo(JSON_STRING);
|
||||
assertEquals(jsonResult, JSON_STRING, false);
|
||||
}
|
||||
|
||||
@Test // DATAES-530
|
||||
public void shouldReadJsonStringToObject() {
|
||||
// Given
|
||||
|
||||
// When
|
||||
Car result = mappingElasticsearchConverter.read(Car.class, Document.parse(JSON_STRING));
|
||||
|
||||
// Then
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getName()).isEqualTo(CAR_NAME);
|
||||
assertThat(result.getModel()).isEqualTo(CAR_MODEL);
|
||||
@ -288,7 +277,6 @@ public class MappingElasticsearchConverterUnitTests {
|
||||
|
||||
@Test // DATAES-530
|
||||
public void shouldMapGeoPointElasticsearchNames() throws JSONException {
|
||||
// given
|
||||
double lon = 5;
|
||||
double lat = 48;
|
||||
Point point = new Point(lon, lat);
|
||||
@ -327,17 +315,14 @@ public class MappingElasticsearchConverterUnitTests {
|
||||
@Test // DATAES-530
|
||||
public void ignoresReadOnlyProperties() {
|
||||
|
||||
// given
|
||||
Sample sample = new Sample();
|
||||
sample.setReadOnly("readOnly");
|
||||
sample.setProperty("property");
|
||||
sample.setJavaTransientProperty("javaTransient");
|
||||
sample.setAnnotatedTransientProperty("transient");
|
||||
|
||||
// when
|
||||
String result = mappingElasticsearchConverter.mapObject(sample).toJson();
|
||||
|
||||
// then
|
||||
assertThat(result).contains("\"property\"");
|
||||
assertThat(result).contains("\"javaTransient\"");
|
||||
|
||||
@ -638,13 +623,15 @@ public class MappingElasticsearchConverterUnitTests {
|
||||
person.birthDate = LocalDate.of(2000, 8, 22);
|
||||
person.gender = Gender.MAN;
|
||||
|
||||
String expected = '{' + //
|
||||
" \"id\": \"4711\"," + //
|
||||
" \"first-name\": \"John\"," + //
|
||||
" \"last-name\": \"Doe\"," + //
|
||||
" \"birth-date\": \"22.08.2000\"," + //
|
||||
" \"gender\": \"MAN\"" + //
|
||||
'}';
|
||||
String expected = """
|
||||
{
|
||||
"id": "4711",
|
||||
"first-name": "John",
|
||||
"last-name": "Doe",
|
||||
"birth-date": "22.08.2000",
|
||||
"gender": "MAN"
|
||||
}
|
||||
""";
|
||||
Document document = Document.create();
|
||||
mappingElasticsearchConverter.write(person, document);
|
||||
String json = document.toJson();
|
||||
@ -961,30 +948,59 @@ public class MappingElasticsearchConverterUnitTests {
|
||||
@Nested
|
||||
class RangeTests {
|
||||
|
||||
static final String JSON = "{"
|
||||
+ "\"_class\":\"org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverterUnitTests$RangeTests$RangeEntity\","
|
||||
+ "\"integerRange\":{\"gt\":\"1\",\"lt\":\"10\"}," //
|
||||
+ "\"floatRange\":{\"gte\":\"1.2\",\"lte\":\"2.5\"}," //
|
||||
+ "\"longRange\":{\"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}";
|
||||
static final String JSON = """
|
||||
{
|
||||
"_class": "org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverterUnitTests$RangeTests$RangeEntity",
|
||||
"integerRange": {
|
||||
"gt": "1",
|
||||
"lt": "10"
|
||||
},
|
||||
"floatRange": {
|
||||
"gte": "1.2",
|
||||
"lte": "2.5"
|
||||
},
|
||||
"longRange": {
|
||||
"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
|
||||
public void shouldReadRanges() throws JSONException {
|
||||
|
||||
// given
|
||||
Document source = Document.parse(JSON);
|
||||
|
||||
// when
|
||||
RangeEntity entity = mappingElasticsearchConverter.read(RangeEntity.class, source);
|
||||
|
||||
// then
|
||||
assertThat(entity) //
|
||||
.isNotNull() //
|
||||
.satisfies(e -> {
|
||||
@ -1010,7 +1026,6 @@ public class MappingElasticsearchConverterUnitTests {
|
||||
@Test
|
||||
public void shouldWriteRanges() throws JSONException {
|
||||
|
||||
// given
|
||||
Document source = Document.parse(JSON);
|
||||
RangeEntity entity = new RangeEntity();
|
||||
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))));
|
||||
entity.setNullRange(null);
|
||||
|
||||
// when
|
||||
Document document = mappingElasticsearchConverter.mapObject(entity);
|
||||
|
||||
// then
|
||||
assertThat(document).isEqualTo(source);
|
||||
}
|
||||
|
||||
@ -1564,7 +1577,6 @@ public class MappingElasticsearchConverterUnitTests {
|
||||
|
||||
Document source = Document.parse(json);
|
||||
|
||||
// when
|
||||
EntityWithCustomValueConverters entity = mappingElasticsearchConverter.read(EntityWithCustomValueConverters.class,
|
||||
source);
|
||||
|
||||
@ -2038,6 +2050,17 @@ public class MappingElasticsearchConverterUnitTests {
|
||||
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
|
||||
public static class Sample {
|
||||
@Nullable public @ReadOnlyProperty String readOnly;
|
||||
@ -2288,149 +2311,20 @@ public class MappingElasticsearchConverterUnitTests {
|
||||
}
|
||||
|
||||
interface Inventory {
|
||||
|
||||
String getLabel();
|
||||
String label();
|
||||
}
|
||||
|
||||
static class Gun implements Inventory {
|
||||
final String label;
|
||||
final int shotsPerMagazine;
|
||||
|
||||
public Gun(@Nullable String label, int shotsPerMagazine) {
|
||||
this.label = label;
|
||||
this.shotsPerMagazine = shotsPerMagazine;
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
}
|
||||
record Gun(String label, int shotsPerMagazine) implements Inventory {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
record Grenade(String label) implements Inventory {
|
||||
}
|
||||
|
||||
@TypeAlias("rifle")
|
||||
static class Rifle 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
|
||||
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;
|
||||
}
|
||||
record Rifle(String label, double weight, int maxShotsPerMagazine) implements Inventory {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
record ShotGun(String label) implements Inventory {
|
||||
}
|
||||
|
||||
static class Address {
|
||||
@ -2599,7 +2493,7 @@ public class MappingElasticsearchConverterUnitTests {
|
||||
public Map<String, Object> convert(ShotGun source) {
|
||||
|
||||
LinkedHashMap<String, Object> target = new LinkedHashMap<>();
|
||||
target.put("model", source.getLabel());
|
||||
target.put("model", source.label());
|
||||
target.put("_class", ShotGun.class.getName());
|
||||
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
|
||||
|
||||
private static String reverse(Object o) {
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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 //
|
||||
) {
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user