diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000000..411d4a9338
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,21 @@
+# GitHub Actions for CodeQL Scanning
+
+name: "CodeQL Advanced"
+
+on:
+ push:
+ pull_request:
+ workflow_dispatch:
+ schedule:
+ # https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule
+ - cron: '0 5 * * *'
+
+permissions: read-all
+
+jobs:
+ codeql-analysis-call:
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+ uses: spring-io/github-actions/.github/workflows/codeql-analysis.yml@1
diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml
new file mode 100644
index 0000000000..4c8108d353
--- /dev/null
+++ b/.github/workflows/project.yml
@@ -0,0 +1,45 @@
+# GitHub Actions to automate GitHub issues for Spring Data Project Management
+
+name: Spring Data GitHub Issues
+
+on:
+ issues:
+ types: [opened, edited, reopened]
+ issue_comment:
+ types: [created]
+ pull_request_target:
+ types: [opened, edited, reopened]
+
+permissions:
+ contents: read
+ issues: write
+ pull-requests: write
+
+jobs:
+ Inbox:
+ runs-on: ubuntu-latest
+ if: github.repository_owner == 'spring-projects' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request == null && !contains(join(github.event.issue.labels.*.name, ', '), 'dependency-upgrade') && !contains(github.event.issue.title, 'Release ')
+ steps:
+ - name: Create or Update Issue Card
+ uses: actions/add-to-project@v1.0.2
+ with:
+ project-url: https://github.com/orgs/spring-projects/projects/25
+ github-token: ${{ secrets.GH_ISSUES_TOKEN_SPRING_DATA }}
+ Pull-Request:
+ runs-on: ubuntu-latest
+ if: github.repository_owner == 'spring-projects' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request != null
+ steps:
+ - name: Create or Update Pull Request Card
+ uses: actions/add-to-project@v1.0.2
+ with:
+ project-url: https://github.com/orgs/spring-projects/projects/25
+ github-token: ${{ secrets.GH_ISSUES_TOKEN_SPRING_DATA }}
+ Feedback-Provided:
+ runs-on: ubuntu-latest
+ if: github.repository_owner == 'spring-projects' && github.event_name == 'issue_comment' && github.event.action == 'created' && github.actor != 'spring-projects-issues' && github.event.pull_request == null && github.event.issue.state == 'open' && contains(toJSON(github.event.issue.labels), 'waiting-for-feedback')
+ steps:
+ - name: Update Project Card
+ uses: actions/add-to-project@v1.0.2
+ with:
+ project-url: https://github.com/orgs/spring-projects/projects/25
+ github-token: ${{ secrets.GH_ISSUES_TOKEN_SPRING_DATA }}
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
index e075a74d86..9609ca640d 100644
--- a/.mvn/wrapper/maven-wrapper.properties
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -1,3 +1,3 @@
-#Thu Nov 07 09:47:28 CET 2024
+#Thu Jul 17 14:00:55 CEST 2025
wrapperUrl=https\://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
-distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
+distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
diff --git a/Jenkinsfile b/Jenkinsfile
index 1d2500ed1e..9df3e280d5 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -9,7 +9,7 @@ pipeline {
triggers {
pollSCM 'H/10 * * * *'
- upstream(upstreamProjects: "spring-data-commons/main", threshold: hudson.model.Result.SUCCESS)
+ upstream(upstreamProjects: "spring-data-commons/3.5.x", threshold: hudson.model.Result.SUCCESS)
}
options {
diff --git a/SECURITY.adoc b/SECURITY.adoc
index 2694f228b5..654bfbea58 100644
--- a/SECURITY.adoc
+++ b/SECURITY.adoc
@@ -1,9 +1,15 @@
-# Security Policy
+= Security Policy
-## Supported Versions
+== Reporting a Vulnerability
-Please see the https://spring.io/projects/spring-data-elasticsearch[Spring Data Elasticsearch] project page for supported versions.
+Please, https://github.com/spring-projects/security-advisories/security/advisories/new[open a draft security advisory] if you need to disclose and discuss a security issue in private with the Spring Data team.
+Note that we only accept reports against https://spring.io/projects/spring-data#support[supported versions].
-## Reporting a Vulnerability
+For more details, check out our https://spring.io/security-policy[security policy].
-Please don't raise security vulnerabilities here. Head over to https://pivotal.io/security to learn how to disclose them responsibly.
+== JAR signing
+
+Spring Data JARs released on Maven Central are signed.
+You'll find more information about the key here: https://spring.io/GPG-KEY-spring.txt
+
+Versions released prior to 2023 may be signed with a different key.
diff --git a/pom.xml b/pom.xml
index 51f60ff7b8..f3084eebb7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,16 +1,16 @@
-
+
4.0.0
org.springframework.data
spring-data-elasticsearch
- 5.5.0
+ 5.5.7-SNAPSHOT
org.springframework.data.build
spring-data-parent
- 3.5.0
+ 3.5.7-SNAPSHOT
Spring Data Elasticsearch
@@ -18,10 +18,10 @@
https://github.com/spring-projects/spring-data-elasticsearch
- 3.5.0
+ 3.5.7-SNAPSHOT
- 8.18.1
+ 8.18.8
0.19.0
2.23.1
@@ -41,6 +41,15 @@
+
+ sothawo
+ Peter-Josef Meisch
+ pj.meisch at sothawo.com
+
+ Project Lead
+
+ +1
+
biomedcentral
BioMed Central Development Team
@@ -77,11 +86,6 @@
-
- Bamboo
- https://build.spring.io/browse/SPRINGDATAES
-
-
GitHub
https://github.com/spring-projects/spring-data-elasticsearch/issues
@@ -462,8 +466,20 @@
-
-
+
+ spring-snapshot
+ https://repo.spring.io/snapshot
+
+ true
+
+
+ false
+
+
+
+ spring-milestone
+ https://repo.spring.io/milestone
+
diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repository-queries.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repository-queries.adoc
index b22e17522d..d22fa21fdb 100644
--- a/src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repository-queries.adoc
+++ b/src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repository-queries.adoc
@@ -376,7 +376,7 @@ So calling the method with a `List` of `["id1", "id2", "id3"]` would produce the
.Declare query on the method using the `@Query` annotation with SpEL expression.
====
-https://docs.spring.io/spring-framework/reference/core/expressions.html[SpEL expression] is also supported when defining query in `@Query`.
+{spring-framework-docs}/core/expressions.html[SpEL expression] is also supported when defining query in `@Query`.
[source,java]
----
@@ -453,7 +453,7 @@ We can pass `new QueryParameter("John")` as the parameter now, and it will produ
.accessing bean property.
====
-https://docs.spring.io/spring-framework/reference/core/expressions/language-ref/bean-references.html[Bean property] is also supported to access.
+{spring-framework-docs}/core/expressions/language-ref/bean-references.html[Bean property] is also supported to access.
Given that there is a bean named `queryParameter` of type `QueryParameter`, we can access the bean with symbol `@` rather than `#`, and there is no need to declare a parameter of type `QueryParameter` in the query method:
[source,java]
@@ -523,7 +523,7 @@ A collection of `names` like `List.of("name1", "name2")` will produce the follow
.access property in the `Collection` param.
====
-https://docs.spring.io/spring-framework/reference/core/expressions/language-ref/collection-projection.html[SpEL Collection Projection] is convenient to use when values in the `Collection` parameter is not plain `String`:
+{spring-framework-docs}/core/expressions/language-ref/collection-projection.html[SpEL Collection Projection] is convenient to use when values in the `Collection` parameter is not plain `String`:
[source,java]
----
diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/versions.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/versions.adoc
index 72ad88a0ee..e24c86ee49 100644
--- a/src/main/antora/modules/ROOT/pages/elasticsearch/versions.adoc
+++ b/src/main/antora/modules/ROOT/pages/elasticsearch/versions.adoc
@@ -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 | 5.5.x | 8.18.1 | 6.2.x
+| 2025.0 | 5.5.x | 8.18.8 | 6.2.x
| 2024.1 | 5.4.x | 8.15.5 | 6.1.x
| 2024.0 | 5.3.xfootnote:oom[Out of maintenance] | 8.13.4 | 6.1.x
| 2023.1 (Vaughan) | 5.2.xfootnote:oom[] | 8.11.1 | 6.1.x
diff --git a/src/main/antora/resources/antora-resources/antora.yml b/src/main/antora/resources/antora-resources/antora.yml
index 0f79f52be7..abd38734ad 100644
--- a/src/main/antora/resources/antora-resources/antora.yml
+++ b/src/main/antora/resources/antora-resources/antora.yml
@@ -3,19 +3,20 @@ prerelease: ${antora-component.prerelease}
asciidoc:
attributes:
- copyright-year: ${current.year}
- version: ${project.version}
- springversionshort: ${spring.short}
- springversion: ${spring}
attribute-missing: 'warn'
- commons: ${springdata.commons.docs}
+ chomp: 'all'
+ version: '${project.version}'
+ copyright-year: '${current.year}'
+ springversionshort: '${spring.short}'
+ springversion: '${spring}'
+ commons: '${springdata.commons.docs}'
include-xml-namespaces: false
- spring-data-commons-docs-url: https://docs.spring.io/spring-data/commons/reference
- spring-data-commons-javadoc-base: https://docs.spring.io/spring-data/commons/docs/${springdata.commons}/api/
- springdocsurl: https://docs.spring.io/spring-framework/reference/{springversionshort}
- springjavadocurl: https://docs.spring.io/spring-framework/docs/${spring}/javadoc-api
+ spring-data-commons-docs-url: '${documentation.baseurl}/spring-data/commons/reference/${springdata.commons.short}'
+ spring-data-commons-javadoc-base: '{spring-data-commons-docs-url}/api/java'
+ springdocsurl: '${documentation.baseurl}/spring-framework/reference/{springversionshort}'
spring-framework-docs: '{springdocsurl}'
+ springjavadocurl: '${documentation.spring-javadoc-url}'
spring-framework-javadoc: '{springjavadocurl}'
- springhateoasversion: ${spring-hateoas}
- releasetrainversion: ${releasetrain}
+ springhateoasversion: '${spring-hateoas}'
+ releasetrainversion: '${releasetrain}'
store: Elasticsearch
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java
index 658b2caee8..b6f8e77b03 100644
--- a/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java
+++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java
@@ -120,9 +120,6 @@ class RequestConverter extends AbstractQueryProcessor {
private static final Log LOGGER = LogFactory.getLog(RequestConverter.class);
- // the default max result window size of Elasticsearch
- public static final Integer INDEX_MAX_RESULT_WINDOW = 10_000;
-
protected final JsonpMapper jsonpMapper;
protected final ElasticsearchConverter elasticsearchConverter;
@@ -1039,18 +1036,7 @@ public DeleteByQueryRequest documentDeleteByQueryRequest(DeleteQuery query, @Nul
List sortOptions = getSortOptions(query.getSort(), persistentEntity);
if (!sortOptions.isEmpty()) {
- dqb.sort(
- sortOptions.stream()
- .map(sortOption -> {
- String order = "asc";
- var sortField = sortOption.field();
- if (sortField.order() != null) {
- order = sortField.order().jsonValue();
- }
-
- return sortField.field() + ':' + order;
- })
- .collect(Collectors.toList()));
+ dqb.sort(sortOptions);
}
}
if (query.getRefresh() != null) {
@@ -1295,15 +1281,8 @@ public MsearchRequest searchMsearchRequest(
.timeout(timeStringMs(query.getTimeout())) //
;
- 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);
+ bb.from((int) (query.getPageable().isPaged() ? query.getPageable().getOffset() : 0))
+ .size(query.getRequestSize());
if (!isEmpty(query.getFields())) {
bb.fields(fb -> {
@@ -1473,14 +1452,8 @@ private void prepareSearchRequest(Query query, @Nullable String routing, @Nu
builder.seqNoPrimaryTerm(true);
}
- 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);
+ builder.from((int) (query.getPageable().isPaged() ? query.getPageable().getOffset() : 0))
+ .size(query.getRequestSize());
if (!isEmpty(query.getFields())) {
var fieldAndFormats = query.getFields().stream().map(field -> FieldAndFormat.of(b -> b.field(field))).toList();
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/TypeUtils.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/TypeUtils.java
index 43747f3a7a..5ff3272977 100644
--- a/src/main/java/org/springframework/data/elasticsearch/client/elc/TypeUtils.java
+++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/TypeUtils.java
@@ -436,7 +436,7 @@ static Integer waitForActiveShardsCount(@Nullable String value) {
// values taken from the RHLC implementation
if (value == null) {
return -2;
- } else if ("all".equals(value.toUpperCase())) {
+ } else if ("all".equals(value.toLowerCase())) {
return -1;
} else {
try {
diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQuery.java
index a174511c44..71b16467e2 100755
--- a/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQuery.java
+++ b/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQuery.java
@@ -27,6 +27,7 @@
import java.util.Optional;
import java.util.stream.Collectors;
+import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
@@ -47,10 +48,15 @@
*/
public class BaseQuery implements Query {
+ public static final int INDEX_MAX_RESULT_WINDOW = 10_000;
+
private static final int DEFAULT_REACTIVE_BATCH_SIZE = 500;
+ // the instance to mark the query pageable initial status, needed to distinguish between the initial
+ // value and a user-set unpaged value; values don't matter, the RequestConverter compares to the isntance.
+ private static final Pageable UNSET_PAGE = PageRequest.of(0, 1);
@Nullable protected Sort sort;
- protected Pageable pageable = DEFAULT_PAGE;
+ protected Pageable pageable = UNSET_PAGE;
protected List fields = new ArrayList<>();
@Nullable protected List storedFields;
@Nullable protected SourceFilter sourceFilter;
@@ -78,7 +84,7 @@ public class BaseQuery implements Query {
private boolean queryIsUpdatedByConverter = false;
@Nullable private Integer reactiveBatchSize = null;
@Nullable private Boolean allowNoIndices = null;
- private EnumSet expandWildcards;
+ private EnumSet expandWildcards = EnumSet.noneOf(IndicesOptions.WildcardStates.class);
private List docValueFields = new ArrayList<>();
private List scriptedFields = new ArrayList<>();
@@ -87,7 +93,7 @@ public BaseQuery() {}
public > BaseQuery(BaseQueryBuilder builder) {
this.sort = builder.getSort();
// do a setPageable after setting the sort, because the pageable may contain an additional sort
- this.setPageable(builder.getPageable() != null ? builder.getPageable() : DEFAULT_PAGE);
+ this.setPageable(builder.getPageable() != null ? builder.getPageable() : UNSET_PAGE);
this.fields = builder.getFields();
this.storedFields = builder.getStoredFields();
this.sourceFilter = builder.getSourceFilter();
@@ -203,7 +209,7 @@ public SourceFilter getSourceFilter() {
@Override
@SuppressWarnings("unchecked")
public final T addSort(@Nullable Sort sort) {
- if (sort == null) {
+ if (sort == null || sort.isUnsorted()) {
return (T) this;
}
@@ -561,4 +567,52 @@ public void addScriptedField(ScriptedField scriptedField) {
public List getScriptedFields() {
return scriptedFields;
}
+
+ @Override
+ public Integer getRequestSize() {
+
+ var pageable = getPageable();
+ Integer requestSize = null;
+
+ if (pageable.isPaged() && pageable != UNSET_PAGE) {
+ // pagesize defined by the user
+ if (!isLimiting()) {
+ // no maxResults
+ requestSize = pageable.getPageSize();
+ } else {
+ // 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.
+ requestSize = Math.min(pageable.getPageSize(), getMaxResults());
+ }
+ } else if (pageable == UNSET_PAGE) {
+ // no user defined pageable
+ if (isLimiting()) {
+ // maxResults
+ requestSize = getMaxResults();
+ } else {
+ requestSize = DEFAULT_PAGE_SIZE;
+ }
+ } else {
+ // explicitly set unpaged
+ if (!isLimiting()) {
+ // no maxResults
+ requestSize = INDEX_MAX_RESULT_WINDOW;
+ } else {
+ // if we have both a implicit 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.
+ requestSize = Math.min(INDEX_MAX_RESULT_WINDOW, getMaxResults());
+ }
+ }
+
+ if (requestSize == null) {
+ // this should not happen
+ requestSize = DEFAULT_PAGE_SIZE;
+ }
+
+ return requestSize;
+ }
}
diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java b/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java
index d398d7ee1f..0a46dcd793 100644
--- a/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java
+++ b/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java
@@ -61,12 +61,16 @@ public class Criteria {
private float boost = Float.NaN;
private boolean negating = false;
- private final CriteriaChain criteriaChain = new CriteriaChain();
- private final Set queryCriteriaEntries = new LinkedHashSet<>();
- private final Set filterCriteriaEntries = new LinkedHashSet<>();
- private final Set subCriteria = new LinkedHashSet<>();
+ // we cash this and recalculate when properties used in equals change
+ // see https://github.com/spring-projects/spring-data-elasticsearch/issues/3083
+ private int hashCode;
- // region criteria creation
+ private final CriteriaChain criteriaChain = new CriteriaChain();
+ private final Set queryCriteriaEntries = new LinkedHashSet<>();
+ private final Set filterCriteriaEntries = new LinkedHashSet<>();
+ private final Set subCriteria = new LinkedHashSet<>();
+
+ // region criteria creation
/**
* @return factory method to create an and-Criteria that is not bound to a field
@@ -84,7 +88,9 @@ public static Criteria or() {
return new OrCriteria();
}
- public Criteria() {}
+ public Criteria() {
+ recalculateHashCode();
+ }
/**
* Creates a new Criteria with provided field name
@@ -107,6 +113,7 @@ public Criteria(Field field) {
this.field = field;
this.criteriaChain.add(this);
+ recalculateHashCode();
}
/**
@@ -136,6 +143,7 @@ protected Criteria(List criteriaChain, Field field) {
this.field = field;
this.criteriaChain.addAll(criteriaChain);
this.criteriaChain.add(this);
+ recalculateHashCode();
}
/**
@@ -189,6 +197,7 @@ public List getCriteriaChain() {
*/
public Criteria not() {
this.negating = true;
+ recalculateHashCode();
return this;
}
@@ -207,6 +216,7 @@ public Criteria boost(float boost) {
Assert.isTrue(boost >= 0, "boost must not be negative");
this.boost = boost;
+ recalculateHashCode();
return this;
}
@@ -223,7 +233,7 @@ public boolean isOr() {
}
/**
- * @return the set ob subCriteria
+ * @return the set of subCriteria
* @since 4.1
*/
public Set getSubCriteria() {
@@ -264,6 +274,7 @@ public Criteria and(Criteria criteria) {
Assert.notNull(criteria, "Cannot chain 'null' criteria.");
this.criteriaChain.add(criteria);
+ recalculateHashCode();
return this;
}
@@ -278,6 +289,7 @@ public Criteria and(Criteria... criterias) {
Assert.notNull(criterias, "Cannot chain 'null' criterias.");
this.criteriaChain.addAll(Arrays.asList(criterias));
+ recalculateHashCode();
return this;
}
@@ -320,6 +332,7 @@ public Criteria or(Criteria criteria) {
orCriteria.subCriteria.addAll(criteria.subCriteria);
orCriteria.boost = criteria.boost;
orCriteria.negating = criteria.isNegating();
+ orCriteria.recalculateHashCode();
return orCriteria;
}
@@ -335,6 +348,7 @@ public Criteria subCriteria(Criteria criteria) {
Assert.notNull(criteria, "criteria must not be null");
subCriteria.add(criteria);
+ recalculateHashCode();
return this;
}
@@ -349,6 +363,7 @@ public Criteria subCriteria(Criteria criteria) {
*/
public Criteria is(Object o) {
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.EQUALS, o));
+ recalculateHashCode();
return this;
}
@@ -360,6 +375,7 @@ public Criteria is(Object o) {
*/
public Criteria exists() {
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.EXISTS));
+ recalculateHashCode();
return this;
}
@@ -378,6 +394,7 @@ public Criteria between(@Nullable Object lowerBound, @Nullable Object upperBound
}
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.BETWEEN, new Object[] { lowerBound, upperBound }));
+ recalculateHashCode();
return this;
}
@@ -393,6 +410,7 @@ public Criteria startsWith(String s) {
assertNoBlankInWildcardQuery(s, false, true);
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.STARTS_WITH, s));
+ recalculateHashCode();
return this;
}
@@ -409,6 +427,7 @@ public Criteria contains(String s) {
assertNoBlankInWildcardQuery(s, true, true);
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.CONTAINS, s));
+ recalculateHashCode();
return this;
}
@@ -425,6 +444,7 @@ public Criteria endsWith(String s) {
assertNoBlankInWildcardQuery(s, true, false);
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.ENDS_WITH, s));
+ recalculateHashCode();
return this;
}
@@ -452,6 +472,7 @@ public Criteria in(Iterable> values) {
Assert.notNull(values, "Collection of 'in' values must not be null");
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.IN, values));
+ recalculateHashCode();
return this;
}
@@ -478,6 +499,7 @@ public Criteria notIn(Iterable> values) {
Assert.notNull(values, "Collection of 'NotIn' values must not be null");
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.NOT_IN, values));
+ recalculateHashCode();
return this;
}
@@ -490,6 +512,7 @@ public Criteria notIn(Iterable> values) {
*/
public Criteria expression(String s) {
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.EXPRESSION, s));
+ recalculateHashCode();
return this;
}
@@ -501,6 +524,7 @@ public Criteria expression(String s) {
*/
public Criteria fuzzy(String s) {
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.FUZZY, s));
+ recalculateHashCode();
return this;
}
@@ -515,6 +539,7 @@ public Criteria lessThanEqual(Object upperBound) {
Assert.notNull(upperBound, "upperBound must not be null");
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.LESS_EQUAL, upperBound));
+ recalculateHashCode();
return this;
}
@@ -529,6 +554,7 @@ public Criteria lessThan(Object upperBound) {
Assert.notNull(upperBound, "upperBound must not be null");
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.LESS, upperBound));
+ recalculateHashCode();
return this;
}
@@ -543,6 +569,7 @@ public Criteria greaterThanEqual(Object lowerBound) {
Assert.notNull(lowerBound, "lowerBound must not be null");
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.GREATER_EQUAL, lowerBound));
+ recalculateHashCode();
return this;
}
@@ -557,6 +584,7 @@ public Criteria greaterThan(Object lowerBound) {
Assert.notNull(lowerBound, "lowerBound must not be null");
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.GREATER, lowerBound));
+ recalculateHashCode();
return this;
}
@@ -573,6 +601,7 @@ public Criteria matches(Object value) {
Assert.notNull(value, "value must not be null");
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.MATCHES, value));
+ recalculateHashCode();
return this;
}
@@ -589,6 +618,7 @@ public Criteria matchesAll(Object value) {
Assert.notNull(value, "value must not be null");
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.MATCHES_ALL, value));
+ recalculateHashCode();
return this;
}
@@ -601,6 +631,7 @@ public Criteria matchesAll(Object value) {
public Criteria empty() {
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.EMPTY));
+ recalculateHashCode();
return this;
}
@@ -613,6 +644,7 @@ public Criteria empty() {
public Criteria notEmpty() {
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.NOT_EMPTY));
+ recalculateHashCode();
return this;
}
@@ -628,6 +660,7 @@ public Criteria regexp(String value) {
Assert.notNull(value, "value must not be null");
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.REGEXP, value));
+ recalculateHashCode();
return this;
}
@@ -646,6 +679,7 @@ public Criteria boundedBy(GeoBox boundingBox) {
Assert.notNull(boundingBox, "boundingBox value for boundedBy criteria must not be null");
filterCriteriaEntries.add(new CriteriaEntry(OperationKey.BBOX, new Object[] { boundingBox }));
+ recalculateHashCode();
return this;
}
@@ -662,6 +696,7 @@ public Criteria boundedBy(Box boundingBox) {
filterCriteriaEntries
.add(new CriteriaEntry(OperationKey.BBOX, new Object[] { boundingBox.getFirst(), boundingBox.getSecond() }));
+ recalculateHashCode();
return this;
}
@@ -679,6 +714,7 @@ public Criteria boundedBy(String topLeftGeohash, String bottomRightGeohash) {
filterCriteriaEntries
.add(new CriteriaEntry(OperationKey.BBOX, new Object[] { topLeftGeohash, bottomRightGeohash }));
+ recalculateHashCode();
return this;
}
@@ -695,6 +731,7 @@ public Criteria boundedBy(GeoPoint topLeftPoint, GeoPoint bottomRightPoint) {
Assert.notNull(bottomRightPoint, "bottomRightPoint must not be null");
filterCriteriaEntries.add(new CriteriaEntry(OperationKey.BBOX, new Object[] { topLeftPoint, bottomRightPoint }));
+ recalculateHashCode();
return this;
}
@@ -712,6 +749,7 @@ public Criteria boundedBy(Point topLeftPoint, Point bottomRightPoint) {
filterCriteriaEntries.add(new CriteriaEntry(OperationKey.BBOX,
new Object[] { GeoPoint.fromPoint(topLeftPoint), GeoPoint.fromPoint(bottomRightPoint) }));
+ recalculateHashCode();
return this;
}
@@ -729,6 +767,7 @@ public Criteria within(GeoPoint location, String distance) {
Assert.notNull(location, "Distance value for near criteria must not be null");
filterCriteriaEntries.add(new CriteriaEntry(OperationKey.WITHIN, new Object[] { location, distance }));
+ recalculateHashCode();
return this;
}
@@ -745,6 +784,7 @@ public Criteria within(Point location, Distance distance) {
Assert.notNull(location, "Distance value for near criteria must not be null");
filterCriteriaEntries.add(new CriteriaEntry(OperationKey.WITHIN, new Object[] { location, distance }));
+ recalculateHashCode();
return this;
}
@@ -761,6 +801,7 @@ public Criteria within(String geoLocation, String distance) {
Assert.isTrue(StringUtils.hasLength(geoLocation), "geoLocation value must not be null");
filterCriteriaEntries.add(new CriteriaEntry(OperationKey.WITHIN, new Object[] { geoLocation, distance }));
+ recalculateHashCode();
return this;
}
@@ -775,6 +816,7 @@ public Criteria intersects(GeoJson> geoShape) {
Assert.notNull(geoShape, "geoShape must not be null");
filterCriteriaEntries.add(new CriteriaEntry(OperationKey.GEO_INTERSECTS, geoShape));
+ recalculateHashCode();
return this;
}
@@ -789,6 +831,7 @@ public Criteria isDisjoint(GeoJson> geoShape) {
Assert.notNull(geoShape, "geoShape must not be null");
filterCriteriaEntries.add(new CriteriaEntry(OperationKey.GEO_IS_DISJOINT, geoShape));
+ recalculateHashCode();
return this;
}
@@ -802,6 +845,7 @@ public Criteria within(GeoJson> geoShape) {
Assert.notNull(geoShape, "geoShape must not be null");
filterCriteriaEntries.add(new CriteriaEntry(OperationKey.GEO_WITHIN, geoShape));
+ recalculateHashCode();
return this;
}
@@ -815,6 +859,7 @@ public Criteria contains(GeoJson> geoShape) {
Assert.notNull(geoShape, "geoShape must not be null");
filterCriteriaEntries.add(new CriteriaEntry(OperationKey.GEO_CONTAINS, geoShape));
+ recalculateHashCode();
return this;
}
@@ -828,6 +873,7 @@ public Criteria hasChild(HasChildQuery query) {
Assert.notNull(query, "has_child query must not be null.");
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.HAS_CHILD, query));
+ recalculateHashCode();
return this;
}
@@ -841,6 +887,7 @@ public Criteria hasParent(HasParentQuery query) {
Assert.notNull(query, "has_parent query must not be null.");
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.HAS_PARENT, query));
+ recalculateHashCode();
return this;
}
// endregion
@@ -887,18 +934,22 @@ public boolean equals(Object o) {
@Override
public int hashCode() {
- 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;
- }
+ return hashCode;
+ }
+
+ private void recalculateHashCode() {
+ 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();
+ this.hashCode = result;
+ }
// endregion
@Override
diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java b/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java
index 2a0dd17b7b..d8d2b3262a 100644
--- a/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java
+++ b/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java
@@ -484,6 +484,13 @@ default Integer getReactiveBatchSize() {
*/
List getScriptedFields();
+ /**
+ * @return the number of documents that should be requested from Elasticsearch in this query. Depends wether a
+ * Pageable and/or maxResult size is set on the query.
+ * @since 5.4.8 5.5.2
+ */
+ public Integer getRequestSize();
+
/**
* @since 4.3
*/
diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt
index 7f6d7c5823..85e26f2351 100644
--- a/src/main/resources/notice.txt
+++ b/src/main/resources/notice.txt
@@ -1,4 +1,4 @@
-Spring Data Elasticsearch 5.5 GA (2025.0.0)
+Spring Data Elasticsearch 5.5.6 (2025.0.6)
Copyright (c) [2013-2022] Pivotal Software, Inc.
This product is licensed to you under the Apache License, Version 2.0 (the "License").
@@ -22,6 +22,12 @@ conditions of the subcomponent's license, as noted in the LICENSE file.
+
+
+
+
+
+
diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryMappingUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryMappingUnitTests.java
index e53a33df67..7d40f824cb 100644
--- a/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryMappingUnitTests.java
+++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryMappingUnitTests.java
@@ -448,7 +448,7 @@ void shouldMapNamesInSourceStoredFields() {
}
// the following test failed because of a wrong implementation in Criteria
- // equals and hscode methods.
+ // equals and hashcode methods.
@Test // #3083
@DisplayName("should map correct subcriteria")
void shouldMapCorrectSubcriteria() throws JSONException {
diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchIntegrationTests.java
index 98449b4cce..55aa1190f5 100755
--- a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchIntegrationTests.java
+++ b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchIntegrationTests.java
@@ -105,8 +105,6 @@
@SpringIntegrationTest
public abstract class ElasticsearchIntegrationTests {
- static final Integer INDEX_MAX_RESULT_WINDOW = 10_000;
-
private static final String MULTI_INDEX_PREFIX = "test-index";
private static final String MULTI_INDEX_ALL = MULTI_INDEX_PREFIX + "*";
private static final String MULTI_INDEX_1_NAME = MULTI_INDEX_PREFIX + "-1";
diff --git a/src/test/java/org/springframework/data/elasticsearch/core/query/BaseQueryTests.java b/src/test/java/org/springframework/data/elasticsearch/core/query/BaseQueryTests.java
new file mode 100644
index 0000000000..946d710f47
--- /dev/null
+++ b/src/test/java/org/springframework/data/elasticsearch/core/query/BaseQueryTests.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2025 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;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.springframework.data.elasticsearch.core.query.BaseQuery.*;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.data.domain.Pageable;
+
+class BaseQueryTests {
+
+ private static final String MATCH_ALL_QUERY = "{\"match_all\":{}}";
+
+ @Test // #3127
+ @DisplayName("query with no Pageable and no maxResults requests 10 docs from 0")
+ void queryWithNoPageableAndNoMaxResultsRequests10DocsFrom0() {
+
+ var query = StringQuery.builder(MATCH_ALL_QUERY)
+ .build();
+
+ var requestSize = query.getRequestSize();
+
+ assertThat(requestSize).isEqualTo(10);
+ }
+
+ @Test // #3127
+ @DisplayName("query with a Pageable and no MaxResults request with values from Pageable")
+ void queryWithAPageableAndNoMaxResultsRequestWithValuesFromPageable() {
+ var query = StringQuery.builder(MATCH_ALL_QUERY)
+ .withPageable(Pageable.ofSize(42))
+ .build();
+
+ var requestSize = query.getRequestSize();
+
+ assertThat(requestSize).isEqualTo(42);
+ }
+
+ @Test // #3127
+ @DisplayName("query with no Pageable and maxResults requests maxResults")
+ void queryWithNoPageableAndMaxResultsRequestsMaxResults() {
+
+ var query = StringQuery.builder(MATCH_ALL_QUERY)
+ .withMaxResults(12_345)
+ .build();
+
+ var requestSize = query.getRequestSize();
+
+ assertThat(requestSize).isEqualTo(12_345);
+ }
+
+ @Test // #3127
+ @DisplayName("query with Pageable and maxResults requests with values from Pageable if Pageable is less than maxResults")
+ void queryWithPageableAndMaxResultsRequestsWithValuesFromPageableIfPageableIsLessThanMaxResults() {
+
+ var query = StringQuery.builder(MATCH_ALL_QUERY)
+ .withPageable(Pageable.ofSize(42))
+ .withMaxResults(123)
+ .build();
+
+ var requestSize = query.getRequestSize();
+
+ assertThat(requestSize).isEqualTo(42);
+ }
+
+ @Test // #3127
+ @DisplayName("query with Pageable and maxResults requests with values from maxResults if Pageable is more than maxResults")
+ void queryWithPageableAndMaxResultsRequestsWithValuesFromMaxResultsIfPageableIsMoreThanMaxResults() {
+
+ var query = StringQuery.builder(MATCH_ALL_QUERY)
+ .withPageable(Pageable.ofSize(420))
+ .withMaxResults(123)
+ .build();
+
+ var requestSize = query.getRequestSize();
+
+ assertThat(requestSize).isEqualTo(123);
+ }
+
+ @Test // #3127
+ @DisplayName("query with explicit unpaged request and no maxResults requests max request window size")
+ void queryWithExplicitUnpagedRequestAndNoMaxResultsRequestsMaxRequestWindowSize() {
+
+ var query = StringQuery.builder(MATCH_ALL_QUERY)
+ .withPageable(Pageable.unpaged())
+ .build();
+
+ var requestSize = query.getRequestSize();
+
+ assertThat(requestSize).isEqualTo(INDEX_MAX_RESULT_WINDOW);
+ }
+}
diff --git a/src/test/java/org/springframework/data/elasticsearch/core/query/CriteriaTest.java b/src/test/java/org/springframework/data/elasticsearch/core/query/CriteriaTest.java
new file mode 100644
index 0000000000..2e8c264b31
--- /dev/null
+++ b/src/test/java/org/springframework/data/elasticsearch/core/query/CriteriaTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2019-2025 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;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Peter-Josef Meisch
+ */
+class CriteriaTest {
+
+ @Test // #3159
+ @DisplayName("should not slow down on calculating hashcode for long criteria chains")
+ void shouldNotSlowDownOnCalculatingHashcodeForLongCriteriaChains() {
+ assertTimeoutPreemptively(Duration.of(1, ChronoUnit.SECONDS), () -> {
+ var criteria = new Criteria();
+ var size = 1000;
+ for (int i = 1; i <= size; i++) {
+ criteria = criteria.or("field-" + i).contains("value-" + i);
+ }
+ final var criteriaChain = criteria.getCriteriaChain();
+ assertEquals(size, criteriaChain.size());
+ final var hashCode = Integer.valueOf(criteria.hashCode());
+ });
+ }
+}
diff --git a/src/test/resources/testcontainers-elasticsearch.properties b/src/test/resources/testcontainers-elasticsearch.properties
index 6860b4150e..5d58b4c889 100644
--- a/src/test/resources/testcontainers-elasticsearch.properties
+++ b/src/test/resources/testcontainers-elasticsearch.properties
@@ -15,7 +15,7 @@
#
#
sde.testcontainers.image-name=docker.elastic.co/elasticsearch/elasticsearch
-sde.testcontainers.image-version=8.18.1
+sde.testcontainers.image-version=8.18.8
#
#
# 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