Compare commits

..

57 Commits

Author SHA1 Message Date
Peter-Josef Meisch
6697755b45
Upgrade Elasticsearch to 9.1.0.
Original Pull Request #3147
Closes #3145
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-08-06 20:52:04 +02:00
Peter-Josef Meisch
e49bb63df4
Fix handling of ResponseException from legacy client dependency.
Original Pull Request #3146
Closes #3144

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-08-04 20:50:47 +02:00
Peter-Josef Meisch
006cda6de6
Adjust Rest5Client building by using and exposing the callbacks provided by the Elasticsearch Java client library.
Original Pull Request #3143
Closes #3129

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-08-02 18:33:04 +02:00
Peter-Josef Meisch
f51efa2cad
Cleanup nullability issues
Original Pull Request #3142
Closes #3141
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-08-02 13:20:28 +02:00
Peter-Josef Meisch
6e30801a59
Upgrade to Elasticsearch 9.0.4
Original Pull Request #3140
Closes #3139

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-08-02 07:49:32 +02:00
Peter-Josef Meisch
e64d0ada62
Polishing
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-07-22 18:36:04 +02:00
Anton
7f7cf3f52e
Fixed PrimaryTerm/SeqNo datatypes in UpdateQuery.
Original Pull Request #3137
Closes #3136

Signed-off-by: Anton Buz <buzdalkin.a@vlprojects.pro>
2025-07-22 18:35:43 +02:00
Mark Paluch
55d470fc91
After release cleanups.
See #3115
2025-07-18 13:14:47 +02:00
Mark Paluch
b7290b5d9c
Prepare next development iteration.
See #3115
2025-07-18 13:14:46 +02:00
Mark Paluch
40414f0bde
Release version 6.0 M4 (2025.1.0).
See #3115
2025-07-18 13:12:28 +02:00
Mark Paluch
cb71caa504
Prepare 6.0 M4 (2025.1.0).
See #3115
2025-07-18 13:12:11 +02:00
Mark Paluch
ed9abce7af
Upgrade to Maven Wrapper 3.9.11.
See #3130
2025-07-17 13:59:56 +02:00
Peter-Josef Meisch
12ddb74fae
Fix the calculation of the requested number of documents.
Original Pull Request #3128
Closes #3127

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-07-15 18:28:51 +02:00
Peter-Josef Meisch
6324b72707
Use the new Rest5Client as default, provide the old RestClient as optional.
Original Pull Request: #3125
Closes #3117

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-06-25 20:09:00 +02:00
Peter-Josef Meisch
6e49980c7c
Upgrade to Elasticsearch 9.0.3.
Original Pull Request #3124
Closes #3123
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-06-25 17:24:08 +02:00
Peter-Josef Meisch
f9509f2696
Upgrade to Elasticsearch 9.0.2.
Original Pull Request #3122
Closes #3121
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-06-14 17:30:22 +02:00
Mark Paluch
7f53944e1b
Adapt to generics changes in CoroutineCrudRepository.
Closes #3118
2025-06-12 09:14:22 +02:00
Peter-Josef Meisch
a9d2aaa93d
Polishing.
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-05-29 18:10:48 +02:00
Laura Trotta
158f5fc342
es java client major update.
Original Pull Request #3116
Closes #3110
Signed-off-by: Laura Trotta <laura.trotta@elastic.co>
2025-05-29 15:56:35 +02:00
Peter-Josef Meisch
6268133506
Update versions documentation
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-05-24 14:58:04 +02:00
Mark Paluch
923787c2d1
After release cleanups.
See #3097
2025-05-16 14:18:48 +02:00
Mark Paluch
62fcbd44fa
Prepare next development iteration.
See #3097
2025-05-16 14:18:47 +02:00
Mark Paluch
77ba620fc6
Release version 6.0 M3 (2025.1.0).
See #3097
2025-05-16 14:16:18 +02:00
Mark Paluch
06704d974d
Prepare 6.0 M3 (2025.1.0).
See #3097
2025-05-16 14:15:56 +02:00
Mark Paluch
897cb0a957
Add optional Querydsl dependency.
Closes #3107
2025-05-16 12:18:51 +02:00
Mark Paluch
a8557a36dc
Update CI Properties.
See #3097
2025-05-16 12:18:51 +02:00
Mark Paluch
2678cdc7b6
After release cleanups.
See #3047
2025-05-16 12:18:51 +02:00
Mark Paluch
eb42312ebe
Prepare next development iteration.
See #3047
2025-05-16 12:18:51 +02:00
Mark Paluch
af13fe0247
Release version 6.0 M2 (2025.1.0).
See #3047
2025-05-16 12:18:51 +02:00
Mark Paluch
e81810c0d7
Prepare 6.0 M2 (2025.1.0).
See #3047
2025-05-16 12:18:51 +02:00
Peter-Josef Meisch
e9c7c0ee95
Switch to jspecify nullability annotations.
Original Pull Request #3065
Closes #2984

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-05-16 12:18:50 +02:00
Peter-Josef Meisch
3a4425053e
Cleanup unneeded imports after deprecation removal
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-05-16 12:18:50 +02:00
Christoph Strobl
09984f86e6
After release cleanups.
See #3006
2025-05-16 12:18:50 +02:00
Christoph Strobl
710526c5f6
Prepare next development iteration.
See #3006
2025-05-16 12:18:50 +02:00
Christoph Strobl
1acd392af7
Release version 6.0 M1 (2025.1.0).
See #3006
2025-05-16 12:18:50 +02:00
Christoph Strobl
76fe240a24
Prepare 6.0 M1 (2025.1.0).
See #3006
2025-05-16 12:18:50 +02:00
Mark Paluch
e298bc9f7a
Adopt to changes in Spring Framework 7.
See #3038
2025-05-16 12:18:50 +02:00
Peter-Josef Meisch
49d5dee5aa
Update versions documentation
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-05-16 12:18:49 +02:00
Mark Paluch
df6a127629
After release cleanups.
See #3096
2025-05-16 12:18:48 +02:00
Mark Paluch
08a1ef3a28
Prepare next development iteration.
See #3096
2025-05-16 11:31:15 +02:00
Mark Paluch
62a34cf09c
Release version 5.5 GA (2025.0.0).
See #3096
2025-05-16 11:28:31 +02:00
Mark Paluch
cc5f149c5a
Prepare 5.5 GA (2025.0.0).
See #3096
2025-05-16 11:28:10 +02:00
Peter-Josef Meisch
0728c8e4aa
Update versions doc for the next release
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-05-15 17:39:17 +02:00
Peter-Josef Meisch
ebbe242a72
Fix missing return value in ByQueryResponse.
Original Pull Request #3109
Closes #3108

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-05-14 13:53:58 +02:00
Mark Paluch
0ce9a1c400
Update CI Properties.
See #3096
2025-05-12 09:33:06 +02:00
Mark Paluch
22763d17a7
Update CI Properties.
See #3096
2025-05-12 09:00:23 +02:00
Mark Paluch
9870de1e77
Update CI Properties.
See #3096
2025-05-12 08:56:22 +02:00
Mark Paluch
8c9d9ae1e7
Update CI Properties.
See #3096
2025-05-12 08:55:54 +02:00
Peter-Josef Meisch
945179e4eb
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>
2025-05-10 21:21:17 +02:00
Peter-Josef Meisch
ea38ef1d41
Upgrade Elasticsearch libraries to 8.18.1.
Original Pull Request #3105
Closes #3103

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-05-09 20:35:30 +02:00
Mark Paluch
acbfba94ac
Exclude commons-logging dependency.
`elasticsearch-rest-client` pulls in `commons-logging` and we don't want that to happen as it conflicts with our dependency setup.

Closes #3104
2025-05-09 12:10:07 +02:00
Peter-Josef Meisch
5a0f556a3b
Upgrade Elasticsearch dependencies to 8.18.0
Original Pull request #3101
Adjust to changes in Elasticsearch.
Closes #3100

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-05-02 10:21:24 +02:00
Peter-Josef Meisch
a07ac3c93d
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>
2025-04-26 10:40:31 +02:00
Mark Paluch
9d025dd469
After release cleanups.
See #3079
2025-04-22 11:41:58 +02:00
Mark Paluch
925921f174
Prepare next development iteration.
See #3079
2025-04-22 11:41:58 +02:00
Mark Paluch
2f0a259045
Release version 5.5 RC1 (2025.0.0).
See #3079
2025-04-22 11:41:58 +02:00
Mark Paluch
9ffcb092db
Prepare 5.5 RC1 (2025.0.0).
See #3079
2025-04-22 11:41:58 +02:00
107 changed files with 2695 additions and 892 deletions

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

View File

@ -1,3 +1,3 @@
#Thu Nov 07 09:47:28 CET 2024
#Thu Jul 17 13:59:56 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

2
Jenkinsfile vendored
View File

