diff --git a/pom.xml b/pom.xml
index 53f548016..04edd59d6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -132,6 +132,7 @@
+
org.elasticsearch.client
elasticsearch-rest-client
@@ -142,6 +143,7 @@
commons-logging
+ true
diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/clients.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/clients.adoc
index 0cf7d5ea3..b8250a9f9 100644
--- a/src/main/antora/modules/ROOT/pages/elasticsearch/clients.adoc
+++ b/src/main/antora/modules/ROOT/pages/elasticsearch/clients.adoc
@@ -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]
+----
+
+ org.elasticsearch.client
+ elasticsearch-rest-client
+ ${elasticsearch-client.version}
+
+
+ commons-logging
+ commons-logging
+
+
+
+
+----
+====
+
+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;
}))
diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc
index b87f7276b..88a474d92 100644
--- a/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc
+++ b/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc
@@ -7,6 +7,7 @@
* Upgarde to Spring 7
* Switch to jspecify nullability annotations
* Upgrade to Elasticsearch 9.0.3
+* Use the new Elasticsearch Rest5Client as default
[[new-features.5-5-0]]
diff --git a/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.5-6.0.adoc b/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.5-6.0.adoc
index 7667701a1..87c62ab42 100644
--- a/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.5-6.0.adoc
+++ b/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.5-6.0.adoc
@@ -6,9 +6,13 @@ This section describes breaking changes from version 5.5.x to 6.0.x and how remo
[[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.
+
[[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
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java b/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java
index f092e2bf6..64949fcf2 100644
--- a/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java
+++ b/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java
@@ -127,10 +127,16 @@ public interface ClientConfiguration {
Optional 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 getHostNameVerifier();
/**
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClients.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClients.java
index c4f6452cc..0e23ab36a 100644
--- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClients.java
+++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClients.java
@@ -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 formattedHosts(List 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 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 {
-
- static ElasticsearchHttpClientConfigurationCallback from(
- Function 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 {
-
- static ElasticsearchRestClientConfigurationCallback from(
- Function restClientBuilderCallback) {
-
- Assert.notNull(restClientBuilderCallback, "restClientBuilderCallback must not be null");
-
- return restClientBuilderCallback::apply;
- }
- }
+ // todo #3117 remove and document that ElasticsearchHttpClientConfigurationCallback has been move to RestClients.
}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchConfiguration.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchConfiguration.java
index 93d26101a..f4a90fff5 100644
--- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchConfiguration.java
+++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchConfiguration.java
@@ -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);
}
}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchExceptionTranslator.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchExceptionTranslator.java
index 224e9c671..6f454e60b 100644
--- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchExceptionTranslator.java
+++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchExceptionTranslator.java
@@ -119,14 +119,19 @@ public class ElasticsearchExceptionTranslator implements PersistenceExceptionTra
String message = null;
if (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);
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchLegacyRestClientConfiguration.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchLegacyRestClientConfiguration.java
new file mode 100644
index 000000000..2005faf5b
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchLegacyRestClientConfiguration.java
@@ -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.
+ * 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);
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchConfiguration.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchConfiguration.java
index 2506b59c0..544927073 100644
--- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchConfiguration.java
+++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchConfiguration.java
@@ -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);
}
}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchLegacyRestClientConfiguration.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchLegacyRestClientConfiguration.java
new file mode 100644
index 000000000..98e3bf8e2
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchLegacyRestClientConfiguration.java
@@ -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.
+ * 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();
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/rest5_client/Rest5Clients.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/rest5_client/Rest5Clients.java
new file mode 100644
index 000000000..4d40e99af
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/rest5_client/Rest5Clients.java
@@ -0,0 +1,274 @@
+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.Locale;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+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.DefaultAuthenticationStrategy;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
+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.data.elasticsearch.support.VersionInfo;
+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_CONNECT_TIMEOUT_MILLIS = 1000;
+ public static final int DEFAULT_SOCKET_TIMEOUT_MILLIS = 30000;
+ public static final int DEFAULT_RESPONSE_TIMEOUT_MILLIS = 0; // meaning infinite
+ public static final int DEFAULT_MAX_CONN_PER_ROUTE = 10;
+ public static final int DEFAULT_MAX_CONN_TOTAL = 30;
+
+ 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));
+ }
+
+ // we need to provide our own HttpClient, as the Rest5ClientBuilder
+ // does not provide a callback for configuration the http client as the old RestClientBuilder.
+ var httpClient = createHttpClient(clientConfiguration);
+ builder.setHttpClient(httpClient);
+
+ for (ClientConfiguration.ClientConfigurationCallback> clientConfigurationCallback : clientConfiguration
+ .getClientConfigurers()) {
+ if (clientConfigurationCallback instanceof ElasticsearchRest5ClientConfigurationCallback configurationCallback) {
+ builder = configurationCallback.configure(builder);
+ }
+ }
+
+ return builder;
+ }
+
+ private static HttpHost @NonNull [] getHttpHosts(ClientConfiguration clientConfiguration) {
+ List 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]);
+ }
+
+ // the basic logic to create the http client is copied from the Rest5ClientBuilder class, this is taken from the
+ // Elasticsearch code, as there is no public usable instance in that
+ private static CloseableHttpAsyncClient createHttpClient(ClientConfiguration clientConfiguration) {
+
+ var requestConfigBuilder = RequestConfig.custom();
+ var connectionConfigBuilder = ConnectionConfig.custom();
+
+ Duration connectTimeout = clientConfiguration.getConnectTimeout();
+
+ if (!connectTimeout.isNegative()) {
+ connectionConfigBuilder.setConnectTimeout(
+ Timeout.of(Math.toIntExact(connectTimeout.toMillis()), TimeUnit.MILLISECONDS));
+ }
+
+ Duration socketTimeout = clientConfiguration.getSocketTimeout();
+
+ if (!socketTimeout.isNegative()) {
+ var soTimeout = Timeout.of(Math.toIntExact(socketTimeout.toMillis()), TimeUnit.MILLISECONDS);
+ connectionConfigBuilder.setSocketTimeout(soTimeout);
+ requestConfigBuilder.setConnectionRequestTimeout(soTimeout);
+ } else {
+ connectionConfigBuilder.setSocketTimeout(Timeout.of(DEFAULT_SOCKET_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS));
+ requestConfigBuilder
+ .setConnectionRequestTimeout(Timeout.of(DEFAULT_RESPONSE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS));
+ }
+
+ try {
+ SSLContext sslContext = clientConfiguration.getCaFingerprint().isPresent()
+ ? TransportUtils.sslContextFromCaFingerprint(clientConfiguration.getCaFingerprint().get())
+ : (clientConfiguration.getSslContext().isPresent()
+ ? clientConfiguration.getSslContext().get()
+ : SSLContext.getDefault());
+
+ ConnectionConfig connectionConfig = connectionConfigBuilder.build();
+
+ PoolingAsyncClientConnectionManager defaultConnectionManager = PoolingAsyncClientConnectionManagerBuilder.create()
+ .setDefaultConnectionConfig(connectionConfig)
+ .setMaxConnPerRoute(DEFAULT_MAX_CONN_PER_ROUTE)
+ .setMaxConnTotal(DEFAULT_MAX_CONN_TOTAL)
+ .setTlsStrategy(new BasicClientTlsStrategy(sslContext))
+ .build();
+
+ var requestConfig = requestConfigBuilder.build();
+
+ var immutableRefToHttpClientBuilder = new Object() {
+ HttpAsyncClientBuilder httpClientBuilder = HttpAsyncClientBuilder.create()
+ .setDefaultRequestConfig(requestConfig)
+ .setConnectionManager(defaultConnectionManager)
+ .setUserAgent(VersionInfo.clientVersions())
+ .setTargetAuthenticationStrategy(new DefaultAuthenticationStrategy())
+ .setThreadFactory(new RestClientThreadFactory());
+ };
+
+ clientConfiguration.getProxy().ifPresent(proxy -> {
+ try {
+ var proxyRoutePlanner = new DefaultProxyRoutePlanner(HttpHost.create(proxy));
+ immutableRefToHttpClientBuilder.httpClientBuilder.setRoutePlanner(proxyRoutePlanner);
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ immutableRefToHttpClientBuilder.httpClientBuilder.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));
+ });
+ });
+
+ for (ClientConfiguration.ClientConfigurationCallback> clientConfigurer : clientConfiguration
+ .getClientConfigurers()) {
+ if (clientConfigurer instanceof ElasticsearchHttpClientConfigurationCallback httpClientConfigurer) {
+ immutableRefToHttpClientBuilder.httpClientBuilder = httpClientConfigurer.configure(immutableRefToHttpClientBuilder.httpClientBuilder);
+ }
+ }
+
+ return immutableRefToHttpClientBuilder.httpClientBuilder.build();
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("could not create the default ssl context", e);
+ }
+ }
+
+ /*
+ * Copied from the Elasticsearch code as this class is not public there.
+ */
+ private static class RestClientThreadFactory implements ThreadFactory {
+ private static final AtomicLong CLIENT_THREAD_POOL_ID_GENERATOR = new AtomicLong();
+ private final long clientThreadPoolId;
+ private final AtomicLong clientThreadId;
+
+ private RestClientThreadFactory() {
+ this.clientThreadPoolId = CLIENT_THREAD_POOL_ID_GENERATOR.getAndIncrement();
+ this.clientThreadId = new AtomicLong();
+ }
+
+ public Thread newThread(Runnable runnable) {
+ return new Thread(runnable, String.format(Locale.ROOT, "elasticsearch-rest-client-%d-thread-%d",
+ this.clientThreadPoolId, this.clientThreadId.incrementAndGet()));
+ }
+ }
+
+ /**
+ * {@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 {
+
+ static Rest5Clients.ElasticsearchHttpClientConfigurationCallback from(
+ Function httpClientBuilderCallback) {
+
+ Assert.notNull(httpClientBuilderCallback, "httpClientBuilderCallback must not be null");
+
+ return httpClientBuilderCallback::apply;
+ }
+ }
+
+ /**
+ * {@link ClientConfiguration.ClientConfigurationCallback} to configure the Rest5Client client with a
+ * {@link Rest5ClientBuilder}
+ *
+ * @since 6.0
+ */
+ public interface ElasticsearchRest5ClientConfigurationCallback
+ extends ClientConfiguration.ClientConfigurationCallback {
+
+ static ElasticsearchRest5ClientConfigurationCallback from(
+ Function rest5ClientBuilderCallback) {
+
+ Assert.notNull(rest5ClientBuilderCallback, "rest5ClientBuilderCallback must not be null");
+
+ return rest5ClientBuilderCallback::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;
+ }
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/rest5_client/package-info.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/rest5_client/package-info.java
new file mode 100644
index 000000000..af2aff84c
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/rest5_client/package-info.java
@@ -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;
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/rest_client/RestClients.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/rest_client/RestClients.java
new file mode 100644
index 000000000..5c51f8c31
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/rest_client/RestClients.java
@@ -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 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 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 {
+
+ static RestClients.ElasticsearchHttpClientConfigurationCallback from(
+ Function 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 {
+
+ static ElasticsearchRestClientConfigurationCallback from(
+ Function 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;
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/rest_client/package-info.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/rest_client/package-info.java
new file mode 100644
index 000000000..bc34f799b
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/rest_client/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ * 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 old (up to Elasticsearch 9) RestClient.
+ */
+@Deprecated(since = "6.0", forRemoval=true)
+@org.jspecify.annotations.NullMarked
+package org.springframework.data.elasticsearch.client.elc.rest_client;
diff --git a/src/main/java/org/springframework/data/elasticsearch/support/VersionInfo.java b/src/main/java/org/springframework/data/elasticsearch/support/VersionInfo.java
index d62ea73f8..cf5e0742f 100644
--- a/src/main/java/org/springframework/data/elasticsearch/support/VersionInfo.java
+++ b/src/main/java/org/springframework/data/elasticsearch/support/VersionInfo.java
@@ -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.
*
diff --git a/src/test/java/org/springframework/data/elasticsearch/client/RestClientsTest.java b/src/test/java/org/springframework/data/elasticsearch/client/RestClientsTest.java
index f8c9b6e49..62c983a4b 100644
--- a/src/test/java/org/springframework/data/elasticsearch/client/RestClientsTest.java
+++ b/src/test/java/org/springframework/data/elasticsearch/client/RestClientsTest.java
@@ -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,10 +41,13 @@ 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;
import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.common.ConsoleNotifier;
import com.github.tomakehurst.wiremock.matching.AnythingPattern;
import com.github.tomakehurst.wiremock.matching.EqualToPattern;
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
@@ -115,25 +119,38 @@ public class RestClientsTest {
return httpHeaders;
});
- if (clientUnderTestFactory instanceof ELCUnderTestFactory) {
+ if (clientUnderTestFactory instanceof ELCRest5ClientUnderTestFactory) {
configurationBuilder.withClientConfigurer(
- ElasticsearchClients.ElasticsearchHttpClientConfigurationCallback.from(httpClientBuilder -> {
+ Rest5Clients.ElasticsearchHttpClientConfigurationCallback.from(httpClientBuilder -> {
httpClientConfigurerCount.incrementAndGet();
return httpClientBuilder;
}));
configurationBuilder.withClientConfigurer(
- ElasticsearchClients.ElasticsearchRestClientConfigurationCallback.from(restClientBuilder -> {
+ Rest5Clients.ElasticsearchRest5ClientConfigurationCallback.from(rest5ClientBuilder -> {
+ restClientConfigurerCount.incrementAndGet();
+ return rest5ClientBuilder;
+ }));
+
+ } 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,8 +172,9 @@ 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(httpClientConfigurerCount).hasValue(1);
@@ -166,8 +184,8 @@ public class RestClientsTest {
@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 +208,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 +217,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 +233,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 +354,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 +370,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 {
/**
@@ -332,16 +407,19 @@ public class RestClientsTest {
protected Integer getExpectedRestClientConfigCalls() {
return 0;
}
+
+ protected abstract int getElasticsearchMajorVersion();
}
/**
- * {@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
@@ -349,6 +427,11 @@ public class RestClientsTest {
return 1;
}
+ @Override
+ protected int getElasticsearchMajorVersion() {
+ return 9;
+ }
+
@Override
ClientUnderTest create(ClientConfiguration clientConfiguration) {
@@ -372,6 +455,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 getExpectedRestClientConfigCalls() {
+ 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 void index(T entity) throws IOException {
+ client.index(ir -> ir.index("index").id("42").document(entity));
+ }
+ };
+ }
+ }
+
/**
* {@link ClientUnderTestFactory} implementation for the {@link ReactiveElasticsearchClient}.
*/
@@ -387,6 +515,11 @@ public class RestClientsTest {
return 1;
}
+ @Override
+ protected int getElasticsearchMajorVersion() {
+ return 9;
+ }
+
@Override
ClientUnderTest create(ClientConfiguration clientConfiguration) {
@@ -416,8 +549,9 @@ public class RestClientsTest {
* @return stream of factories
*/
static Stream clientUnderTestFactorySource() {
- return Stream.of( //
- new ELCUnderTestFactory(), //
+ return Stream.of(
+ new ELCRestClientUnderTestFactory(),
+ new ELCRest5ClientUnderTestFactory(),
new ReactiveELCUnderTestFactory());
}
}
diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/DevTests.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/DevTests.java
index 25308b569..c0f9b085b 100644
--- a/src/test/java/org/springframework/data/elasticsearch/client/elc/DevTests.java
+++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/DevTests.java
@@ -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 {
diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/ELCWiremockTests.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/rest5_client/ELCRest5ClientWiremockTests.java
similarity index 93%
rename from src/test/java/org/springframework/data/elasticsearch/client/elc/ELCWiremockTests.java
rename to src/test/java/org/springframework/data/elasticsearch/client/elc/rest5_client/ELCRest5ClientWiremockTests.java
index 875fd752e..1bead7a8b 100644
--- a/src/test/java/org/springframework/data/elasticsearch/client/elc/ELCWiremockTests.java
+++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/rest5_client/ELCRest5ClientWiremockTests.java
@@ -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
}
diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/rest_client/ELCRestClientWiremockTests.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/rest_client/ELCRestClientWiremockTests.java
new file mode 100644
index 000000000..22dd2a08a
--- /dev/null
+++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/rest_client/ELCRestClientWiremockTests.java
@@ -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;
+ }
+ }
+}
diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/rest_client/package-info.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/rest_client/package-info.java
new file mode 100644
index 000000000..98699f58e
--- /dev/null
+++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/rest_client/package-info.java
@@ -0,0 +1,2 @@
+@Deprecated(since = "6.0", forRemoval=true)
+package org.springframework.data.elasticsearch.client.elc.rest_client;
diff --git a/src/test/java/org/springframework/data/elasticsearch/config/configuration/ElasticsearchConfigurationELCTests.java b/src/test/java/org/springframework/data/elasticsearch/config/configuration/ElasticsearchConfigurationELCTests.java
index 973f129b3..118ba7c62 100644
--- a/src/test/java/org/springframework/data/elasticsearch/config/configuration/ElasticsearchConfigurationELCTests.java
+++ b/src/test/java/org/springframework/data/elasticsearch/config/configuration/ElasticsearchConfigurationELCTests.java
@@ -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();
diff --git a/src/test/java/org/springframework/data/elasticsearch/config/configuration/ElasticsearchLegacyRestClientConfigurationELCTests.java b/src/test/java/org/springframework/data/elasticsearch/config/configuration/ElasticsearchLegacyRestClientConfigurationELCTests.java
new file mode 100644
index 000000000..4af7fdc4a
--- /dev/null
+++ b/src/test/java/org/springframework/data/elasticsearch/config/configuration/ElasticsearchLegacyRestClientConfigurationELCTests.java
@@ -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 {}
+}
diff --git a/src/test/java/org/springframework/data/elasticsearch/config/configuration/ReactiveElasticsearchConfigurationELCTests.java b/src/test/java/org/springframework/data/elasticsearch/config/configuration/ReactiveElasticsearchConfigurationELCTests.java
index 77b290eec..b0bab91c0 100644
--- a/src/test/java/org/springframework/data/elasticsearch/config/configuration/ReactiveElasticsearchConfigurationELCTests.java
+++ b/src/test/java/org/springframework/data/elasticsearch/config/configuration/ReactiveElasticsearchConfigurationELCTests.java
@@ -66,7 +66,6 @@ public class ReactiveElasticsearchConfigurationELCTests {
@Test
public void providesRequiredBeans() {
- // assertThat(webClient).isNotNull();
assertThat(reactiveElasticsearchClient).isNotNull();
assertThat(reactiveElasticsearchOperations).isNotNull();
assertThat(repository).isNotNull();
diff --git a/src/test/java/org/springframework/data/elasticsearch/config/configuration/ReactiveElasticsearchLegacyRestClientConfigurationELCTests.java b/src/test/java/org/springframework/data/elasticsearch/config/configuration/ReactiveElasticsearchLegacyRestClientConfigurationELCTests.java
new file mode 100644
index 000000000..445309e5e
--- /dev/null
+++ b/src/test/java/org/springframework/data/elasticsearch/config/configuration/ReactiveElasticsearchLegacyRestClientConfigurationELCTests.java
@@ -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 {}
+}