Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f0a259045 | |||
| 9ffcb092db | |||
| 0e5af90581 | |||
| 95059b3282 | |||
| 1ae6301c2f | |||
| 2366f67bba | |||
| 6f424318ec | |||
| 300fe2ac8b | |||
| 1fdee7399f | |||
| ace17b9751 | |||
| 42383624ea | |||
| 35e7b45f1a | |||
| 89f60f2356 | |||
| fa979249fc | |||
| 8b43af2d33 | |||
| 64f88ae9ac | |||
| 15f086359d | |||
| 78ea67b6a6 |
@@ -33,3 +33,4 @@ node
|
||||
package-lock.json
|
||||
|
||||
.mvn/.develocity
|
||||
/src/test/resources/testcontainers-local.properties
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ public class MyService {
|
||||
|
||||
=== Using the RestClient
|
||||
|
||||
Please check the [official documentation](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#elasticsearch.clients.configuration).
|
||||
Please check the https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#elasticsearch.clients.configuration[official documentation].
|
||||
|
||||
=== Maven configuration
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-elasticsearch</artifactId>
|
||||
<version>5.5.0-M1</version>
|
||||
<version>5.5.0-RC1</version>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.data.build</groupId>
|
||||
<artifactId>spring-data-parent</artifactId>
|
||||
<version>3.5.0-M1</version>
|
||||
<version>3.5.0-RC1</version>
|
||||
</parent>
|
||||
|
||||
<name>Spring Data Elasticsearch</name>
|
||||
@@ -18,10 +18,10 @@
|
||||
<url>https://github.com/spring-projects/spring-data-elasticsearch</url>
|
||||
|
||||
<properties>
|
||||
<springdata.commons>3.5.0-M1</springdata.commons>
|
||||
<springdata.commons>3.5.0-RC1</springdata.commons>
|
||||
|
||||
<!-- version of the ElasticsearchClient -->
|
||||
<elasticsearch-java>8.17.2</elasticsearch-java>
|
||||
<elasticsearch-java>8.17.4</elasticsearch-java>
|
||||
|
||||
<hoverfly>0.19.0</hoverfly>
|
||||
<log4j>2.23.1</log4j>
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
[[new-features.5-5-0]]
|
||||
== New in Spring Data Elasticsearch 5.5
|
||||
|
||||
* Upgrade to Elasticsearch 8.17.2.
|
||||
* Upgrade to Elasticsearch 8.17.4.
|
||||
* Add support for the `@SearchTemplateQuery` annotation on repository methods.
|
||||
* Scripted field properties of type collection can be populated from scripts returning arrays.
|
||||
|
||||
[[new-features.5-4-0]]
|
||||
== New in Spring Data Elasticsearch 5.4
|
||||
|
||||
@@ -6,7 +6,7 @@ The following table shows the Elasticsearch and Spring versions that are used by
|
||||
[cols="^,^,^,^",options="header"]
|
||||
|===
|
||||
| Spring Data Release Train | Spring Data Elasticsearch | Elasticsearch | Spring Framework
|
||||
| 2025.0 (in development) | 5.5.x | 8.17.2 | 6.2.x
|
||||
| 2025.0 (in development) | 5.5.x | 8.17.4 | 6.2.x
|
||||
| 2024.1 | 5.4.x | 8.15.5 | 6.1.x
|
||||
| 2024.0 | 5.3.x | 8.13.4 | 6.1.x
|
||||
| 2023.1 (Vaughan) | 5.2.xfootnote:oom[Out of maintenance] | 8.11.1 | 6.1.x
|
||||
|
||||
@@ -20,3 +20,11 @@ Some classes that probably are not used by a library user have been renamed, the
|
||||
|===
|
||||
|
||||
=== Removals
|
||||
|
||||
The following methods that had been deprecated since release 5.3 have been removed:
|
||||
```
|
||||
DocumentOperations.delete(Query, Class<?>)
|
||||
DocumentOperations.delete(Query, Class<?>, IndexCoordinates)
|
||||
ReactiveDocumentOperations.delete(Query, Class<?>)
|
||||
ReactiveDocumentOperations.delete(Query, Class<?>, IndexCoordinates)
|
||||
```
|
||||
|
||||
-13
@@ -181,19 +181,6 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
|
||||
return delete(query, clazz, getIndexCoordinatesFor(clazz));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByQueryResponse delete(Query query, Class<?> clazz, IndexCoordinates index) {
|
||||
|
||||
Assert.notNull(query, "query must not be null");
|
||||
|
||||
DeleteByQueryRequest request = requestConverter.documentDeleteByQueryRequest(query, routingResolver.getRouting(),
|
||||
clazz, index, getRefreshPolicy());
|
||||
|
||||
DeleteByQueryResponse response = execute(client -> client.deleteByQuery(request));
|
||||
|
||||
return responseConverter.byQueryResponse(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByQueryResponse delete(DeleteQuery query, Class<?> clazz, IndexCoordinates index) {
|
||||
Assert.notNull(query, "query must not be null");
|
||||
|
||||
-10
@@ -167,16 +167,6 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
|
||||
.onErrorReturn(NoSuchIndexException.class, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ByQueryResponse> delete(Query query, Class<?> entityType, IndexCoordinates index) {
|
||||
|
||||
Assert.notNull(query, "query must not be null");
|
||||
|
||||
DeleteByQueryRequest request = requestConverter.documentDeleteByQueryRequest(query, routingResolver.getRouting(),
|
||||
entityType, index, getRefreshPolicy());
|
||||
return Mono.from(execute(client -> client.deleteByQuery(request))).map(responseConverter::byQueryResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ByQueryResponse> delete(DeleteQuery query, Class<?> entityType, IndexCoordinates index) {
|
||||
Assert.notNull(query, "query must not be null");
|
||||
|
||||
@@ -400,7 +400,6 @@ class ResponseConverter {
|
||||
private ReindexResponse.Failure reindexResponseFailureOf(BulkIndexByScrollFailure failure) {
|
||||
return ReindexResponse.Failure.builder() //
|
||||
.withIndex(failure.index()) //
|
||||
.withType(failure.type()) //
|
||||
.withId(failure.id()) //
|
||||
.withStatus(failure.status())//
|
||||
.withErrorCause(toErrorCause(failure.cause())) //
|
||||
@@ -411,7 +410,6 @@ class ResponseConverter {
|
||||
private ByQueryResponse.Failure byQueryResponseFailureOf(BulkIndexByScrollFailure failure) {
|
||||
return ByQueryResponse.Failure.builder() //
|
||||
.withIndex(failure.index()) //
|
||||
.withType(failure.type()) //
|
||||
.withId(failure.id()) //
|
||||
.withStatus(failure.status())//
|
||||
.withErrorCause(toErrorCause(failure.cause())).build();
|
||||
|
||||
-6
@@ -298,12 +298,6 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
|
||||
return this.delete(id, getIndexCoordinatesFor(entityType));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public ByQueryResponse delete(Query query, Class<?> clazz) {
|
||||
return delete(query, clazz, getIndexCoordinatesFor(clazz));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String delete(Object entity) {
|
||||
return delete(entity, getIndexCoordinatesFor(entity.getClass()));
|
||||
|
||||
-6
@@ -408,12 +408,6 @@ abstract public class AbstractReactiveElasticsearchTemplate
|
||||
|
||||
abstract protected Mono<String> doDeleteById(String id, @Nullable String routing, IndexCoordinates index);
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public Mono<ByQueryResponse> delete(Query query, Class<?> entityType) {
|
||||
return delete(query, entityType, getIndexCoordinatesFor(entityType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ByQueryResponse> delete(DeleteQuery query, Class<?> entityType) {
|
||||
return delete(query, entityType, getIndexCoordinatesFor(entityType));
|
||||
|
||||
@@ -272,19 +272,6 @@ public interface DocumentOperations {
|
||||
*/
|
||||
String delete(Object entity, IndexCoordinates index);
|
||||
|
||||
/**
|
||||
* Delete all records matching the query.
|
||||
*
|
||||
* @param query query defining the objects
|
||||
* @param clazz The entity class, must be annotated with
|
||||
* {@link org.springframework.data.elasticsearch.annotations.Document}
|
||||
* @return response with detailed information
|
||||
* @since 4.1
|
||||
* @deprecated since 5.3.0, use {@link #delete(DeleteQuery, Class)}
|
||||
*/
|
||||
@Deprecated
|
||||
ByQueryResponse delete(Query query, Class<?> clazz);
|
||||
|
||||
/**
|
||||
* Delete all records matching the query.
|
||||
*
|
||||
@@ -296,19 +283,6 @@ public interface DocumentOperations {
|
||||
*/
|
||||
ByQueryResponse delete(DeleteQuery query, Class<?> clazz);
|
||||
|
||||
/**
|
||||
* Delete all records matching the query.
|
||||
*
|
||||
* @param query query defining the objects
|
||||
* @param clazz The entity class, must be annotated with
|
||||
* {@link org.springframework.data.elasticsearch.annotations.Document}
|
||||
* @param index the index from which to delete
|
||||
* @return response with detailed information
|
||||
* @deprecated since 5.3.0, use {@link #delete(DeleteQuery, Class, IndexCoordinates)}
|
||||
*/
|
||||
@Deprecated
|
||||
ByQueryResponse delete(Query query, Class<?> clazz, IndexCoordinates index);
|
||||
|
||||
/**
|
||||
* Delete all records matching the query.
|
||||
*
|
||||
|
||||
-23
@@ -326,17 +326,6 @@ public interface ReactiveDocumentOperations {
|
||||
*/
|
||||
Mono<String> delete(String id, Class<?> entityType);
|
||||
|
||||
/**
|
||||
* Delete the documents matching the given {@link Query} extracting index from entity metadata.
|
||||
*
|
||||
* @param query must not be {@literal null}.
|
||||
* @param entityType must not be {@literal null}.
|
||||
* @return a {@link Mono} emitting the number of the removed documents.
|
||||
* @deprecated since 5.3.0, use {@link #delete(DeleteQuery, Class)}
|
||||
*/
|
||||
@Deprecated
|
||||
Mono<ByQueryResponse> delete(Query query, Class<?> entityType);
|
||||
|
||||
/**
|
||||
* Delete the documents matching the given {@link Query} extracting index from entity metadata.
|
||||
*
|
||||
@@ -347,18 +336,6 @@ public interface ReactiveDocumentOperations {
|
||||
*/
|
||||
Mono<ByQueryResponse> delete(DeleteQuery query, Class<?> entityType);
|
||||
|
||||
/**
|
||||
* Delete the documents matching the given {@link Query} extracting index from entity metadata.
|
||||
*
|
||||
* @param query must not be {@literal null}.
|
||||
* @param entityType must not be {@literal null}.
|
||||
* @param index the target index, must not be {@literal null}
|
||||
* @return a {@link Mono} emitting the number of the removed documents.
|
||||
* @deprecated since 5.3.0, use {@link #delete(DeleteQuery, Class, IndexCoordinates)}
|
||||
*/
|
||||
@Deprecated
|
||||
Mono<ByQueryResponse> delete(Query query, Class<?> entityType, IndexCoordinates index);
|
||||
|
||||
/**
|
||||
* Delete the documents matching the given {@link Query} extracting index from entity metadata.
|
||||
*
|
||||
|
||||
+138
-91
@@ -38,6 +38,7 @@ import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.env.EnvironmentCapable;
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
import org.springframework.data.convert.CustomConversions;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.elasticsearch.annotations.FieldType;
|
||||
import org.springframework.data.elasticsearch.annotations.ScriptedField;
|
||||
import org.springframework.data.elasticsearch.core.document.Document;
|
||||
@@ -50,6 +51,7 @@ import org.springframework.data.elasticsearch.core.query.Criteria;
|
||||
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
|
||||
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
|
||||
import org.springframework.data.elasticsearch.core.query.Field;
|
||||
import org.springframework.data.elasticsearch.core.query.Order;
|
||||
import org.springframework.data.elasticsearch.core.query.Query;
|
||||
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
|
||||
import org.springframework.data.elasticsearch.core.query.SourceFilter;
|
||||
@@ -392,7 +394,7 @@ public class MappingElasticsearchConverter
|
||||
}
|
||||
|
||||
if (source instanceof SearchDocument searchDocument) {
|
||||
populateScriptFields(targetEntity, result, searchDocument);
|
||||
populateScriptedFields(targetEntity, result, searchDocument);
|
||||
}
|
||||
return result;
|
||||
} catch (ConversionException e) {
|
||||
@@ -650,7 +652,16 @@ public class MappingElasticsearchConverter
|
||||
return conversionService.convert(value, target);
|
||||
}
|
||||
|
||||
private <T> void populateScriptFields(ElasticsearchPersistentEntity<?> entity, T result,
|
||||
/**
|
||||
* Checks if any of the properties of the entity is annotated with
|
||||
*
|
||||
* @{@link ScriptedField}. If so, the value of this property is set from the returned fields in the document.
|
||||
* @param entity the entity to defining the persistent property
|
||||
* @param result the rsult to populate
|
||||
* @param searchDocument the search result caontaining the fields
|
||||
* @param <T> the result type
|
||||
*/
|
||||
private <T> void populateScriptedFields(ElasticsearchPersistentEntity<?> entity, T result,
|
||||
SearchDocument searchDocument) {
|
||||
Map<String, List<Object>> fields = searchDocument.getFields();
|
||||
entity.doWithProperties((SimplePropertyHandler) property -> {
|
||||
@@ -659,8 +670,13 @@ public class MappingElasticsearchConverter
|
||||
// noinspection ConstantConditions
|
||||
String name = scriptedField.name().isEmpty() ? property.getName() : scriptedField.name();
|
||||
if (fields.containsKey(name)) {
|
||||
Object value = searchDocument.getFieldValue(name);
|
||||
entity.getPropertyAccessor(result).setProperty(property, value);
|
||||
if (property.isCollectionLike()) {
|
||||
List<Object> values = searchDocument.getFieldValues(name);
|
||||
entity.getPropertyAccessor(result).setProperty(property, values);
|
||||
} else {
|
||||
Object value = searchDocument.getFieldValue(name);
|
||||
entity.getPropertyAccessor(result).setProperty(property, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1231,7 +1247,7 @@ public class MappingElasticsearchConverter
|
||||
return;
|
||||
}
|
||||
|
||||
updatePropertiesInFieldsAndSourceFilter(query, domainClass);
|
||||
updatePropertiesInFieldsSortAndSourceFilter(query, domainClass);
|
||||
|
||||
if (query instanceof CriteriaQuery criteriaQuery) {
|
||||
updatePropertiesInCriteriaQuery(criteriaQuery, domainClass);
|
||||
@@ -1242,7 +1258,14 @@ public class MappingElasticsearchConverter
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePropertiesInFieldsAndSourceFilter(Query query, Class<?> domainClass) {
|
||||
/**
|
||||
* replaces the names of fields in the query, the sort or soucre filters with the field names used in Elasticsearch
|
||||
* when they are defined on the ElasticsearchProperties
|
||||
*
|
||||
* @param query the query to process
|
||||
* @param domainClass the domain class (persistent entity)
|
||||
*/
|
||||
private void updatePropertiesInFieldsSortAndSourceFilter(Query query, Class<?> domainClass) {
|
||||
|
||||
ElasticsearchPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(domainClass);
|
||||
|
||||
@@ -1250,12 +1273,12 @@ public class MappingElasticsearchConverter
|
||||
List<String> fields = query.getFields();
|
||||
|
||||
if (!fields.isEmpty()) {
|
||||
query.setFields(updateFieldNames(fields, persistentEntity));
|
||||
query.setFields(propertyToFieldNames(fields, persistentEntity));
|
||||
}
|
||||
|
||||
List<String> storedFields = query.getStoredFields();
|
||||
if (!CollectionUtils.isEmpty(storedFields)) {
|
||||
query.setStoredFields(updateFieldNames(storedFields, persistentEntity));
|
||||
query.setStoredFields(propertyToFieldNames(storedFields, persistentEntity));
|
||||
}
|
||||
|
||||
SourceFilter sourceFilter = query.getSourceFilter();
|
||||
@@ -1266,37 +1289,60 @@ public class MappingElasticsearchConverter
|
||||
String[] excludes = null;
|
||||
|
||||
if (sourceFilter.getIncludes() != null) {
|
||||
includes = updateFieldNames(Arrays.asList(sourceFilter.getIncludes()), persistentEntity)
|
||||
includes = propertyToFieldNames(Arrays.asList(sourceFilter.getIncludes()), persistentEntity)
|
||||
.toArray(new String[] {});
|
||||
}
|
||||
|
||||
if (sourceFilter.getExcludes() != null) {
|
||||
excludes = updateFieldNames(Arrays.asList(sourceFilter.getExcludes()), persistentEntity)
|
||||
excludes = propertyToFieldNames(Arrays.asList(sourceFilter.getExcludes()), persistentEntity)
|
||||
.toArray(new String[] {});
|
||||
}
|
||||
|
||||
query.addSourceFilter(new FetchSourceFilter(sourceFilter.fetchSource(), includes, excludes));
|
||||
}
|
||||
|
||||
if (query.getSort() != null) {
|
||||
var sort = query.getSort();
|
||||
// stream the orders and map them to a new order with the changed names,
|
||||
// then replace the existing sort with a new sort containing the new orders.
|
||||
var newOrders = sort.stream().map(order -> {
|
||||
var fieldNames = updateFieldNames(order.getProperty(), persistentEntity);
|
||||
|
||||
if (order instanceof Order springDataElasticsearchOrder) {
|
||||
return springDataElasticsearchOrder.withProperty(fieldNames);
|
||||
} else {
|
||||
return new Sort.Order(order.getDirection(),
|
||||
fieldNames,
|
||||
order.isIgnoreCase(),
|
||||
order.getNullHandling());
|
||||
}
|
||||
}).toList();
|
||||
|
||||
if (query instanceof BaseQuery baseQuery) {
|
||||
baseQuery.setSort(Sort.by(newOrders));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* relaces the fieldName with the property name of a property of the persistentEntity with the corresponding
|
||||
* fieldname. If no such property exists, the original fieldName is kept.
|
||||
* replaces property name of a property of the persistentEntity with the corresponding fieldname. If no such property
|
||||
* exists, the original fieldName is kept.
|
||||
*
|
||||
* @param fieldNames list of fieldnames
|
||||
* @param propertyNames list of fieldnames
|
||||
* @param persistentEntity the persistent entity to check
|
||||
* @return an updated list of field names
|
||||
*/
|
||||
private List<String> updateFieldNames(List<String> fieldNames, ElasticsearchPersistentEntity<?> persistentEntity) {
|
||||
return fieldNames.stream().map(fieldName -> updateFieldName(persistentEntity, fieldName))
|
||||
private List<String> propertyToFieldNames(List<String> propertyNames,
|
||||
ElasticsearchPersistentEntity<?> persistentEntity) {
|
||||
return propertyNames.stream().map(propertyName -> propertyToFieldName(persistentEntity, propertyName))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String updateFieldName(ElasticsearchPersistentEntity<?> persistentEntity, String fieldName) {
|
||||
ElasticsearchPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(fieldName);
|
||||
return persistentProperty != null ? persistentProperty.getFieldName() : fieldName;
|
||||
private String propertyToFieldName(ElasticsearchPersistentEntity<?> persistentEntity, String propertyName) {
|
||||
ElasticsearchPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(propertyName);
|
||||
return persistentProperty != null ? persistentProperty.getFieldName() : propertyName;
|
||||
}
|
||||
|
||||
private void updatePropertiesInCriteriaQuery(CriteriaQuery criteriaQuery, Class<?> domainClass) {
|
||||
@@ -1326,15 +1372,85 @@ public class MappingElasticsearchConverter
|
||||
return;
|
||||
}
|
||||
|
||||
String[] fieldNames = field.getName().split("\\.");
|
||||
var propertyNamesUpdate = updatePropertyNames(persistentEntity, field.getName());
|
||||
|
||||
var fieldNames = propertyNamesUpdate.names();
|
||||
field.setName(String.join(".", fieldNames));
|
||||
|
||||
if (propertyNamesUpdate.propertyCount() > 1 && propertyNamesUpdate.nestedProperty()) {
|
||||
List<String> propertyNames = Arrays.asList(fieldNames);
|
||||
field.setPath(String.join(".", propertyNames.subList(0, propertyNamesUpdate.propertyCount - 1)));
|
||||
}
|
||||
|
||||
if (propertyNamesUpdate.persistentProperty != null) {
|
||||
|
||||
if (propertyNamesUpdate.persistentProperty.hasPropertyValueConverter()) {
|
||||
PropertyValueConverter propertyValueConverter = Objects
|
||||
.requireNonNull(propertyNamesUpdate.persistentProperty.getPropertyValueConverter());
|
||||
criteria.getQueryCriteriaEntries().forEach(criteriaEntry -> {
|
||||
|
||||
if (criteriaEntry.getKey().hasValue()) {
|
||||
Object value = criteriaEntry.getValue();
|
||||
|
||||
if (value.getClass().isArray()) {
|
||||
Object[] objects = (Object[]) value;
|
||||
|
||||
for (int i = 0; i < objects.length; i++) {
|
||||
objects[i] = propertyValueConverter.write(objects[i]);
|
||||
}
|
||||
} else {
|
||||
criteriaEntry.setValue(propertyValueConverter.write(value));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
org.springframework.data.elasticsearch.annotations.Field fieldAnnotation = propertyNamesUpdate.persistentProperty
|
||||
.findAnnotation(org.springframework.data.elasticsearch.annotations.Field.class);
|
||||
|
||||
if (fieldAnnotation != null) {
|
||||
field.setFieldType(fieldAnnotation.type());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static record PropertyNamesUpdate(
|
||||
String[] names,
|
||||
Boolean nestedProperty,
|
||||
Integer propertyCount,
|
||||
ElasticsearchPersistentProperty persistentProperty) {
|
||||
}
|
||||
|
||||
@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 propertyNamesUpdate = updatePropertyNames(persistentEntity, propertyPath);
|
||||
return String.join(".", propertyNamesUpdate.names());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a propertyPath and replace the path values with the field names from a persistentEntity. path entries not
|
||||
* found in the entity are kept as they are.
|
||||
*
|
||||
* @return the eventually modified names, a flag if a nested entity was encountered the number of processed
|
||||
* propertiesand the last processed PersistentProperty.
|
||||
*/
|
||||
PropertyNamesUpdate updatePropertyNames(ElasticsearchPersistentEntity<?> persistentEntity, String propertyPath) {
|
||||
|
||||
String[] propertyNames = propertyPath.split("\\.");
|
||||
String[] fieldNames = Arrays.copyOf(propertyNames, propertyNames.length);
|
||||
|
||||
ElasticsearchPersistentEntity<?> currentEntity = persistentEntity;
|
||||
ElasticsearchPersistentProperty persistentProperty = null;
|
||||
|
||||
int propertyCount = 0;
|
||||
boolean isNested = false;
|
||||
|
||||
for (int i = 0; i < fieldNames.length; i++) {
|
||||
persistentProperty = currentEntity.getPersistentProperty(fieldNames[i]);
|
||||
for (int i = 0; i < propertyNames.length; i++) {
|
||||
persistentProperty = currentEntity.getPersistentProperty(propertyNames[i]);
|
||||
|
||||
if (persistentProperty != null) {
|
||||
propertyCount++;
|
||||
@@ -1361,77 +1477,8 @@ public class MappingElasticsearchConverter
|
||||
}
|
||||
}
|
||||
|
||||
field.setName(String.join(".", fieldNames));
|
||||
|
||||
if (propertyCount > 1 && isNested) {
|
||||
List<String> propertyNames = Arrays.asList(fieldNames);
|
||||
field.setPath(String.join(".", propertyNames.subList(0, propertyCount - 1)));
|
||||
}
|
||||
|
||||
if (persistentProperty != null) {
|
||||
|
||||
if (persistentProperty.hasPropertyValueConverter()) {
|
||||
PropertyValueConverter propertyValueConverter = Objects
|
||||
.requireNonNull(persistentProperty.getPropertyValueConverter());
|
||||
criteria.getQueryCriteriaEntries().forEach(criteriaEntry -> {
|
||||
|
||||
if (criteriaEntry.getKey().hasValue()) {
|
||||
Object value = criteriaEntry.getValue();
|
||||
|
||||
if (value.getClass().isArray()) {
|
||||
Object[] objects = (Object[]) value;
|
||||
|
||||
for (int i = 0; i < objects.length; i++) {
|
||||
objects[i] = propertyValueConverter.write(objects[i]);
|
||||
}
|
||||
} else {
|
||||
criteriaEntry.setValue(propertyValueConverter.write(value));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
org.springframework.data.elasticsearch.annotations.Field fieldAnnotation = persistentProperty
|
||||
.findAnnotation(org.springframework.data.elasticsearch.annotations.Field.class);
|
||||
|
||||
if (fieldAnnotation != null) {
|
||||
field.setFieldType(fieldAnnotation.type());
|
||||
}
|
||||
}
|
||||
return new PropertyNamesUpdate(fieldNames, isNested, propertyCount, persistentProperty);
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
if (persistentProperty != null) {
|
||||
ElasticsearchPersistentEntity<?> nestedPersistentEntity = mappingContext
|
||||
.getPersistentEntity(persistentProperty);
|
||||
if (nestedPersistentEntity != null) {
|
||||
return fieldName + '.' + updateFieldNames(properties[1], nestedPersistentEntity);
|
||||
} else {
|
||||
return fieldName;
|
||||
}
|
||||
}
|
||||
}
|
||||
return fieldName;
|
||||
} else {
|
||||
return propertyPath;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
@SuppressWarnings("ClassCanBeRecord")
|
||||
|
||||
+14
@@ -57,6 +57,20 @@ public interface SearchDocument extends Document {
|
||||
return (V) values.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name the field name
|
||||
* @param <V> the type of elements
|
||||
* @return the values of the given field.
|
||||
*/
|
||||
@Nullable
|
||||
default <V> List<V> getFieldValues(final String name) {
|
||||
List<Object> values = getFields().get(name);
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
return (List<V>) values;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the sort values for the search hit
|
||||
*/
|
||||
|
||||
@@ -21,7 +21,7 @@ import org.springframework.data.elasticsearch.core.document.Document;
|
||||
|
||||
/**
|
||||
* Interface definition for structures defined in <a href="https://geojson.org">GeoJSON</a>
|
||||
* format. copied from Spring Data Mongodb
|
||||
* format. copied from Spring Data Mongodb
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
|
||||
+2
-2
@@ -69,7 +69,7 @@ public class GeoJsonLineString implements GeoJson<Iterable<Point>> {
|
||||
Assert.notNull(second, "Second point must not be null!");
|
||||
Assert.notNull(others, "Additional points must not be null!");
|
||||
|
||||
List<Point> points = new ArrayList<>();
|
||||
List<Point> points = new ArrayList<>(2 + others.length);
|
||||
points.add(first);
|
||||
points.add(second);
|
||||
points.addAll(Arrays.asList(others));
|
||||
@@ -103,7 +103,7 @@ public class GeoJsonLineString implements GeoJson<Iterable<Point>> {
|
||||
Assert.notNull(second, "Second point must not be null!");
|
||||
Assert.notNull(others, "Additional points must not be null!");
|
||||
|
||||
List<Point> points = new ArrayList<>();
|
||||
List<Point> points = new ArrayList<>(2 + others.length);
|
||||
points.add(GeoPoint.toPoint(first));
|
||||
points.add(GeoPoint.toPoint(second));
|
||||
points.addAll(Arrays.stream(others).map(GeoPoint::toPoint).collect(Collectors.toList()));
|
||||
|
||||
+2
-2
@@ -69,7 +69,7 @@ public class GeoJsonMultiPoint implements GeoJson<Iterable<Point>> {
|
||||
Assert.notNull(second, "Second point must not be null!");
|
||||
Assert.notNull(others, "Additional points must not be null!");
|
||||
|
||||
List<Point> points = new ArrayList<>();
|
||||
List<Point> points = new ArrayList<>(2 + others.length);
|
||||
points.add(first);
|
||||
points.add(second);
|
||||
points.addAll(Arrays.asList(others));
|
||||
@@ -103,7 +103,7 @@ public class GeoJsonMultiPoint implements GeoJson<Iterable<Point>> {
|
||||
Assert.notNull(second, "Second point must not be null!");
|
||||
Assert.notNull(others, "Additional points must not be null!");
|
||||
|
||||
List<Point> points = new ArrayList<>();
|
||||
List<Point> points = new ArrayList<>(2 + others.length);
|
||||
points.add(GeoPoint.toPoint(first));
|
||||
points.add(GeoPoint.toPoint(second));
|
||||
points.addAll(Arrays.stream(others).map(GeoPoint::toPoint).collect(Collectors.toList()));
|
||||
|
||||
@@ -189,7 +189,13 @@ public class GeoJsonPolygon implements GeoJson<Iterable<GeoJsonLineString>> {
|
||||
@SafeVarargs
|
||||
private static <T> List<T> asList(T first, T second, T third, T fourth, T... others) {
|
||||
|
||||
ArrayList<T> result = new ArrayList<>(3 + others.length);
|
||||
Assert.notNull(first, "First element must not be null!");
|
||||
Assert.notNull(second, "Second element must not be null!");
|
||||
Assert.notNull(third, "Third element must not be null!");
|
||||
Assert.notNull(fourth, "Fourth element must not be null!");
|
||||
Assert.notNull(others, "Additional elements must not be null!");
|
||||
|
||||
ArrayList<T> result = new ArrayList<>(4 + others.length);
|
||||
|
||||
result.add(first);
|
||||
result.add(second);
|
||||
|
||||
+2
-15
@@ -167,7 +167,6 @@ public class ByQueryResponse {
|
||||
public static class Failure {
|
||||
|
||||
@Nullable private final String index;
|
||||
@Nullable private final String type;
|
||||
@Nullable private final String id;
|
||||
@Nullable private final Exception cause;
|
||||
@Nullable private final Integer status;
|
||||
@@ -176,11 +175,10 @@ public class ByQueryResponse {
|
||||
@Nullable private final Boolean aborted;
|
||||
@Nullable private final ElasticsearchErrorCause elasticsearchErrorCause;
|
||||
|
||||
private Failure(@Nullable String index, @Nullable String type, @Nullable String id, @Nullable Exception cause,
|
||||
private Failure(@Nullable String index, @Nullable String id, @Nullable Exception cause,
|
||||
@Nullable Integer status, @Nullable Long seqNo, @Nullable Long term, @Nullable Boolean aborted,
|
||||
@Nullable ElasticsearchErrorCause elasticsearchErrorCause) {
|
||||
this.index = index;
|
||||
this.type = type;
|
||||
this.id = id;
|
||||
this.cause = cause;
|
||||
this.status = status;
|
||||
@@ -195,11 +193,6 @@ public class ByQueryResponse {
|
||||
return index;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getId() {
|
||||
return id;
|
||||
@@ -250,7 +243,6 @@ public class ByQueryResponse {
|
||||
*/
|
||||
public static final class FailureBuilder {
|
||||
@Nullable private String index;
|
||||
@Nullable private String type;
|
||||
@Nullable private String id;
|
||||
@Nullable private Exception cause;
|
||||
@Nullable private Integer status;
|
||||
@@ -266,11 +258,6 @@ public class ByQueryResponse {
|
||||
return this;
|
||||
}
|
||||
|
||||
public FailureBuilder withType(String type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public FailureBuilder withId(String id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
@@ -307,7 +294,7 @@ public class ByQueryResponse {
|
||||
}
|
||||
|
||||
public Failure build() {
|
||||
return new Failure(index, type, id, cause, status, seqNo, term, aborted, elasticsearchErrorCause);
|
||||
return new Failure(index, id, cause, status, seqNo, term, aborted, elasticsearchErrorCause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.data.elasticsearch.core.geo.GeoBox;
|
||||
@@ -859,6 +860,7 @@ public class Criteria {
|
||||
|
||||
// endregion
|
||||
|
||||
// region equals/hashcode
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
@@ -874,6 +876,8 @@ public class Criteria {
|
||||
return false;
|
||||
if (!Objects.equals(field, criteria.field))
|
||||
return false;
|
||||
if (!criteriaChain.filter(this).equals(criteria.criteriaChain.filter(criteria)))
|
||||
return false;
|
||||
if (!queryCriteriaEntries.equals(criteria.queryCriteriaEntries))
|
||||
return false;
|
||||
if (!filterCriteriaEntries.equals(criteria.filterCriteriaEntries))
|
||||
@@ -886,11 +890,16 @@ public class Criteria {
|
||||
int result = field != null ? field.hashCode() : 0;
|
||||
result = 31 * result + (boost != +0.0f ? Float.floatToIntBits(boost) : 0);
|
||||
result = 31 * result + (negating ? 1 : 0);
|
||||
// the criteriaChain contains "this" object, so we need to filter it out
|
||||
// to avoid a stackoverflow here, because the hashcode implementation
|
||||
// uses the element's hashcodes
|
||||
result = 31 * result + criteriaChain.filter(this).hashCode();
|
||||
result = 31 * result + queryCriteriaEntries.hashCode();
|
||||
result = 31 * result + filterCriteriaEntries.hashCode();
|
||||
result = 31 * result + subCriteria.hashCode();
|
||||
return result;
|
||||
}
|
||||
// endregion
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
@@ -938,7 +947,17 @@ public class Criteria {
|
||||
*
|
||||
* @since 4.1
|
||||
*/
|
||||
public static class CriteriaChain extends LinkedList<Criteria> {}
|
||||
public static class CriteriaChain extends LinkedList<Criteria> {
|
||||
/**
|
||||
* return a copy of this list with the given element filtered out.
|
||||
*
|
||||
* @param criteria the element to filter
|
||||
* @return the filtered list
|
||||
*/
|
||||
List<Criteria> filter(Criteria criteria) {
|
||||
return this.stream().filter(c -> c != criteria).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Operator to join the entries of the criteria chain
|
||||
|
||||
@@ -20,7 +20,7 @@ import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* SourceFilter for providing includes and excludes. Using these helps in reducing the amount of data that is returned
|
||||
* from Elasticsearch especially when the stored docuements are large and only some fields from these documents are
|
||||
* from Elasticsearch especially when the stored documents are large and only some fields from these documents are
|
||||
* needed. If the SourceFilter includes the name of a property that has a different name mapped in Elasticsearch (see
|
||||
* {@link Field#name()} this will automatically be mapped.
|
||||
*
|
||||
|
||||
+2
-15
@@ -187,7 +187,6 @@ public class ReindexResponse {
|
||||
public static class Failure {
|
||||
|
||||
@Nullable private final String index;
|
||||
@Nullable private final String type;
|
||||
@Nullable private final String id;
|
||||
@Nullable private final Exception cause;
|
||||
@Nullable private final Integer status;
|
||||
@@ -196,11 +195,10 @@ public class ReindexResponse {
|
||||
@Nullable private final Boolean aborted;
|
||||
@Nullable private final ElasticsearchErrorCause elasticsearchErrorCause;
|
||||
|
||||
private Failure(@Nullable String index, @Nullable String type, @Nullable String id, @Nullable Exception cause,
|
||||
private Failure(@Nullable String index, @Nullable String id, @Nullable Exception cause,
|
||||
@Nullable Integer status, @Nullable Long seqNo, @Nullable Long term, @Nullable Boolean aborted,
|
||||
@Nullable ElasticsearchErrorCause elasticsearchErrorCause) {
|
||||
this.index = index;
|
||||
this.type = type;
|
||||
this.id = id;
|
||||
this.cause = cause;
|
||||
this.status = status;
|
||||
@@ -215,11 +213,6 @@ public class ReindexResponse {
|
||||
return index;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getId() {
|
||||
return id;
|
||||
@@ -269,7 +262,6 @@ public class ReindexResponse {
|
||||
*/
|
||||
public static final class FailureBuilder {
|
||||
@Nullable private String index;
|
||||
@Nullable private String type;
|
||||
@Nullable private String id;
|
||||
@Nullable private Exception cause;
|
||||
@Nullable private Integer status;
|
||||
@@ -285,11 +277,6 @@ public class ReindexResponse {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Failure.FailureBuilder withType(String type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Failure.FailureBuilder withId(String id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
@@ -326,7 +313,7 @@ public class ReindexResponse {
|
||||
}
|
||||
|
||||
public Failure build() {
|
||||
return new Failure(index, type, id, cause, status, seqNo, term, aborted, elasticsearchErrorCause);
|
||||
return new Failure(index, id, cause, status, seqNo, term, aborted, elasticsearchErrorCause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -384,7 +384,7 @@ public class SimpleElasticsearchRepository<T, ID> implements ElasticsearchReposi
|
||||
public void deleteAll() {
|
||||
|
||||
executeAndRefresh((OperationsCallback<Void>) operations -> {
|
||||
operations.delete(Query.findAll(), entityClass, getIndexCoordinates());
|
||||
operations.delete(DeleteQuery.builder(Query.findAll()).build(), entityClass, getIndexCoordinates());
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Spring Data Elasticsearch 5.5 M1 (2025.0.0)
|
||||
Spring Data Elasticsearch 5.5 RC1 (2025.0.0)
|
||||
Copyright (c) [2013-2022] Pivotal Software, Inc.
|
||||
|
||||
This product is licensed to you under the Apache License, Version 2.0 (the "License").
|
||||
@@ -26,3 +26,5 @@ conditions of the subcomponent's license, as noted in the LICENSE file.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+104
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.elasticsearch.client.elc;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.skyscreamer.jsonassert.JSONAssert.*;
|
||||
import static org.springframework.data.elasticsearch.client.elc.JsonUtils.*;
|
||||
|
||||
@@ -22,6 +23,7 @@ import co.elastic.clients.json.JsonpMapper;
|
||||
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
@@ -445,6 +447,108 @@ public class CriteriaQueryMappingUnitTests {
|
||||
softly.assertAll();
|
||||
}
|
||||
|
||||
// the following test failed because of a wrong implementation in Criteria
|
||||
// equals and hscode methods.
|
||||
@Test // #3083
|
||||
@DisplayName("should map correct subcriteria")
|
||||
void shouldMapCorrectSubcriteria() throws JSONException {
|
||||
Criteria criteria = new Criteria("first").is("hello");
|
||||
|
||||
List<Criteria> criterias = new ArrayList<>();
|
||||
criterias.add(new Criteria().or("second").exists());
|
||||
|
||||
List<Criteria> subCriterias = new ArrayList<>();
|
||||
subCriterias.add(new Criteria("third").exists()
|
||||
.and(new Criteria("fourth").is("ciao")));
|
||||
subCriterias.add(new Criteria("third").exists()
|
||||
.and(new Criteria("fourth").is("hi")));
|
||||
|
||||
Criteria result = Criteria.or();
|
||||
|
||||
for (Criteria c : criterias) {
|
||||
result = result.or(c);
|
||||
}
|
||||
|
||||
for (Criteria c : subCriterias) {
|
||||
result = result.subCriteria(c);
|
||||
}
|
||||
criteria = criteria.subCriteria(result);
|
||||
CriteriaQuery criteriaQuery = new CriteriaQuery(criteria);
|
||||
|
||||
String expected = """
|
||||
{
|
||||
"bool": {
|
||||
"must": [
|
||||
{
|
||||
"query_string": {
|
||||
"default_operator": "and",
|
||||
"fields": [
|
||||
"first"
|
||||
],
|
||||
"query": "hello"
|
||||
}
|
||||
},
|
||||
{
|
||||
"bool": {
|
||||
"should": [
|
||||
{
|
||||
"exists": {
|
||||
"field": "second"
|
||||
}
|
||||
},
|
||||
{
|
||||
"bool": {
|
||||
"must": [
|
||||
{
|
||||
"exists": {
|
||||
"field": "third"
|
||||
}
|
||||
},
|
||||
{
|
||||
"query_string": {
|
||||
"default_operator": "and",
|
||||
"fields": [
|
||||
"fourth"
|
||||
],
|
||||
"query": "ciao"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"bool": {
|
||||
"must": [
|
||||
{
|
||||
"exists": {
|
||||
"field": "third"
|
||||
}
|
||||
},
|
||||
{
|
||||
"query_string": {
|
||||
"default_operator": "and",
|
||||
"fields": [
|
||||
"fourth"
|
||||
],
|
||||
"query": "hi"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
mappingElasticsearchConverter.updateQuery(criteriaQuery, Person.class);
|
||||
var queryString = queryToJson(CriteriaQueryProcessor.createQuery(criteriaQuery.getCriteria()), mapper);
|
||||
|
||||
assertEquals(expected, queryString, false);
|
||||
}
|
||||
// endregion
|
||||
// region helper functions
|
||||
|
||||
|
||||
+112
-1
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.elasticsearch.core.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.skyscreamer.jsonassert.JSONAssert.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
@@ -23,9 +24,11 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
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.Field;
|
||||
import org.springframework.data.elasticsearch.annotations.FieldType;
|
||||
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
|
||||
@@ -35,7 +38,6 @@ import org.springframework.data.elasticsearch.repository.query.ElasticsearchQuer
|
||||
import org.springframework.data.elasticsearch.repository.query.RepositoryPartQuery;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
|
||||
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
|
||||
import org.springframework.data.repository.query.ValueExpressionDelegate;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
@@ -640,6 +642,84 @@ public abstract class RepositoryPartQueryIntegrationTests {
|
||||
assertEquals(expected, query, false);
|
||||
}
|
||||
|
||||
@Test // #3072
|
||||
@DisplayName("should build sort object with correct field names")
|
||||
void shouldBuildSortObjectWithCorrectFieldNames() throws NoSuchMethodException, JSONException {
|
||||
|
||||
String methodName = "findByNameOrderBySortAuthor_SortName";
|
||||
Class<?>[] parameterClasses = new Class[] { String.class };
|
||||
Object[] parameters = new Object[] { BOOK_TITLE };
|
||||
|
||||
String query = getQueryString(methodName, parameterClasses, parameters);
|
||||
|
||||
String expected = """
|
||||
|
||||
{
|
||||
"query": {
|
||||
"bool": {
|
||||
"must": [
|
||||
{
|
||||
"query_string": {
|
||||
"query": "Title",
|
||||
"fields": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"sort": [
|
||||
{
|
||||
"sort_author.sort_name": {
|
||||
"order": "asc"
|
||||
}
|
||||
}
|
||||
]
|
||||
}""";
|
||||
|
||||
assertEquals(expected, query, false);
|
||||
}
|
||||
|
||||
@Test // #3081
|
||||
@DisplayName("should build sort object with unknown field names")
|
||||
void shouldBuildSortObjectWithUnknownFieldNames() throws NoSuchMethodException, JSONException {
|
||||
|
||||
String methodName = "findByName";
|
||||
Class<?>[] parameterClasses = new Class[] { String.class, Sort.class };
|
||||
Object[] parameters = new Object[] { BOOK_TITLE, Sort.by("sortAuthor.sortName.raw") };
|
||||
|
||||
String query = getQueryString(methodName, parameterClasses, parameters);
|
||||
|
||||
String expected = """
|
||||
|
||||
{
|
||||
"query": {
|
||||
"bool": {
|
||||
"must": [
|
||||
{
|
||||
"query_string": {
|
||||
"query": "Title",
|
||||
"fields": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"sort": [
|
||||
{
|
||||
"sort_author.sort_name.raw": {
|
||||
"order": "asc"
|
||||
}
|
||||
}
|
||||
]
|
||||
}""";
|
||||
|
||||
assertEquals(expected, query, false);
|
||||
}
|
||||
|
||||
private String getQueryString(String methodName, Class<?>[] parameterClasses, Object[] parameters)
|
||||
throws NoSuchMethodException {
|
||||
|
||||
@@ -727,6 +807,9 @@ public abstract class RepositoryPartQueryIntegrationTests {
|
||||
|
||||
List<Book> findByAvailableTrueOrderByNameDesc();
|
||||
|
||||
List<Book> findByNameOrderBySortAuthor_SortName(String name);
|
||||
|
||||
List<Book> findByName(String name, Sort sort);
|
||||
}
|
||||
|
||||
public static class Book {
|
||||
@@ -736,6 +819,10 @@ public abstract class RepositoryPartQueryIntegrationTests {
|
||||
@Nullable private Integer price;
|
||||
@Field(type = FieldType.Boolean) private boolean available;
|
||||
|
||||
// this is needed for the #3072 test
|
||||
@Nullable
|
||||
@Field(name = "sort_author", type = FieldType.Object) private Author sortAuthor;
|
||||
|
||||
@Nullable
|
||||
public String getId() {
|
||||
return id;
|
||||
@@ -767,8 +854,32 @@ public abstract class RepositoryPartQueryIntegrationTests {
|
||||
return available;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Author getSortAuthor() {
|
||||
return sortAuthor;
|
||||
}
|
||||
|
||||
public void setSortAuthor(@Nullable Author sortAuthor) {
|
||||
this.sortAuthor = sortAuthor;
|
||||
}
|
||||
|
||||
public void setAvailable(Boolean available) {
|
||||
this.available = available;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static class Author {
|
||||
@Nullable
|
||||
@Field(name = "sort_name", type = FieldType.Keyword) private String sortName;
|
||||
|
||||
@Nullable
|
||||
public String getSortName() {
|
||||
return sortName;
|
||||
}
|
||||
|
||||
public void setSortName(@Nullable String sortName) {
|
||||
this.sortName = sortName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+129
-29
@@ -99,6 +99,8 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
|
||||
@DisplayName("should use runtime-field without script")
|
||||
void shouldUseRuntimeFieldWithoutScript() {
|
||||
|
||||
// a runtime field without a script can be used to redefine the type of a field for the search,
|
||||
// here we change the type from text to double
|
||||
insert("1", "11", 10);
|
||||
Query query = new CriteriaQuery(new Criteria("description").matches(11.0));
|
||||
RuntimeField runtimeField = new RuntimeField("description", "double");
|
||||
@@ -133,6 +135,25 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
|
||||
assertThat(foundPerson.getBirthDate()).isEqualTo(birthDate);
|
||||
}
|
||||
|
||||
@Test // #3076
|
||||
@DisplayName("should return scripted fields that are lists")
|
||||
void shouldReturnScriptedFieldsThatAreLists() {
|
||||
var person = new Person();
|
||||
person.setFirstName("John");
|
||||
person.setLastName("Doe");
|
||||
operations.save(person);
|
||||
var query = Query.findAll();
|
||||
query.addFields("allNames");
|
||||
query.addSourceFilter(new FetchSourceFilterBuilder().withIncludes("*").build());
|
||||
|
||||
var searchHits = operations.search(query, Person.class);
|
||||
|
||||
assertThat(searchHits.getTotalHits()).isEqualTo(1);
|
||||
var foundPerson = searchHits.getSearchHit(0).getContent();
|
||||
// the painless script seems to return the data sorted no matter in which order the values are emitted
|
||||
assertThat(foundPerson.getAllNames()).containsExactlyInAnyOrderElementsOf(List.of("John", "Doe"));
|
||||
}
|
||||
|
||||
@Test // #2035
|
||||
@DisplayName("should use repository method with ScriptedField parameters")
|
||||
void shouldUseRepositoryMethodWithScriptedFieldParameters() {
|
||||
@@ -143,9 +164,11 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
|
||||
|
||||
repository.save(entity);
|
||||
|
||||
org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1 = getScriptedField("scriptedValue1",
|
||||
org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1 = buildScriptedField(
|
||||
"scriptedValue1",
|
||||
2);
|
||||
org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2 = getScriptedField("scriptedValue2",
|
||||
org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2 = buildScriptedField(
|
||||
"scriptedValue2",
|
||||
3);
|
||||
|
||||
var searchHits = repository.findByValue(3, scriptedField1, scriptedField2);
|
||||
@@ -157,17 +180,6 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
|
||||
assertThat(foundEntity.getScriptedValue2()).isEqualTo(9);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static org.springframework.data.elasticsearch.core.query.ScriptedField getScriptedField(String fieldName,
|
||||
int factor) {
|
||||
return org.springframework.data.elasticsearch.core.query.ScriptedField.of(
|
||||
fieldName,
|
||||
ScriptData.of(b -> b
|
||||
.withType(ScriptType.INLINE)
|
||||
.withScript("doc['value'].size() > 0 ? doc['value'].value * params['factor'] : 0")
|
||||
.withParams(Map.of("factor", factor))));
|
||||
}
|
||||
|
||||
@Test // #2035
|
||||
@DisplayName("should use repository string query method with ScriptedField parameters")
|
||||
void shouldUseRepositoryStringQueryMethodWithScriptedFieldParameters() {
|
||||
@@ -178,9 +190,11 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
|
||||
|
||||
repository.save(entity);
|
||||
|
||||
org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1 = getScriptedField("scriptedValue1",
|
||||
org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1 = buildScriptedField(
|
||||
"scriptedValue1",
|
||||
2);
|
||||
org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2 = getScriptedField("scriptedValue2",
|
||||
org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2 = buildScriptedField(
|
||||
"scriptedValue2",
|
||||
3);
|
||||
|
||||
var searchHits = repository.findWithScriptedFields(3, scriptedField1, scriptedField2);
|
||||
@@ -202,8 +216,8 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
|
||||
|
||||
repository.save(entity);
|
||||
|
||||
var runtimeField1 = getRuntimeField("scriptedValue1", 3);
|
||||
var runtimeField2 = getRuntimeField("scriptedValue2", 4);
|
||||
var runtimeField1 = buildRuntimeField("scriptedValue1", 3);
|
||||
var runtimeField2 = buildRuntimeField("scriptedValue2", 4);
|
||||
|
||||
var searchHits = repository.findByValue(3, runtimeField1, runtimeField2);
|
||||
|
||||
@@ -214,14 +228,6 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
|
||||
assertThat(foundEntity.getScriptedValue2()).isEqualTo(12);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static RuntimeField getRuntimeField(String fieldName, int factor) {
|
||||
return new RuntimeField(
|
||||
fieldName,
|
||||
"long",
|
||||
String.format("emit(doc['value'].size() > 0 ? doc['value'].value * %d : 0)", factor));
|
||||
}
|
||||
|
||||
@Test // #2035
|
||||
@DisplayName("should use repository string query method with RuntimeField parameters")
|
||||
void shouldUseRepositoryStringQueryMethodWithRuntimeFieldParameters() {
|
||||
@@ -232,8 +238,8 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
|
||||
|
||||
repository.save(entity);
|
||||
|
||||
var runtimeField1 = getRuntimeField("scriptedValue1", 3);
|
||||
var runtimeField2 = getRuntimeField("scriptedValue2", 4);
|
||||
var runtimeField1 = buildRuntimeField("scriptedValue1", 3);
|
||||
var runtimeField2 = buildRuntimeField("scriptedValue2", 4);
|
||||
|
||||
var searchHits = repository.findWithRuntimeFields(3, runtimeField1, runtimeField2);
|
||||
|
||||
@@ -263,8 +269,7 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
|
||||
"priceWithTax",
|
||||
"double",
|
||||
"emit(doc['price'].value * params.tax)",
|
||||
Map.of("tax", 1.19)
|
||||
);
|
||||
Map.of("tax", 1.19));
|
||||
var query = CriteriaQuery.builder(
|
||||
Criteria.where("priceWithTax").greaterThan(100.0))
|
||||
.withRuntimeFields(List.of(runtimeField))
|
||||
@@ -275,6 +280,56 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
|
||||
assertThat(searchHits).hasSize(1);
|
||||
}
|
||||
|
||||
@Test // #3076
|
||||
@DisplayName("should use runtime fields in queries returning lists")
|
||||
void shouldUseRuntimeFieldsInQueriesReturningLists() {
|
||||
|
||||
insert("1", "item 1", 80.0);
|
||||
|
||||
var runtimeField = new RuntimeField(
|
||||
"someStrings",
|
||||
"keyword",
|
||||
"emit('foo'); emit('bar');",
|
||||
null);
|
||||
|
||||
var query = Query.findAll();
|
||||
query.addRuntimeField(runtimeField);
|
||||
query.addFields("someStrings");
|
||||
query.addSourceFilter(new FetchSourceFilterBuilder().withIncludes("*").build());
|
||||
|
||||
var searchHits = operations.search(query, SomethingToBuy.class);
|
||||
|
||||
assertThat(searchHits).hasSize(1);
|
||||
var somethingToBuy = searchHits.getSearchHit(0).getContent();
|
||||
assertThat(somethingToBuy.someStrings).containsExactlyInAnyOrder("foo", "bar");
|
||||
}
|
||||
|
||||
/**
|
||||
* build a {@link org.springframework.data.elasticsearch.core.query.ScriptedField} to return the product of the
|
||||
* document's value property and the given factor
|
||||
*/
|
||||
@NotNull
|
||||
private static org.springframework.data.elasticsearch.core.query.ScriptedField buildScriptedField(String fieldName,
|
||||
int factor) {
|
||||
return org.springframework.data.elasticsearch.core.query.ScriptedField.of(
|
||||
fieldName,
|
||||
ScriptData.of(b -> b
|
||||
.withType(ScriptType.INLINE)
|
||||
.withScript("doc['value'].size() > 0 ? doc['value'].value * params['factor'] : 0")
|
||||
.withParams(Map.of("factor", factor))));
|
||||
}
|
||||
|
||||
/**
|
||||
* build a {@link RuntimeField} to return the product of the document's value property and the given factor
|
||||
*/
|
||||
@NotNull
|
||||
private static RuntimeField buildRuntimeField(String fieldName, int factor) {
|
||||
return new RuntimeField(
|
||||
fieldName,
|
||||
"long",
|
||||
String.format("emit(doc['value'].size() > 0 ? doc['value'].value * %d : 0)", factor));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Document(indexName = "#{@indexNameProvider.indexName()}-something-to-by")
|
||||
private static class SomethingToBuy {
|
||||
@@ -286,6 +341,9 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
|
||||
@Nullable
|
||||
@Field(type = FieldType.Double) private Double price;
|
||||
|
||||
@Nullable
|
||||
@ScriptedField private List<String> someStrings;
|
||||
|
||||
@Nullable
|
||||
public String getId() {
|
||||
return id;
|
||||
@@ -312,6 +370,15 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
|
||||
public void setPrice(@Nullable Double price) {
|
||||
this.price = price;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public List<String> getSomeStrings() {
|
||||
return someStrings;
|
||||
}
|
||||
|
||||
public void setSomeStrings(@Nullable List<String> someStrings) {
|
||||
this.someStrings = someStrings;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@@ -320,6 +387,13 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
|
||||
public static class Person {
|
||||
@Nullable private String id;
|
||||
|
||||
// need keywords as we are using them in the script
|
||||
@Nullable
|
||||
@Field(type = FieldType.Keyword) private String firstName;
|
||||
@Nullable
|
||||
@Field(type = FieldType.Keyword) private String lastName;
|
||||
@ScriptedField private List<String> allNames = List.of();
|
||||
|
||||
@Field(type = FieldType.Date, format = DateFormat.basic_date)
|
||||
@Nullable private LocalDate birthDate;
|
||||
|
||||
@@ -335,6 +409,24 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(@Nullable String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(@Nullable String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public LocalDate getBirthDate() {
|
||||
return birthDate;
|
||||
@@ -352,6 +444,14 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
|
||||
public void setAge(@Nullable Integer age) {
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
public List<String> getAllNames() {
|
||||
return allNames;
|
||||
}
|
||||
|
||||
public void setAllNames(List<String> allNames) {
|
||||
this.allNames = allNames;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
|
||||
+3
@@ -129,6 +129,9 @@ public class ClusterConnection implements ExtensionContext.Store.CloseableResour
|
||||
Map<String, String> testcontainersProperties = testcontainersProperties(
|
||||
"testcontainers-" + testcontainersConfiguration + ".properties");
|
||||
|
||||
var testcontainersPropertiesLocal = testcontainersProperties("testcontainers-local.properties");
|
||||
testcontainersProperties.putAll(testcontainersPropertiesLocal);
|
||||
|
||||
DockerImageName dockerImageName = getDockerImageName(testcontainersProperties);
|
||||
|
||||
ElasticsearchContainer elasticsearchContainer = new SpringDataElasticsearchContainer(dockerImageName)
|
||||
|
||||
@@ -5,5 +5,12 @@
|
||||
"lang": "painless",
|
||||
"source": "Instant currentDate = Instant.ofEpochMilli(new Date().getTime()); Instant startDate = doc['birthDate'].value.toInstant(); emit(ChronoUnit.DAYS.between(startDate, currentDate) / 365);"
|
||||
}
|
||||
},
|
||||
"allNames": {
|
||||
"type": "keyword",
|
||||
"script": {
|
||||
"lang": "painless",
|
||||
"source": "emit(doc['firstName'].value);emit(doc['lastName'].value);"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#
|
||||
#
|
||||
sde.testcontainers.image-name=docker.elastic.co/elasticsearch/elasticsearch
|
||||
sde.testcontainers.image-version=8.17.2
|
||||
sde.testcontainers.image-version=8.17.4
|
||||
#
|
||||
#
|
||||
# needed as we do a DELETE /* at the end of the tests, will be required from 8.0 on, produces a warning since 7.13
|
||||
|
||||
Reference in New Issue
Block a user