@ -9,7 +9,7 @@ pipeline {
triggers {
pollSCM 'H/10 * * * *'
upstream(upstreamProjects: "spring-data-commons/4.0.x", threshold: hudson.model.Result.SUCCESS)
upstream(upstreamProjects: "spring-data-commons/main", threshold: hudson.model.Result.SUCCESS)
}
options {

View File

@ -1,25 +1,20 @@
# Java versions
java.main.tag=17.0.13_11-jdk-focal
java.next.tag=23.0.1_11-jdk-noble
java.main.tag=24.0.1_9-jdk-noble
java.next.tag=24.0.1_9-jdk-noble
# Docker container images - standard
docker.java.main.image=library/eclipse-temurin:${java.main.tag}
docker.java.next.image=library/eclipse-temurin:${java.next.tag}
# Supported versions of MongoDB
docker.mongodb.4.4.version=4.4.25
docker.mongodb.5.0.version=5.0.21
docker.mongodb.6.0.version=6.0.10
docker.mongodb.7.0.version=7.0.2
docker.mongodb.8.0.version=8.0.0
docker.mongodb.6.0.version=6.0.23
docker.mongodb.7.0.version=7.0.20
docker.mongodb.8.0.version=8.0.9
# Supported versions of Redis
docker.redis.6.version=6.2.13
docker.redis.7.version=7.2.4
# Supported versions of Cassandra
docker.cassandra.3.version=3.11.16
# Docker environment settings
docker.java.inside.basic=-v $HOME:/tmp/jenkins-home
docker.java.inside.docker=-u root -v /var/run/docker.sock:/var/run/docker.sock -v /usr/bin/docker:/usr/bin/docker -v $HOME:/tmp/jenkins-home

47
pom.xml
View File

@ -1,16 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>6.0.0-M2</version>
<version>6.0.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<version>4.0.0-M2</version>
<version>4.0.0-SNAPSHOT</version>
</parent>
<name>Spring Data Elasticsearch</name>
@ -18,10 +18,10 @@
<url>https://github.com/spring-projects/spring-data-elasticsearch</url>
<properties>
<springdata.commons>4.0.0-M2</springdata.commons>
<springdata.commons>4.0.0-SNAPSHOT</springdata.commons>
<!-- version of the ElasticsearchClient -->
<elasticsearch-java>8.17.4</elasticsearch-java>
<elasticsearch-java>9.1.0</elasticsearch-java>
<hoverfly>0.19.0</hoverfly>
<log4j>2.23.1</log4j>
@ -132,6 +132,27 @@
</exclusions>
</dependency>
<!-- the old RestCLient is an optional dependency for user that still want to use it-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>${elasticsearch-java}</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-core</artifactId>
<version>${querydsl}</version>
<optional>true</optional>
</dependency>
<!-- Jackson JSON Mapper -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
@ -450,8 +471,20 @@
</profiles>
<repositories>
<repository>
<id>spring-snapshot</id>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<id>spring-milestone</id>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
</project>

View File

@ -12,6 +12,7 @@
*** xref:migration-guides/migration-guide-5.2-5.3.adoc[]
*** xref:migration-guides/migration-guide-5.3-5.4.adoc[]
*** xref:migration-guides/migration-guide-5.4-5.5.adoc[]
*** xref:migration-guides/migration-guide-5.5-6.0.adoc[]
* xref:elasticsearch.adoc[]

View File

@ -6,10 +6,10 @@ This chapter illustrates configuration and usage of supported Elasticsearch clie
Spring Data Elasticsearch operates upon an Elasticsearch client (provided by Elasticsearch client libraries) that is connected to a single Elasticsearch node or a cluster.
Although the Elasticsearch Client can be used directly to work with the cluster, applications using Spring Data Elasticsearch normally use the higher level abstractions of xref:elasticsearch/template.adoc[Elasticsearch Operations] and xref:elasticsearch/repositories/elasticsearch-repositories.adoc[Elasticsearch Repositories].
[[elasticsearch.clients.restclient]]
== Imperative Rest Client
[[elasticsearch.clients.rest5client]]
== Imperative Rest5Client
To use the imperative (non-reactive) client, a configuration bean must be configured like this:
To use the imperative (non-reactive) Rest5Client, a configuration bean must be configured like this:
====
[source,java]
@ -31,7 +31,7 @@ public class MyClientConfig extends ElasticsearchConfiguration {
<.> for a detailed description of the builder methods see xref:elasticsearch/clients.adoc#elasticsearch.clients.configuration[Client Configuration]
====
The javadoc:org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration[]] class allows further configuration by overriding for example the `jsonpMapper()` or `transportOptions()` methods.
The javadoc:org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration[] class allows further configuration by overriding for example the `jsonpMapper()` or `transportOptions()` methods.
The following beans can then be injected in other Spring components:
@ -46,7 +46,81 @@ ElasticsearchOperations operations; <.>
ElasticsearchClient elasticsearchClient; <.>
@Autowired
RestClient restClient; <.>
Rest5Client rest5Client; <.>
@Autowired
JsonpMapper jsonpMapper; <.>
----
<.> an implementation of javadoc:org.springframework.data.elasticsearch.core.ElasticsearchOperations[]
<.> the `co.elastic.clients.elasticsearch.ElasticsearchClient` that is used.
<.> the low level `Rest5Client` from the Elasticsearch libraries
<.> the `JsonpMapper` user by the Elasticsearch `Transport`
====
Basically one should just use the javadoc:org.springframework.data.elasticsearch.core.ElasticsearchOperations[] to interact with the Elasticsearch cluster.
When using repositories, this instance is used under the hood as well.
[[elasticsearch.clients.restclient]]
== Deprecated Imperative RestClient
To use the imperative (non-reactive) RestClient - deprecated since version 6 - , the following dependency needs to be added, adapt the correct version. The exclusion is needed in a Spring Boot application:
====
[source,xml]
----
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>${elasticsearch-client.version}</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
----
====
The configuration bean must be configured like this:
====
[source,java]
----
import org.springframework.data.elasticsearch.client.elc.ElasticsearchLegacyRestClientConfiguration;
@Configuration
public class MyClientConfig extends ElasticsearchLegacyRestClientConfiguration {
@Override
public ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder() <.>
.connectedTo("localhost:9200")
.build();
}
}
----
<.> for a detailed description of the builder methods see xref:elasticsearch/clients.adoc#elasticsearch.clients.configuration[Client Configuration]
====
The javadoc:org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration[] class allows further configuration by overriding for example the `jsonpMapper()` or `transportOptions()` methods.
The following beans can then be injected in other Spring components:
====
[source,java]
----
import org.springframework.beans.factory.annotation.Autowired;@Autowired
ElasticsearchOperations operations; <.>
@Autowired
ElasticsearchClient elasticsearchClient; <.>
@Autowired
RestClient restClient; <.>
@Autowired
JsonpMapper jsonpMapper; <.>
@ -61,8 +135,8 @@ JsonpMapper jsonpMapper; <.>
Basically one should just use the javadoc:org.springframework.data.elasticsearch.core.ElasticsearchOperations[] to interact with the Elasticsearch cluster.
When using repositories, this instance is used under the hood as well.
[[elasticsearch.clients.reactiverestclient]]
== Reactive Rest Client
[[elasticsearch.clients.reactiverest5client]]
== Reactive Rest5Client
When working with the reactive stack, the configuration must be derived from a different class:
@ -99,6 +173,65 @@ ReactiveElasticsearchOperations operations; <.>
@Autowired
ReactiveElasticsearchClient elasticsearchClient; <.>
@Autowired
Rest5Client rest5Client; <.>
@Autowired
JsonpMapper jsonpMapper; <.>
----
the following can be injected:
<.> an implementation of javadoc:org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations[]
<.> the `org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchClient` that is used.
This is a reactive implementation based on the Elasticsearch client implementation.
<.> the low level `RestClient` from the Elasticsearch libraries
<.> the `JsonpMapper` user by the Elasticsearch `Transport`
====
Basically one should just use the javadoc:org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations[] to interact with the Elasticsearch cluster.
When using repositories, this instance is used under the hood as well.
[[elasticsearch.clients.reactiverestclient]]
== Deprecated Reactive RestClient
See the section above for the imperative code to use the deprecated RestClient for the necessary dependencies to include.
When working with the reactive stack, the configuration must be derived from a different class:
====
[source,java]
----
import org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchLegacyRestClientConfiguration;
@Configuration
public class MyClientConfig extends ReactiveElasticsearchLegacyRestClientConfiguration {
@Override
public ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder() <.>
.connectedTo("localhost:9200")
.build();
}
}
----
<.> for a detailed description of the builder methods see xref:elasticsearch/clients.adoc#elasticsearch.clients.configuration[Client Configuration]
====
The javadoc:org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchConfiguration[] class allows further configuration by overriding for example the `jsonpMapper()` or `transportOptions()` methods.
The following beans can then be injected in other Spring components:
====
[source,java]
----
@Autowired
ReactiveElasticsearchOperations operations; <.>
@Autowired
ReactiveElasticsearchClient elasticsearchClient; <.>
@Autowired
RestClient restClient; <.>
@ -183,8 +316,8 @@ In the case this is not enough, the user can add callback functions by using the
The following callbacks are provided:
[[elasticsearch.clients.configuration.callbacks.rest]]
==== Configuration of the low level Elasticsearch `RestClient`:
[[elasticsearch.clients.configuration.callbacks.rest5]]
==== Configuration of the low level Elasticsearch `Rest5Client`:
This callback provides a `org.elasticsearch.client.RestClientBuilder` that can be used to configure the Elasticsearch
`RestClient`:
@ -193,7 +326,24 @@ This callback provides a `org.elasticsearch.client.RestClientBuilder` that can b
----
ClientConfiguration.builder()
.connectedTo("localhost:9200", "localhost:9291")
.withClientConfigurer(ElasticsearchClients.ElasticsearchRestClientConfigurationCallback.from(restClientBuilder -> {
.withClientConfigurer(Rest5Clients.ElasticsearchRest5ClientConfigurationCallback.from(restClientBuilder -> {
// configure the Elasticsearch Rest5Client
return restClientBuilder;
}))
.build();
----
====
[[elasticsearch.clients.configuration.callbacks.rest]]
==== Configuration of the deprecated low level Elasticsearch `RestClient`:
This callback provides a `org.elasticsearch.client.RestClientBuilder` that can be used to configure the Elasticsearch
`RestClient`:
====
[source,java]
----
ClientConfiguration.builder()
.connectedTo("localhost:9200", "localhost:9291")
.withClientConfigurer(RestClients.ElasticsearchRestClientConfigurationCallback.from(restClientBuilder -> {
// configure the Elasticsearch RestClient
return restClientBuilder;
}))
@ -201,10 +351,29 @@ ClientConfiguration.builder()
----
====
[[elasticsearch.clients.configurationcallbacks.httpasync]]
==== Configuration of the HttpAsyncClient used by the low level Elasticsearch `RestClient`:
[[elasticsearch.clients.configurationcallbacks.httpasync5]]
==== Configuration of the HttpAsyncClient used by the low level Elasticsearch `Rest5Client`:
This callback provides a `org.apache.http.impl.nio.client.HttpAsyncClientBuilder` to configure the HttpCLient that is
This callback provides a `org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder` to configure the HttpClient that is
used by the `Rest5Client`.
====
[source,java]
----
ClientConfiguration.builder()
.connectedTo("localhost:9200", "localhost:9291")
.withClientConfigurer(Rest5Clients.ElasticsearchHttpClientConfigurationCallback.from(httpAsyncClientBuilder -> {
// configure the HttpAsyncClient
return httpAsyncClientBuilder;
}))
.build();
----
====
[[elasticsearch.clients.configurationcallbacks.httpasync]]
==== Configuration of the HttpAsyncClient used by the deprecated low level Elasticsearch `RestClient`:
This callback provides a `org.apache.http.impl.nio.client.HttpAsyncClientBuilder` to configure the HttpClient that is
used by the `RestClient`.
====
@ -212,7 +381,7 @@ used by the `RestClient`.
----
ClientConfiguration.builder()
.connectedTo("localhost:9200", "localhost:9291")
.withClientConfigurer(ElasticsearchClients.ElasticsearchHttpClientConfigurationCallback.from(httpAsyncClientBuilder -> {
.withClientConfigurer(RestClients.ElasticsearchHttpClientConfigurationCallback.from(httpAsyncClientBuilder -> {
// configure the HttpAsyncClient
return httpAsyncClientBuilder;
}))
@ -220,6 +389,63 @@ ClientConfiguration.builder()
----
====
[[elasticsearch.clients.configurationcallbacks.connectionconfig]]
==== Configuration of the ConnectionConfig used by the low level Elasticsearch `Rest5Client`:
This callback provides a `org.apache.hc.client5.http.config.ConnectionConfig` to configure the connection that is
used by the `Rest5Client`.
====
[source,java]
----
ClientConfiguration.builder()
.connectedTo("localhost:9200", "localhost:9291")
.withClientConfigurer(Rest5Clients.ElasticsearchConnectionConfigurationCallback.from(connectionConfigBuilder -> {
// configure the connection
return connectionConfigBuilder;
}))
.build();
----
====
[[elasticsearch.clients.configurationcallbacks.connectioncmanager]]
==== Configuration of the ConnectionManager used by the low level Elasticsearch `Rest5Client`:
This callback provides a `org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder` to configure the connection manager that is
used by the `Rest5Client`.
====
[source,java]
----
ClientConfiguration.builder()
.connectedTo("localhost:9200", "localhost:9291")
.withClientConfigurer(Rest5Clients.ElasticsearchConnectionManagerCallback.from(connectionManagerBuilder -> {
// configure the connection manager
return connectionManagerBuilder;
}))
.build();
----
====
[[elasticsearch.clients.configurationcallbacks.requestconfig]]
==== Configuration of the RequestConfig used by the low level Elasticsearch `Rest5Client`:
This callback provides a `org.apache.hc.client5.http.config.RequestConfig` to configure the RequestConfig that is
used by the `Rest5Client`.
====
[source,java]
----
ClientConfiguration.builder()
.connectedTo("localhost:9200", "localhost:9291")
.withClientConfigurer(Rest5Clients.ElasticsearchRequestConfigCallback.from(requestConfigBuilder -> {
// configure the request config
return requestConfigBuilder;
}))
.build();
----
====
[[elasticsearch.clients.logging]]
== Client Logging

View File

@ -1,10 +1,19 @@
[[new-features]]
= What's new
[[new-features.6-0-0]]
== New in Spring Data Elasticsearch 6.0
* Upgarde to Spring 7
* Switch to jspecify nullability annotations
* Upgrade to Elasticsearch 9.1.0
* Use the new Elasticsearch Rest5Client as default
[[new-features.5-5-0]]
== New in Spring Data Elasticsearch 5.5
* Upgrade to Elasticsearch 8.17.4.
* Upgrade to Elasticsearch 8.18.1.
* Add support for the `@SearchTemplateQuery` annotation on repository methods.
* Scripted field properties of type collection can be populated from scripts returning arrays.

View File

@ -6,11 +6,11 @@ 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.1 (in development) | 6.0.x | 8.17.2 | 6.2.x
| 2025.0 (in development) | 5.5.x | 8.17.2 | 6.2.x
| 2025.1 (in development) | 6.0.x | 9.1.0 | 7.0.x
| 2025.0 | 5.5.x | 8.18.1 | 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
| 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
| 2023.0 (Ullmann) | 5.1.xfootnote:oom[] | 8.7.1 | 6.0.x
| 2022.0 (Turing) | 5.0.xfootnote:oom[] | 8.5.3 | 6.0.x
| 2021.2 (Raj) | 4.4.xfootnote:oom[] | 7.17.3 | 5.3.x

View File

@ -0,0 +1,27 @@
[[elasticsearch-migration-guide-5.5-6.0]]
= Upgrading from 5.5.x to 6.0.x
This section describes breaking changes from version 5.5.x to 6.0.x and how removed features can be replaced by new introduced features.
[[elasticsearch-migration-guide-5.5-6.0.breaking-changes]]
== Breaking Changes
From version 6.0 on, Spring Data Elasticsearch uses the Elasticsearch 9 libraries and as default the new `Rest5Client` provided by these libraries. It is still possible to use the old `RestClient`, check xref:elasticsearch/clients.adoc[Elasticsearch clients] for information. The configuration callbacks for this `RestClient` have been moved from `org.springframework.data.elasticsearch.client.elc.ElasticsearchClients` to the `org.springframework.data.elasticsearch.client.elc.rest_client.RestClients` class.
In the `org.springframework.data.elasticsearch.core.query.UpdateQuery` class the type of the two fields `ifSeqNo` and `ifPrimaryTerm` has changed from `Integer` to `Long` to align with the normal query and the underlying Elasticsearch client.
[[elasticsearch-migration-guide-5.5-6.0.deprecations]]
== Deprecations
All the code using the old `RestClient` has been moved to the `org.springframework.data.elasticsearch.client.elc.rest_client` package and has been deprecated. Users should switch to the classes from the `org.springframework.data.elasticsearch.client.elc.rest5_client` package.
=== Removals
The `org.springframework.data.elasticsearch.core.query.ScriptType` enum has been removed. To distinguish between an inline and a stored script set the appropriate values in the `org.springframework.data.elasticsearch.core.query.ScriptData` record.
These methods have been removed because the Elasticsearch Client 9 does not support them anymore:
```
org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchIndicesClient.unfreeze(UnfreezeRequest)
org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchIndicesClient.unfreeze(Function<UnfreezeRequest.Builder, ObjectBuilder<UnfreezeRequest>>)
```

View File

@ -127,10 +127,16 @@ public interface ClientConfiguration {
Optional<String> getCaFingerprint();
/**
* Returns the {@link HostnameVerifier} to use. Can be {@link Optional#empty()} if not configured.
* Returns the {@link HostnameVerifier} to use. Must be {@link Optional#empty()} if not configured.
* Cannot be used with the Rest5Client used from Elasticsearch 9 on as the underlying Apache http components 5 does not offer a way
* to set this. Users that need a hostname verifier must integrate this in a SSLContext.
* Returning a value here is ignored in this case
*
* @return the {@link HostnameVerifier} to use. Can be {@link Optional#empty()} if not configured.
* @deprecated since 6.0
*/
// todo #3117 document this
@Deprecated(since = "6.0", forRemoval=true)
Optional<HostnameVerifier> getHostNameVerifier();
/**

View File

@ -24,14 +24,6 @@ import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.NestedIdentity;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.json.JsonpMapper;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
@ -44,6 +36,13 @@ import org.springframework.data.elasticsearch.core.document.SearchDocumentAdapte
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Utility class to adapt different Elasticsearch responses to a
* {@link org.springframework.data.elasticsearch.core.document.Document}
@ -55,187 +54,188 @@ import org.springframework.util.Assert;
*/
final class DocumentAdapters {
private static final Log LOGGER = LogFactory.getLog(DocumentAdapters.class);
private static final Log LOGGER = LogFactory.getLog(DocumentAdapters.class);
private DocumentAdapters() {}
private DocumentAdapters() {
}
/**
* Creates a {@link SearchDocument} from a {@link Hit} returned by the Elasticsearch client.
*
* @param hit the hit object
* @param jsonpMapper to map JsonData objects
* @return the created {@link SearchDocument}
*/
public static SearchDocument from(Hit<?> hit, JsonpMapper jsonpMapper) {
/**
* Creates a {@link SearchDocument} from a {@link Hit} returned by the Elasticsearch client.
*
* @param hit the hit object
* @param jsonpMapper to map JsonData objects
* @return the created {@link SearchDocument}
*/
public static SearchDocument from(Hit<?> hit, JsonpMapper jsonpMapper) {
Assert.notNull(hit, "hit must not be null");
Assert.notNull(hit, "hit must not be null");
Map<String, List<String>> highlightFields = hit.highlight();
Map<String, List<String>> highlightFields = hit.highlight();
Map<String, SearchDocumentResponse> innerHits = new LinkedHashMap<>();
hit.innerHits().forEach((name, innerHitsResult) -> {
// noinspection ReturnOfNull
innerHits.put(name, SearchDocumentResponseBuilder.from(innerHitsResult.hits(), null, null, null, 0, null, null,
searchDocument -> null, jsonpMapper));
});
Map<String, SearchDocumentResponse> innerHits = new LinkedHashMap<>();
hit.innerHits().forEach((name, innerHitsResult) -> {
// noinspection ReturnOfNull
innerHits.put(name, SearchDocumentResponseBuilder.from(innerHitsResult.hits(), null, null, null, 0, null, null,
searchDocument -> null, jsonpMapper));
});
NestedMetaData nestedMetaData = from(hit.nested());
NestedMetaData nestedMetaData = from(hit.nested());
Explanation explanation = from(hit.explanation());
Explanation explanation = from(hit.explanation());
List<String> matchedQueries = hit.matchedQueries();
Map<String, Double> matchedQueries = hit.matchedQueries();
Function<Map<String, JsonData>, EntityAsMap> fromFields = fields -> {
StringBuilder sb = new StringBuilder("{");
final boolean[] firstField = { true };
hit.fields().forEach((key, jsonData) -> {
if (!firstField[0]) {
sb.append(',');
}
sb.append('"').append(key).append("\":") //
.append(jsonData.toJson(jsonpMapper).toString());
firstField[0] = false;
});
sb.append('}');
return new EntityAsMap().fromJson(sb.toString());
};
Function<Map<String, JsonData>, EntityAsMap> fromFields = fields -> {
StringBuilder sb = new StringBuilder("{");
final boolean[] firstField = {true};
hit.fields().forEach((key, jsonData) -> {
if (!firstField[0]) {
sb.append(',');
}
sb.append('"').append(key).append("\":") //
.append(jsonData.toJson(jsonpMapper).toString());
firstField[0] = false;
});
sb.append('}');
return new EntityAsMap().fromJson(sb.toString());
};
EntityAsMap hitFieldsAsMap = fromFields.apply(hit.fields());
EntityAsMap hitFieldsAsMap = fromFields.apply(hit.fields());
Map<String, List<Object>> documentFields = new LinkedHashMap<>();
hitFieldsAsMap.forEach((key, value) -> {
if (value instanceof List) {
// noinspection unchecked
documentFields.put(key, (List<Object>) value);
} else {
documentFields.put(key, Collections.singletonList(value));
}
});
Map<String, List<Object>> documentFields = new LinkedHashMap<>();
hitFieldsAsMap.forEach((key, value) -> {
if (value instanceof List) {
// noinspection unchecked
documentFields.put(key, (List<Object>) value);
} else {
documentFields.put(key, Collections.singletonList(value));
}
});
Document document;
Object source = hit.source();
if (source == null) {
document = Document.from(hitFieldsAsMap);
} else {
if (source instanceof EntityAsMap entityAsMap) {
document = Document.from(entityAsMap);
} else if (source instanceof JsonData jsonData) {
document = Document.from(jsonData.to(EntityAsMap.class));
} else {
Document document;
Object source = hit.source();
if (source == null) {
document = Document.from(hitFieldsAsMap);
} else {
if (source instanceof EntityAsMap entityAsMap) {
document = Document.from(entityAsMap);
} else if (source instanceof JsonData jsonData) {
document = Document.from(jsonData.to(EntityAsMap.class));
} else {
if (LOGGER.isWarnEnabled()) {
LOGGER.warn(String.format("Cannot map from type " + source.getClass().getName()));
}
document = Document.create();
}
}
document.setIndex(hit.index());
document.setId(hit.id());
if (LOGGER.isWarnEnabled()) {
LOGGER.warn(String.format("Cannot map from type " + source.getClass().getName()));
}
document = Document.create();
}
}
document.setIndex(hit.index());
document.setId(hit.id());
if (hit.version() != null) {
document.setVersion(hit.version());
}
document.setSeqNo(hit.seqNo() != null && hit.seqNo() >= 0 ? hit.seqNo() : -2); // -2 was the default value in the
// old client
document.setPrimaryTerm(hit.primaryTerm() != null && hit.primaryTerm() > 0 ? hit.primaryTerm() : 0);
if (hit.version() != null) {
document.setVersion(hit.version());
}
document.setSeqNo(hit.seqNo() != null && hit.seqNo() >= 0 ? hit.seqNo() : -2); // -2 was the default value in the
// old client
document.setPrimaryTerm(hit.primaryTerm() != null && hit.primaryTerm() > 0 ? hit.primaryTerm() : 0);
float score = hit.score() != null ? hit.score().floatValue() : Float.NaN;
return new SearchDocumentAdapter(document, score, hit.sort().stream().map(TypeUtils::toObject).toArray(),
documentFields, highlightFields, innerHits, nestedMetaData, explanation, matchedQueries, hit.routing());
}
float score = hit.score() != null ? hit.score().floatValue() : Float.NaN;
return new SearchDocumentAdapter(document, score, hit.sort().stream().map(TypeUtils::toObject).toArray(),
documentFields, highlightFields, innerHits, nestedMetaData, explanation, matchedQueries, hit.routing());
}
public static SearchDocument from(CompletionSuggestOption<EntityAsMap> completionSuggestOption) {
public static SearchDocument from(CompletionSuggestOption<EntityAsMap> completionSuggestOption) {
Document document = completionSuggestOption.source() != null ? Document.from(completionSuggestOption.source())
: Document.create();
document.setIndex(completionSuggestOption.index());
Document document = completionSuggestOption.source() != null ? Document.from(completionSuggestOption.source())
: Document.create();
document.setIndex(completionSuggestOption.index());
if (completionSuggestOption.id() != null) {
document.setId(completionSuggestOption.id());
}
if (completionSuggestOption.id() != null) {
document.setId(completionSuggestOption.id());
}
float score = completionSuggestOption.score() != null ? completionSuggestOption.score().floatValue() : Float.NaN;
return new SearchDocumentAdapter(document, score, new Object[] {}, Collections.emptyMap(), Collections.emptyMap(),
Collections.emptyMap(), null, null, null, completionSuggestOption.routing());
}
float score = completionSuggestOption.score() != null ? completionSuggestOption.score().floatValue() : Float.NaN;
return new SearchDocumentAdapter(document, score, new Object[]{}, Collections.emptyMap(), Collections.emptyMap(),
Collections.emptyMap(), null, null, null, completionSuggestOption.routing());
}
@Nullable
private static Explanation from(co.elastic.clients.elasticsearch.core.explain.@Nullable Explanation explanation) {
@Nullable
private static Explanation from(co.elastic.clients.elasticsearch.core.explain.@Nullable Explanation explanation) {
if (explanation == null) {
return null;
}
List<Explanation> details = explanation.details().stream().map(DocumentAdapters::from).collect(Collectors.toList());
return new Explanation(true, (double) explanation.value(), explanation.description(), details);
}
if (explanation == null) {
return null;
}
List<Explanation> details = explanation.details().stream().map(DocumentAdapters::from).collect(Collectors.toList());
return new Explanation(true, (double) explanation.value(), explanation.description(), details);
}
private static Explanation from(ExplanationDetail explanationDetail) {
private static Explanation from(ExplanationDetail explanationDetail) {
List<Explanation> details = explanationDetail.details().stream().map(DocumentAdapters::from)
.collect(Collectors.toList());
return new Explanation(null, (double) explanationDetail.value(), explanationDetail.description(), details);
}
List<Explanation> details = explanationDetail.details().stream().map(DocumentAdapters::from)
.collect(Collectors.toList());
return new Explanation(null, (double) explanationDetail.value(), explanationDetail.description(), details);
}
@Nullable
private static NestedMetaData from(@Nullable NestedIdentity nestedIdentity) {
@Nullable
private static NestedMetaData from(@Nullable NestedIdentity nestedIdentity) {
if (nestedIdentity == null) {
return null;
}
if (nestedIdentity == null) {
return null;
}
NestedMetaData child = from(nestedIdentity.nested());
return NestedMetaData.of(nestedIdentity.field(), nestedIdentity.offset(), child);
}
NestedMetaData child = from(nestedIdentity.nested());
return NestedMetaData.of(nestedIdentity.field(), nestedIdentity.offset(), child);
}
/**
* Creates a {@link Document} from a {@link GetResponse} where the found document is contained as {@link EntityAsMap}.
*
* @param getResponse the response instance
* @return the Document
*/
@Nullable
public static Document from(GetResult<EntityAsMap> getResponse) {
/**
* Creates a {@link Document} from a {@link GetResponse} where the found document is contained as {@link EntityAsMap}.
*
* @param getResponse the response instance
* @return the Document
*/
@Nullable
public static Document from(GetResult<EntityAsMap> getResponse) {
Assert.notNull(getResponse, "getResponse must not be null");
Assert.notNull(getResponse, "getResponse must not be null");
if (!getResponse.found()) {
return null;
}
if (!getResponse.found()) {
return null;
}
Document document = getResponse.source() != null ? Document.from(getResponse.source()) : Document.create();
document.setIndex(getResponse.index());
document.setId(getResponse.id());
Document document = getResponse.source() != null ? Document.from(getResponse.source()) : Document.create();
document.setIndex(getResponse.index());
document.setId(getResponse.id());
if (getResponse.version() != null) {
document.setVersion(getResponse.version());
}
if (getResponse.version() != null) {
document.setVersion(getResponse.version());
}
if (getResponse.seqNo() != null) {
document.setSeqNo(getResponse.seqNo());
}
if (getResponse.seqNo() != null) {
document.setSeqNo(getResponse.seqNo());
}
if (getResponse.primaryTerm() != null) {
document.setPrimaryTerm(getResponse.primaryTerm());
}
if (getResponse.primaryTerm() != null) {
document.setPrimaryTerm(getResponse.primaryTerm());
}
return document;
}
return document;
}
/**
* Creates a list of {@link MultiGetItem}s from a {@link MgetResponse} where the data is contained as
* {@link EntityAsMap} instances.
*
* @param mgetResponse the response instance
* @return list of multiget items
*/
public static List<MultiGetItem<Document>> from(MgetResponse<EntityAsMap> mgetResponse) {
/**
* Creates a list of {@link MultiGetItem}s from a {@link MgetResponse} where the data is contained as
* {@link EntityAsMap} instances.
*
* @param mgetResponse the response instance
* @return list of multiget items
*/
public static List<MultiGetItem<Document>> from(MgetResponse<EntityAsMap> mgetResponse) {
Assert.notNull(mgetResponse, "mgetResponse must not be null");
Assert.notNull(mgetResponse, "mgetResponse must not be null");
return mgetResponse.docs().stream() //
.map(itemResponse -> MultiGetItem.of( //
itemResponse.isFailure() ? null : from(itemResponse.result()), //
ResponseConverter.getFailure(itemResponse)))
.collect(Collectors.toList());
}
return mgetResponse.docs().stream() //
.map(itemResponse -> MultiGetItem.of( //
itemResponse.isFailure() ? null : from(itemResponse.result()), //
ResponseConverter.getFailure(itemResponse)))
.collect(Collectors.toList());
}
}

View File

@ -15,44 +15,38 @@
*/
package org.springframework.data.elasticsearch.client.elc;
import static org.springframework.data.elasticsearch.client.elc.rest5_client.Rest5Clients.*;
import static org.springframework.data.elasticsearch.client.elc.rest_client.RestClients.*;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.TransportOptions;
import co.elastic.clients.transport.TransportUtils;
import co.elastic.clients.transport.Version;
import co.elastic.clients.transport.rest5_client.Rest5ClientOptions;
import co.elastic.clients.transport.rest5_client.Rest5ClientTransport;
import co.elastic.clients.transport.rest5_client.low_level.RequestOptions;
import co.elastic.clients.transport.rest5_client.low_level.Rest5Client;
import co.elastic.clients.transport.rest_client.RestClientOptions;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import java.net.InetSocketAddress;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpContext;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.jspecify.annotations.Nullable;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.support.HttpHeaders;
import org.springframework.data.elasticsearch.support.VersionInfo;
import org.springframework.util.Assert;
/**
* Utility class to create the different Elasticsearch clients
* Utility class to create the different Elasticsearch clients. The RestClient class is the one used in Elasticsearch
* until version 9, it is still available, but it's use is deprecated. The Rest5Client class is the one that should be
* used from Elasticsearch 9 on.
*
* @author Peter-Josef Meisch
* @since 4.4
@ -119,18 +113,32 @@ public final class ElasticsearchClients {
*
* @param restClient the underlying {@link RestClient}
* @return the {@link ReactiveElasticsearchClient}
* @deprecated since 6.0, use the version with a Rest5Client.
*/
@Deprecated(since = "6.0", forRemoval = true)
public static ReactiveElasticsearchClient createReactive(RestClient restClient) {
return createReactive(restClient, null, DEFAULT_JSONP_MAPPER);
}
/**
* Creates a new {@link ReactiveElasticsearchClient}.
*
* @param rest5Client the underlying {@link RestClient}
* @return the {@link ReactiveElasticsearchClient}
*/
public static ReactiveElasticsearchClient createReactive(Rest5Client rest5Client) {
return createReactive(rest5Client, null, DEFAULT_JSONP_MAPPER);
}
/**
* Creates a new {@link ReactiveElasticsearchClient}.
*
* @param restClient the underlying {@link RestClient}
* @param transportOptions options to be added to each request.
* @return the {@link ReactiveElasticsearchClient}
* @deprecated since 6.0, use the version with a Rest5Client.
*/
@Deprecated(since = "6.0", forRemoval = true)
public static ReactiveElasticsearchClient createReactive(RestClient restClient,
@Nullable TransportOptions transportOptions, JsonpMapper jsonpMapper) {
@ -139,6 +147,21 @@ public final class ElasticsearchClients {
var transport = getElasticsearchTransport(restClient, REACTIVE_CLIENT, transportOptions, jsonpMapper);
return createReactive(transport);
}
/**
* Creates a new {@link ReactiveElasticsearchClient}.
*
* @param rest5Client the underlying {@link RestClient}
* @param transportOptions options to be added to each request.
* @return the {@link ReactiveElasticsearchClient}
*/
public static ReactiveElasticsearchClient createReactive(Rest5Client rest5Client,
@Nullable TransportOptions transportOptions, JsonpMapper jsonpMapper) {
Assert.notNull(rest5Client, "restClient must not be null");
var transport = getElasticsearchTransport(rest5Client, REACTIVE_CLIENT, transportOptions, jsonpMapper);
return createReactive(transport);
}
/**
* Creates a new {@link ReactiveElasticsearchClient} that uses the given {@link ElasticsearchTransport}.
@ -156,17 +179,21 @@ public final class ElasticsearchClients {
// region imperative client
/**
* Creates a new imperative {@link ElasticsearchClient}
* Creates a new imperative {@link ElasticsearchClient}. This uses a RestClient, if the old RestClient is needed, this
* must be created with the {@link org.springframework.data.elasticsearch.client.elc.rest_client.RestClients} class
* and passed in as parameter.
*
* @param clientConfiguration configuration options, must not be {@literal null}.
* @return the {@link ElasticsearchClient}
*/
public static ElasticsearchClient createImperative(ClientConfiguration clientConfiguration) {
return createImperative(getRestClient(clientConfiguration), null, DEFAULT_JSONP_MAPPER);
return createImperative(getRest5Client(clientConfiguration), null, DEFAULT_JSONP_MAPPER);
}
/**
* Creates a new imperative {@link ElasticsearchClient}
* Creates a new imperative {@link ElasticsearchClient}. This uses a RestClient, if the old RestClient is needed, this
* must be created with the {@link org.springframework.data.elasticsearch.client.elc.rest_client.RestClients} class
* and passed in as parameter.
*
* @param clientConfiguration configuration options, must not be {@literal null}.
* @param transportOptions options to be added to each request.
@ -174,7 +201,7 @@ public final class ElasticsearchClients {
*/
public static ElasticsearchClient createImperative(ClientConfiguration clientConfiguration,
TransportOptions transportOptions) {
return createImperative(getRestClient(clientConfiguration), transportOptions, DEFAULT_JSONP_MAPPER);
return createImperative(getRest5Client(clientConfiguration), transportOptions, DEFAULT_JSONP_MAPPER);
}
/**
@ -182,11 +209,23 @@ public final class ElasticsearchClients {
*
* @param restClient the RestClient to use
* @return the {@link ElasticsearchClient}
* @deprecated since 6.0, use the version with a Rest5Client.
*/
@Deprecated(since = "6.0", forRemoval = true)
public static ElasticsearchClient createImperative(RestClient restClient) {
return createImperative(restClient, null, DEFAULT_JSONP_MAPPER);
}
/**
* Creates a new imperative {@link ElasticsearchClient}
*
* @param rest5Client the Rest5Client to use
* @return the {@link ElasticsearchClient}
*/
public static ElasticsearchClient createImperative(Rest5Client rest5Client) {
return createImperative(rest5Client, null, DEFAULT_JSONP_MAPPER);
}
/**
* Creates a new imperative {@link ElasticsearchClient}
*
@ -194,7 +233,9 @@ public final class ElasticsearchClients {
* @param transportOptions options to be added to each request.
* @param jsonpMapper the mapper for the transport to use
* @return the {@link ElasticsearchClient}
* @deprecated since 6.0, use the version with a Rest5Client.
*/
@Deprecated(since = "6.0", forRemoval = true)
public static ElasticsearchClient createImperative(RestClient restClient, @Nullable TransportOptions transportOptions,
JsonpMapper jsonpMapper) {
@ -206,6 +247,27 @@ public final class ElasticsearchClients {
return createImperative(transport);
}
/**
* Creates a new imperative {@link ElasticsearchClient}
*
* @param rest5Client the Rest5Client to use
* @param transportOptions options to be added to each request.
* @param jsonpMapper the mapper for the transport to use
* @return the {@link ElasticsearchClient}
* @since 6.0
*/
public static ElasticsearchClient createImperative(Rest5Client rest5Client,
@Nullable TransportOptions transportOptions,
JsonpMapper jsonpMapper) {
Assert.notNull(rest5Client, "restClient must not be null");
ElasticsearchTransport transport = getElasticsearchTransport(rest5Client, IMPERATIVE_CLIENT, transportOptions,
jsonpMapper);
return createImperative(transport);
}
/**
* Creates a new {@link ElasticsearchClient} that uses the given {@link ElasticsearchTransport}.
*
@ -220,96 +282,6 @@ public final class ElasticsearchClients {
}
// endregion
// region low level RestClient
private static RestClientOptions.Builder getRestClientOptionsBuilder(@Nullable TransportOptions transportOptions) {
if (transportOptions instanceof RestClientOptions restClientOptions) {
return restClientOptions.toBuilder();
}
var builder = new RestClientOptions.Builder(RequestOptions.DEFAULT.toBuilder());
if (transportOptions != null) {
transportOptions.headers().forEach(header -> builder.addHeader(header.getKey(), header.getValue()));
transportOptions.queryParameters().forEach(builder::setParameter);
builder.onWarnings(transportOptions.onWarnings());
}
return builder;
}
/**
* Creates a low level {@link RestClient} for the given configuration.
*
* @param clientConfiguration must not be {@literal null}
* @return the {@link RestClient}
*/
public static RestClient getRestClient(ClientConfiguration clientConfiguration) {
return getRestClientBuilder(clientConfiguration).build();
}
private static RestClientBuilder getRestClientBuilder(ClientConfiguration clientConfiguration) {
HttpHost[] httpHosts = formattedHosts(clientConfiguration.getEndpoints(), clientConfiguration.useSsl()).stream()
.map(HttpHost::create).toArray(HttpHost[]::new);
RestClientBuilder builder = RestClient.builder(httpHosts);
if (clientConfiguration.getPathPrefix() != null) {
builder.setPathPrefix(clientConfiguration.getPathPrefix());
}
HttpHeaders headers = clientConfiguration.getDefaultHeaders();
if (!headers.isEmpty()) {
builder.setDefaultHeaders(toHeaderArray(headers));
}
builder.setHttpClientConfigCallback(clientBuilder -> {
if (clientConfiguration.getCaFingerprint().isPresent()) {
clientBuilder
.setSSLContext(TransportUtils.sslContextFromCaFingerprint(clientConfiguration.getCaFingerprint().get()));
}
clientConfiguration.getSslContext().ifPresent(clientBuilder::setSSLContext);
clientConfiguration.getHostNameVerifier().ifPresent(clientBuilder::setSSLHostnameVerifier);
clientBuilder.addInterceptorLast(new CustomHeaderInjector(clientConfiguration.getHeadersSupplier()));
RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
Duration connectTimeout = clientConfiguration.getConnectTimeout();
if (!connectTimeout.isNegative()) {
requestConfigBuilder.setConnectTimeout(Math.toIntExact(connectTimeout.toMillis()));
}
Duration socketTimeout = clientConfiguration.getSocketTimeout();
if (!socketTimeout.isNegative()) {
requestConfigBuilder.setSocketTimeout(Math.toIntExact(socketTimeout.toMillis()));
requestConfigBuilder.setConnectionRequestTimeout(Math.toIntExact(socketTimeout.toMillis()));
}
clientBuilder.setDefaultRequestConfig(requestConfigBuilder.build());
clientConfiguration.getProxy().map(HttpHost::create).ifPresent(clientBuilder::setProxy);
for (ClientConfiguration.ClientConfigurationCallback<?> clientConfigurer : clientConfiguration
.getClientConfigurers()) {
if (clientConfigurer instanceof ElasticsearchHttpClientConfigurationCallback restClientConfigurationCallback) {
clientBuilder = restClientConfigurationCallback.configure(clientBuilder);
}
}
return clientBuilder;
});
for (ClientConfiguration.ClientConfigurationCallback<?> clientConfigurationCallback : clientConfiguration
.getClientConfigurers()) {
if (clientConfigurationCallback instanceof ElasticsearchRestClientConfigurationCallback configurationCallback) {
builder = configurationCallback.configure(builder);
}
}
return builder;
}
// endregion
// region Elasticsearch transport
/**
* Creates an {@link ElasticsearchTransport} that will use the given client that additionally is customized with a
@ -320,7 +292,9 @@ public final class ElasticsearchClients {
* @param transportOptions options for the transport
* @param jsonpMapper mapper for the transport
* @return ElasticsearchTransport
* @deprecated since 6.0, use the version taking a Rest5Client
*/
@Deprecated(since = "6.0", forRemoval = true)
public static ElasticsearchTransport getElasticsearchTransport(RestClient restClient, String clientType,
@Nullable TransportOptions transportOptions, JsonpMapper jsonpMapper) {
@ -329,7 +303,7 @@ public final class ElasticsearchClients {
Assert.notNull(jsonpMapper, "jsonpMapper must not be null");
TransportOptions.Builder transportOptionsBuilder = transportOptions != null ? transportOptions.toBuilder()
: new RestClientOptions(RequestOptions.DEFAULT, false).toBuilder();
: new RestClientOptions(org.elasticsearch.client.RequestOptions.DEFAULT, false).toBuilder();
RestClientOptions.Builder restClientOptionsBuilder = getRestClientOptionsBuilder(transportOptions);
@ -353,70 +327,35 @@ public final class ElasticsearchClients {
return new RestClientTransport(restClient, jsonpMapper, restClientOptionsBuilder.build());
}
/**
* Creates an {@link ElasticsearchTransport} that will use the given client that additionally is customized with a
* header to contain the clientType
*
* @param rest5Client the client to use
* @param clientType the client type to pass in each request as header
* @param transportOptions options for the transport
* @param jsonpMapper mapper for the transport
* @return ElasticsearchTransport
*/
public static ElasticsearchTransport getElasticsearchTransport(Rest5Client rest5Client, String clientType,
@Nullable TransportOptions transportOptions, JsonpMapper jsonpMapper) {
Assert.notNull(rest5Client, "restClient must not be null");
Assert.notNull(clientType, "clientType must not be null");
Assert.notNull(jsonpMapper, "jsonpMapper must not be null");
TransportOptions.Builder transportOptionsBuilder = transportOptions != null ? transportOptions.toBuilder()
: new Rest5ClientOptions(RequestOptions.DEFAULT, false).toBuilder();
Rest5ClientOptions.Builder rest5ClientOptionsBuilder = getRest5ClientOptionsBuilder(transportOptions);
rest5ClientOptionsBuilder.addHeader(X_SPRING_DATA_ELASTICSEARCH_CLIENT,
VersionInfo.clientVersions() + " / " + clientType);
return new Rest5ClientTransport(rest5Client, jsonpMapper, rest5ClientOptionsBuilder.build());
}
// endregion
private static List<String> formattedHosts(List<InetSocketAddress> hosts, boolean useSsl) {
return hosts.stream().map(it -> (useSsl ? "https" : "http") + "://" + it.getHostString() + ':' + it.getPort())
.collect(Collectors.toList());
}
private static org.apache.http.Header[] toHeaderArray(HttpHeaders headers) {
return headers.entrySet().stream() //
.flatMap(entry -> entry.getValue().stream() //
.map(value -> new BasicHeader(entry.getKey(), value))) //
.toArray(org.apache.http.Header[]::new);
}
/**
* Interceptor to inject custom supplied headers.
*
* @since 4.4
*/
private record CustomHeaderInjector(Supplier<HttpHeaders> headersSupplier) implements HttpRequestInterceptor {
@Override
public void process(HttpRequest request, HttpContext context) {
HttpHeaders httpHeaders = headersSupplier.get();
if (httpHeaders != null && !httpHeaders.isEmpty()) {
Arrays.stream(toHeaderArray(httpHeaders)).forEach(request::addHeader);
}
}
}
/**
* {@link org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationCallback} to configure
* the Elasticsearch RestClient's Http client with a {@link HttpAsyncClientBuilder}
*
* @since 4.4
*/
public interface ElasticsearchHttpClientConfigurationCallback
extends ClientConfiguration.ClientConfigurationCallback<HttpAsyncClientBuilder> {
static ElasticsearchHttpClientConfigurationCallback from(
Function<HttpAsyncClientBuilder, HttpAsyncClientBuilder> httpClientBuilderCallback) {
Assert.notNull(httpClientBuilderCallback, "httpClientBuilderCallback must not be null");
return httpClientBuilderCallback::apply;
}
}
/**
* {@link org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationCallback} to configure
* the RestClient client with a {@link RestClientBuilder}
*
* @since 5.0
*/
public interface ElasticsearchRestClientConfigurationCallback
extends ClientConfiguration.ClientConfigurationCallback<RestClientBuilder> {
static ElasticsearchRestClientConfigurationCallback from(
Function<RestClientBuilder, RestClientBuilder> restClientBuilderCallback) {
Assert.notNull(restClientBuilderCallback, "restClientBuilderCallback must not be null");
return restClientBuilderCallback::apply;
}
}
// todo #3117 remove and document that ElasticsearchHttpClientConfigurationCallback has been move to RestClients.
}

View File

@ -20,12 +20,13 @@ import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.TransportOptions;
import co.elastic.clients.transport.rest_client.RestClientOptions;
import co.elastic.clients.transport.rest5_client.Rest5ClientOptions;
import co.elastic.clients.transport.rest5_client.low_level.RequestOptions;
import co.elastic.clients.transport.rest5_client.low_level.Rest5Client;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.springframework.context.annotation.Bean;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.rest5_client.Rest5Clients;
import org.springframework.data.elasticsearch.config.ElasticsearchConfigurationSupport;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
@ -38,7 +39,9 @@ import com.fasterxml.jackson.databind.SerializationFeature;
/**
* Base class for a @{@link org.springframework.context.annotation.Configuration} class to set up the Elasticsearch
* connection using the Elasticsearch Client. This class exposes different parts of the setup as Spring beans. Deriving
* classes must provide the {@link ClientConfiguration} to use.
* classes must provide the {@link ClientConfiguration} to use. From Version 6.0 on, this class uses the new Rest5Client
* from Elasticsearch 9. The old implementation using the RestClient is still available under the name
* {@link ElasticsearchLegacyRestClientConfiguration}.
*
* @author Peter-Josef Meisch
* @since 4.4
@ -60,27 +63,27 @@ public abstract class ElasticsearchConfiguration extends ElasticsearchConfigurat
* @return RestClient
*/
@Bean
public RestClient elasticsearchRestClient(ClientConfiguration clientConfiguration) {
public Rest5Client elasticsearchRest5Client(ClientConfiguration clientConfiguration) {
Assert.notNull(clientConfiguration, "clientConfiguration must not be null");
return ElasticsearchClients.getRestClient(clientConfiguration);
return Rest5Clients.getRest5Client(clientConfiguration);
}
/**
* Provides the Elasticsearch transport to be used. The default implementation uses the {@link RestClient} bean and
* Provides the Elasticsearch transport to be used. The default implementation uses the {@link Rest5Client} bean and
* the {@link JsonpMapper} bean provided in this class.
*
* @return the {@link ElasticsearchTransport}
* @since 5.2
*/
@Bean
public ElasticsearchTransport elasticsearchTransport(RestClient restClient, JsonpMapper jsonpMapper) {
public ElasticsearchTransport elasticsearchTransport(Rest5Client rest5Client, JsonpMapper jsonpMapper) {
Assert.notNull(restClient, "restClient must not be null");
Assert.notNull(rest5Client, "restClient must not be null");
Assert.notNull(jsonpMapper, "jsonpMapper must not be null");
return ElasticsearchClients.getElasticsearchTransport(restClient, ElasticsearchClients.IMPERATIVE_CLIENT,
return ElasticsearchClients.getElasticsearchTransport(rest5Client, ElasticsearchClients.IMPERATIVE_CLIENT,
transportOptions(), jsonpMapper);
}
@ -115,7 +118,7 @@ public abstract class ElasticsearchConfiguration extends ElasticsearchConfigurat
}
/**
* Provides the JsonpMapper bean that is used in the {@link #elasticsearchTransport(RestClient, JsonpMapper)} method.
* Provides the JsonpMapper bean that is used in the {@link #elasticsearchTransport(Rest5Client, JsonpMapper)} method.
*
* @return the {@link JsonpMapper} to use
* @since 5.2
@ -135,6 +138,6 @@ public abstract class ElasticsearchConfiguration extends ElasticsearchConfigurat
* @return the options that should be added to every request. Must not be {@literal null}
*/
public TransportOptions transportOptions() {
return new RestClientOptions(RequestOptions.DEFAULT, false);
return new Rest5ClientOptions(RequestOptions.DEFAULT, false);
}
}

View File

@ -24,6 +24,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.elasticsearch.client.ResponseException;
import org.jspecify.annotations.Nullable;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.dao.DataIntegrityViolationException;
@ -33,6 +34,7 @@ import org.springframework.data.elasticsearch.NoSuchIndexException;
import org.springframework.data.elasticsearch.ResourceNotFoundException;
import org.springframework.data.elasticsearch.UncategorizedElasticsearchException;
import org.springframework.data.elasticsearch.VersionConflictException;
import org.springframework.util.ClassUtils;
/**
* Simple {@link PersistenceExceptionTranslator} for Elasticsearch. Convert the given runtime exception to an
@ -45,6 +47,9 @@ import org.springframework.data.elasticsearch.VersionConflictException;
*/
public class ElasticsearchExceptionTranslator implements PersistenceExceptionTranslator {
public static final boolean LEGACY_RESTCLIENT_PRESENT = ClassUtils
.isPresent("org.elasticsearch.client.ResponseException", ElasticsearchExceptionTranslator.class.getClassLoader());
private final JsonpMapper jsonpMapper;
public ElasticsearchExceptionTranslator(JsonpMapper jsonpMapper) {
@ -68,7 +73,7 @@ public class ElasticsearchExceptionTranslator implements PersistenceExceptionTra
}
@Override
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
public @Nullable DataAccessException translateExceptionIfPossible(RuntimeException ex) {
checkForConflictException(ex);
@ -118,15 +123,20 @@ public class ElasticsearchExceptionTranslator implements PersistenceExceptionTra
Integer status = null;
String message = null;
if (exception instanceof ResponseException responseException) {
if (LEGACY_RESTCLIENT_PRESENT && exception instanceof ResponseException responseException) {
// this code is for the old RestClient
status = responseException.getResponse().getStatusLine().getStatusCode();
message = responseException.getMessage();
} else if (exception instanceof ElasticsearchException elasticsearchException) {
// using the RestClient throws this
status = elasticsearchException.status();
message = elasticsearchException.getMessage();
} else if (exception.getCause() != null) {
checkForConflictException(exception.getCause());
}
if (status != null && message != null) {
if (status == 409 && message.contains("type\":\"version_conflict_engine_exception"))
if (status == 409 && message.contains("version_conflict_engine_exception"))
if (message.contains("version conflict, required seqNo")) {
throw new OptimisticLockingFailureException("Cannot index a document due to seq_no+primary_term conflict",
exception);

View File

@ -0,0 +1,144 @@
/*
* Copyright 2021-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.client.elc;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.TransportOptions;
import co.elastic.clients.transport.rest_client.RestClientOptions;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.springframework.context.annotation.Bean;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.rest_client.RestClients;
import org.springframework.data.elasticsearch.config.ElasticsearchConfigurationSupport;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
/**
* Base class for a @{@link org.springframework.context.annotation.Configuration} class to set up the Elasticsearch
* connection using the Elasticsearch Client. This class exposes different parts of the setup as Spring beans. Deriving
* classes must provide the {@link ClientConfiguration} to use. <br/>
* This class uses the Elasticsearch RestClient which was replaced by the Rest5Client in Elasticsearch 9. It is still
* available here but deprecated.
*
* @author Peter-Josef Meisch
* @since 4.4
* @deprecated since 6.0, use {@link ElasticsearchConfiguration}
*/
@Deprecated(since = "6.0", forRemoval=true)
public abstract class ElasticsearchLegacyRestClientConfiguration extends ElasticsearchConfigurationSupport {
/**
* Must be implemented by deriving classes to provide the {@link ClientConfiguration}.
*
* @return configuration, must not be {@literal null}
*/
@Bean(name = "elasticsearchClientConfiguration")
public abstract ClientConfiguration clientConfiguration();
/**
* Provides the underlying low level Elasticsearch RestClient.
*
* @param clientConfiguration configuration for the client, must not be {@literal null}
* @return RestClient
*/
@Bean
public RestClient elasticsearchRestClient(ClientConfiguration clientConfiguration) {
Assert.notNull(clientConfiguration, "clientConfiguration must not be null");
return RestClients.getRestClient(clientConfiguration);
}
/**
* Provides the Elasticsearch transport to be used. The default implementation uses the {@link RestClient} bean and
* the {@link JsonpMapper} bean provided in this class.
*
* @return the {@link ElasticsearchTransport}
* @since 5.2
*/
@Bean
public ElasticsearchTransport elasticsearchTransport(RestClient restClient, JsonpMapper jsonpMapper) {
Assert.notNull(restClient, "restClient must not be null");
Assert.notNull(jsonpMapper, "jsonpMapper must not be null");
return ElasticsearchClients.getElasticsearchTransport(restClient, ElasticsearchClients.IMPERATIVE_CLIENT,
transportOptions(), jsonpMapper);
}
/**
* Provides the {@link ElasticsearchClient} to be used.
*
* @param transport the {@link ElasticsearchTransport} to use
* @return ElasticsearchClient instance
*/
@Bean
public ElasticsearchClient elasticsearchClient(ElasticsearchTransport transport) {
Assert.notNull(transport, "transport must not be null");
return ElasticsearchClients.createImperative(transport);
}
/**
* Creates a {@link ElasticsearchOperations} implementation using an {@link ElasticsearchClient}.
*
* @return never {@literal null}.
*/
@Bean(name = { "elasticsearchOperations", "elasticsearchTemplate" })
public ElasticsearchOperations elasticsearchOperations(ElasticsearchConverter elasticsearchConverter,
ElasticsearchClient elasticsearchClient) {
ElasticsearchTemplate template = new ElasticsearchTemplate(elasticsearchClient, elasticsearchConverter);
template.setRefreshPolicy(refreshPolicy());
return template;
}
/**
* Provides the JsonpMapper bean that is used in the {@link #elasticsearchTransport(RestClient, JsonpMapper)} method.
*
* @return the {@link JsonpMapper} to use
* @since 5.2
*/
@Bean
public JsonpMapper jsonpMapper() {
// we need to create our own objectMapper that keeps null values in order to provide the storeNullValue
// functionality. The one Elasticsearch would provide removes the nulls. We remove unwanted nulls before they get
// into this mapper, so we can safely keep them here.
var objectMapper = (new ObjectMapper())
.configure(SerializationFeature.INDENT_OUTPUT, false)
.setSerializationInclusion(JsonInclude.Include.ALWAYS);
return new JacksonJsonpMapper(objectMapper);
}
/**
* @return the options that should be added to every request. Must not be {@literal null}
*/
public TransportOptions transportOptions() {
return new RestClientOptions(RequestOptions.DEFAULT, false);
}
}

View File

@ -658,7 +658,7 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
QueryResponse response = sqlClient.query(requestConverter.sqlQueryRequest(query));
return responseConverter.sqlResponse(response);
} catch (IOException e) {
} catch (Exception e) {
throw exceptionTranslator.translateException(e);
}
}

View File

@ -17,6 +17,8 @@ package org.springframework.data.elasticsearch.client.elc;
import static org.springframework.data.elasticsearch.client.elc.TypeUtils.*;
import co.elastic.clients.util.NamedValue;
import java.util.Arrays;
import java.util.stream.Collectors;
@ -60,10 +62,11 @@ class HighlightQueryBuilder {
for (HighlightField highlightField : highlight.getFields()) {
String mappedName = mapFieldName(highlightField.getName(), type);
highlightBuilder.fields(mappedName, hf -> {
addParameters(highlightField.getParameters(), hf, type);
return hf;
});
highlightBuilder.fields(
NamedValue.of(mappedName, co.elastic.clients.elasticsearch.core.search.HighlightField.of(hf -> {
addParameters(highlightField.getParameters(), hf, type);
return hf;
})));
}
return highlightBuilder.build();

View File

@ -30,6 +30,7 @@ import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.elasticsearch.ResourceNotFoundException;
import org.springframework.data.elasticsearch.UncategorizedElasticsearchException;
import org.springframework.data.elasticsearch.annotations.Mapping;
import org.springframework.data.elasticsearch.core.IndexInformation;
@ -315,15 +316,20 @@ public class IndicesTemplate extends ChildTemplate<ElasticsearchTransport, Elast
}
@Override
public TemplateData getTemplate(GetTemplateRequest getTemplateRequest) {
public @Nullable TemplateData getTemplate(GetTemplateRequest getTemplateRequest) {
Assert.notNull(getTemplateRequest, "getTemplateRequest must not be null");
co.elastic.clients.elasticsearch.indices.GetTemplateRequest getTemplateRequestES = requestConverter
.indicesGetTemplateRequest(getTemplateRequest);
GetTemplateResponse getTemplateResponse = execute(client -> client.getTemplate(getTemplateRequestES));
return responseConverter.indicesGetTemplateData(getTemplateResponse, getTemplateRequest.getTemplateName());
try {
GetTemplateResponse getTemplateResponse = execute(client -> client.getTemplate(getTemplateRequestES));
return responseConverter.indicesGetTemplateData(getTemplateResponse, getTemplateRequest.getTemplateName());
} catch (ResourceNotFoundException e) {
// since Elasticsearch 9.1.0 we get an exception (404) instead of an empty result
return null;
}
}
@Override

View File

@ -19,12 +19,18 @@ import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.TransportOptions;
import co.elastic.clients.transport.rest5_client.Rest5ClientOptions;
import co.elastic.clients.transport.rest5_client.low_level.RequestOptions;
import co.elastic.clients.transport.rest5_client.low_level.Rest5Client;
import co.elastic.clients.transport.rest_client.RestClientOptions;
import org.elasticsearch.client.RequestOptions;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.elasticsearch.client.RestClient;
import org.springframework.context.annotation.Bean;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.rest5_client.Rest5Clients;
import org.springframework.data.elasticsearch.config.ElasticsearchConfigurationSupport;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
@ -55,11 +61,11 @@ public abstract class ReactiveElasticsearchConfiguration extends ElasticsearchCo
* @return RestClient
*/
@Bean
public RestClient elasticsearchRestClient(ClientConfiguration clientConfiguration) {
public Rest5Client elasticsearchRestClient(ClientConfiguration clientConfiguration) {
Assert.notNull(clientConfiguration, "clientConfiguration must not be null");
return ElasticsearchClients.getRestClient(clientConfiguration);
return Rest5Clients.getRest5Client(clientConfiguration);
}
/**
@ -70,12 +76,12 @@ public abstract class ReactiveElasticsearchConfiguration extends ElasticsearchCo
* @since 5.2
*/
@Bean
public ElasticsearchTransport elasticsearchTransport(RestClient restClient, JsonpMapper jsonpMapper) {
public ElasticsearchTransport elasticsearchTransport(Rest5Client rest5Client, JsonpMapper jsonpMapper) {
Assert.notNull(restClient, "restClient must not be null");
Assert.notNull(rest5Client, "restClient must not be null");
Assert.notNull(jsonpMapper, "jsonpMapper must not be null");
return ElasticsearchClients.getElasticsearchTransport(restClient, ElasticsearchClients.REACTIVE_CLIENT,
return ElasticsearchClients.getElasticsearchTransport(rest5Client, ElasticsearchClients.REACTIVE_CLIENT,
transportOptions(), jsonpMapper);
}
@ -110,7 +116,7 @@ public abstract class ReactiveElasticsearchConfiguration extends ElasticsearchCo
}
/**
* Provides the JsonpMapper that is used in the {@link #elasticsearchTransport(RestClient, JsonpMapper)} method and
* Provides the JsonpMapper that is used in the {@link #elasticsearchTransport(Rest5Client, JsonpMapper)} method and
* exposes it as a bean.
*
* @return the {@link JsonpMapper} to use
@ -118,13 +124,19 @@ public abstract class ReactiveElasticsearchConfiguration extends ElasticsearchCo
*/
@Bean
public JsonpMapper jsonpMapper() {
return new JacksonJsonpMapper();
// we need to create our own objectMapper that keeps null values in order to provide the storeNullValue
// functionality. The one Elasticsearch would provide removes the nulls. We remove unwanted nulls before they get
// into this mapper, so we can safely keep them here.
var objectMapper = (new ObjectMapper())
.configure(SerializationFeature.INDENT_OUTPUT, false)
.setSerializationInclusion(JsonInclude.Include.ALWAYS);
return new JacksonJsonpMapper(objectMapper);
}
/**
* @return the options that should be added to every request. Must not be {@literal null}
*/
public TransportOptions transportOptions() {
return new RestClientOptions(RequestOptions.DEFAULT, false).toBuilder().build();
return new Rest5ClientOptions(RequestOptions.DEFAULT, false);
}
}

View File

@ -539,14 +539,6 @@ public class ReactiveElasticsearchIndicesClient
return stats(builder -> builder);
}
public Mono<UnfreezeResponse> unfreeze(UnfreezeRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, UnfreezeRequest._ENDPOINT, transportOptions));
}
public Mono<UnfreezeResponse> unfreeze(Function<UnfreezeRequest.Builder, ObjectBuilder<UnfreezeRequest>> fn) {
return unfreeze(fn.apply(new UnfreezeRequest.Builder()).build());
}
public Mono<UpdateAliasesResponse> updateAliases(UpdateAliasesRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, UpdateAliasesRequest._ENDPOINT, transportOptions));
}

View File

@ -0,0 +1,144 @@
/*
* Copyright 2021-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.client.elc;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.TransportOptions;
import co.elastic.clients.transport.rest_client.RestClientOptions;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.springframework.context.annotation.Bean;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.rest_client.RestClients;
import org.springframework.data.elasticsearch.config.ElasticsearchConfigurationSupport;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.util.Assert;
/**
* Base class for a @{@link org.springframework.context.annotation.Configuration} class to set up the Elasticsearch
* connection using the {@link ReactiveElasticsearchClient}. This class exposes different parts of the setup as Spring
* beans. Deriving * classes must provide the {@link ClientConfiguration} to use. <br/>
* This class uses the Elasticsearch RestClient which was replaced b y the Rest5Client in Elasticsearch 9. It is still
* available here but deprecated. *
*
* @author Peter-Josef Meisch
* @since 4.4
* @deprecated since 6.0 use {@link ReactiveElasticsearchConfiguration}
*/
@Deprecated(since = "6.0", forRemoval=true)
public abstract class ReactiveElasticsearchLegacyRestClientConfiguration extends ElasticsearchConfigurationSupport {
/**
* Must be implemented by deriving classes to provide the {@link ClientConfiguration}.
*
* @return configuration, must not be {@literal null}
*/
@Bean(name = "elasticsearchClientConfiguration")
public abstract ClientConfiguration clientConfiguration();
/**
* Provides the underlying low level RestClient.
*
* @param clientConfiguration configuration for the client, must not be {@literal null}
* @return RestClient
*/
@Bean
public RestClient elasticsearchRestClient(ClientConfiguration clientConfiguration) {
Assert.notNull(clientConfiguration, "clientConfiguration must not be null");
return RestClients.getRestClient(clientConfiguration);
}
/**
* Provides the Elasticsearch transport to be used. The default implementation uses the {@link RestClient} bean and
* the {@link JsonpMapper} bean provided in this class.
*
* @return the {@link ElasticsearchTransport}
* @since 5.2
*/
@Bean
public ElasticsearchTransport elasticsearchTransport(RestClient restClient, JsonpMapper jsonpMapper) {
Assert.notNull(restClient, "restClient must not be null");
Assert.notNull(jsonpMapper, "jsonpMapper must not be null");
return ElasticsearchClients.getElasticsearchTransport(restClient, ElasticsearchClients.REACTIVE_CLIENT,
transportOptions(), jsonpMapper);
}
/**
* Provides the {@link ReactiveElasticsearchClient} instance used.
*
* @param transport the ElasticsearchTransport to use
* @return ReactiveElasticsearchClient instance.
*/
@Bean
public ReactiveElasticsearchClient reactiveElasticsearchClient(ElasticsearchTransport transport) {
Assert.notNull(transport, "transport must not be null");
return ElasticsearchClients.createReactive(transport);
}
/**
* Creates {@link ReactiveElasticsearchOperations}.
*
* @return never {@literal null}.
*/
@Bean(name = { "reactiveElasticsearchOperations", "reactiveElasticsearchTemplate" })
public ReactiveElasticsearchOperations reactiveElasticsearchOperations(ElasticsearchConverter elasticsearchConverter,
ReactiveElasticsearchClient reactiveElasticsearchClient) {
ReactiveElasticsearchTemplate template = new ReactiveElasticsearchTemplate(reactiveElasticsearchClient,
elasticsearchConverter);
template.setRefreshPolicy(refreshPolicy());
return template;
}
/**
* Provides the JsonpMapper that is used in the {@link #elasticsearchTransport(RestClient, JsonpMapper)} method and
* exposes it as a bean.
*
* @return the {@link JsonpMapper} to use
* @since 5.2
*/
@Bean
public JsonpMapper jsonpMapper() {
// we need to create our own objectMapper that keeps null values in order to provide the storeNullValue
// functionality. The one Elasticsearch would provide removes the nulls. We remove unwanted nulls before they get
// into this mapper, so we can safely keep them here.
var objectMapper = (new ObjectMapper())
.configure(SerializationFeature.INDENT_OUTPUT, false)
.setSerializationInclusion(JsonInclude.Include.ALWAYS);
return new JacksonJsonpMapper(objectMapper);
}
/**
* @return the options that should be added to every request. Must not be {@literal null}
*/
public TransportOptions transportOptions() {
return new RestClientOptions(RequestOptions.DEFAULT, false).toBuilder().build();
}
}

View File

@ -25,6 +25,7 @@ import co.elastic.clients.elasticsearch.core.search.ResponseBody;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.transport.Version;
import co.elastic.clients.transport.endpoints.BooleanResponse;
import org.jspecify.annotations.NonNull;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
@ -162,7 +163,7 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
ExistsRequest existsRequest = requestConverter.documentExistsRequest(id, routingResolver.getRouting(), index);
return Mono.from(execute(
((ClientCallback<Publisher<BooleanResponse>>) client -> client.exists(existsRequest))))
((ClientCallback<@NonNull Publisher<BooleanResponse>>) client -> client.exists(existsRequest))))
.map(BooleanResponse::value) //
.onErrorReturn(NoSuchIndexException.class, false);
}

View File

@ -21,6 +21,7 @@ import co.elastic.clients.elasticsearch._types.AcknowledgedResponseBase;
import co.elastic.clients.elasticsearch.indices.*;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.endpoints.BooleanResponse;
import org.springframework.data.elasticsearch.ResourceNotFoundException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@ -389,7 +390,8 @@ public class ReactiveIndicesTemplate
co.elastic.clients.elasticsearch.indices.GetTemplateRequest getTemplateRequestES = requestConverter
.indicesGetTemplateRequest(getTemplateRequest);
Mono<GetTemplateResponse> getTemplateResponse = Mono
.from(execute(client -> client.getTemplate(getTemplateRequestES)));
.from(execute(client -> client.getTemplate(getTemplateRequestES)))
.onErrorComplete(ResourceNotFoundException.class);
return getTemplateResponse.flatMap(response -> {
if (response != null) {

View File

@ -42,10 +42,10 @@ import co.elastic.clients.elasticsearch.core.bulk.CreateOperation;
import co.elastic.clients.elasticsearch.core.bulk.IndexOperation;
import co.elastic.clients.elasticsearch.core.bulk.UpdateOperation;
import co.elastic.clients.elasticsearch.core.mget.MultiGetOperation;
import co.elastic.clients.elasticsearch.core.msearch.MultisearchBody;
import co.elastic.clients.elasticsearch.core.msearch.MultisearchHeader;
import co.elastic.clients.elasticsearch.core.search.Highlight;
import co.elastic.clients.elasticsearch.core.search.Rescore;
import co.elastic.clients.elasticsearch.core.search.SearchRequestBody;
import co.elastic.clients.elasticsearch.core.search.SourceConfig;
import co.elastic.clients.elasticsearch.indices.*;
import co.elastic.clients.elasticsearch.indices.ExistsIndexTemplateRequest;
@ -55,6 +55,7 @@ import co.elastic.clients.elasticsearch.sql.query.SqlFormat;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.json.JsonpDeserializer;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.util.NamedValue;
import co.elastic.clients.util.ObjectBuilder;
import jakarta.json.stream.JsonParser;
@ -72,6 +73,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@ -118,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;
@ -724,12 +723,11 @@ class RequestConverter extends AbstractQueryProcessor {
return a;
});
uob //
.routing(query.getRouting()) //
.ifSeqNo(query.getIfSeqNo() != null ? Long.valueOf(query.getIfSeqNo()) : null) //
.ifPrimaryTerm(query.getIfPrimaryTerm() != null ? Long.valueOf(query.getIfPrimaryTerm()) : null) //
.retryOnConflict(query.getRetryOnConflict()) //
;
uob
.routing(query.getRouting())
.ifSeqNo(query.getIfSeqNo())
.ifPrimaryTerm(query.getIfPrimaryTerm())
.retryOnConflict(query.getRetryOnConflict());
// no refresh, timeout, waitForActiveShards on UpdateOperation or UpdateAction
@ -749,11 +747,10 @@ class RequestConverter extends AbstractQueryProcessor {
}
return co.elastic.clients.elasticsearch._types.Script.of(sb -> {
sb.lang(scriptData.language())
.params(params);
if (scriptData.type() == ScriptType.INLINE) {
sb.source(scriptData.script());
} else if (scriptData.type() == ScriptType.STORED) {
sb.id(scriptData.script());
.params(params)
.id(scriptData.scriptName());
if (scriptData.script() != null) {
sb.source(s -> s.scriptString(scriptData.script()));
}
return sb;
});
@ -925,9 +922,13 @@ class RequestConverter extends AbstractQueryProcessor {
ReindexRequest.Script script = reindexRequest.getScript();
if (script != null) {
builder.script(sb -> sb
.lang(script.getLang())
.source(script.getSource()));
builder.script(sb -> {
if (script.getSource() != null) {
sb.source(s -> s.scriptString(script.getSource()));
}
sb.lang(script.getLang());
return sb;
});
}
builder.timeout(time(reindexRequest.getTimeout())) //
@ -1084,27 +1085,25 @@ class RequestConverter extends AbstractQueryProcessor {
uqb.script(sb -> {
sb.lang(query.getLang()).params(params);
if (query.getScriptType() == ScriptType.INLINE) {
sb.source(query.getScript()); //
} else if (query.getScriptType() == ScriptType.STORED) {
sb.id(query.getScript());
if (query.getScript() != null) {
sb.source(s -> s.scriptString(query.getScript()));
}
sb.id(query.getId());
return sb;
});
}
uqb //
.doc(query.getDocument()) //
.upsert(query.getUpsert()) //
.routing(query.getRouting() != null ? query.getRouting() : routing) //
.scriptedUpsert(query.getScriptedUpsert()) //
.docAsUpsert(query.getDocAsUpsert()) //
.ifSeqNo(query.getIfSeqNo() != null ? Long.valueOf(query.getIfSeqNo()) : null) //
.ifPrimaryTerm(query.getIfPrimaryTerm() != null ? Long.valueOf(query.getIfPrimaryTerm()) : null) //
.refresh(query.getRefreshPolicy() != null ? refresh(query.getRefreshPolicy()) : refresh(refreshPolicy)) //
.retryOnConflict(query.getRetryOnConflict()) //
;
uqb
.doc(query.getDocument())
.upsert(query.getUpsert())
.routing(query.getRouting() != null ? query.getRouting() : routing)
.scriptedUpsert(query.getScriptedUpsert())
.docAsUpsert(query.getDocAsUpsert())
.ifSeqNo(query.getIfSeqNo())
.ifPrimaryTerm(query.getIfPrimaryTerm())
.refresh(query.getRefreshPolicy() != null ? refresh(query.getRefreshPolicy()) : refresh(refreshPolicy))
.retryOnConflict(query.getRetryOnConflict());
if (query.getFetchSource() != null) {
uqb.source(sc -> sc.fetch(query.getFetchSource()));
@ -1252,11 +1251,11 @@ class RequestConverter extends AbstractQueryProcessor {
mtrb.searchTemplates(stb -> stb
.header(msearchHeaderBuilder(query, param.index(), routing))
.body(bb -> {
bb //
.explain(query.getExplain()) //
.id(query.getId()) //
.source(query.getSource()) //
;
bb.explain(query.getExplain()) //
.id(query.getId()); //
if (query.getSource() != null) {
bb.source(s -> s.scriptString(query.getSource()));
}
if (!CollectionUtils.isEmpty(query.getParams())) {
Map<String, JsonData> params = getTemplateParams(query.getParams().entrySet());
@ -1292,11 +1291,8 @@ class RequestConverter extends AbstractQueryProcessor {
.timeout(timeStringMs(query.getTimeout())) //
;
if (query.getPageable().isPaged()) {
bb //
.from((int) query.getPageable().getOffset()) //
.size(query.getPageable().getPageSize());
}
bb.from((int) (query.getPageable().isPaged() ? query.getPageable().getOffset() : 0))
.size(query.getRequestSize());
if (!isEmpty(query.getFields())) {
bb.fields(fb -> {
@ -1309,10 +1305,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());
}
@ -1347,7 +1339,9 @@ class RequestConverter extends AbstractQueryProcessor {
if (script != null) {
rfb.script(s -> {
s.source(script);
if (script != null) {
s.source(so -> so.scriptString(script));
}
if (runtimeField.getParams() != null) {
s.params(TypeUtils.paramsMap(runtimeField.getParams()));
@ -1364,9 +1358,14 @@ class RequestConverter extends AbstractQueryProcessor {
}
if (!isEmpty(query.getIndicesBoost())) {
bb.indicesBoost(query.getIndicesBoost().stream()
.map(indexBoost -> Map.of(indexBoost.getIndexName(), (double) indexBoost.getBoost()))
.collect(Collectors.toList()));
Stream<NamedValue<Double>> namedValueStream = query.getIndicesBoost().stream()
.map(indexBoost -> {
var namedValue = new NamedValue(indexBoost.getIndexName(),
Float.valueOf(indexBoost.getBoost()).doubleValue());
return namedValue;
});
List<NamedValue<Double>> namedValueList = namedValueStream.collect(Collectors.toList());
bb.indicesBoost(namedValueList);
}
query.getScriptedFields().forEach(scriptedField -> bb.scriptFields(scriptedField.getFieldName(),
@ -1465,13 +1464,8 @@ 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);
}
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();
@ -1486,10 +1480,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());
}
@ -1547,7 +1537,9 @@ class RequestConverter extends AbstractQueryProcessor {
String script = runtimeField.getScript();
if (script != null) {
rfb.script(s -> {
s.source(script);
if (script != null) {
s.source(so -> so.scriptString(script));
}
if (runtimeField.getParams() != null) {
s.params(TypeUtils.paramsMap(runtimeField.getParams()));
@ -1575,9 +1567,14 @@ class RequestConverter extends AbstractQueryProcessor {
}
if (!isEmpty(query.getIndicesBoost())) {
builder.indicesBoost(query.getIndicesBoost().stream()
.map(indexBoost -> Map.of(indexBoost.getIndexName(), (double) indexBoost.getBoost()))
.collect(Collectors.toList()));
Stream<NamedValue<Double>> namedValueStream = query.getIndicesBoost().stream()
.map(indexBoost -> {
var namedValue = new NamedValue(indexBoost.getIndexName(),
Float.valueOf(indexBoost.getBoost()).doubleValue());
return namedValue;
});
List<NamedValue<Double>> namedValueList = namedValueStream.collect(Collectors.toList());
builder.indicesBoost(namedValueList);
}
if (!isEmpty(query.getDocValueFields())) {
@ -1639,7 +1636,7 @@ class RequestConverter extends AbstractQueryProcessor {
builder.highlight(highlight);
}
private void addHighlight(Query query, MultisearchBody.Builder builder) {
private void addHighlight(Query query, SearchRequestBody.Builder builder) {
Highlight highlight = query.getHighlightQuery()
.map(highlightQuery -> new HighlightQueryBuilder(elasticsearchConverter.getMappingContext(), this)
@ -1763,7 +1760,7 @@ class RequestConverter extends AbstractQueryProcessor {
}
@SuppressWarnings("DuplicatedCode")
private void prepareNativeSearch(NativeQuery query, MultisearchBody.Builder builder) {
private void prepareNativeSearch(NativeQuery query, SearchRequestBody.Builder builder) {
builder //
.suggest(query.getSuggester()) //
@ -1882,10 +1879,11 @@ class RequestConverter extends AbstractQueryProcessor {
.id(query.getId()) //
.index(Arrays.asList(index.getIndexNames())) //
.preference(query.getPreference()) //
.searchType(searchType(query.getSearchType())) //
.source(query.getSource()) //
;
.searchType(searchType(query.getSearchType())); //
if (query.getSource() != null) {
builder.source(so -> so.scriptString(query.getSource()));
}
if (query.getRoute() != null) {
builder.routing(query.getRoute());
} else if (StringUtils.hasText(routing)) {
@ -1928,7 +1926,7 @@ class RequestConverter extends AbstractQueryProcessor {
.id(script.id()) //
.script(sb -> sb //
.lang(script.language()) //
.source(script.source())));
.source(s -> s.scriptString(script.source()))));
}
public GetScriptRequest scriptGet(String name) {

View File

@ -92,7 +92,7 @@ class ResponseConverter {
return ClusterHealth.builder() //
.withActivePrimaryShards(healthResponse.activePrimaryShards()) //
.withActiveShards(healthResponse.activeShards()) //
.withActiveShardsPercent(Double.parseDouble(healthResponse.activeShardsPercentAsNumber()))//
.withActiveShardsPercent(healthResponse.activeShardsPercentAsNumber())//
.withClusterName(healthResponse.clusterName()) //
.withDelayedUnassignedShards(healthResponse.delayedUnassignedShards()) //
.withInitializingShards(healthResponse.initializingShards()) //
@ -191,7 +191,7 @@ class ResponseConverter {
Assert.notNull(getMappingResponse, "getMappingResponse must not be null");
Assert.notNull(indexCoordinates, "indexCoordinates must not be null");
Map<String, IndexMappingRecord> mappings = getMappingResponse.result();
Map<String, IndexMappingRecord> mappings = getMappingResponse.mappings();
if (mappings == null || mappings.isEmpty()) {
return Document.create();
@ -219,7 +219,7 @@ class ResponseConverter {
List<IndexInformation> indexInformationList = new ArrayList<>();
getIndexResponse.result().forEach((indexName, indexState) -> {
getIndexResponse.indices().forEach((indexName, indexState) -> {
Settings settings = indexState.settings() != null ? Settings.parse(toJson(indexState.settings(), jsonpMapper))
: new Settings();
Document mappings = indexState.mappings() != null ? Document.parse(toJson(indexState.mappings(), jsonpMapper))
@ -239,7 +239,7 @@ class ResponseConverter {
Assert.notNull(getAliasResponse, "getAliasResponse must not be null");
Map<String, Set<AliasData>> aliasDataMap = new HashMap<>();
getAliasResponse.result().forEach((indexName, alias) -> {
getAliasResponse.aliases().forEach((indexName, alias) -> {
Set<AliasData> aliasDataSet = new HashSet<>();
alias.aliases()
.forEach((aliasName, aliasDefinition) -> aliasDataSet.add(indicesGetAliasData(aliasName, aliasDefinition)));
@ -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()));
}
@ -531,7 +535,7 @@ class ResponseConverter {
? Script.builder() //
.withId(response.id()) //
.withLanguage(response.script().lang()) //
.withSource(response.script().source()).build() //
.withSource(response.script().source().scriptString()).build() //
: null;
}
// endregion

View File

@ -0,0 +1,308 @@
package org.springframework.data.elasticsearch.client.elc.rest5_client;
import co.elastic.clients.transport.TransportOptions;
import co.elastic.clients.transport.TransportUtils;
import co.elastic.clients.transport.rest5_client.Rest5ClientOptions;
import co.elastic.clients.transport.rest5_client.low_level.RequestOptions;
import co.elastic.clients.transport.rest5_client.low_level.Rest5Client;
import co.elastic.clients.transport.rest5_client.low_level.Rest5ClientBuilder;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import javax.net.ssl.SSLContext;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.http.nio.ssl.BasicClientTlsStrategy;
import org.apache.hc.core5.util.Timeout;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.support.HttpHeaders;
import org.springframework.util.Assert;
/**
* Utility class containing the functions to create the Elasticsearch Rest5Client used from Elasticsearch 9 on.
*
* @since 6.0
*/
public final class Rest5Clients {
// values copied from Rest5ClientBuilder
public static final int DEFAULT_SOCKET_TIMEOUT_MILLIS = 30000;
public static final int DEFAULT_RESPONSE_TIMEOUT_MILLIS = 0; // meaning infinite
private Rest5Clients() {}
/**
* Creates a low level {@link Rest5Client} for the given configuration.
*
* @param clientConfiguration must not be {@literal null}
* @return the {@link Rest5Client}
*/
public static Rest5Client getRest5Client(ClientConfiguration clientConfiguration) {
return getRest5ClientBuilder(clientConfiguration).build();
}
private static Rest5ClientBuilder getRest5ClientBuilder(ClientConfiguration clientConfiguration) {
HttpHost[] httpHosts = getHttpHosts(clientConfiguration);
Rest5ClientBuilder builder = Rest5Client.builder(httpHosts);
if (clientConfiguration.getPathPrefix() != null) {
builder.setPathPrefix(clientConfiguration.getPathPrefix());
}
HttpHeaders headers = clientConfiguration.getDefaultHeaders();
if (!headers.isEmpty()) {
builder.setDefaultHeaders(toHeaderArray(headers));
}
// RestClientBuilder configuration callbacks from the consumer
for (ClientConfiguration.ClientConfigurationCallback<?> clientConfigurationCallback : clientConfiguration
.getClientConfigurers()) {
if (clientConfigurationCallback instanceof ElasticsearchRest5ClientConfigurationCallback configurationCallback) {
builder = configurationCallback.configure(builder);
}
}
Duration connectTimeout = clientConfiguration.getConnectTimeout();
Duration socketTimeout = clientConfiguration.getSocketTimeout();
builder.setHttpClientConfigCallback(httpAsyncClientBuilder -> {
if (clientConfiguration.getProxy().isPresent()) {
var proxy = clientConfiguration.getProxy().get();
try {
var proxyRoutePlanner = new DefaultProxyRoutePlanner(HttpHost.create(proxy));
httpAsyncClientBuilder.setRoutePlanner(proxyRoutePlanner);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
httpAsyncClientBuilder.addRequestInterceptorFirst((request, entity, context) -> {
clientConfiguration.getHeadersSupplier().get().forEach((header, values) -> {
// The accept and content-type headers are already put on the request, despite this being the first
// interceptor.
if ("Accept".equalsIgnoreCase(header) || " Content-Type".equalsIgnoreCase(header)) {
request.removeHeaders(header);
}
values.forEach(value -> request.addHeader(header, value));
});
});
// add httpclient configurator callbacks provided by the configuration
for (ClientConfiguration.ClientConfigurationCallback<?> clientConfigurer : clientConfiguration
.getClientConfigurers()) {
if (clientConfigurer instanceof ElasticsearchHttpClientConfigurationCallback httpClientConfigurer) {
httpAsyncClientBuilder = httpClientConfigurer.configure(httpAsyncClientBuilder);
}
}
});
builder.setConnectionConfigCallback(connectionConfigBuilder -> {
if (!connectTimeout.isNegative()) {
connectionConfigBuilder.setConnectTimeout(
Timeout.of(Math.toIntExact(connectTimeout.toMillis()), TimeUnit.MILLISECONDS));
}
if (!socketTimeout.isNegative()) {
var soTimeout = Timeout.of(Math.toIntExact(socketTimeout.toMillis()), TimeUnit.MILLISECONDS);
connectionConfigBuilder.setSocketTimeout(soTimeout);
} else {
connectionConfigBuilder.setSocketTimeout(Timeout.of(DEFAULT_SOCKET_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS));
}
// add connectionConfig configurator callbacks provided by the configuration
for (ClientConfiguration.ClientConfigurationCallback<?> clientConfigurer : clientConfiguration
.getClientConfigurers()) {
if (clientConfigurer instanceof ElasticsearchConnectionConfigurationCallback connectionConfigurationCallback) {
connectionConfigBuilder = connectionConfigurationCallback.configure(connectionConfigBuilder);
}
}
});
builder.setConnectionManagerCallback(poolingAsyncClientConnectionManagerBuilder -> {
SSLContext sslContext = null;
try {
sslContext = clientConfiguration.getCaFingerprint().isPresent()
? TransportUtils.sslContextFromCaFingerprint(clientConfiguration.getCaFingerprint().get())
: (clientConfiguration.getSslContext().isPresent()
? clientConfiguration.getSslContext().get()
: SSLContext.getDefault());
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("could not create the default ssl context", e);
}
poolingAsyncClientConnectionManagerBuilder.setTlsStrategy(new BasicClientTlsStrategy(sslContext));
// add connectionManager configurator callbacks provided by the configuration
for (ClientConfiguration.ClientConfigurationCallback<?> clientConfigurer : clientConfiguration
.getClientConfigurers()) {
if (clientConfigurer instanceof ElasticsearchConnectionManagerCallback connectionManagerCallback) {
poolingAsyncClientConnectionManagerBuilder = connectionManagerCallback
.configure(poolingAsyncClientConnectionManagerBuilder);
}
}
});
builder.setRequestConfigCallback(requestConfigBuilder -> {
if (!socketTimeout.isNegative()) {
var soTimeout = Timeout.of(Math.toIntExact(socketTimeout.toMillis()), TimeUnit.MILLISECONDS);
requestConfigBuilder.setConnectionRequestTimeout(soTimeout);
} else {
requestConfigBuilder
.setConnectionRequestTimeout(Timeout.of(DEFAULT_RESPONSE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS));
}
// add connectionConfig configurator callbacks provided by the configuration
for (ClientConfiguration.ClientConfigurationCallback<?> clientConfigurer : clientConfiguration
.getClientConfigurers()) {
if (clientConfigurer instanceof ElasticsearchRequestConfigCallback requestConfigCallback) {
requestConfigBuilder = requestConfigCallback.configure(requestConfigBuilder);
}
}
});
return builder;
}
private static HttpHost @NonNull [] getHttpHosts(ClientConfiguration clientConfiguration) {
List<InetSocketAddress> hosts = clientConfiguration.getEndpoints();
boolean useSsl = clientConfiguration.useSsl();
return hosts.stream()
.map(it -> (useSsl ? "https" : "http") + "://" + it.getHostString() + ':' + it.getPort())
.map(URI::create)
.map(HttpHost::create)
.toArray(HttpHost[]::new);
}
private static Header[] toHeaderArray(HttpHeaders headers) {
return headers.entrySet().stream() //
.flatMap(entry -> entry.getValue().stream() //
.map(value -> new BasicHeader(entry.getKey(), value))) //
.toList().toArray(new Header[0]);
}
/**
* {@link ClientConfiguration.ClientConfigurationCallback} to configure the Rest5Client client with a
* {@link Rest5ClientBuilder}
*
* @since 6.0
*/
public interface ElasticsearchRest5ClientConfigurationCallback
extends ClientConfiguration.ClientConfigurationCallback<Rest5ClientBuilder> {
static ElasticsearchRest5ClientConfigurationCallback from(
Function<Rest5ClientBuilder, Rest5ClientBuilder> rest5ClientBuilderCallback) {
Assert.notNull(rest5ClientBuilderCallback, "rest5ClientBuilderCallback must not be null");
return rest5ClientBuilderCallback::apply;
}
}
/**
* {@link org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationCallback} to configure
* the Elasticsearch Rest5Client's Http client with a {@link HttpAsyncClientBuilder}
*
* @since 6.0
*/
public interface ElasticsearchHttpClientConfigurationCallback
extends ClientConfiguration.ClientConfigurationCallback<HttpAsyncClientBuilder> {
static Rest5Clients.ElasticsearchHttpClientConfigurationCallback from(
Function<HttpAsyncClientBuilder, HttpAsyncClientBuilder> httpClientBuilderCallback) {
Assert.notNull(httpClientBuilderCallback, "httpClientBuilderCallback must not be null");
return httpClientBuilderCallback::apply;
}
}
/**
* {@link org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationCallback} to configure
* the Elasticsearch Rest5Client's connection with a {@link ConnectionConfig.Builder}
*
* @since 6.0
*/
public interface ElasticsearchConnectionConfigurationCallback
extends ClientConfiguration.ClientConfigurationCallback<ConnectionConfig.Builder> {
static ElasticsearchConnectionConfigurationCallback from(
Function<ConnectionConfig.Builder, ConnectionConfig.Builder> connectionConfigBuilderCallback) {
Assert.notNull(connectionConfigBuilderCallback, "connectionConfigBuilderCallback must not be null");
return connectionConfigBuilderCallback::apply;
}
}
/**
* {@link org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationCallback} to configure
* the Elasticsearch Rest5Client's connection manager with a {@link PoolingAsyncClientConnectionManagerBuilder}
*
* @since 6.0
*/
public interface ElasticsearchConnectionManagerCallback
extends ClientConfiguration.ClientConfigurationCallback<PoolingAsyncClientConnectionManagerBuilder> {
static ElasticsearchConnectionManagerCallback from(
Function<PoolingAsyncClientConnectionManagerBuilder, PoolingAsyncClientConnectionManagerBuilder> connectionManagerBuilderCallback) {
Assert.notNull(connectionManagerBuilderCallback, "connectionManagerBuilderCallback must not be null");
return connectionManagerBuilderCallback::apply;
}
}
/**
* {@link org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationCallback} to configure
* the Elasticsearch Rest5Client's connection manager with a {@link RequestConfig.Builder}
*
* @since 6.0
*/
public interface ElasticsearchRequestConfigCallback
extends ClientConfiguration.ClientConfigurationCallback<RequestConfig.Builder> {
static ElasticsearchRequestConfigCallback from(
Function<RequestConfig.Builder, RequestConfig.Builder> requestConfigBuilderCallback) {
Assert.notNull(requestConfigBuilderCallback, "requestConfigBuilderCallback must not be null");
return requestConfigBuilderCallback::apply;
}
}
public static Rest5ClientOptions.Builder getRest5ClientOptionsBuilder(@Nullable TransportOptions transportOptions) {
if (transportOptions instanceof Rest5ClientOptions rest5ClientOptions) {
return rest5ClientOptions.toBuilder();
}
var builder = new Rest5ClientOptions.Builder(RequestOptions.DEFAULT.toBuilder());
if (transportOptions != null) {
transportOptions.headers().forEach(header -> builder.addHeader(header.getKey(), header.getValue()));
transportOptions.queryParameters().forEach(builder::setParameter);
builder.onWarnings(transportOptions.onWarnings());
}
return builder;
}
}

View File

@ -0,0 +1,21 @@
/*
* 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.
*/
/**
* This package contains related to the new (from Elasticsearch 9 on) Rest5Client. There are also classes copied over from Elasticsearch in order to have a Rest5ClientBuilder that allows to configure the http client.
*/
@org.jspecify.annotations.NullMarked
package org.springframework.data.elasticsearch.client.elc.rest5_client;

View File

@ -0,0 +1,195 @@
package org.springframework.data.elasticsearch.client.elc.rest_client;
import co.elastic.clients.transport.TransportOptions;
import co.elastic.clients.transport.TransportUtils;
import co.elastic.clients.transport.rest_client.RestClientOptions;
import java.net.InetSocketAddress;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HttpContext;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchClients;
import org.springframework.data.elasticsearch.support.HttpHeaders;
import org.springframework.util.Assert;
/**
* Utility class containing the functions to create the Elasticsearch RestClient used up to Elasticsearch 9.
*
* @since 6.0
* @deprecated since 6.0, use the new Rest5Client the code for that is in the package ../rest_client.
*/
@Deprecated(since = "6.0", forRemoval = true)
public final class RestClients {
/**
* Creates a low level {@link RestClient} for the given configuration.
*
* @param clientConfiguration must not be {@literal null}
* @return the {@link RestClient}
*/
public static RestClient getRestClient(ClientConfiguration clientConfiguration) {
return getRestClientBuilder(clientConfiguration).build();
}
private static RestClientBuilder getRestClientBuilder(ClientConfiguration clientConfiguration) {
HttpHost[] httpHosts = getHttpHosts(clientConfiguration);
RestClientBuilder builder = RestClient.builder(httpHosts);
if (clientConfiguration.getPathPrefix() != null) {
builder.setPathPrefix(clientConfiguration.getPathPrefix());
}
HttpHeaders headers = clientConfiguration.getDefaultHeaders();
if (!headers.isEmpty()) {
builder.setDefaultHeaders(toHeaderArray(headers));
}
builder.setHttpClientConfigCallback(clientBuilder -> {
if (clientConfiguration.getCaFingerprint().isPresent()) {
clientBuilder
.setSSLContext(TransportUtils.sslContextFromCaFingerprint(clientConfiguration.getCaFingerprint().get()));
}
clientConfiguration.getSslContext().ifPresent(clientBuilder::setSSLContext);
clientConfiguration.getHostNameVerifier().ifPresent(clientBuilder::setSSLHostnameVerifier);
clientBuilder.addInterceptorLast(new CustomHeaderInjector(clientConfiguration.getHeadersSupplier()));
RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
Duration connectTimeout = clientConfiguration.getConnectTimeout();
if (!connectTimeout.isNegative()) {
requestConfigBuilder.setConnectTimeout(Math.toIntExact(connectTimeout.toMillis()));
}
Duration socketTimeout = clientConfiguration.getSocketTimeout();
if (!socketTimeout.isNegative()) {
requestConfigBuilder.setSocketTimeout(Math.toIntExact(socketTimeout.toMillis()));
requestConfigBuilder.setConnectionRequestTimeout(Math.toIntExact(socketTimeout.toMillis()));
}
clientBuilder.setDefaultRequestConfig(requestConfigBuilder.build());
clientConfiguration.getProxy().map(HttpHost::create).ifPresent(clientBuilder::setProxy);
for (ClientConfiguration.ClientConfigurationCallback<?> clientConfigurer : clientConfiguration
.getClientConfigurers()) {
if (clientConfigurer instanceof RestClients.ElasticsearchHttpClientConfigurationCallback restClientConfigurationCallback) {
clientBuilder = restClientConfigurationCallback.configure(clientBuilder);
}
}
return clientBuilder;
});
for (ClientConfiguration.ClientConfigurationCallback<?> clientConfigurationCallback : clientConfiguration
.getClientConfigurers()) {
if (clientConfigurationCallback instanceof ElasticsearchRestClientConfigurationCallback configurationCallback) {
builder = configurationCallback.configure(builder);
}
}
return builder;
}
private static HttpHost @NonNull [] getHttpHosts(ClientConfiguration clientConfiguration) {
List<InetSocketAddress> hosts = clientConfiguration.getEndpoints();
boolean useSsl = clientConfiguration.useSsl();
return hosts.stream()
.map(it -> (useSsl ? "https" : "http") + "://" + it.getHostString() + ':' + it.getPort())
.map(HttpHost::create).toArray(HttpHost[]::new);
}
private static org.apache.http.Header[] toHeaderArray(HttpHeaders headers) {
return headers.entrySet().stream() //
.flatMap(entry -> entry.getValue().stream() //
.map(value -> new BasicHeader(entry.getKey(), value))) //
.toArray(org.apache.http.Header[]::new);
}
/**
* Interceptor to inject custom supplied headers.
*
* @since 4.4
*/
record CustomHeaderInjector(Supplier<HttpHeaders> headersSupplier) implements HttpRequestInterceptor {
@Override
public void process(HttpRequest request, HttpContext context) {
HttpHeaders httpHeaders = headersSupplier.get();
if (httpHeaders != null && !httpHeaders.isEmpty()) {
Arrays.stream(toHeaderArray(httpHeaders)).forEach(request::addHeader);
}
}
}
/**
* {@link org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationCallback} to configure
* the Elasticsearch RestClient's Http client with a {@link HttpAsyncClientBuilder}
*
* @since 4.4
*/
public interface ElasticsearchHttpClientConfigurationCallback
extends ClientConfiguration.ClientConfigurationCallback<HttpAsyncClientBuilder> {
static RestClients.ElasticsearchHttpClientConfigurationCallback from(
Function<HttpAsyncClientBuilder, HttpAsyncClientBuilder> httpClientBuilderCallback) {
Assert.notNull(httpClientBuilderCallback, "httpClientBuilderCallback must not be null");
return httpClientBuilderCallback::apply;
}
}
/**
* {@link org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationCallback} to configure
* the RestClient client with a {@link RestClientBuilder}
*
* @since 5.0
*/
public interface ElasticsearchRestClientConfigurationCallback
extends ClientConfiguration.ClientConfigurationCallback<RestClientBuilder> {
static ElasticsearchRestClientConfigurationCallback from(
Function<RestClientBuilder, RestClientBuilder> restClientBuilderCallback) {
Assert.notNull(restClientBuilderCallback, "restClientBuilderCallback must not be null");
return restClientBuilderCallback::apply;
}
}
public static RestClientOptions.Builder getRestClientOptionsBuilder(@Nullable TransportOptions transportOptions) {
if (transportOptions instanceof RestClientOptions restClientOptions) {
return restClientOptions.toBuilder();
}
var builder = new RestClientOptions.Builder(RequestOptions.DEFAULT.toBuilder());
if (transportOptions != null) {
transportOptions.headers().forEach(header -> builder.addHeader(header.getKey(), header.getValue()));
transportOptions.queryParameters().forEach(builder::setParameter);
builder.onWarnings(transportOptions.onWarnings());
}
return builder;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2021-2025 the original author or authors.
* Copyright 2022-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.
@ -13,15 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.query;
/**
* Define script types for update queries.
*
* @author Farid Faoudi
* @since 4.2
* This package contains related to the old (up to Elasticsearch 9) RestClient.
*/
public enum ScriptType {
INLINE, STORED
}
@Deprecated(since = "6.0", forRemoval=true)
@org.jspecify.annotations.NullMarked
package org.springframework.data.elasticsearch.client.elc.rest_client;

View File

@ -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();

View File

@ -17,6 +17,7 @@ package org.springframework.data.elasticsearch.core;
import java.util.Map;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
@ -369,7 +370,7 @@ public class EntityOperations {
* @see org.springframework.data.elasticsearch.core.EntityOperations.AdaptableEntity#initializeVersionProperty()
*/
@Override
public T initializeVersionProperty() {
public @NonNull T initializeVersionProperty() {
return map;
}
@ -389,7 +390,7 @@ public class EntityOperations {
}
@Override
public SeqNoPrimaryTerm getSeqNoPrimaryTerm() {
public @Nullable SeqNoPrimaryTerm getSeqNoPrimaryTerm() {
return null;
}
@ -398,7 +399,7 @@ public class EntityOperations {
* @see org.springframework.data.elasticsearch.core.EntityOperations.AdaptableEntity#incrementVersion()
*/
@Override
public T incrementVersion() {
public @NonNull T incrementVersion() {
return map;
}
@ -407,7 +408,7 @@ public class EntityOperations {
* @see org.springframework.data.elasticsearch.core.EntityOperations.Entity#getBean()
*/
@Override
public T getBean() {
public @NonNull T getBean() {
return map;
}
@ -425,12 +426,12 @@ public class EntityOperations {
* @see org.springframework.data.elasticsearch.core.EntityOperations.Entity#getPersistentEntity()
*/
@Override
public ElasticsearchPersistentEntity<?> getPersistentEntity() {
public @Nullable ElasticsearchPersistentEntity<?> getPersistentEntity() {
return null;
}
@Override
public String getRouting() {
public @Nullable String getRouting() {
return null;
}
}
@ -650,7 +651,7 @@ public class EntityOperations {
}
@Override
public String getRouting() {
public @Nullable String getRouting() {
String routing = routingResolver.getRouting(propertyAccessor.getBean());

View File

@ -48,12 +48,12 @@ public class SearchHit<T> {
@Nullable private final NestedMetaData nestedMetaData;
@Nullable private final String routing;
@Nullable private final Explanation explanation;
private final List<String> matchedQueries = new ArrayList<>();
private final Map<String, Double> matchedQueries = new LinkedHashMap<>();
public SearchHit(@Nullable String index, @Nullable String id, @Nullable String routing, float score,
@Nullable Object[] sortValues, @Nullable Map<String, List<String>> highlightFields,
@Nullable Map<String, SearchHits<?>> innerHits, @Nullable NestedMetaData nestedMetaData,
@Nullable Explanation explanation, @Nullable List<String> matchedQueries, T content) {
@Nullable Explanation explanation, @Nullable Map<String, Double> matchedQueries, T content) {
this.index = index;
this.id = id;
this.routing = routing;
@ -73,7 +73,7 @@ public class SearchHit<T> {
this.content = content;
if (matchedQueries != null) {
this.matchedQueries.addAll(matchedQueries);
this.matchedQueries.putAll(matchedQueries);
}
}
@ -194,7 +194,7 @@ public class SearchHit<T> {
* @return the matched queries for this SearchHit.
*/
@Nullable
public List<String> getMatchedQueries() {
public Map<String, Double> getMatchedQueries() {
return matchedQueries;
}
}

View File

@ -714,7 +714,7 @@ public class MappingElasticsearchConverter
}
@Override
public <T> T getPropertyValue(ElasticsearchPersistentProperty property) {
public <T> @Nullable T getPropertyValue(ElasticsearchPersistentProperty property) {
String expression = property.getSpelExpression();
Object value = expression != null ? evaluator.evaluate(expression) : accessor.get(property);

View File

@ -125,5 +125,5 @@ public interface SearchDocument extends Document {
* @return the matched queries for the SearchHit.
*/
@Nullable
List<String> getMatchedQueries();
Map<String, Double> getMatchedQueries();
}

View File

@ -41,12 +41,12 @@ public class SearchDocumentAdapter implements SearchDocument {
private final Map<String, SearchDocumentResponse> innerHits = new HashMap<>();
@Nullable private final NestedMetaData nestedMetaData;
@Nullable private final Explanation explanation;
@Nullable private final List<String> matchedQueries;
@Nullable private final Map<String, Double> matchedQueries;
@Nullable private final String routing;
public SearchDocumentAdapter(Document delegate, float score, Object[] sortValues, Map<String, List<Object>> fields,
Map<String, List<String>> highlightFields, Map<String, SearchDocumentResponse> innerHits,
@Nullable NestedMetaData nestedMetaData, @Nullable Explanation explanation, @Nullable List<String> matchedQueries,
@Nullable NestedMetaData nestedMetaData, @Nullable Explanation explanation, @Nullable Map<String, Double> matchedQueries,
@Nullable String routing) {
this.delegate = delegate;
@ -249,7 +249,7 @@ public class SearchDocumentAdapter implements SearchDocument {
@Override
@Nullable
public List<String> getMatchedQueries() {
public Map<String, Double> getMatchedQueries() {
return matchedQueries;
}

View File

@ -66,8 +66,7 @@ public class AliasActionParameters {
return indices;
}
@Nullable
public String[] getAliases() {
public String@Nullable[] getAliases() {
return aliases;
}
@ -107,8 +106,8 @@ public class AliasActionParameters {
}
public static final class Builder {
@Nullable private String[] indices;
@Nullable private String[] aliases;
private String @Nullable [] indices;
private String @Nullable [] aliases;
@Nullable private Query filterQuery;
@Nullable private Class<?> filterQueryClass;
@Nullable private Boolean isHidden;

View File

@ -55,6 +55,7 @@ public class AliasActions {
public AliasActions add(@Nullable AliasAction... actions) {
if (actions != null) {
// noinspection NullableProblems
this.actions.addAll(Arrays.asList(actions));
}

View File

@ -29,6 +29,7 @@ import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.annotation.Transient;
@ -273,7 +274,7 @@ public class MappingBuilder {
writeTypeHintMapping(propertiesNode);
if (entity != null) {
entity.doWithProperties((PropertyHandler<ElasticsearchPersistentProperty>) property -> {
entity.doWithProperties((PropertyHandler<@NonNull ElasticsearchPersistentProperty>) property -> {
try {
if (property.isAnnotationPresent(Transient.class) || isInIgnoreFields(property, parentFieldAnnotation)) {
return;

View File

@ -35,7 +35,7 @@ public record PutIndexTemplateRequest(String name, String[] indexPatterns, @Null
public static class Builder {
@Nullable private String name;
@Nullable private String[] indexPatterns;
private String @Nullable [] indexPatterns;
@Nullable private Settings settings;
@Nullable private Document mapping;
@Nullable AliasActions aliasActions;

View File

@ -81,7 +81,7 @@ public class TemplateData {
@Nullable Document mapping;
int order;
@Nullable Integer version;
@Nullable private String[] indexPatterns;
@Nullable private String@Nullable [] indexPatterns;
@Nullable private Map<String, AliasData> aliases;
private TemplateDataBuilder() {}

View File

@ -65,7 +65,7 @@ public interface ElasticsearchPersistentEntity<T> extends PersistentEntity<T, El
String getIndexStoreType();
@Override
ElasticsearchPersistentProperty getVersionProperty();
@Nullable ElasticsearchPersistentProperty getVersionProperty();
@Nullable
String settingPath();

View File

@ -23,6 +23,7 @@ import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.dao.InvalidDataAccessApiUsageException;
@ -298,7 +299,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
return fieldNamePropertyCache.computeIfAbsent(fieldName, key -> {
AtomicReference<ElasticsearchPersistentProperty> propertyRef = new AtomicReference<>();
doWithProperties((PropertyHandler<ElasticsearchPersistentProperty>) property -> {
doWithProperties((PropertyHandler<@NonNull ElasticsearchPersistentProperty>) property -> {
if (key.equals(property.getFieldName())) {
propertyRef.set(property);
}
@ -552,10 +553,10 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
short replicas;
@Nullable String refreshIntervall;
@Nullable String indexStoreType;
@Nullable private String[] sortFields;
private Setting.@Nullable SortOrder[] sortOrders;
private Setting.@Nullable SortMode[] sortModes;
private Setting.@Nullable SortMissing[] sortMissingValues;
private String @Nullable [] sortFields;
private Setting.@Nullable SortOrder @Nullable [] sortOrders;
private Setting.@Nullable SortMode @Nullable [] sortModes;
private Setting.@Nullable SortMissing @Nullable [] sortMissingValues;
Settings toSettings() {

View File

@ -28,6 +28,7 @@ import java.util.Optional;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.util.Assert;
@ -47,10 +48,15 @@ import org.springframework.util.Assert;
*/
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<String> fields = new ArrayList<>();
@Nullable protected List<String> 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<IndicesOptions.WildcardStates> expandWildcards;
private EnumSet<IndicesOptions.WildcardStates> expandWildcards = EnumSet.noneOf(IndicesOptions.WildcardStates.class);
private List<DocValueField> docValueFields = new ArrayList<>();
private List<ScriptedField> scriptedFields = new ArrayList<>();
@ -87,7 +93,7 @@ public class BaseQuery implements Query {
public <Q extends BaseQuery, B extends BaseQueryBuilder<Q, B>> BaseQuery(BaseQueryBuilder<Q, B> 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 class BaseQuery implements Query {
@Override
@SuppressWarnings("unchecked")
public final <T extends Query> T addSort(@Nullable Sort sort) {
if (sort == null) {
if (sort == null || sort.isUnsorted()) {
return (T) this;
}
@ -561,4 +567,52 @@ public class BaseQuery implements Query {
public List<ScriptedField> 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;
}
}

View File

@ -200,7 +200,7 @@ public abstract class BaseQueryBuilder<Q extends BaseQuery, SELF extends BaseQue
/**
* @since 5.1
*/
public Integer getReactiveBatchSize() {
public @Nullable Integer getReactiveBatchSize() {
return reactiveBatchSize;
}

View File

@ -344,8 +344,8 @@ public class ByQueryResponse {
*
* @return a new {@link SearchFailureBuilder} to build {@link SearchFailure}
*/
public static SearchFailureBuilder builder() {
return new SearchFailureBuilder();
public static SearchFailureBuilder builder(Throwable reason) {
return new SearchFailureBuilder(reason);
}
/**
@ -358,7 +358,9 @@ public class ByQueryResponse {
@Nullable private Integer shardId;
@Nullable private String nodeId;
private SearchFailureBuilder() {}
private SearchFailureBuilder(Throwable reason) {
this.reason = reason;
}
public SearchFailureBuilder withReason(Throwable reason) {
this.reason = reason;

View File

@ -63,12 +63,12 @@ public class FetchSourceFilter implements SourceFilter {
}
@Override
public String[] getIncludes() {
public @Nullable String[] getIncludes() {
return includes;
}
@Override
public String[] getExcludes() {
public @Nullable String[] getExcludes() {
return excludes;
}
}

View File

@ -26,8 +26,8 @@ import org.jspecify.annotations.Nullable;
public class FetchSourceFilterBuilder {
@Nullable private Boolean fetchSource;
@Nullable private String[] includes;
@Nullable private String[] excludes;
private String @Nullable [] includes;
private String @Nullable [] excludes;
public FetchSourceFilterBuilder withIncludes(String... includes) {
this.includes = includes;

View File

@ -15,6 +15,7 @@
*/
package org.springframework.data.elasticsearch.core.query;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
@ -92,7 +93,7 @@ public class GeoDistanceOrder extends Order {
getIgnoreUnmapped());
}
public GeoDistanceOrder with(Mode mode) {
public GeoDistanceOrder with(@Nullable Mode mode) {
return new GeoDistanceOrder(getProperty(), getGeoPoint(), getDirection(), getDistanceType(), mode, getUnit(),
getIgnoreUnmapped());
}

View File

@ -121,7 +121,7 @@ public class HasChildQuery {
public static final class Builder {
private final String type;
private Query query;
@Nullable private Query query;
@Nullable private Boolean ignoreUnmapped;

View File

@ -92,7 +92,7 @@ public class HasParentQuery {
public static class Builder {
private final String parentType;
private Query query;
@Nullable private Query query;
@Nullable private Boolean score;
@Nullable private Boolean ignoreUnmapped;

View File

@ -484,6 +484,13 @@ public interface Query {
*/
List<ScriptedField> 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
*/

View File

@ -15,100 +15,110 @@
*/
package org.springframework.data.elasticsearch.core.query;
import java.util.Map;
import java.util.function.Function;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
import java.util.Map;
import java.util.function.Function;
/**
* value class combining script information.
* <p>
* A script is either an inline script, then the script parameters must be set
* or it refers to a stored script, then the name parameter is required.
*
* @param language the language when the script is passed in the script parameter
* @param script the script to use as inline script
* @param scriptName the name when using a stored script
* @param params the script parameters
* @author Peter-Josef Meisch
* @since 4.4
*/
public record ScriptData(ScriptType type, @Nullable String language, @Nullable String script,
@Nullable String scriptName, @Nullable Map<String, Object> params) {
public record ScriptData(@Nullable String language, @Nullable String script,
@Nullable String scriptName, @Nullable Map<String, Object> params) {
public ScriptData(ScriptType type, @Nullable String language, @Nullable String script, @Nullable String scriptName,
@Nullable Map<String, Object> params) {
/*
* constructor overload to check the parameters
*/
public ScriptData(@Nullable String language, @Nullable String script, @Nullable String scriptName,
@Nullable Map<String, Object> params) {
Assert.notNull(type, "type must not be null");
Assert.isTrue(script != null || scriptName != null, "script or scriptName is required");
this.type = type;
this.language = language;
this.script = script;
this.scriptName = scriptName;
this.params = params;
}
this.language = language;
this.script = script;
this.scriptName = scriptName;
this.params = params;
}
/**
* @since 5.2
*/
public static ScriptData of(ScriptType type, @Nullable String language, @Nullable String script,
@Nullable String scriptName, @Nullable Map<String, Object> params) {
return new ScriptData(type, language, script, scriptName, params);
}
/**
* factory method to create a ScriptData object.
*
* @since 5.2
*/
public static ScriptData of(@Nullable String language, @Nullable String script,
@Nullable String scriptName, @Nullable Map<String, Object> params) {
return new ScriptData(language, script, scriptName, params);
}
public static ScriptData of(Function<Builder, Builder> builderFunction) {
/**
* factory method to create a ScriptData object using a ScriptBuilder callback.
*
* @param builderFunction function called to populate the builder
* @return
*/
public static ScriptData of(Function<Builder, Builder> builderFunction) {
Assert.notNull(builderFunction, "f must not be null");
Assert.notNull(builderFunction, "builderFunction must not be null");
return builderFunction.apply(new Builder()).build();
}
return builderFunction.apply(new Builder()).build();
}
/**
* @since 5.2
*/
public static Builder builder() {
return new Builder();
}
/**
* @since 5.2
*/
public static Builder builder() {
return new Builder();
}
/**
* @since 5.2
*/
public static final class Builder {
@Nullable private ScriptType type;
@Nullable private String language;
@Nullable private String script;
@Nullable private String scriptName;
@Nullable private Map<String, Object> params;
/**
* @since 5.2
*/
public static final class Builder {
@Nullable
private String language;
@Nullable
private String script;
@Nullable
private String scriptName;
@Nullable
private Map<String, Object> params;
private Builder() {}
private Builder() {
}
public Builder withType(ScriptType type) {
public Builder withLanguage(@Nullable String language) {
this.language = language;
return this;
}
Assert.notNull(type, "type must not be null");
public Builder withScript(@Nullable String script) {
this.script = script;
return this;
}
this.type = type;
return this;
}
public Builder withScriptName(@Nullable String scriptName) {
this.scriptName = scriptName;
return this;
}
public Builder withLanguage(@Nullable String language) {
this.language = language;
return this;
}
public Builder withParams(@Nullable Map<String, Object> params) {
this.params = params;
return this;
}
public Builder withScript(@Nullable String script) {
this.script = script;
return this;
}
public Builder withScriptName(@Nullable String scriptName) {
this.scriptName = scriptName;
return this;
}
public Builder withParams(@Nullable Map<String, Object> params) {
this.params = params;
return this;
}
public ScriptData build() {
Assert.notNull(type, "type must be set");
return new ScriptData(type, language, script, scriptName, params);
}
}
public ScriptData build() {
return new ScriptData(language, script, scriptName, params);
}
}
}

View File

@ -32,14 +32,14 @@ public interface SourceFilter {
/**
* @return the name of the fields to include in a response.
*/
@Nullable
String[] getIncludes();
String@Nullable[] getIncludes();
/**
* @return the names of the fields to exclude from a response.
*/
@Nullable
String[] getExcludes();
String@Nullable[] getExcludes();
/**
* Flag to set the _source parameter in a query to true or false. If this is not null, the values returned from

View File

@ -42,8 +42,8 @@ public class UpdateQuery {
@Nullable private final Boolean fetchSource;
@Nullable private final List<String> fetchSourceIncludes;
@Nullable private final List<String> fetchSourceExcludes;
@Nullable private final Integer ifSeqNo;
@Nullable private final Integer ifPrimaryTerm;
@Nullable private final Long ifSeqNo;
@Nullable private final Long ifPrimaryTerm;
@Nullable private final RefreshPolicy refreshPolicy;
@Nullable private final Integer retryOnConflict;
@Nullable private final String timeout;
@ -71,12 +71,12 @@ public class UpdateQuery {
private UpdateQuery(String id, @Nullable String script, @Nullable Map<String, Object> params,
@Nullable Document document, @Nullable Document upsert, @Nullable String lang, @Nullable String routing,
@Nullable Boolean scriptedUpsert, @Nullable Boolean docAsUpsert, @Nullable Boolean fetchSource,
@Nullable List<String> fetchSourceIncludes, @Nullable List<String> fetchSourceExcludes, @Nullable Integer ifSeqNo,
@Nullable Integer ifPrimaryTerm, @Nullable RefreshPolicy refreshPolicy, @Nullable Integer retryOnConflict,
@Nullable List<String> fetchSourceIncludes, @Nullable List<String> fetchSourceExcludes, @Nullable Long ifSeqNo,
@Nullable Long ifPrimaryTerm, @Nullable RefreshPolicy refreshPolicy, @Nullable Integer retryOnConflict,
@Nullable String timeout, @Nullable String waitForActiveShards, @Nullable Query query,
@Nullable Boolean abortOnVersionConflict, @Nullable Integer batchSize, @Nullable Integer maxDocs,
@Nullable Integer maxRetries, @Nullable String pipeline, @Nullable Float requestsPerSecond,
@Nullable Boolean shouldStoreResult, @Nullable Integer slices, @Nullable ScriptType scriptType,
@Nullable Boolean shouldStoreResult, @Nullable Integer slices,
@Nullable String scriptName, @Nullable String indexName) {
this.id = id;
@ -105,8 +105,8 @@ public class UpdateQuery {
this.slices = slices;
this.indexName = indexName;
if (scriptType != null || lang != null || script != null || scriptName != null || params != null) {
this.scriptData = new ScriptData(scriptType, lang, script, scriptName, params);
if (lang != null || script != null || scriptName != null || params != null) {
this.scriptData = new ScriptData(lang, script, scriptName, params);
} else {
this.scriptData = null;
}
@ -172,12 +172,12 @@ public class UpdateQuery {
}
@Nullable
public Integer getIfSeqNo() {
public Long getIfSeqNo() {
return ifSeqNo;
}
@Nullable
public Integer getIfPrimaryTerm() {
public Long getIfPrimaryTerm() {
return ifPrimaryTerm;
}
@ -246,11 +246,6 @@ public class UpdateQuery {
return slices;
}
@Nullable
public ScriptType getScriptType() {
return scriptData != null ? scriptData.type() : null;
}
@Nullable
public String getScriptName() {
return scriptData != null ? scriptData.scriptName() : null;
@ -273,7 +268,7 @@ public class UpdateQuery {
}
public static final class Builder {
private String id;
private String id = "";
@Nullable private String script = null;
@Nullable private Map<String, Object> params;
@Nullable private Document document = null;
@ -283,8 +278,8 @@ public class UpdateQuery {
@Nullable private Boolean scriptedUpsert;
@Nullable private Boolean docAsUpsert;
@Nullable private Boolean fetchSource;
@Nullable private Integer ifSeqNo;
@Nullable private Integer ifPrimaryTerm;
@Nullable private Long ifSeqNo;
@Nullable private Long ifPrimaryTerm;
@Nullable private RefreshPolicy refreshPolicy;
@Nullable private Integer retryOnConflict;
@Nullable private String timeout;
@ -300,7 +295,6 @@ public class UpdateQuery {
@Nullable private Float requestsPerSecond;
@Nullable private Boolean shouldStoreResult;
@Nullable private Integer slices;
@Nullable private ScriptType scriptType;
@Nullable private String scriptName;
@Nullable private String indexName;
@ -357,12 +351,12 @@ public class UpdateQuery {
return this;
}
public Builder withIfSeqNo(Integer ifSeqNo) {
public Builder withIfSeqNo(Long ifSeqNo) {
this.ifSeqNo = ifSeqNo;
return this;
}
public Builder withIfPrimaryTerm(Integer ifPrimaryTerm) {
public Builder withIfPrimaryTerm(Long ifPrimaryTerm) {
this.ifPrimaryTerm = ifPrimaryTerm;
return this;
}
@ -437,11 +431,6 @@ public class UpdateQuery {
return this;
}
public Builder withScriptType(ScriptType scriptType) {
this.scriptType = scriptType;
return this;
}
public Builder withScriptName(String scriptName) {
this.scriptName = scriptName;
return this;
@ -456,7 +445,7 @@ public class UpdateQuery {
return new UpdateQuery(id, script, params, document, upsert, lang, routing, scriptedUpsert, docAsUpsert,
fetchSource, fetchSourceIncludes, fetchSourceExcludes, ifSeqNo, ifPrimaryTerm, refreshPolicy, retryOnConflict,
timeout, waitForActiveShards, query, abortOnVersionConflict, batchSize, maxDocs, maxRetries, pipeline,
requestsPerSecond, shouldStoreResult, slices, scriptType, scriptName, indexName);
requestsPerSecond, shouldStoreResult, slices, scriptName, indexName);
}
public Builder withIndex(@Nullable String indexName) {

View File

@ -249,16 +249,15 @@ public class ReindexRequest {
}
public static class Script {
private final String source;
@Nullable private final String source;
@Nullable private final String lang;
private Script(String source, @Nullable String lang) {
Assert.notNull(source, "source must not be null");
private Script(@Nullable String source, @Nullable String lang) {
this.source = source;
this.lang = lang;
}
@Nullable
public String getSource() {
return source;
}

View File

@ -29,7 +29,6 @@ public record Script(String id, String language, String source) {
Assert.notNull(id, "id must not be null");
Assert.notNull(language, "language must not be null");
Assert.notNull(source, "source must not be null");
}
public static ScriptBuilder builder() {

View File

@ -18,6 +18,7 @@ package org.springframework.data.elasticsearch.repository.query;
import java.util.LinkedHashMap;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.SearchTemplateQuery;
@ -33,7 +34,7 @@ import org.springframework.util.Assert;
public class ReactiveRepositorySearchTemplateQuery extends AbstractReactiveElasticsearchRepositoryQuery {
private String id;
private Map<String, Object> params;
private Map<String, Object> params = Map.of();
public ReactiveRepositorySearchTemplateQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations elasticsearchOperations,

View File

@ -33,7 +33,7 @@ import org.springframework.util.Assert;
public class RepositorySearchTemplateQuery extends AbstractElasticsearchRepositoryQuery {
private String id;
private Map<String, Object> params;
private Map<String, Object> params = Map.of();
public RepositorySearchTemplateQuery(ElasticsearchQueryMethod queryMethod,
ElasticsearchOperations elasticsearchOperations, ValueExpressionDelegate valueExpressionDelegate,

View File

@ -82,7 +82,7 @@ class ExampleCriteriaMapper {
Object propertyValue = propertyAccessor.getProperty(property);
if (property.isMap() && propertyValue != null) {
for (Map.Entry<String, Object> entry : ((Map<String, Object>) propertyValue).entrySet()) {
for (Map.Entry<String, @Nullable Object> entry : ((Map<String, @Nullable Object>) propertyValue).entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
criteria = applyPropertySpec(propertyPath + "." + key, value, exampleSpecAccessor, property, matchMode,
@ -96,7 +96,7 @@ class ExampleCriteriaMapper {
return criteria;
}
private Criteria applyPropertySpec(String path, Object propertyValue, ExampleMatcherAccessor exampleSpecAccessor,
private Criteria applyPropertySpec(String path, @Nullable Object propertyValue, ExampleMatcherAccessor exampleSpecAccessor,
ElasticsearchPersistentProperty property, ExampleMatcher.MatchMode matchMode, Criteria criteria) {
if (exampleSpecAccessor.isIgnoreCaseForPath(path)) {

View File

@ -47,13 +47,14 @@ public class QueryByExampleElasticsearchExecutor<T> implements QueryByExampleExe
@Override
public <S extends T> Optional<S> findOne(Example<S> example) {
CriteriaQuery criteriaQuery = CriteriaQuery.builder(exampleCriteriaMapper.criteria(example)).withMaxResults(2).build();
CriteriaQuery criteriaQuery = CriteriaQuery.builder(exampleCriteriaMapper.criteria(example)).withMaxResults(2)
.build();
SearchHits<S> searchHits = operations.search(criteriaQuery, example.getProbeType(),
operations.getIndexCoordinatesFor(example.getProbeType()));
if (searchHits.getTotalHits() > 1) {
throw new org.springframework.dao.IncorrectResultSizeDataAccessException(1);
}
return Optional.ofNullable(searchHits).filter(SearchHits::hasSearchHits)
return Optional.of(searchHits).filter(SearchHits::hasSearchHits)
.map(result -> (List<S>) SearchHitSupport.unwrapSearchHits(result)).map(s -> s.get(0));
}

View File

@ -39,11 +39,11 @@ import org.springframework.util.MultiValueMapAdapter;
* @author Peter-Josef Meisch
* @since 5.0
*/
public class HttpHeaders implements MultiValueMap<String, String> {
public class HttpHeaders implements MultiValueMap<String, @Nullable String> {
public static final String AUTHORIZATION = "Authorization";
private final MultiValueMap<String, String> delegate;
private final MultiValueMap<String, @Nullable String> delegate;
public HttpHeaders() {
this.delegate = new MultiValueMapAdapter<>(new LinkedCaseInsensitiveMap<>(Locale.ENGLISH));
@ -62,12 +62,12 @@ public class HttpHeaders implements MultiValueMap<String, String> {
}
@Override
public void addAll(String key, List<? extends String> values) {
public void addAll(String key, List<? extends @Nullable String> values) {
delegate.addAll(key, values);
}
@Override
public void addAll(MultiValueMap<String, String> values) {
public void addAll(MultiValueMap<String, @Nullable String> values) {
delegate.addAll(values);
}
@ -77,12 +77,12 @@ public class HttpHeaders implements MultiValueMap<String, String> {
}
@Override
public void setAll(Map<String, String> values) {
public void setAll(Map<String, @Nullable String> values) {
delegate.setAll(values);
}
@Override
public Map<String, String> toSingleValueMap() {
public Map<String, @Nullable String> toSingleValueMap() {
return delegate.toSingleValueMap();
}
// endregion
@ -110,18 +110,18 @@ public class HttpHeaders implements MultiValueMap<String, String> {
@Override
@Nullable
public List<String> get(Object key) {
public List<@Nullable String> get(Object key) {
return delegate.get(key);
}
@Nullable
@Override
public List<String> put(String key, List<String> value) {
public List<@Nullable String> put(String key, List<@Nullable String> value) {
return delegate.put(key, value);
}
@Override
public List<String> remove(Object key) {
public List<@Nullable String> remove(Object key) {
return delegate.remove(key);
}

View File

@ -92,6 +92,17 @@ public final class VersionInfo {
}
}
/**
* @return a String to use in a header, containing the versions of Spring Data Elasticsearch and the Elasticsearch
* library
* @since 6.0
*/
public static String clientVersions() {
return String.format("spring-data-elasticsearch %s / elasticsearch client %s",
versionProperties.getProperty(VERSION_SPRING_DATA_ELASTICSEARCH),
versionProperties.getProperty(VERSION_ELASTICSEARCH_CLIENT));
}
/**
* gets the version properties from the classpath resource.
*

View File

@ -24,4 +24,4 @@ import org.springframework.data.repository.kotlin.CoroutineSortingRepository
* @since 5.2
*/
@NoRepositoryBean
interface CoroutineElasticsearchRepository<T, ID> : CoroutineCrudRepository<T, ID>, CoroutineSortingRepository<T, ID>
interface CoroutineElasticsearchRepository<T : Any, ID : Any> : CoroutineCrudRepository<T, ID>, CoroutineSortingRepository<T, ID>

View File

@ -1,4 +1,4 @@
Spring Data Elasticsearch 6.0 M2 (2025.1.0)
Spring Data Elasticsearch 6.0 M4 (2025.1.0)
Copyright (c) [2013-2022] Pivotal Software, Inc.
This product is licensed to you under the Apache License, Version 2.0 (the "License").
@ -9,3 +9,5 @@ separate copyright notices and license terms. Your use of the source
code for the these subcomponents is subject to the terms and
conditions of the subcomponent's license, as noted in the LICENSE file.

View File

@ -1,21 +1,22 @@
/*
* 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.
*/
* 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.client;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static io.specto.hoverfly.junit.dsl.HoverflyDsl.*;
import static io.specto.hoverfly.junit.verification.HoverflyVerifications.*;
@ -40,6 +41,8 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchClients;
import org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchClient;
import org.springframework.data.elasticsearch.client.elc.rest5_client.Rest5Clients;
import org.springframework.data.elasticsearch.client.elc.rest_client.RestClients;
import org.springframework.data.elasticsearch.support.HttpHeaders;
import com.github.tomakehurst.wiremock.WireMockServer;
@ -100,8 +103,11 @@ public class RestClientsTest {
defaultHeaders.add("def2", "def2-1");
AtomicInteger supplierCount = new AtomicInteger(1);
AtomicInteger httpClientConfigurerCount = new AtomicInteger(0);
AtomicInteger restClientConfigurerCount = new AtomicInteger(0);
AtomicInteger httpClientConfigurerCount = new AtomicInteger(0);
AtomicInteger connectionConfigurerCount = new AtomicInteger(0);
AtomicInteger connectionManagerConfigurerCount = new AtomicInteger(0);
AtomicInteger requestConfigurerCount = new AtomicInteger(0);
ClientConfigurationBuilder configurationBuilder = new ClientConfigurationBuilder();
configurationBuilder //
@ -115,25 +121,52 @@ public class RestClientsTest {
return httpHeaders;
});
if (clientUnderTestFactory instanceof ELCUnderTestFactory) {
if (clientUnderTestFactory instanceof ELCRest5ClientUnderTestFactory) {
configurationBuilder.withClientConfigurer(
ElasticsearchClients.ElasticsearchHttpClientConfigurationCallback.from(httpClientBuilder -> {
Rest5Clients.ElasticsearchRest5ClientConfigurationCallback.from(rest5ClientBuilder -> {
restClientConfigurerCount.incrementAndGet();
return rest5ClientBuilder;
}));
configurationBuilder.withClientConfigurer(
Rest5Clients.ElasticsearchHttpClientConfigurationCallback.from(httpClientBuilder -> {
httpClientConfigurerCount.incrementAndGet();
return httpClientBuilder;
}));
configurationBuilder.withClientConfigurer(
ElasticsearchClients.ElasticsearchRestClientConfigurationCallback.from(restClientBuilder -> {
Rest5Clients.ElasticsearchConnectionConfigurationCallback.from(connectionConfigBuilder -> {
connectionConfigurerCount.incrementAndGet();
return connectionConfigBuilder;
}));
configurationBuilder.withClientConfigurer(
Rest5Clients.ElasticsearchConnectionManagerCallback.from(connectionManagerBuilder -> {
connectionManagerConfigurerCount.incrementAndGet();
return connectionManagerBuilder;
}));
configurationBuilder.withClientConfigurer(
Rest5Clients.ElasticsearchRequestConfigCallback.from(requestConfigBuilder -> {
requestConfigurerCount.incrementAndGet();
return requestConfigBuilder;
}));
} else if (clientUnderTestFactory instanceof ELCRestClientUnderTestFactory) {
configurationBuilder.withClientConfigurer(
RestClients.ElasticsearchHttpClientConfigurationCallback.from(httpClientBuilder -> {
httpClientConfigurerCount.incrementAndGet();
return httpClientBuilder;
}));
configurationBuilder.withClientConfigurer(
RestClients.ElasticsearchRestClientConfigurationCallback.from(restClientBuilder -> {
restClientConfigurerCount.incrementAndGet();
return restClientBuilder;
}));
} else if (clientUnderTestFactory instanceof ReactiveELCUnderTestFactory) {
configurationBuilder
.withClientConfigurer(ElasticsearchClients.ElasticsearchHttpClientConfigurationCallback.from(webClient -> {
.withClientConfigurer(RestClients.ElasticsearchHttpClientConfigurationCallback.from(webClient -> {
httpClientConfigurerCount.incrementAndGet();
return webClient;
}));
configurationBuilder.withClientConfigurer(
ElasticsearchClients.ElasticsearchRestClientConfigurationCallback.from(restClientBuilder -> {
RestClients.ElasticsearchRestClientConfigurationCallback.from(restClientBuilder -> {
restClientConfigurerCount.incrementAndGet();
return restClientBuilder;
}));
@ -155,19 +188,24 @@ public class RestClientsTest {
.withHeader("def2", new EqualToPattern("def2-1")) //
.withHeader("supplied", new EqualToPattern("val0")) //
// on the first call Elasticsearch does the version check and thus already increments the counter
.withHeader("supplied", new EqualToPattern("val" + (i))) //
);
.withHeader("supplied", new EqualToPattern("val" + i)) //
.withHeader("supplied", including("val0", "val" + i)));
;
}
assertThat(restClientConfigurerCount).hasValue(clientUnderTestFactory.getExpectedRestClientConfigurerCalls());
assertThat(httpClientConfigurerCount).hasValue(1);
assertThat(restClientConfigurerCount).hasValue(clientUnderTestFactory.getExpectedRestClientConfigCalls());
assertThat(connectionConfigurerCount).hasValue(clientUnderTestFactory.getExpectedConnectionConfigurerCalls());
assertThat(connectionManagerConfigurerCount)
.hasValue(clientUnderTestFactory.getExpectedConnectionManagerConfigurerCalls());
assertThat(requestConfigurerCount).hasValue(clientUnderTestFactory.getExpectedRequestConfigurerCalls());
});
}
@ParameterizedTest // #2088
@MethodSource("clientUnderTestFactorySource")
@DisplayName("should set compatibility headers")
void shouldSetCompatibilityHeaders(ClientUnderTestFactory clientUnderTestFactory) {
@DisplayName("should set explicit compatibility headers")
void shouldSetExplicitCompatibilityHeaders(ClientUnderTestFactory clientUnderTestFactory) {
wireMockServer(server -> {
@ -190,7 +228,8 @@ public class RestClientsTest {
}
""" //
, 201) //
.withHeader("Content-Type", "application/vnd.elasticsearch+json;compatible-with=7") //
.withHeader("Content-Type",
"application/vnd.elasticsearch+json;compatible-with=7") //
.withHeader("X-Elastic-Product", "Elasticsearch")));
ClientConfigurationBuilder configurationBuilder = new ClientConfigurationBuilder();
@ -198,8 +237,8 @@ public class RestClientsTest {
.connectedTo("localhost:" + server.port()) //
.withHeaders(() -> {
HttpHeaders defaultCompatibilityHeaders = new HttpHeaders();
defaultCompatibilityHeaders.add("Accept", "application/vnd.elasticsearch+json;compatible-with=7");
defaultCompatibilityHeaders.add("Content-Type", "application/vnd.elasticsearch+json;compatible-with=7");
defaultCompatibilityHeaders.add("Accept", "application/vnd.elasticsearch+json; compatible-with=7");
defaultCompatibilityHeaders.add("Content-Type", "application/vnd.elasticsearch+json; compatible-with=7");
return defaultCompatibilityHeaders;
});
@ -214,11 +253,66 @@ public class RestClientsTest {
}
}
clientUnderTest.index(new Foo("42"));
clientUnderTest.index(new Foo("42"));
verify(putRequestedFor(urlMatching(urlPattern)) //
.withHeader("Accept", new EqualToPattern("application/vnd.elasticsearch+json;compatible-with=7")) //
.withHeader("Content-Type", new EqualToPattern("application/vnd.elasticsearch+json;compatible-with=7")) //
.withHeader("Accept", new EqualToPattern("application/vnd.elasticsearch+json; compatible-with=7"))
.withHeader("Content-Type", new EqualToPattern("application/vnd.elasticsearch+json; compatible-with=7")));
});
}
@ParameterizedTest
@MethodSource("clientUnderTestFactorySource")
@DisplayName("should set implicit compatibility headers")
void shouldSetImplicitCompatibilityHeaders(ClientUnderTestFactory clientUnderTestFactory) {
wireMockServer(server -> {
String urlPattern = "^/index/_doc/42(\\?.*)?$";
var elasticsearchMajorVersion = clientUnderTestFactory.getElasticsearchMajorVersion();
stubFor(put(urlMatching(urlPattern)) //
.willReturn(jsonResponse("""
{
"_id": "42",
"_index": "test",
"_primary_term": 1,
"_seq_no": 0,
"_shards": {
"failed": 0,
"successful": 1,
"total": 2
},
"_type": "_doc",
"_version": 1,
"result": "created"
}
""" //
, 201) //
.withHeader("Content-Type",
"application/vnd.elasticsearch+json; compatible-with=" + elasticsearchMajorVersion) //
.withHeader("X-Elastic-Product", "Elasticsearch")));
ClientConfigurationBuilder configurationBuilder = new ClientConfigurationBuilder();
configurationBuilder.connectedTo("localhost:" + server.port());
ClientConfiguration clientConfiguration = configurationBuilder.build();
ClientUnderTest clientUnderTest = clientUnderTestFactory.create(clientConfiguration);
class Foo {
public final String id;
Foo(String id) {
this.id = id;
}
}
clientUnderTest.index(new Foo("42"));
verify(putRequestedFor(urlMatching(urlPattern)) //
.withHeader("Accept",
new EqualToPattern("application/vnd.elasticsearch+json; compatible-with=" + elasticsearchMajorVersion)) //
.withHeader("Content-Type",
new EqualToPattern("application/vnd.elasticsearch+json; compatible-with=" + elasticsearchMajorVersion)) //
);
});
}
@ -280,6 +374,7 @@ public class RestClientsTest {
private void wireMockServer(WiremockConsumer consumer) {
WireMockServer wireMockServer = new WireMockServer(options() //
.dynamicPort() //
// .notifier(new ConsoleNotifier(true)) // for debugging output
.usingFilesUnderDirectory("src/test/resources/wiremock-mappings")); // needed, otherwise Wiremock goes to
// test/resources/mappings
try {
@ -295,7 +390,7 @@ public class RestClientsTest {
}
/**
* The client to be tested. Abstraction to be able to test reactive and non-reactive clients.
* The client to be tested. Abstraction to be able to test reactive and non-reactive clients in different versions.
*/
interface ClientUnderTest {
/**
@ -329,26 +424,61 @@ public class RestClientsTest {
protected abstract String getDisplayName();
protected Integer getExpectedRestClientConfigCalls() {
protected Integer getExpectedRestClientConfigurerCalls() {
return 0;
}
protected abstract int getElasticsearchMajorVersion();
public Integer getExpectedConnectionConfigurerCalls() {
return 0;
}
public Integer getExpectedConnectionManagerConfigurerCalls() {
return 0;
}
public Integer getExpectedRequestConfigurerCalls() {
return 0;
}
}
/**
* {@link ClientUnderTestFactory} implementation for the {@link co.elastic.clients.elasticsearch.ElasticsearchClient}.
* {@link ClientUnderTestFactory} implementation for the {@link co.elastic.clients.elasticsearch.ElasticsearchClient}
* using the Rest5_Client.
*/
static class ELCUnderTestFactory extends ClientUnderTestFactory {
static class ELCRest5ClientUnderTestFactory extends ClientUnderTestFactory {
@Override
protected String getDisplayName() {
return "ElasticsearchClient";
return "ElasticsearchRest5Client";
}
@Override
protected Integer getExpectedRestClientConfigCalls() {
protected Integer getExpectedRestClientConfigurerCalls() {
return 1;
}
@Override
public Integer getExpectedConnectionConfigurerCalls() {
return 1;
}
@Override
public Integer getExpectedConnectionManagerConfigurerCalls() {
return 1;
}
@Override
public Integer getExpectedRequestConfigurerCalls() {
return 1;
}
@Override
protected int getElasticsearchMajorVersion() {
return 9;
}
@Override
ClientUnderTest create(ClientConfiguration clientConfiguration) {
@ -372,6 +502,51 @@ public class RestClientsTest {
}
}
/**
* {@link ClientUnderTestFactory} implementation for the {@link co.elastic.clients.elasticsearch.ElasticsearchClient}
* using the old Rest_Client.
*/
static class ELCRestClientUnderTestFactory extends ClientUnderTestFactory {
@Override
protected String getDisplayName() {
return "ElasticsearchRestClient";
}
@Override
protected Integer getExpectedRestClientConfigurerCalls() {
return 1;
}
@Override
protected int getElasticsearchMajorVersion() {
return 9;
}
@Override
ClientUnderTest create(ClientConfiguration clientConfiguration) {
var restClient = RestClients.getRestClient(clientConfiguration);
ElasticsearchClient client = ElasticsearchClients.createImperative(restClient);
return new ClientUnderTest() {
@Override
public boolean ping() throws Exception {
return client.ping().value();
}
@Override
public boolean usesInitialRequest() {
return false;
}
@Override
public <T> void index(T entity) throws IOException {
client.index(ir -> ir.index("index").id("42").document(entity));
}
};
}
}
/**
* {@link ClientUnderTestFactory} implementation for the {@link ReactiveElasticsearchClient}.
*/
@ -383,10 +558,15 @@ public class RestClientsTest {
}
@Override
protected Integer getExpectedRestClientConfigCalls() {
protected Integer getExpectedRestClientConfigurerCalls() {
return 1;
}
@Override
protected int getElasticsearchMajorVersion() {
return 9;
}
@Override
ClientUnderTest create(ClientConfiguration clientConfiguration) {
@ -416,8 +596,9 @@ public class RestClientsTest {
* @return stream of factories
*/
static Stream<ClientUnderTestFactory> clientUnderTestFactorySource() {
return Stream.of( //
new ELCUnderTestFactory(), //
return Stream.of(
new ELCRestClientUnderTestFactory(),
new ELCRest5ClientUnderTestFactory(),
new ReactiveELCUnderTestFactory());
}
}

View File

@ -37,6 +37,9 @@ import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.TransportOptions;
import co.elastic.clients.transport.rest_client.RestClientOptions;
import org.elasticsearch.client.RestClient;
import org.springframework.data.elasticsearch.client.elc.rest5_client.Rest5Clients;
import org.springframework.data.elasticsearch.client.elc.rest_client.RestClients;
import reactor.core.publisher.Mono;
import java.io.IOException;
@ -85,7 +88,7 @@ public class DevTests {
private final ReactiveElasticsearchClient reactiveElasticsearchClient = ElasticsearchClients
.createReactive(clientConfiguration(), transportOptions);
private final ElasticsearchClient imperativeElasticsearchClient = ElasticsearchClients
.createImperative(ElasticsearchClients.getRestClient(clientConfiguration()), transportOptions, jsonpMapper);
.createImperative(Rest5Clients.getRest5Client(clientConfiguration()), transportOptions, jsonpMapper);
@Test
void someTest() throws IOException {

View File

@ -23,6 +23,7 @@ import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.assertj.core.api.SoftAssertions;
import org.assertj.core.data.Offset;
@ -144,17 +145,17 @@ class DocumentAdaptersUnitTests {
Hit<EntityAsMap> searchHit = new Hit.Builder<EntityAsMap>() //
.index("index") //
.id("42") //
.matchedQueries("query1", "query2") //
.matchedQueries("query1", 1D) //
.build();
SearchDocument searchDocument = DocumentAdapters.from(searchHit, jsonpMapper);
SoftAssertions softly = new SoftAssertions();
List<String> matchedQueries = searchDocument.getMatchedQueries();
Map<String, Double> matchedQueries = searchDocument.getMatchedQueries();
softly.assertThat(matchedQueries).isNotNull();
softly.assertThat(matchedQueries).hasSize(2);
softly.assertThat(matchedQueries).isEqualTo(Arrays.asList("query1", "query2"));
softly.assertThat(matchedQueries).hasSize(1);
softly.assertThat(matchedQueries).isEqualTo(Map.of("query1",1D));
softly.assertAll();
}
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.client.elc;
package org.springframework.data.elasticsearch.client.elc.rest5_client;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.*;
@ -29,6 +29,7 @@ import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ -41,7 +42,7 @@ import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
*/
@SuppressWarnings("UastIncorrectHttpHeaderInspection")
@ExtendWith(SpringExtension.class)
public class ELCWiremockTests {
public class ELCRest5ClientWiremockTests {
@RegisterExtension static WireMockExtension wireMock = WireMockExtension.newInstance()
.options(wireMockConfig()
@ -69,7 +70,7 @@ public class ELCWiremockTests {
wireMock.stubFor(put(urlPathEqualTo("/null-fields/_doc/42"))
.withRequestBody(equalToJson("""
{
"_class": "org.springframework.data.elasticsearch.client.elc.ELCWiremockTests$EntityWithNullFields",
"_class": "org.springframework.data.elasticsearch.client.elc.rest5_client.ELCRest5ClientWiremockTests$EntityWithNullFields",
"id": "42",
"field1": null
}

View File

@ -0,0 +1,145 @@
/*
* Copyright 2024-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.client.elc.rest_client;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.*;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchLegacyRestClientConfiguration;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
/**
* Tests that need to check the data produced by the Elasticsearch client
*
* @author Peter-Josef Meisch
*/
@SuppressWarnings("UastIncorrectHttpHeaderInspection")
@Deprecated(since = "6.0", forRemoval = true)
@ExtendWith(SpringExtension.class)
public class ELCRestClientWiremockTests {
@RegisterExtension static WireMockExtension wireMock = WireMockExtension.newInstance()
.options(wireMockConfig()
.dynamicPort()
// needed, otherwise Wiremock goes to test/resources/mappings
.usingFilesUnderDirectory("src/test/resources/wiremock-mappings"))
.build();
@Configuration
static class Config extends ElasticsearchLegacyRestClientConfiguration {
@Override
public ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder()
.connectedTo("localhost:" + wireMock.getPort())
.build();
}
}
@Autowired ElasticsearchOperations operations;
@Test // #2839
@DisplayName("should store null values if configured")
void shouldStoreNullValuesIfConfigured() {
wireMock.stubFor(put(urlPathEqualTo("/null-fields/_doc/42"))
.withRequestBody(equalToJson("""
{
"_class": "org.springframework.data.elasticsearch.client.elc.rest_client.ELCRestClientWiremockTests$EntityWithNullFields",
"id": "42",
"field1": null
}
"""))
.willReturn(
aResponse()
.withStatus(200)
.withHeader("X-elastic-product", "Elasticsearch")
.withHeader("content-type", "application/vnd.elasticsearch+json;compatible-with=8")
.withBody("""
{
"_index": "null-fields",
"_id": "42",
"_version": 1,
"result": "created",
"forced_refresh": true,
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 1,
"_primary_term": 1
}
""")));
var entity = new EntityWithNullFields();
entity.setId("42");
operations.save(entity);
// no need to assert anything, if the field1:null is not sent, we run into a 404 error
}
@Document(indexName = "null-fields")
static class EntityWithNullFields {
@Nullable
@Id private String id;
@Nullable
@Field(storeNullValue = true) private String field1;
@Nullable
@Field private String field2;
@Nullable
public String getId() {
return id;
}
public void setId(@Nullable String id) {
this.id = id;
}
@Nullable
public String getField1() {
return field1;
}
public void setField1(@Nullable String field1) {
this.field1 = field1;
}
@Nullable
public String getField2() {
return field2;
}
public void setField2(@Nullable String field2) {
this.field2 = field2;
}
}
}

View File

@ -0,0 +1,2 @@
@Deprecated(since = "6.0", forRemoval=true)
package org.springframework.data.elasticsearch.client.elc.rest_client;

View File

@ -19,7 +19,8 @@ import static org.assertj.core.api.Assertions.*;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import org.elasticsearch.client.RestClient;
import co.elastic.clients.transport.rest5_client.low_level.Rest5Client;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -50,7 +51,7 @@ public class ElasticsearchConfigurationELCTests {
considerNestedRepositories = true)
static class Config extends ElasticsearchConfiguration {
@Override
public ClientConfiguration clientConfiguration() {
public @NonNull ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder() //
.connectedTo("localhost:9200") //
.build();
@ -61,7 +62,7 @@ public class ElasticsearchConfigurationELCTests {
* using a repository with an entity that is set to createIndex = false as we have no elastic running for this test
* and just check that all the necessary beans are created.
*/
@Autowired private RestClient restClient;
@Autowired private Rest5Client rest5Client;
@Autowired private ElasticsearchClient elasticsearchClient;
@Autowired private ElasticsearchOperations elasticsearchOperations;
@ -69,7 +70,7 @@ public class ElasticsearchConfigurationELCTests {
@Test
public void providesRequiredBeans() {
assertThat(restClient).isNotNull();
assertThat(rest5Client).isNotNull();
assertThat(elasticsearchClient).isNotNull();
assertThat(elasticsearchOperations).isNotNull();
assertThat(repository).isNotNull();

View File

@ -0,0 +1,88 @@
/*
* Copyright 2021-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.config.configuration;
import static org.assertj.core.api.Assertions.*;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import org.elasticsearch.client.RestClient;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchLegacyRestClientConfiguration;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* Tests for {@link ElasticsearchConfiguration}.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
public class ElasticsearchLegacyRestClientConfigurationELCTests {
@Configuration
@EnableElasticsearchRepositories(basePackages = { "org.springframework.data.elasticsearch.config.configuration" },
considerNestedRepositories = true)
static class Config extends ElasticsearchLegacyRestClientConfiguration {
@Override
public @NonNull ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder() //
.connectedTo("localhost:9200") //
.build();
}
}
/*
* using a repository with an entity that is set to createIndex = false as we have no elastic running for this test
* and just check that all the necessary beans are created.
*/
@Autowired private RestClient restClient;
@Autowired private ElasticsearchClient elasticsearchClient;
@Autowired private ElasticsearchOperations elasticsearchOperations;
@Autowired private CreateIndexFalseRepository repository;
@Test
public void providesRequiredBeans() {
assertThat(restClient).isNotNull();
assertThat(elasticsearchClient).isNotNull();
assertThat(elasticsearchOperations).isNotNull();
assertThat(repository).isNotNull();
}
@Document(indexName = "test-index-config-configuration", createIndex = false)
static class CreateIndexFalseEntity {
@Nullable
@Id private String id;
}
interface CreateIndexFalseRepository extends ElasticsearchRepository<CreateIndexFalseEntity, String> {}
}

View File

@ -17,6 +17,7 @@ package org.springframework.data.elasticsearch.config.configuration;
import static org.assertj.core.api.Assertions.*;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -49,7 +50,7 @@ public class ReactiveElasticsearchConfigurationELCTests {
static class Config extends ReactiveElasticsearchConfiguration {
@Override
public ClientConfiguration clientConfiguration() {
public @NonNull ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder() //
.connectedTo("localhost:9200") //
.build();
@ -66,7 +67,6 @@ public class ReactiveElasticsearchConfigurationELCTests {
@Test
public void providesRequiredBeans() {
// assertThat(webClient).isNotNull();
assertThat(reactiveElasticsearchClient).isNotNull();
assertThat(reactiveElasticsearchOperations).isNotNull();
assertThat(repository).isNotNull();

View File

@ -0,0 +1,85 @@
/*
* 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.config.configuration;
import static org.assertj.core.api.Assertions.*;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchClient;
import org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchConfiguration;
import org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchLegacyRestClientConfiguration;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository;
import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* @author Peter-Josef Meisch
* @since 4.4
*/
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@ExtendWith(SpringExtension.class)
@ContextConfiguration
public class ReactiveElasticsearchLegacyRestClientConfigurationELCTests {
@Configuration
@EnableReactiveElasticsearchRepositories(
basePackages = { "org.springframework.data.elasticsearch.config.configuration" },
considerNestedRepositories = true)
static class Config extends ReactiveElasticsearchLegacyRestClientConfiguration {
@Override
public @NonNull ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder() //
.connectedTo("localhost:9200") //
.build();
}
}
/*
* using a repository with an entity that is set to createIndex = false as we have no elastic running for this test
* and just check that all the necessary beans are created.
*/
@Autowired private ReactiveElasticsearchClient reactiveElasticsearchClient;
@Autowired private ReactiveElasticsearchOperations reactiveElasticsearchOperations;
@Autowired private CreateIndexFalseRepository repository;
@Test
public void providesRequiredBeans() {
// assertThat(webClient).isNotNull();
assertThat(reactiveElasticsearchClient).isNotNull();
assertThat(reactiveElasticsearchOperations).isNotNull();
assertThat(repository).isNotNull();
}
@Document(indexName = "test-index-config-configuration", createIndex = false)
static class CreateIndexFalseEntity {
@Nullable
@Id private String id;
}
interface CreateIndexFalseRepository extends ReactiveElasticsearchRepository<CreateIndexFalseEntity, String> {}
}

View File

@ -71,10 +71,10 @@ public abstract class EnableNestedRepositoriesIntegrationTests {
@Field(type = Text, store = true, fielddata = true) private String type;
@Nullable
@Field(type = Text, store = true, fielddata = true) private String message;
@Nullable private int rate;
private int rate;
@Nullable
@ScriptedField private Double scriptedRate;
@Nullable private boolean available;
private boolean available;
@Nullable private String highlightedMessage;
@Nullable private GeoPoint location;
@Nullable

View File

@ -114,10 +114,10 @@ public abstract class EnableRepositoriesIntegrationTests implements ApplicationC
@Field(type = Text, store = true, fielddata = true) private String type;
@Nullable
@Field(type = Text, store = true, fielddata = true) private String message;
@Nullable private int rate;
private int rate;
@Nullable
@ScriptedField private Double scriptedRate;
@Nullable private boolean available;
private boolean available;
@Nullable private String highlightedMessage;
@Nullable private GeoPoint location;
@Nullable
@ -208,10 +208,10 @@ public abstract class EnableRepositoriesIntegrationTests implements ApplicationC
@Nullable private String type;
@Nullable
@Field(type = FieldType.Text, fielddata = true) private String message;
@Nullable private int rate;
private int rate;
@Nullable
@ScriptedField private Long scriptedRate;
@Nullable private boolean available;
private boolean available;
@Nullable private String highlightedMessage;
@Nullable private GeoPoint location;
@Nullable

View File

@ -46,7 +46,6 @@ import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.RescorerQuery;
import org.springframework.data.elasticsearch.core.query.ScriptData;
import org.springframework.data.elasticsearch.core.query.ScriptType;
import org.springframework.data.elasticsearch.core.query.ScriptedField;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
@ -183,7 +182,7 @@ public class ElasticsearchELCIntegrationTests extends ElasticsearchIntegrationTe
return nativeQueryBuilder.withScriptedField(new ScriptedField( //
fieldName, //
new ScriptData(ScriptType.INLINE, "expression", script, null, params))) //
new ScriptData( "expression", script, null, params))) //
.build();
}

View File

@ -104,8 +104,6 @@ import org.springframework.data.util.StreamUtils;
@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";
@ -1630,12 +1628,13 @@ public abstract class ElasticsearchIntegrationTests {
final Query query = operations.matchAllQuery();
final UpdateQuery updateQuery = UpdateQuery.builder(query)
.withScriptType(ScriptType.INLINE)
.withScript("ctx._source['message'] = params['newMessage']").withLang("painless")
.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()));
@ -3901,10 +3900,10 @@ public abstract class ElasticsearchIntegrationTests {
@Field(type = Text, store = true, fielddata = true) private String type;
@Nullable
@Field(type = Text, store = true, fielddata = true) private String message;
@Nullable private int rate;
private int rate;
@Nullable
@ScriptedField private Double scriptedRate;
@Nullable private boolean available;
private boolean available;
@Nullable private GeoPoint location;
@Nullable
@Version private Long version;
@ -3919,7 +3918,7 @@ public abstract class ElasticsearchIntegrationTests {
@Nullable private String type;
@Nullable private String message;
@Nullable private Long version;
@Nullable private int rate;
private int rate;
@Nullable private GeoPoint location;
public Builder id(String id) {
@ -4116,10 +4115,10 @@ public abstract class ElasticsearchIntegrationTests {
@Nullable private String type;
@Nullable
@Field(type = FieldType.Text, fielddata = true) private String message;
@Nullable private int rate;
private int rate;
@Nullable
@ScriptedField private Long scriptedRate;
@Nullable private boolean available;
private boolean available;
@Nullable private GeoPoint location;
@Nullable
@Version private Long version;
@ -4807,8 +4806,7 @@ public abstract class ElasticsearchIntegrationTests {
@Document(indexName = "#{@indexNameProvider.indexName()}-immutable-scripted")
public static final class ImmutableWithScriptedEntity {
@Id private final String id;
@Field(type = Integer)
@Nullable private final int rate;
@Field(type = Integer) private final int rate;
@Nullable
@ScriptedField private final Double scriptedRate;
@ -4864,9 +4862,11 @@ public abstract class ElasticsearchIntegrationTests {
@Document(indexName = "#{@indexNameProvider.indexName()}-readonly-id")
static class ReadonlyIdEntity {
@Field(type = FieldType.Keyword) private String part1;
@Field(type = FieldType.Keyword)
@Nullable private String part1;
@Field(type = FieldType.Keyword) private String part2;
@Field(type = FieldType.Keyword)
@Nullable private String part2;
@Id
@WriteOnlyProperty
@ -4875,7 +4875,7 @@ public abstract class ElasticsearchIntegrationTests {
return part1 + '-' + part2;
}
public String getPart1() {
public @Nullable String getPart1() {
return part1;
}
@ -4883,7 +4883,7 @@ public abstract class ElasticsearchIntegrationTests {
this.part1 = part1;
}
public String getPart2() {
public @Nullable String getPart2() {
return part2;
}
@ -5007,9 +5007,11 @@ public abstract class ElasticsearchIntegrationTests {
private static class RootEntity {
@Id private String id;
@Field(type = FieldType.Object) private Child child;
@Field(type = FieldType.Object)
@Nullable private Child child;
@Field(type = FieldType.Object) private Parent parent;
@Field(type = FieldType.Object)
@Nullable private Parent parent;
@JoinTypeRelations(relations = {
@JoinTypeRelation(parent = "parent", children = { "child" })
@ -5031,7 +5033,7 @@ public abstract class ElasticsearchIntegrationTests {
this.id = id;
}
public Child getChild() {
public @Nullable Child getChild() {
return child;
}
@ -5039,7 +5041,7 @@ public abstract class ElasticsearchIntegrationTests {
this.child = child;
}
public Parent getParent() {
public @Nullable Parent getParent() {
return parent;
}

View File

@ -245,7 +245,7 @@ class EntityOperationsUnitTests {
@IndexedIndexName
@Nullable private String indexName;
public String getId() {
public @Nullable String getId() {
return id;
}

View File

@ -134,7 +134,7 @@ public abstract class LogEntityIntegrationTests {
@Nullable
@Id private String id;
@Nullable private String action;
@Nullable private long sequenceCode;
private long sequenceCode;
@Nullable
@Field(type = Ip) private String ip;
@Field(type = Date, format = DateFormat.date_time) private java.util.@Nullable Date date;
@ -149,7 +149,7 @@ public abstract class LogEntityIntegrationTests {
return format;
}
public String getId() {
public @Nullable String getId() {
return id;
}
@ -157,7 +157,7 @@ public abstract class LogEntityIntegrationTests {
this.id = id;
}
public String getAction() {
public @Nullable String getAction() {
return action;
}
@ -173,7 +173,7 @@ public abstract class LogEntityIntegrationTests {
this.sequenceCode = sequenceCode;
}
public String getIp() {
public @Nullable String getIp() {
return ip;
}
@ -181,7 +181,7 @@ public abstract class LogEntityIntegrationTests {
this.ip = ip;
}
public java.util.Date getDate() {
public java.util.@Nullable Date getDate() {
return date;
}

View File

@ -1298,7 +1298,7 @@ public abstract class ReactiveElasticsearchIntegrationTests {
@Id private String id;
@Nullable
@Field(type = Text, store = true, fielddata = true) private String message;
@Nullable private int rate;
private int rate;
@Nullable
@Version private Long version;
@ -1568,9 +1568,9 @@ public abstract class ReactiveElasticsearchIntegrationTests {
@Document(indexName = "#{@indexNameProvider.indexName()}-readonly-id")
static class ReadonlyIdEntity {
@Field(type = FieldType.Keyword) private String part1;
@Field(type = FieldType.Keyword) private @Nullable String part1;
@Field(type = FieldType.Keyword) private String part2;
@Field(type = FieldType.Keyword) private @Nullable String part2;
@Id
@WriteOnlyProperty
@ -1579,7 +1579,7 @@ public abstract class ReactiveElasticsearchIntegrationTests {
return part1 + '-' + part2;
}
public String getPart1() {
public @Nullable String getPart1() {
return part1;
}
@ -1587,7 +1587,7 @@ public abstract class ReactiveElasticsearchIntegrationTests {
this.part1 = part1;
}
public String getPart2() {
public @Nullable String getPart2() {
return part2;
}

View File

@ -164,7 +164,7 @@ public abstract class AggregationIntegrationTests {
@Nullable
@Field(type = Integer, store = true) private List<Integer> publishedYears = new ArrayList<>();
@Nullable private int score;
private int score;
public ArticleEntity(@Nullable String id) {
this.id = id;

View File

@ -25,7 +25,15 @@ import java.time.LocalTime;
import java.time.OffsetTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import org.intellij.lang.annotations.Language;
@ -128,7 +136,7 @@ public class MappingElasticsearchConverterUnitTests {
public void init() {
SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext();
mappingContext.setInitialEntitySet(Collections.singleton(Rifle.class));
mappingContext.setInitialEntitySet(singleton(Rifle.class));
mappingContext.afterPropertiesSet();
mappingElasticsearchConverter = new MappingElasticsearchConverter(mappingContext, new GenericConversionService());
@ -389,7 +397,7 @@ public class MappingElasticsearchConverterUnitTests {
@Test // DATAES-530
public void readListOfConcreteTypesCorrectly() {
sarahAsMap.put("coWorkers", Collections.singletonList(kyleAsMap));
sarahAsMap.put("coWorkers", singletonList(kyleAsMap));
Person target = mappingElasticsearchConverter.read(Person.class, sarahAsMap);
@ -414,7 +422,7 @@ public class MappingElasticsearchConverterUnitTests {
Map<String, Object> target = writeToMap(sarahConnor);
assertThat(target.get("shippingAddresses")).isInstanceOf(Map.class);
assertThat(target.get("shippingAddresses")).isEqualTo(Collections.singletonMap("home", gratiotAveAsMap));
assertThat(target.get("shippingAddresses")).isEqualTo(singletonMap("home", gratiotAveAsMap));
}
@Test // DATAES-530
@ -433,7 +441,7 @@ public class MappingElasticsearchConverterUnitTests {
@Test // DATAES-530
public void readConcreteMapCorrectly() {
sarahAsMap.put("shippingAddresses", Collections.singletonMap("home", gratiotAveAsMap));
sarahAsMap.put("shippingAddresses", singletonMap("home", gratiotAveAsMap));
Person target = mappingElasticsearchConverter.read(Person.class, sarahAsMap);
@ -443,7 +451,7 @@ public class MappingElasticsearchConverterUnitTests {
@Test // DATAES-530
public void readInterfaceMapCorrectly() {
sarahAsMap.put("inventoryMap", Collections.singletonMap("glock19", gunAsMap));
sarahAsMap.put("inventoryMap", singletonMap("glock19", gunAsMap));
Person target = mappingElasticsearchConverter.read(Person.class, sarahAsMap);
@ -490,7 +498,7 @@ public class MappingElasticsearchConverterUnitTests {
public void readGenericListList() {
Document source = Document.create();
source.put("objectList", Collections.singletonList(Arrays.asList(t800AsMap, gunAsMap)));
source.put("objectList", singletonList(Arrays.asList(t800AsMap, gunAsMap)));
Skynet target = mappingElasticsearchConverter.read(Skynet.class, source);
@ -515,7 +523,7 @@ public class MappingElasticsearchConverterUnitTests {
public void readGenericMap() {
Document source = Document.create();
source.put("objectMap", Collections.singletonMap("glock19", gunAsMap));
source.put("objectMap", singletonMap("glock19", gunAsMap));
Skynet target = mappingElasticsearchConverter.read(Skynet.class, source);
@ -527,23 +535,23 @@ public class MappingElasticsearchConverterUnitTests {
Skynet skynet = new Skynet();
skynet.objectMap = new LinkedHashMap<>();
skynet.objectMap.put("inventory", Collections.singletonMap("glock19", gun));
skynet.objectMap.put("inventory", singletonMap("glock19", gun));
Map<String, Object> target = writeToMap(skynet);
assertThat((Map<String, Object>) target.get("objectMap")).containsEntry("inventory",
Collections.singletonMap("glock19", gunAsMap));
singletonMap("glock19", gunAsMap));
}
@Test // DATAES-530
public void readGenericMapMap() {
Document source = Document.create();
source.put("objectMap", Collections.singletonMap("inventory", Collections.singletonMap("glock19", gunAsMap)));
source.put("objectMap", singletonMap("inventory", singletonMap("glock19", gunAsMap)));
Skynet target = mappingElasticsearchConverter.read(Skynet.class, source);
assertThat(target.getObjectMap()).containsEntry("inventory", Collections.singletonMap("glock19", gun));
assertThat(target.getObjectMap()).containsEntry("inventory", singletonMap("glock19", gun));
}
@Test // DATAES-530
@ -575,7 +583,7 @@ public class MappingElasticsearchConverterUnitTests {
@Test // DATAES-530
public void writesNestedAliased() {
t800.inventoryList = Collections.singletonList(rifle);
t800.inventoryList = singletonList(rifle);
Map<String, Object> target = writeToMap(t800);
assertThat((List<Document>) target.get("inventoryList")).contains(rifleAsMap);
@ -589,7 +597,7 @@ public class MappingElasticsearchConverterUnitTests {
@Test // DATAES-530
public void readsNestedAliased() {
t800AsMap.put("inventoryList", Collections.singletonList(rifleAsMap));
t800AsMap.put("inventoryList", singletonList(rifleAsMap));
assertThat(mappingElasticsearchConverter.read(Person.class, t800AsMap).getInventoryList()).containsExactly(rifle);
}
@ -911,12 +919,12 @@ public class MappingElasticsearchConverterUnitTests {
void shouldWriteMapContainingCollectionContainingMap() throws JSONException {
class EntityWithMapCollectionMap {
Map<String, Object> map;
Map<String, Object> map = Map.of();
}
class InnerEntity {
String prop1;
@Nullable String prop1;
String prop2;
@Nullable String prop2;
public InnerEntity() {}
@ -928,8 +936,8 @@ public class MappingElasticsearchConverterUnitTests {
}
var entity = new EntityWithMapCollectionMap();
entity.map = Collections.singletonMap("collection",
Collections.singletonList(Collections.singletonMap("destination", new InnerEntity("prop1", "prop2"))));
entity.map = singletonMap("collection",
singletonList(singletonMap("destination", new InnerEntity("prop1", "prop2"))));
var expected = """
{
@ -1069,67 +1077,79 @@ public class MappingElasticsearchConverterUnitTests {
class RangeEntity {
@Id private String id;
@Field(type = FieldType.Integer_Range) private Range<Integer> integerRange;
@Field(type = FieldType.Float_Range) private Range<Float> floatRange;
@Field(type = FieldType.Long_Range) private Range<Long> longRange;
@Field(type = FieldType.Double_Range) private Range<Double> doubleRange;
@Field(type = FieldType.Date_Range) private Range<Date> dateRange;
@Field(type = FieldType.Date_Range, format = DateFormat.year_month_day) private Range<LocalDate> localDateRange;
@Field(type = FieldType.Integer_Range)
@Nullable private Range<Integer> integerRange;
@Field(type = FieldType.Float_Range)
@Nullable private Range<Float> floatRange;
@Field(type = FieldType.Long_Range)
@Nullable private Range<Long> longRange;
@Field(type = FieldType.Double_Range)
@Nullable private Range<Double> doubleRange;
@Field(type = FieldType.Date_Range)
@Nullable private Range<Date> dateRange;
@Field(type = FieldType.Date_Range, format = DateFormat.year_month_day)
@Nullable private Range<LocalDate> localDateRange;
@Field(type = FieldType.Date_Range,
format = DateFormat.hour_minute_second_millis) private Range<LocalTime> localTimeRange;
format = DateFormat.hour_minute_second_millis)
@Nullable private Range<LocalTime> localTimeRange;
@Field(type = FieldType.Date_Range,
format = DateFormat.date_hour_minute_second_millis) private Range<LocalDateTime> localDateTimeRange;
@Field(type = FieldType.Date_Range, format = DateFormat.time) private Range<OffsetTime> offsetTimeRange;
@Field(type = FieldType.Date_Range) private Range<ZonedDateTime> zonedDateTimeRange;
@Field(type = FieldType.Date_Range, storeNullValue = true) private Range<ZonedDateTime> nullRange;
format = DateFormat.date_hour_minute_second_millis)
@Nullable private Range<LocalDateTime> localDateTimeRange;
@Field(type = FieldType.Date_Range, format = DateFormat.time)
@Nullable private Range<OffsetTime> offsetTimeRange;
@Field(type = FieldType.Date_Range)
@Nullable private Range<ZonedDateTime> zonedDateTimeRange;
@Field(type = FieldType.Date_Range, storeNullValue = true)
@Nullable private Range<ZonedDateTime> nullRange;
@Field(type = FieldType.Integer_Range) private List<Range<Integer>> integerRangeList;
@Field(type = FieldType.Integer_Range)
@Nullable private List<Range<Integer>> integerRangeList;
public String getId() {
return id;
}
public Range<Integer> getIntegerRange() {
public @Nullable Range<Integer> getIntegerRange() {
return integerRange;
}
public Range<Float> getFloatRange() {
public @Nullable Range<Float> getFloatRange() {
return floatRange;
}
public Range<Long> getLongRange() {
public @Nullable Range<Long> getLongRange() {
return longRange;
}
public Range<Double> getDoubleRange() {
public @Nullable Range<Double> getDoubleRange() {
return doubleRange;
}
public Range<Date> getDateRange() {
public @Nullable Range<Date> getDateRange() {
return dateRange;
}
public Range<LocalDate> getLocalDateRange() {
public @Nullable Range<LocalDate> getLocalDateRange() {
return localDateRange;
}
public Range<LocalTime> getLocalTimeRange() {
public @Nullable Range<LocalTime> getLocalTimeRange() {
return localTimeRange;
}
public Range<LocalDateTime> getLocalDateTimeRange() {
public @Nullable Range<LocalDateTime> getLocalDateTimeRange() {
return localDateTimeRange;
}
public Range<OffsetTime> getOffsetTimeRange() {
public @Nullable Range<OffsetTime> getOffsetTimeRange() {
return offsetTimeRange;
}
public Range<ZonedDateTime> getZonedDateTimeRange() {
public @Nullable Range<ZonedDateTime> getZonedDateTimeRange() {
return zonedDateTimeRange;
}
public Range<ZonedDateTime> getNullRange() {
public @Nullable Range<ZonedDateTime> getNullRange() {
return nullRange;
}
@ -1181,7 +1201,7 @@ public class MappingElasticsearchConverterUnitTests {
this.nullRange = nullRange;
}
public List<Range<Integer>> getIntegerRangeList() {
public @Nullable List<Range<Integer>> getIntegerRangeList() {
return integerRangeList;
}
@ -2639,8 +2659,7 @@ public class MappingElasticsearchConverterUnitTests {
@Nullable private GeoPoint pointB;
@Nullable
@GeoPointField private String pointC;
@Nullable
@GeoPointField private double[] pointD;
@GeoPointField private double @Nullable [] pointD;
@Nullable
public String getId() {
@ -2705,12 +2724,11 @@ public class MappingElasticsearchConverterUnitTests {
this.pointC = pointC;
}
@Nullable
public double[] getPointD() {
public double @Nullable [] getPointD() {
return pointD;
}
public void setPointD(@Nullable double[] pointD) {
public void setPointD(double @Nullable [] pointD) {
this.pointD = pointD;
}
}
@ -3259,7 +3277,8 @@ public class MappingElasticsearchConverterUnitTests {
@Id
@Nullable private String id;
@Field(type = FieldType.Nested, name = "level-one") private List<Level1> level1Entries;
@Field(type = FieldType.Nested, name = "level-one")
@Nullable private List<Level1> level1Entries;
@Nullable
public String getId() {
@ -3270,7 +3289,7 @@ public class MappingElasticsearchConverterUnitTests {
this.id = id;
}
public List<Level1> getLevel1Entries() {
public @Nullable List<Level1> getLevel1Entries() {
return level1Entries;
}
@ -3279,9 +3298,10 @@ public class MappingElasticsearchConverterUnitTests {
}
static class Level1 {
@Field(type = FieldType.Nested, name = "level-two") private List<Level2> level2Entries;
@Field(type = FieldType.Nested, name = "level-two")
@Nullable private List<Level2> level2Entries;
public List<Level2> getLevel2Entries() {
public @Nullable List<Level2> getLevel2Entries() {
return level2Entries;
}
@ -3291,9 +3311,10 @@ public class MappingElasticsearchConverterUnitTests {
}
static class Level2 {
@Field(type = FieldType.Keyword, name = "key-word") private String keyWord;
@Field(type = FieldType.Keyword, name = "key-word")
@Nullable private String keyWord;
public String getKeyWord() {
public @Nullable String getKeyWord() {
return keyWord;
}

View File

@ -424,12 +424,11 @@ public abstract class GeoIntegrationTests {
@Nullable private String name;
@Nullable
@GeoPointField private String locationAsString;
@Nullable
@GeoPointField private double[] locationAsArray;
@GeoPointField private double @Nullable [] locationAsArray;
@Nullable
@GeoPointField private String locationAsGeoHash;
public String getId() {
public @Nullable String getId() {
return id;
}
@ -437,7 +436,7 @@ public abstract class GeoIntegrationTests {
this.id = id;
}
public String getName() {
public @Nullable String getName() {
return name;
}
@ -445,7 +444,7 @@ public abstract class GeoIntegrationTests {
this.name = name;
}
public String getLocationAsString() {
public @Nullable String getLocationAsString() {
return locationAsString;
}
@ -461,7 +460,7 @@ public abstract class GeoIntegrationTests {
this.locationAsArray = locationAsArray;
}
public String getLocationAsGeoHash() {
public @Nullable String getLocationAsGeoHash() {
return locationAsGeoHash;
}

View File

@ -724,8 +724,7 @@ public abstract class MappingBuilderIntegrationTests extends MappingContextBaseT
static class DenseVectorEntity {
@Nullable
@Id private String id;
@Nullable
@Field(type = Dense_Vector, dims = 3) private float[] dense_vector;
@Field(type = Dense_Vector, dims = 3) private float @Nullable [] dense_vector;
@Nullable
public String getId() {
@ -736,12 +735,11 @@ public abstract class MappingBuilderIntegrationTests extends MappingContextBaseT
this.id = id;
}
@Nullable
public float[] getDense_vector() {
public float @Nullable [] getDense_vector() {
return dense_vector;
}
public void setDense_vector(@Nullable float[] dense_vector) {
public void setDense_vector(float @Nullable [] dense_vector) {
this.dense_vector = dense_vector;
}
}
@ -915,7 +913,8 @@ public abstract class MappingBuilderIntegrationTests extends MappingContextBaseT
@Nullable
@Id private String id;
@Field(type = FieldType.Dense_Vector, dims = 42, knnSimilarity = KnnSimilarity.COSINE) private double[] denseVector;
@Field(type = FieldType.Dense_Vector, dims = 42,
knnSimilarity = KnnSimilarity.COSINE) private double @Nullable [] denseVector;
}
@Mapping(aliases = {

View File

@ -1398,7 +1398,7 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
@Field("mapping-property")
@Mapping(mappingPath = "/mappings/test-field-analyzed-mappings.json") //
@Nullable private byte[] mappingProperty;
private byte @Nullable [] mappingProperty;
}
@SuppressWarnings("unused")
@ -1820,8 +1820,7 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
@Nullable private GeoPoint pointB;
@Nullable
@GeoPointField private String pointC;
@Nullable
@GeoPointField private double[] pointD;
@GeoPointField private double @Nullable [] pointD;
@Nullable
@GeoShapeField private String shape1;
@ -1892,12 +1891,11 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
this.pointC = pointC;
}
@Nullable
public double[] getPointD() {
public double @Nullable [] getPointD() {
return pointD;
}
public void setPointD(@Nullable double[] pointD) {
public void setPointD(double @Nullable [] pointD) {
this.pointD = pointD;
}
@ -2171,8 +2169,7 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
static class DenseVectorEntity {
@Nullable
@Id private String id;
@Nullable
@Field(type = FieldType.Dense_Vector, dims = 16) private float[] my_vector;
@Field(type = FieldType.Dense_Vector, dims = 16) private float @Nullable [] my_vector;
@Nullable
public String getId() {
@ -2183,12 +2180,11 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
this.id = id;
}
@Nullable
public float[] getMy_vector() {
public float @Nullable [] getMy_vector() {
return my_vector;
}
public void setMy_vector(@Nullable float[] my_vector) {
public void setMy_vector(float @Nullable [] my_vector) {
this.my_vector = my_vector;
}
}
@ -2198,10 +2194,9 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
@Nullable
@Id private String id;
@Nullable
@Field(type = FieldType.Dense_Vector, dims = 16, elementType = FieldElementType.FLOAT,
knnIndexOptions = @KnnIndexOptions(type = KnnAlgorithmType.HNSW, m = 16, efConstruction = 100),
knnSimilarity = KnnSimilarity.DOT_PRODUCT) private float[] my_vector;
knnSimilarity = KnnSimilarity.DOT_PRODUCT) private float @Nullable [] my_vector;
@Nullable
public String getId() {
@ -2212,12 +2207,11 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
this.id = id;
}
@Nullable
public float[] getMy_vector() {
public float @Nullable [] getMy_vector() {
return my_vector;
}
public void setMy_vector(@Nullable float[] my_vector) {
public void setMy_vector(float @Nullable [] my_vector) {
this.my_vector = my_vector;
}
}
@ -2277,7 +2271,7 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
static class DenseVectorMisMatchConfidenceIntervalClass {
@Field(type = Dense_Vector, dims = 16, elementType = FieldElementType.FLOAT,
knnIndexOptions = @KnnIndexOptions(type = KnnAlgorithmType.HNSW, m = 16, confidenceInterval = 0.95F),
knnSimilarity = KnnSimilarity.DOT_PRODUCT) private float[] dense_vector;
knnSimilarity = KnnSimilarity.DOT_PRODUCT) private float @Nullable [] dense_vector;
}
static class DisabledMappingProperty {

View File

@ -110,10 +110,10 @@ public class MappingParametersTest extends MappingContextBaseTests {
}
static class DenseVectorInvalidDimsClass {
@Field(type = Dense_Vector, dims = 4097) private float[] dense_vector;
@Field(type = Dense_Vector, dims = 4097) private float @Nullable [] dense_vector;
}
static class DenseVectorMissingDimsClass {
@Field(type = Dense_Vector) private float[] dense_vector;
@Field(type = Dense_Vector) private float @Nullable [] dense_vector;
}
}

View File

@ -444,7 +444,8 @@ public abstract class ReactiveIndexOperationsIntegrationTests {
})
private static class EntityWithAliases {
@Nullable private @Id String id;
@Field(type = Text) private String type;
@Field(type = Text)
@Nullable private String type;
@Nullable
public String getId() {
@ -455,7 +456,7 @@ public abstract class ReactiveIndexOperationsIntegrationTests {
this.id = id;
}
public String getType() {
public @Nullable String getType() {
return type;
}

View File

@ -325,7 +325,7 @@ public class SimpleElasticsearchPersistentPropertyUnitTests {
static class FieldNamingStrategyEntity {
@Nullable private String withoutCustomFieldName;
@Field(name = "CUStomFIEldnAME") private String withCustomFieldName;
@Field(name = "CUStomFIEldnAME") @Nullable private String withCustomFieldName;
@Nullable
public String getWithoutCustomFieldName() {
@ -336,7 +336,7 @@ public class SimpleElasticsearchPersistentPropertyUnitTests {
this.withoutCustomFieldName = withoutCustomFieldName;
}
public String getWithCustomFieldName() {
public @Nullable String getWithCustomFieldName() {
return withCustomFieldName;
}

View File

@ -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);
}
}

View File

@ -715,7 +715,7 @@ public abstract class CriteriaQueryIntegrationTests {
@Field(type = Text, store = true, fielddata = true) private String type;
@Nullable
@Field(type = Text, store = true, fielddata = true) private String message;
@Nullable private int rate;
private int rate;
@Nullable
@Version private Long version;

View File

@ -48,7 +48,6 @@ import org.springframework.data.elasticsearch.core.query.FetchSourceFilterBuilde
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.RuntimeField;
import org.springframework.data.elasticsearch.core.query.ScriptData;
import org.springframework.data.elasticsearch.core.query.ScriptType;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
@ -165,7 +164,6 @@ public abstract class ReactiveScriptedAndRuntimeFieldsIntegrationTests {
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))));
}

View File

@ -45,7 +45,6 @@ import org.springframework.data.elasticsearch.core.query.FetchSourceFilterBuilde
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.RuntimeField;
import org.springframework.data.elasticsearch.core.query.ScriptData;
import org.springframework.data.elasticsearch.core.query.ScriptType;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
@ -314,7 +313,6 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
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))));
}

View File

@ -27,7 +27,6 @@ import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.elasticsearch.ElasticsearchContainer;
@ -41,7 +40,7 @@ import org.testcontainers.utility.DockerImageName;
*
* @author Peter-Josef Meisch
*/
public class ClusterConnection implements ExtensionContext.Store.CloseableResource {
public class ClusterConnection implements AutoCloseable {
private static final Log LOGGER = LogFactory.getLog(ClusterConnection.class);
@ -106,7 +105,7 @@ public class ClusterConnection implements ExtensionContext.Store.CloseableResour
LOGGER.warn("DATAES_ELASTICSEARCH_PORT does not contain a number");
}
return ClusterConnectionInfo.builder().withIntegrationtestEnvironment(IntegrationtestEnvironment.get())
return ClusterConnectionInfo.builder(IntegrationtestEnvironment.get())
.withHostAndPort(host, port).build();
}
@ -138,11 +137,10 @@ public class ClusterConnection implements ExtensionContext.Store.CloseableResour
.withEnv(testcontainersProperties).withStartupTimeout(Duration.ofMinutes(2)).withReuse(true);
elasticsearchContainer.start();
return ClusterConnectionInfo.builder() //
.withIntegrationtestEnvironment(integrationtestEnvironment)
return ClusterConnectionInfo.builder(integrationtestEnvironment)
.withHostAndPort(elasticsearchContainer.getHost(),
elasticsearchContainer.getMappedPort(ELASTICSEARCH_DEFAULT_PORT)) //
.withElasticsearchContainer(elasticsearchContainer) //
elasticsearchContainer.getMappedPort(ELASTICSEARCH_DEFAULT_PORT))
.withElasticsearchContainer(elasticsearchContainer)
.build();
} catch (Exception e) {
LOGGER.error("Could not start Elasticsearch container", e);

View File

@ -24,7 +24,7 @@ import org.testcontainers.elasticsearch.ElasticsearchContainer;
* The {@link #host}, {@link #httpPort} and {@link #useSsl} values specify the values needed to connect to the cluster
* with a rest client for both a local started cluster and for one defined by the cluster URL when creating the
* {@link ClusterConnection}.<br/>
* The object must be created by using a {@link ClusterConnectionInfo.Builder}.
* The object must be created by using a {@link Builder}.
*
* @author Peter-Josef Meisch
*/
@ -36,8 +36,8 @@ public final class ClusterConnectionInfo {
private final String clusterName;
@Nullable private final ElasticsearchContainer elasticsearchContainer;
public static Builder builder() {
return new Builder();
public static Builder builder(IntegrationtestEnvironment integrationtestEnvironment) {
return new Builder(integrationtestEnvironment);
}
private ClusterConnectionInfo(IntegrationtestEnvironment integrationtestEnvironment, String host, int httpPort,
@ -84,13 +84,12 @@ public final class ClusterConnectionInfo {
public static class Builder {
private IntegrationtestEnvironment integrationtestEnvironment;
private boolean useSsl = false;
private String host;
private String host = "";
private int httpPort;
@Nullable private ElasticsearchContainer elasticsearchContainer;
public Builder withIntegrationtestEnvironment(IntegrationtestEnvironment integrationtestEnvironment) {
public Builder(IntegrationtestEnvironment integrationtestEnvironment) {
this.integrationtestEnvironment = integrationtestEnvironment;
return this;
}
public Builder withHostAndPort(String host, int httpPort) {

View File

@ -176,7 +176,7 @@ public class CdiRepositoryTests {
@Nullable
@Field(type = FieldType.Float) private Float price;
@Nullable private Integer popularity;
@Nullable private boolean available;
private boolean available;
@Nullable private String location;
@Nullable private Date lastModified;
@ -293,11 +293,12 @@ public class CdiRepositoryTests {
@Id private String id;
private String name;
@Nullable private String name;
@Field(type = FieldType.Nested) private List<Car> car;
@Field(type = FieldType.Nested)
@Nullable private List<Car> car;
@Field(type = FieldType.Nested, includeInParent = true) private List<Book> books;
@Field(type = FieldType.Nested, includeInParent = true) private @Nullable List<Book> books;
}

View File

@ -2443,8 +2443,8 @@ public abstract class CustomMethodRepositoryIntegrationTests {
@Field(type = Text, store = true, fielddata = true) private String message;
@Nullable
@Field(type = Keyword) private String keyword;
@Nullable private int rate;
@Nullable private boolean available;
private int rate;
private boolean available;
@Nullable private GeoPoint location;
@Nullable
@Version private Long version;

View File

@ -116,8 +116,7 @@ public abstract class GeoRepositoryIntegrationTests {
@Nullable private GeoPoint pointB;
@Nullable
@GeoPointField private String pointC;
@Nullable
@GeoPointField private double[] pointD;
@GeoPointField private double @Nullable [] pointD;
@Nullable
public String getId() {
@ -182,12 +181,11 @@ public abstract class GeoRepositoryIntegrationTests {
this.pointC = pointC;
}
@Nullable
public double[] getPointD() {
public double @Nullable [] getPointD() {
return pointD;
}
public void setPointD(@Nullable double[] pointD) {
public void setPointD(double @Nullable [] pointD) {
this.pointD = pointD;
}
}

View File

@ -165,12 +165,11 @@ public abstract class KnnSearchIntegrationTests {
this.message = message;
}
@Nullable
public float[] getVector() {
public float @Nullable [] getVector() {
return vector;
}
public void setVector(@Nullable float[] vector) {
public void setVector(float @Nullable [] vector) {
this.vector = vector;
}
}

Some files were not shown because too many files have changed in this diff Show More