1
0
mirror of synced 2026-05-23 04:33:17 +00:00

Compare commits

...

14 Commits

Author SHA1 Message Date
Mark Paluch c5cf3b62d4 Release version 5.3.12 (2024.0.12).
See #3094
2025-05-16 10:05:55 +02:00
Mark Paluch 84c18d1808 Prepare 5.3.12 (2024.0.12).
See #3094
2025-05-16 10:05:35 +02:00
Peter-Josef Meisch 2772c5f3d5 Fix missing return value in ByQueryResponse.
Original Pull Request #3109
Closes #3108

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
(cherry picked from commit ebbe242a72)
(cherry picked from commit ea51ce062e)
2025-05-14 14:37:40 +02:00
Mark Paluch 144d388203 Update CI Properties.
See #3094
2025-05-12 08:59:50 +02:00
Peter-Josef Meisch 5d6004f450 Fix handling of page size and max results in search request preparation.
Original Pull Request #3106
Closes #3089

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
(cherry picked from commit 945179e4eb)
(cherry picked from commit 6c4fc59a11)
2025-05-10 21:27:37 +02:00
Peter-Josef Meisch 71383536d4 Fix code not terminating on repository saving an empty flux.
Original Pull Request #3099
Closes: #3039

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
(cherry picked from commit a07ac3c93d)
(cherry picked from commit 0e8401d3b1)
2025-04-26 11:11:59 +02:00
Mark Paluch b10bc3d693 After release cleanups.
See #3060
2025-04-22 10:01:25 +02:00
Mark Paluch a34b363732 Prepare next development iteration.
See #3060
2025-04-22 10:01:24 +02:00
Mark Paluch e110fe12a2 Release version 5.3.11 (2024.0.11).
See #3060
2025-04-22 09:58:41 +02:00
Mark Paluch a707b0bc47 Prepare 5.3.11 (2024.0.11).
See #3060
2025-04-22 09:58:21 +02:00
Peter-Josef Meisch ec563e02f4 Fix implementation of equlas/hashcode for Criteria class.
Original Pull Request: #3088
Closes: #3083

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
(cherry picked from commit 0e5af90581)
(cherry picked from commit 46fdc8a84b)
2025-04-05 20:55:37 +02:00
Peter-Josef Meisch f32cbb81f8 Fix cutting of unknown properties in property paths for search.
Original Pull Request #3082
Closes #3081

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
(cherry picked from commit 1ae6301c2f)
(cherry picked from commit 394fb7a831)
2025-03-24 21:56:35 +01:00
Mark Paluch 7b782c7c62 After release cleanups.
See #3056
2025-03-14 07:38:28 +01:00
Mark Paluch 6d0560751d Prepare next development iteration.
See #3056
2025-03-14 07:38:27 +01:00
13 changed files with 301 additions and 99 deletions
+4
View File
@@ -8,3 +8,7 @@
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
--add-opens=java.base/java.util=ALL-UNNAMED
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED
--add-opens=java.base/java.text=ALL-UNNAMED
--add-opens=java.desktop/java.awt.font=ALL-UNNAMED
+1 -1
View File
@@ -1,5 +1,5 @@
# Java versions
java.main.tag=17.0.13_11-jdk-focal
java.main.tag=17.0.15_6-jdk-focal
java.next.tag=22.0.2_9-jdk-jammy
# Docker container images - standard
+3 -3
View File
@@ -5,12 +5,12 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>5.3.10</version>
<version>5.3.12</version>
<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<version>3.3.10</version>
<version>3.3.12</version>
</parent>
<name>Spring Data Elasticsearch</name>
@@ -18,7 +18,7 @@
<url>https://github.com/spring-projects/spring-data-elasticsearch</url>
<properties>
<springdata.commons>3.3.10</springdata.commons>
<springdata.commons>3.3.12</springdata.commons>
<!-- version of the ElasticsearchClient -->
<elasticsearch-java>8.13.4</elasticsearch-java>
@@ -1269,11 +1269,15 @@ class RequestConverter extends AbstractQueryProcessor {
.timeout(timeStringMs(query.getTimeout())) //
;
if (query.getPageable().isPaged()) {
bb //
.from((int) query.getPageable().getOffset()) //
.size(query.getPageable().getPageSize());
}
var offset = query.getPageable().isPaged() ? query.getPageable().getOffset() : 0;
var pageSize = query.getPageable().isPaged() ? query.getPageable().getPageSize()
: INDEX_MAX_RESULT_WINDOW;
// if we have both a page size and a max results, we take the min, this is necessary for
// searchForStream to work correctly (#3098) as there the page size defines what is
// returned in a single request, and the max result determines the total number of
// documents returned
var size = query.isLimiting() ? Math.min(pageSize, query.getMaxResults()) : pageSize;
bb.from((int) offset).size(size);
if (!isEmpty(query.getFields())) {
bb.fields(fb -> {
@@ -1286,10 +1290,6 @@ class RequestConverter extends AbstractQueryProcessor {
bb.storedFields(query.getStoredFields());
}
if (query.isLimiting()) {
bb.size(query.getMaxResults());
}
if (query.getMinScore() > 0) {
bb.minScore((double) query.getMinScore());
}
@@ -1443,13 +1443,14 @@ class RequestConverter extends AbstractQueryProcessor {
builder.seqNoPrimaryTerm(true);
}
if (query.getPageable().isPaged()) {
builder //
.from((int) query.getPageable().getOffset()) //
.size(query.getPageable().getPageSize());
} else {
builder.from(0).size(INDEX_MAX_RESULT_WINDOW);
}
var offset = query.getPageable().isPaged() ? query.getPageable().getOffset() : 0;
var pageSize = query.getPageable().isPaged() ? query.getPageable().getPageSize() : INDEX_MAX_RESULT_WINDOW;
// if we have both a page size and a max results, we take the min, this is necessary for
// searchForStream to work correctly (#3098) as there the page size defines what is
// returned in a single request, and the max result determines the total number of
// documents returned
var size = query.isLimiting() ? Math.min(pageSize, query.getMaxResults()) : pageSize;
builder.from((int) offset).size(size);
if (!isEmpty(query.getFields())) {
var fieldAndFormats = query.getFields().stream().map(field -> FieldAndFormat.of(b -> b.field(field))).toList();
@@ -1464,10 +1465,6 @@ class RequestConverter extends AbstractQueryProcessor {
addIndicesOptions(builder, query.getIndicesOptions());
}
if (query.isLimiting()) {
builder.size(query.getMaxResults());
}
if (query.getMinScore() > 0) {
builder.minScore((double) query.getMinScore());
}
@@ -497,6 +497,10 @@ class ResponseConverter {
builder.withDeleted(response.deleted());
}
if(response.updated() != null) {
builder.withUpdated(response.updated());
}
if (response.batches() != null) {
builder.withBatches(Math.toIntExact(response.batches()));
}
@@ -233,6 +233,7 @@ abstract public class AbstractReactiveElasticsearchTemplate
.subscribe(new Subscriber<>() {
@Nullable private Subscription subscription = null;
private final AtomicBoolean upstreamComplete = new AtomicBoolean(false);
private final AtomicBoolean onNextHasBeenCalled = new AtomicBoolean(false);
@Override
public void onSubscribe(Subscription subscription) {
@@ -242,6 +243,7 @@ abstract public class AbstractReactiveElasticsearchTemplate
@Override
public void onNext(List<T> entityList) {
onNextHasBeenCalled.set(true);
saveAll(entityList, index)
.map(sink::tryEmitNext)
.doOnComplete(() -> {
@@ -267,6 +269,10 @@ abstract public class AbstractReactiveElasticsearchTemplate
@Override
public void onComplete() {
upstreamComplete.set(true);
if (!onNextHasBeenCalled.get()) {
// this happens when an empty flux is saved
sink.tryEmitComplete();
}
}
});
return sink.asFlux();
@@ -1358,15 +1358,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++;
@@ -1393,75 +1463,7 @@ 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());
}
}
}
@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 = propertyToFieldName(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;
}
return new PropertyNamesUpdate(fieldNames, isNested, propertyCount, persistentProperty);
}
// endregion
@@ -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
+3 -1
View File
@@ -1,4 +1,4 @@
Spring Data Elasticsearch 5.3.10 (2024.0.10)
Spring Data Elasticsearch 5.3.12 (2024.0.12)
Copyright (c) [2013-2022] Pivotal Software, Inc.
This product is licensed to you under the Apache License, Version 2.0 (the "License").
@@ -29,6 +29,8 @@ conditions of the subcomponent's license, as noted in the LICENSE file.
@@ -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
@@ -1634,7 +1634,9 @@ public abstract class ElasticsearchIntegrationTests {
.withParams(Collections.singletonMap("newMessage", messageAfterUpdate)).withAbortOnVersionConflict(true)
.build();
operations.updateByQuery(updateQuery, IndexCoordinates.of(indexNameProvider.indexName()));
var byQueryResponse = operations.updateByQuery(updateQuery, IndexCoordinates.of(indexNameProvider.indexName()));
assertThat(byQueryResponse.getUpdated()).isEqualTo(1);
SampleEntity indexedEntity = operations.get(documentId, SampleEntity.class,
IndexCoordinates.of(indexNameProvider.indexName()));
@@ -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;
@@ -27,6 +28,7 @@ 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;
@@ -679,6 +681,45 @@ public abstract class ElasticsearchPartQueryIntegrationTests {
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 {
@@ -768,6 +809,7 @@ public abstract class ElasticsearchPartQueryIntegrationTests {
List<Book> findByNameOrderBySortAuthor_SortName(String name);
List<Book> findByName(String name, Sort sort);
}
public static class Book {
@@ -105,6 +105,26 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests {
return operations.exists(id, IndexCoordinates.of(indexNameProvider.indexName()));
}
@Test // #3093
@DisplayName("should save all from empty collection")
void shouldSaveAllFromEmptyCollection() {
repository.saveAll(Collections.emptyList())
.as(StepVerifier::create)
.expectNextCount(0)
.verifyComplete();
}
@Test // #3093
@DisplayName("should save all from empty flux")
void shouldSaveAllFromEmptyFlux() {
repository.saveAll(Flux.empty())
.as(StepVerifier::create)
.expectNextCount(0)
.verifyComplete();
}
@Test // DATAES-519
void saveShouldComputeMultipleEntities() {