Add new Elasticsearch client integration.

Original Pull Request #2115
This commit is contained in:
Peter-Josef Meisch 2022-03-19 18:51:01 +01:00 committed by GitHub
parent 4e39fb30bc
commit 4eb8f08ad8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
291 changed files with 18267 additions and 3608 deletions

3
.gitignore vendored
View File

@ -21,3 +21,6 @@ target
*.iws
.idea
/.env
/zap.env

View File

@ -1,3 +1,4 @@
/*
* Copyright 2007-present the original author or authors.
*
@ -20,98 +21,95 @@ import java.util.Properties;
public class MavenWrapperDownloader {
private static final String WRAPPER_VERSION = "0.5.6";
/**
* Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
*/
private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
private static final String WRAPPER_VERSION = "0.5.6";
/**
* Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
*/
private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
/**
* Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
* use instead of the default one.
*/
private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
".mvn/wrapper/maven-wrapper.properties";
/**
* Path to the maven-wrapper.properties file, which might contain a downloadUrl property to use instead of the default
* one.
*/
private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties";
/**
* Path where the maven-wrapper.jar will be saved to.
*/
private static final String MAVEN_WRAPPER_JAR_PATH =
".mvn/wrapper/maven-wrapper.jar";
/**
* Path where the maven-wrapper.jar will be saved to.
*/
private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar";
/**
* Name of the property which should be used to override the default download url for the wrapper.
*/
private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
/**
* Name of the property which should be used to override the default download url for the wrapper.
*/
private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
public static void main(String args[]) {
System.out.println("- Downloader started");
File baseDirectory = new File(args[0]);
System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
public static void main(String args[]) {
System.out.println("- Downloader started");
File baseDirectory = new File(args[0]);
System.out.println("- Using transport directory: " + baseDirectory.getAbsolutePath());
// If the maven-wrapper.properties exists, read it and check if it contains a custom
// wrapperUrl parameter.
File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
String url = DEFAULT_DOWNLOAD_URL;
if(mavenWrapperPropertyFile.exists()) {
FileInputStream mavenWrapperPropertyFileInputStream = null;
try {
mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
Properties mavenWrapperProperties = new Properties();
mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
} catch (IOException e) {
System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
} finally {
try {
if(mavenWrapperPropertyFileInputStream != null) {
mavenWrapperPropertyFileInputStream.close();
}
} catch (IOException e) {
// Ignore ...
}
}
}
System.out.println("- Downloading from: " + url);
// If the maven-wrapper.properties exists, read it and check if it contains a custom
// wrapperUrl parameter.
File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
String url = DEFAULT_DOWNLOAD_URL;
if (mavenWrapperPropertyFile.exists()) {
FileInputStream mavenWrapperPropertyFileInputStream = null;
try {
mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
Properties mavenWrapperProperties = new Properties();
mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
} catch (IOException e) {
System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
} finally {
try {
if (mavenWrapperPropertyFileInputStream != null) {
mavenWrapperPropertyFileInputStream.close();
}
} catch (IOException e) {
// Ignore ...
}
}
}
System.out.println("- Downloading from: " + url);
File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
if(!outputFile.getParentFile().exists()) {
if(!outputFile.getParentFile().mkdirs()) {
System.out.println(
"- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
}
}
System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
try {
downloadFileFromURL(url, outputFile);
System.out.println("Done");
System.exit(0);
} catch (Throwable e) {
System.out.println("- Error downloading");
e.printStackTrace();
System.exit(1);
}
}
File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
if (!outputFile.getParentFile().exists()) {
if (!outputFile.getParentFile().mkdirs()) {
System.out.println("- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
}
}
System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
try {
downloadFileFromURL(url, outputFile);
System.out.println("Done");
System.exit(0);
} catch (Throwable e) {
System.out.println("- Error downloading");
e.printStackTrace();
System.exit(1);
}
}
private static void downloadFileFromURL(String urlString, File destination) throws Exception {
if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
String username = System.getenv("MVNW_USERNAME");
char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
}
URL website = new URL(urlString);
ReadableByteChannel rbc;
rbc = Channels.newChannel(website.openStream());
FileOutputStream fos = new FileOutputStream(destination);
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
fos.close();
rbc.close();
}
private static void downloadFileFromURL(String urlString, File destination) throws Exception {
if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
String username = System.getenv("MVNW_USERNAME");
char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
}
URL website = new URL(urlString);
ReadableByteChannel rbc;
rbc = Channels.newChannel(website.openStream());
FileOutputStream fos = new FileOutputStream(destination);
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
fos.close();
rbc.close();
}
}

View File

@ -5,3 +5,7 @@ You find the contribution guidelines for Spring Data projects https://github.com
== Running the test locally
In order to run the tests locally with `./mvnw test` you need to have docker running because Spring Data Elasticsearch uses https://www.testcontainers.org/[Testcontainers] to start a local running Elasticsearch instance.
== Class names of the test classes
Tset classes that do depend on the client have either `ERHLC` (when using the deprecated Elasticsearch `RestHighLevelClient`) or `ELC` (the new `ElasticsearchClient`) in their name.

2
mvnw vendored
View File

@ -162,7 +162,7 @@ fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
# first directory with .mvn subdirectory is considered project transport directory
find_maven_basedir() {
if [ -z "$1" ]

28
pom.xml
View File

@ -19,6 +19,7 @@
<properties>
<elasticsearch>7.17.1</elasticsearch>
<elasticsearch-java>7.17.1</elasticsearch-java>
<log4j>2.17.1</log4j>
<netty>4.1.65.Final</netty>
<springdata.commons>2.7.0-SNAPSHOT</springdata.commons>
@ -134,7 +135,7 @@
<scope>test</scope>
</dependency>
<!-- Elasticsearch -->
<!-- Elasticsearch RestHighLevelClient, will be removed probably in SDE 5 -->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
@ -147,6 +148,30 @@
</exclusions>
</dependency>
<!-- new Elasticsearch client, needs the low-level rest client and json api -->
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>${elasticsearch-java}</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId> <!-- is Apache 2-->
<version>${elasticsearch}</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Jackson JSON Mapper -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
@ -480,6 +505,7 @@
<id>local-maven-repo</id>
<url>file:///${project.basedir}/src/test/resources/local-maven-repo</url>
</repository>
</repositories>
<pluginRepositories>

View File

@ -6,14 +6,159 @@ This section describes breaking changes from version 4.3.x to 4.4.x and how remo
[[elasticsearch-migration-guide-4.3-4.4.deprecations]]
== Deprecations
=== org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations
The method `<T> Publisher<T> execute(ClientCallback<Publisher<T>> callback)` has been deprecated.
As there now are multiple implementations using different client libraries the `execute` method is still available in the different implementations, but there is no more method in the interface, because there is no common callback interface for the different clients.
[[elasticsearch-migration-guide-4.3-4.4.breaking-changes]]
== Breaking Changes
=== Removal of deprecated classes
==== `ElasticsearchTemplate` has been removed
==== `org.springframework.data.elasticsearch.core.ElasticsearchTemplate` has been removed
As of version 4.4 Spring Data Elasticsearch does not use the `TransportClient` from Elasticsearch anymore (which itself is deprecated since Elasticsearch 7.0).
This means that the `ElasticsearchTemplate` class which was deprecated since Spring Data Elasticsearch 4.0 has been removed.
This means that the `org.springframework.data.elasticsearch.core.ElasticsearchTemplate` class which was deprecated since Spring Data Elasticsearch 4.0 has been removed.
This was the implementation of the `ElasticsearchOperations` interface that was using the `TransportClient`.
Connections to Elasticsearch must be made using either the imperative `ElasticsearchRestTemplate` or the reactive `ReactiveElasticsearchTemplate`.
=== Package changes
In 4.3 two classes (`ElasticsearchAggregations` and `ElasticsearchAggregation`) had been moved to the `org.springframework.data.elasticsearch.core.clients.elasticsearch7` package in preparation for the integration of the new Elasticsearch client.
The were moved back to the `org.springframework.data.elasticsearch.core` package as we keep the classes use the old Elasticsearch client where they were.
[[elasticsearch-migration-guide-4.3-4.4.new-clients]]
== New Elasticsearch client
Elasticsearch has introduced it's new `ElasticsearchClient` and has deprecated the previous `RestHighLevelClient`.
Spring Data Elasticsearch 4.4 still uses the old client as the default client for the following reasons:
* The new client forces applications to use the `jakarta.json.spi.JsonProvider` package whereas Spring Boot will stick to `javax.json.spi.JsonProvider` until version 3. So switching the default implementaiton in Spring Data Elasticsearch can only come with Spring Data Elasticsearch 5 (Spring Data 3, Spring 6).
* There are still some bugs in the Elasticsearch client which need to be resolved
* The implementation using the new client in Spring Data Elasticsearch is not yet complete, due to limited resources working on that - remember Spring Data Elasticsearch is a community driven project that lives from public contributions.
=== How to use the new client
CAUTION: The implementation using the new client is not complete, some operations will throw a `java.lang.UnsupportedOperationException` or might throw NPE (for example when the Elasticsearch cannot parse a response from the server, this still happens sometimes) +
Use the new client to test the implementations but do not use it in productive code yet!
In order to try and use the new client the following steps are necessary:
==== Make sure not to configure the existing default client
If using Spring Boot, exclude Spring Data Elasticsearch from the autoconfiguration
====
[source,java]
----
@SpringBootApplication(exclude = ElasticsearchDataAutoConfiguration.class)
public class SpringdataElasticTestApplication {
// ...
}
----
====
Remove Spring Data Elasticsearch related properties from your application configuration.
If Spring Data Elasticsearch was configured using a programmatic configuration (see <<elastisearch.clients>>), remove these beans from the Spring application context.
==== Add dependencies
The dependencies for the new Elasticsearch client are still optional in Spring Data Elasticsearch so they need to be added explicitly:
====
[source,xml]
----
<dependencies>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>7.17.1</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId> <!-- is Apache 2-->
<version>7.17.1</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
----
====
When using Spring Boot, it is necessary to set the following property in the _pom.xml_.
====
[source,xml]
----
<properties>
<jakarta-json.version>2.0.1</jakarta-json.version>
</properties>
----
====
==== New configuration classes
===== Imperative style
In order configure Spring Data Elasticsearch to use the new client, it is necessary to create a configuration bean that derives from `org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration`:
====
[source,java]
----
@Configuration
public class NewRestClientConfig extends ElasticsearchConfiguration {
@Override
public ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder() //
.connectedTo("localhost:9200") //
.build();
}
}
----
====
The configuration is done in the same way as with the old client, but it is not necessary anymore to create more than the configuration bean.
With this configuration, the following beans will be available in the Spring application context:
* a `RestClient` bean, that is the configured low level `RestClient` that is used by the Elasticsearch client
* an `ElasticsearchClient` bean, this is the new client that uses the `RestClient`
* an `ElasticsearchOperations` bean, available with the bean names _elasticsearchOperations_ and _elasticsearchTemplate_, this uses the `ElasticsearchClient`
===== Reactive style
To use the new client in a reactive environment the only difference is the class from which to derive the configuration:
====
[source,java]
----
@Configuration
public class NewRestClientConfig extends ReactiveElasticsearchConfiguration {
@Override
public ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder() //
.connectedTo("localhost:9200") //
.build();
}
}
----
====
With this configuration, the following beans will be available in the Spring application context:
* a `RestClient` bean, that is the configured low level `RestClient` that is used by the Elasticsearch client
* an `ReactiveElasticsearchClient` bean, this is the new reactive client that uses the `RestClient`
* an `ReactiveElasticsearchOperations` bean, available with the bean names _reactiveElasticsearchOperations_ and _reactiveElasticsearchTemplate_, this uses the `ReactiveElasticsearchClient`

View File

@ -4,6 +4,7 @@
[[new-features.4-4-0]]
== New in Spring Data Elasticsearch 4.4
* Introduction of new imperative and reactive clients using the classes from the new Elasticsearch Java client
* Upgrade to Elasticsearch 7.17.1.
[[new-features.4-3-0]]

View File

@ -10,4 +10,6 @@ include::elasticsearch-migration-guide-4.0-4.1.adoc[]
include::elasticsearch-migration-guide-4.1-4.2.adoc[]
include::elasticsearch-migration-guide-4.2-4.3.adoc[]
include::elasticsearch-migration-guide-4.3-4.4.adoc[]
:leveloffset: -1

View File

@ -0,0 +1,78 @@
/*
* Copyright 2022 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;
import java.util.List;
import javax.annotation.Nullable;
/**
* Object describing an Elasticsearch error
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public class ElasticsearchErrorCause {
@Nullable private final String type;
private final String reason;
@Nullable private final String stackTrace;
@Nullable private final ElasticsearchErrorCause causedBy;
private final List<ElasticsearchErrorCause> rootCause;
private final List<ElasticsearchErrorCause> suppressed;
public ElasticsearchErrorCause(@Nullable String type, String reason, @Nullable String stackTrace,
@Nullable ElasticsearchErrorCause causedBy, List<ElasticsearchErrorCause> rootCause,
List<ElasticsearchErrorCause> suppressed) {
this.type = type;
this.reason = reason;
this.stackTrace = stackTrace;
this.causedBy = causedBy;
this.rootCause = rootCause;
this.suppressed = suppressed;
}
@Nullable
public String getType() {
return type;
}
public String getReason() {
return reason;
}
@Nullable
public String getStackTrace() {
return stackTrace;
}
@Nullable
public ElasticsearchErrorCause getCausedBy() {
return causedBy;
}
public List<ElasticsearchErrorCause> getRootCause() {
return rootCause;
}
public List<ElasticsearchErrorCause> getSuppressed() {
return suppressed;
}
}

View File

@ -16,6 +16,7 @@
package org.springframework.data.elasticsearch;
import org.springframework.dao.UncategorizedDataAccessException;
import org.springframework.lang.Nullable;
/**
* @author Peter-Josef Meisch
@ -23,11 +24,48 @@ import org.springframework.dao.UncategorizedDataAccessException;
*/
public class UncategorizedElasticsearchException extends UncategorizedDataAccessException {
/**
* the response status code from Elasticsearch if available
*
* @since 4.4
*/
@Nullable private final Integer statusCode;
/**
* The response body from Elasticsearch if available
*
* @since 4.4
*/
@Nullable final String responseBody;
public UncategorizedElasticsearchException(String msg) {
super(msg, null);
this(msg, null);
}
public UncategorizedElasticsearchException(String msg, Throwable cause) {
public UncategorizedElasticsearchException(String msg, @Nullable Throwable cause) {
this(msg, null, null, cause);
}
public UncategorizedElasticsearchException(String msg, @Nullable Integer statusCode, @Nullable String responseBody,
@Nullable Throwable cause) {
super(msg, cause);
this.statusCode = statusCode;
this.responseBody = responseBody;
}
/**
* @since 4.4
*/
@Nullable
public Integer getStatusCode() {
return statusCode;
}
/**
* @since 4.4
*/
@Nullable
public String getResponseBody() {
return responseBody;
}
}

View File

@ -124,6 +124,22 @@ public @interface Document {
* @since 4.3
*/
enum VersionType {
INTERNAL, EXTERNAL, EXTERNAL_GTE
INTERNAL("internal"), //
EXTERNAL("external"), //
EXTERNAL_GTE("external_gte"), //
/**
* @since 4.4
*/
FORCE("force");
private final String esName;
VersionType(String esName) {
this.esName = esName;
}
public String getEsName() {
return esName;
}
}
}

View File

@ -29,11 +29,11 @@ import org.springframework.util.ObjectUtils;
*
* @author Mark Paluch
* @author Christoph Strobl
* @author Peter-Josef Meisch
* @since 3.2
*/
public abstract class ClientLogger {
private static final String lineSeparator = System.getProperty("line.separator");
private static final Log WIRE_LOGGER = LogFactory.getLog("org.springframework.data.elasticsearch.client.WIRE");
private ClientLogger() {}
@ -63,6 +63,24 @@ public abstract class ClientLogger {
}
}
/**
* Log an outgoing HTTP request.
*
* @param logId the correlation id, see {@link #newLogId()}.
* @param method HTTP method
* @param endpoint URI
* @param parameters optional parameters.
* @param headers a String containing the headers
* @since 4.4
*/
public static void logRequest(String logId, String method, String endpoint, Object parameters, String headers) {
if (isEnabled()) {
WIRE_LOGGER.trace(String.format("[%s] Sending request%n%s %s%nParameters: %s%nHeaders: %s", logId,
method.toUpperCase(), endpoint, parameters, headers));
}
}
/**
* Log an outgoing HTTP request with a request body.
*
@ -76,8 +94,28 @@ public abstract class ClientLogger {
Supplier<Object> body) {
if (isEnabled()) {
WIRE_LOGGER.trace(String.format("[%s] Sending request %s %s with parameters: %s%sRequest body: %s", logId,
method.toUpperCase(), endpoint, parameters, lineSeparator, body.get()));
WIRE_LOGGER.trace(String.format("[%s] Sending request %s %s with parameters: %s%nRequest body: %s", logId,
method.toUpperCase(), endpoint, parameters, body.get()));
}
}
/**
* Log an outgoing HTTP request with a request body.
*
* @param logId the correlation id, see {@link #newLogId()}.
* @param method HTTP method
* @param endpoint URI
* @param parameters optional parameters.
* @param headers a String containing the headers
* @param body body content supplier.
* @since 4.4
*/
public static void logRequest(String logId, String method, String endpoint, Object parameters, String headers,
Supplier<Object> body) {
if (isEnabled()) {
WIRE_LOGGER.trace(String.format("[%s] Sending request%n%s %s%nParameters: %s%nHeaders: %s%nRequest body: %s",
logId, method.toUpperCase(), endpoint, parameters, headers, body.get()));
}
}
@ -94,6 +132,20 @@ public abstract class ClientLogger {
}
}
/**
* Log a raw HTTP response without logging the body.
*
* @param logId the correlation id, see {@link #newLogId()}.
* @param statusCode the HTTP status code.
* @param headers a String containing the headers
*/
public static void logRawResponse(String logId, @Nullable HttpStatus statusCode, String headers) {
if (isEnabled()) {
WIRE_LOGGER.trace(String.format("[%s] Received response: %s%n%s", logId, statusCode, headers));
}
}
/**
* Log a raw HTTP response along with the body.
*
@ -104,8 +156,24 @@ public abstract class ClientLogger {
public static void logResponse(String logId, HttpStatus statusCode, String body) {
if (isEnabled()) {
WIRE_LOGGER.trace(
String.format("[%s] Received response: %s%sResponse body: %s", logId, statusCode, lineSeparator, body));
WIRE_LOGGER.trace(String.format("[%s] Received response: %s%nResponse body: %s", logId, statusCode, body));
}
}
/**
* Log a raw HTTP response along with the body.
*
* @param logId the correlation id, see {@link #newLogId()}.
* @param statusCode the HTTP status code.
* @param headers a String containing the headers
* @param body body content.
* @since 4.4
*/
public static void logResponse(String logId, @Nullable HttpStatus statusCode, String headers, String body) {
if (isEnabled()) {
WIRE_LOGGER.trace(String.format("[%s] Received response: %s%nHeaders: %s%nResponse body: %s", logId, statusCode,
headers, body));
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2022 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;
/**
* Exception to be thrown by a backend implementation on operations that are not supported for that backend.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public class UnsupportedBackendOperation extends RuntimeException {
public UnsupportedBackendOperation() {}
public UnsupportedBackendOperation(String message) {
super(message);
}
public UnsupportedBackendOperation(String message, Throwable cause) {
super(message, cause);
}
public UnsupportedBackendOperation(Throwable cause) {
super(cause);
}
public UnsupportedBackendOperation(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright 2021-2022 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.elasticsearch.cluster.ElasticsearchClusterClient;
import co.elastic.clients.transport.ElasticsearchTransport;
import org.elasticsearch.client.RestClient;
import org.springframework.util.Assert;
/**
* Extension of the {@link ElasticsearchClient} class that implements {@link AutoCloseable}. As the underlying
* {@link RestClient} must be closed properly this is handled in the {@link #close()} method.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public class AutoCloseableElasticsearchClient extends ElasticsearchClient implements AutoCloseable {
public AutoCloseableElasticsearchClient(ElasticsearchTransport transport) {
super(transport);
Assert.notNull(transport, "transport must not be null");
}
@Override
public void close() throws Exception {
transport.close();
}
@Override
public ElasticsearchClusterClient cluster() {
return super.cluster();
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright 2021-2022 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.ApiClient;
import co.elastic.clients.elasticsearch.cluster.ElasticsearchClusterClient;
import co.elastic.clients.json.JsonpMapper;
import java.io.IOException;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.util.Assert;
/**
* base class for a template that uses one of the {@link co.elastic.clients.elasticsearch.ElasticsearchClient}'s child
* clients like {@link ElasticsearchClusterClient} or
* {@link co.elastic.clients.elasticsearch.indices.ElasticsearchIndicesClient}.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public abstract class ChildTemplate<CLIENT extends ApiClient> {
protected final CLIENT client;
protected final RequestConverter requestConverter;
protected final ResponseConverter responseConverter;
protected final ElasticsearchExceptionTranslator exceptionTranslator;
public ChildTemplate(CLIENT client, ElasticsearchConverter elasticsearchConverter) {
this.client = client;
JsonpMapper jsonpMapper = client._transport().jsonpMapper();
requestConverter = new RequestConverter(elasticsearchConverter, jsonpMapper);
responseConverter = new ResponseConverter(jsonpMapper);
exceptionTranslator = new ElasticsearchExceptionTranslator(jsonpMapper);
}
/**
* Callback interface to be used with {@link #execute(ClientCallback)} for operating directly on the client.
*/
@FunctionalInterface
public interface ClientCallback<CLIENT, RESULT> {
RESULT doWithClient(CLIENT client) throws IOException;
}
/**
* Execute a callback with the client and provide exception translation.
*
* @param callback the callback to execute, must not be {@literal null}
* @param <RESULT> the type returned from the callback
* @return the callback result
*/
public <RESULT> RESULT execute(ClientCallback<CLIENT, RESULT> callback) {
Assert.notNull(callback, "callback must not be null");
try {
return callback.doWithClient(client);
} catch (IOException | RuntimeException e) {
throw exceptionTranslator.translateException(e);
}
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2021-2022 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.cluster.ElasticsearchClusterClient;
import co.elastic.clients.elasticsearch.cluster.HealthRequest;
import co.elastic.clients.elasticsearch.cluster.HealthResponse;
import org.springframework.data.elasticsearch.core.cluster.ClusterHealth;
import org.springframework.data.elasticsearch.core.cluster.ClusterOperations;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
/**
* Implementation of the {@link ClusterOperations} interface using en {@link ElasticsearchClusterClient}.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public class ClusterTemplate extends ChildTemplate<ElasticsearchClusterClient> implements ClusterOperations {
public ClusterTemplate(ElasticsearchClusterClient client, ElasticsearchConverter elasticsearchConverter) {
super(client, elasticsearchConverter);
}
@Override
public ClusterHealth health() {
HealthRequest healthRequest = requestConverter.clusterHealthRequest();
HealthResponse healthResponse = execute(client -> client.health(healthRequest));
return responseConverter.clusterHealth(healthResponse);
}
}

View File

@ -0,0 +1,321 @@
/*
* Copyright 2021-2022 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._types.GeoDistanceType;
import co.elastic.clients.elasticsearch._types.GeoShapeRelation;
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.GeoBoundingBoxQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.GeoDistanceQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.GeoShapeQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders;
import co.elastic.clients.elasticsearch._types.query_dsl.QueryVariant;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.util.ObjectBuilder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.data.elasticsearch.core.convert.GeoConverters;
import org.springframework.data.elasticsearch.core.geo.GeoBox;
import org.springframework.data.elasticsearch.core.geo.GeoJson;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.util.Assert;
/**
* Class to convert a {@link org.springframework.data.elasticsearch.core.query.CriteriaQuery} into an Elasticsearch
* filter.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
class CriteriaFilterProcessor {
/**
* Creates a filter query from the given criteria.
*
* @param criteria the criteria to process
* @return the optional query, empty if the criteria did not contain filter relevant elements
*/
public static Optional<Query> createQuery(Criteria criteria) {
Assert.notNull(criteria, "criteria must not be null");
List<Query> filterQueries = new ArrayList<>();
for (Criteria chainedCriteria : criteria.getCriteriaChain()) {
if (chainedCriteria.isOr()) {
// todo #1973
} else if (chainedCriteria.isNegating()) {
// todo #1973
} else {
filterQueries.addAll(queriesForEntries(chainedCriteria));
}
}
if (filterQueries.isEmpty()) {
return Optional.empty();
} else {
if (filterQueries.size() == 1) {
return Optional.of(filterQueries.get(0));
} else {
BoolQuery.Builder boolQueryBuilder = QueryBuilders.bool();
filterQueries.forEach(boolQueryBuilder::must);
BoolQuery boolQuery = boolQueryBuilder.build();
return Optional.of(boolQuery._toQuery());
}
}
}
private static Collection<? extends Query> queriesForEntries(Criteria criteria) {
Assert.notNull(criteria.getField(), "criteria must have a field");
String fieldName = criteria.getField().getName();
Assert.notNull(fieldName, "Unknown field");
return criteria.getFilterCriteriaEntries().stream()
.map(entry -> queryFor(entry.getKey(), entry.getValue(), fieldName)) //
.filter(Optional::isPresent) //
.map(Optional::get) //
.collect(Collectors.toList());
}
private static Optional<Query> queryFor(Criteria.OperationKey key, Object value, String fieldName) {
ObjectBuilder<? extends QueryVariant> queryBuilder = null;
switch (key) {
case WITHIN:
Assert.isTrue(value instanceof Object[], "Value of a geo distance filter should be an array of two values.");
queryBuilder = withinQuery(fieldName, (Object[]) value);
break;
case BBOX:
Assert.isTrue(value instanceof Object[],
"Value of a boundedBy filter should be an array of one or two values.");
queryBuilder = boundingBoxQuery(fieldName, (Object[]) value);
break;
case GEO_INTERSECTS:
Assert.isTrue(value instanceof GeoJson<?>, "value of a GEO_INTERSECTS filter must be a GeoJson object");
queryBuilder = geoJsonQuery(fieldName, (GeoJson<?>) value, "intersects");
break;
case GEO_IS_DISJOINT:
Assert.isTrue(value instanceof GeoJson<?>, "value of a GEO_IS_DISJOINT filter must be a GeoJson object");
queryBuilder = geoJsonQuery(fieldName, (GeoJson<?>) value, "disjoint");
break;
case GEO_WITHIN:
Assert.isTrue(value instanceof GeoJson<?>, "value of a GEO_WITHIN filter must be a GeoJson object");
queryBuilder = geoJsonQuery(fieldName, (GeoJson<?>) value, "within");
break;
case GEO_CONTAINS:
Assert.isTrue(value instanceof GeoJson<?>, "value of a GEO_CONTAINS filter must be a GeoJson object");
queryBuilder = geoJsonQuery(fieldName, (GeoJson<?>) value, "contains");
break;
}
return Optional.ofNullable(queryBuilder != null ? queryBuilder.build()._toQuery() : null);
}
private static ObjectBuilder<GeoDistanceQuery> withinQuery(String fieldName, Object[] values) {
Assert.noNullElements(values, "Geo distance filter takes 2 not null elements array as parameter.");
Assert.isTrue(values.length == 2, "Geo distance filter takes a 2-elements array as parameter.");
Assert.isTrue(values[0] instanceof GeoPoint || values[0] instanceof String || values[0] instanceof Point,
"First element of a geo distance filter must be a GeoPoint, a Point or a text");
Assert.isTrue(values[1] instanceof String || values[1] instanceof Distance,
"Second element of a geo distance filter must be a text or a Distance");
String dist = (values[1] instanceof Distance) ? extractDistanceString((Distance) values[1]) : (String) values[1];
return QueryBuilders.geoDistance() //
.field(fieldName) //
.distance(dist) //
.distanceType(GeoDistanceType.Plane) //
.location(location -> {
if (values[0] instanceof GeoPoint) {
GeoPoint loc = (GeoPoint) values[0];
location.latlon(latlon -> latlon.lat(loc.getLat()).lon(loc.getLon()));
} else if (values[0] instanceof Point) {
GeoPoint loc = GeoPoint.fromPoint((Point) values[0]);
location.latlon(latlon -> latlon.lat(loc.getLat()).lon(loc.getLon()));
} else {
String loc = (String) values[0];
if (loc.contains(",")) {
String[] c = loc.split(",");
location.latlon(latlon -> latlon.lat(Double.parseDouble(c[0])).lon(Double.parseDouble(c[1])));
} else {
location.geohash(geohash -> geohash.geohash(loc));
}
}
return location;
});
}
private static ObjectBuilder<GeoBoundingBoxQuery> boundingBoxQuery(String fieldName, Object[] values) {
Assert.noNullElements(values, "Geo boundedBy filter takes a not null element array as parameter.");
GeoBoundingBoxQuery.Builder queryBuilder = QueryBuilders.geoBoundingBox() //
.field(fieldName);
if (values.length == 1) {
// GeoEnvelop
oneParameterBBox(queryBuilder, values[0]);
} else if (values.length == 2) {
// 2x GeoPoint
// 2x text
twoParameterBBox(queryBuilder, values);
} else {
throw new IllegalArgumentException(
"Geo distance filter takes a 1-elements array(GeoBox) or 2-elements array(GeoPoints or Strings(format lat,lon or geohash)).");
}
return queryBuilder;
}
private static void oneParameterBBox(GeoBoundingBoxQuery.Builder queryBuilder, Object value) {
Assert.isTrue(value instanceof GeoBox || value instanceof Box,
"single-element of boundedBy filter must be type of GeoBox or Box");
GeoBox geoBBox;
if (value instanceof Box) {
geoBBox = GeoBox.fromBox((Box) value);
} else {
geoBBox = (GeoBox) value;
}
queryBuilder.boundingBox(bb -> bb //
.tlbr(tlbr -> tlbr //
.topLeft(glb -> glb //
.latlon(latlon -> latlon //
.lat(geoBBox.getTopLeft().getLat()) //
.lon(geoBBox.getTopLeft().getLon()))) //
.bottomRight(glb -> glb //
.latlon(latlon -> latlon //
.lat(geoBBox.getBottomRight().getLat())//
.lon(geoBBox.getBottomRight().getLon()// )
)))));
}
private static void twoParameterBBox(GeoBoundingBoxQuery.Builder queryBuilder, Object[] values) {
Assert.isTrue(allElementsAreOfType(values, GeoPoint.class) || allElementsAreOfType(values, String.class),
" both elements of boundedBy filter must be type of GeoPoint or text(format lat,lon or geohash)");
if (values[0] instanceof GeoPoint) {
GeoPoint topLeft = (GeoPoint) values[0];
GeoPoint bottomRight = (GeoPoint) values[1];
queryBuilder.boundingBox(bb -> bb //
.tlbr(tlbr -> tlbr //
.topLeft(glb -> glb //
.latlon(latlon -> latlon //
.lat(topLeft.getLat()) //
.lon(topLeft.getLon()))) //
.bottomRight(glb -> glb //
.latlon(latlon -> latlon //
.lat(bottomRight.getLat()) //
.lon(bottomRight.getLon()))) //
) //
);
} else {
String topLeft = (String) values[0];
String bottomRight = (String) values[1];
boolean isGeoHash = !topLeft.contains(",");
queryBuilder.boundingBox(bb -> bb //
.tlbr(tlbr -> tlbr //
.topLeft(glb -> {
if (isGeoHash) {
glb.geohash(gh -> gh.geohash(topLeft));
} else {
glb.text(topLeft);
}
return glb;
}) //
.bottomRight(glb -> {
if (isGeoHash) {
glb.geohash(gh -> gh.geohash(bottomRight));
} else {
glb.text(bottomRight);
}
return glb;
}) //
));
}
}
private static boolean allElementsAreOfType(Object[] array, Class<?> clazz) {
for (Object o : array) {
if (!clazz.isInstance(o)) {
return false;
}
}
return true;
}
private static ObjectBuilder<? extends QueryVariant> geoJsonQuery(String fieldName, GeoJson<?> geoJson,
String relation) {
return buildGeoShapeQuery(fieldName, geoJson, relation);
}
private static ObjectBuilder<GeoShapeQuery> buildGeoShapeQuery(String fieldName, GeoJson<?> geoJson,
String relation) {
return QueryBuilders.geoShape().field(fieldName) //
.shape(gsf -> gsf //
.shape(JsonData.of(GeoConverters.GeoJsonToMapConverter.INSTANCE.convert(geoJson))) //
.relation(toRelation(relation))); //
}
private static GeoShapeRelation toRelation(String relation) {
for (GeoShapeRelation geoShapeRelation : GeoShapeRelation.values()) {
if (geoShapeRelation.name().equalsIgnoreCase(relation)) {
return geoShapeRelation;
}
}
throw new IllegalArgumentException("Unknown geo_shape relation: " + relation);
}
/**
* extract the distance string from a {@link org.springframework.data.geo.Distance} object.
*
* @param distance distance object to extract string from
*/
private static String extractDistanceString(Distance distance) {
StringBuilder sb = new StringBuilder();
sb.append((int) distance.getValue());
switch ((Metrics) distance.getMetric()) {
case KILOMETERS:
sb.append("km");
break;
case MILES:
sb.append("mi");
break;
}
return sb.toString();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright2020-2021 the original author or authors.
* Copyright 2021-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -13,13 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.routing;
package org.springframework.data.elasticsearch.client.elc;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchRestTemplateConfiguration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.dao.UncategorizedDataAccessException;
/**
* @author Peter-Josef Meisch
* @since 4.4
*/
@ContextConfiguration(classes = { ElasticsearchRestTemplateConfiguration.class })
public class RoutingRestTemplateIntegrationTests extends RoutingIntegrationTests {}
public class CriteriaQueryException extends UncategorizedDataAccessException {
public CriteriaQueryException(String msg) {
super(msg, null);
}
}

View File

@ -0,0 +1,368 @@
/*
* Copyright 2021-2022 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 static org.springframework.data.elasticsearch.client.elc.QueryBuilders.*;
import static org.springframework.util.StringUtils.*;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.query_dsl.ChildScoreMode;
import co.elastic.clients.elasticsearch._types.query_dsl.Operator;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.json.JsonData;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.lucene.queryparser.flexible.standard.QueryParserUtil;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.Field;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Class to convert a {@link org.springframework.data.elasticsearch.core.query.CriteriaQuery} into an Elasticsearch
* query.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
class CriteriaQueryProcessor {
/**
* creates a query from the criteria
*
* @param criteria the {@link Criteria}
* @return the optional query, null if the criteria did not contain filter relevant elements
*/
@Nullable
public static Query createQuery(Criteria criteria) {
Assert.notNull(criteria, "criteria must not be null");
List<Query> shouldQueries = new ArrayList<>();
List<Query> mustNotQueries = new ArrayList<>();
List<Query> mustQueries = new ArrayList<>();
Query firstQuery = null;
boolean negateFirstQuery = false;
for (Criteria chainedCriteria : criteria.getCriteriaChain()) {
Query queryFragment = queryForEntries(chainedCriteria);
if (queryFragment != null) {
if (firstQuery == null) {
firstQuery = queryFragment;
negateFirstQuery = chainedCriteria.isNegating();
continue;
}
if (chainedCriteria.isOr()) {
shouldQueries.add(queryFragment);
} else if (chainedCriteria.isNegating()) {
mustNotQueries.add(queryFragment);
} else {
mustQueries.add(queryFragment);
}
}
}
for (Criteria subCriteria : criteria.getSubCriteria()) {
Query subQuery = createQuery(subCriteria);
if (subQuery != null) {
if (criteria.isOr()) {
shouldQueries.add(subQuery);
} else if (criteria.isNegating()) {
mustNotQueries.add(subQuery);
} else {
mustQueries.add(subQuery);
}
}
}
if (firstQuery != null) {
if (!shouldQueries.isEmpty() && mustNotQueries.isEmpty() && mustQueries.isEmpty()) {
shouldQueries.add(0, firstQuery);
} else {
if (negateFirstQuery) {
mustNotQueries.add(0, firstQuery);
} else {
mustQueries.add(0, firstQuery);
}
}
}
if (shouldQueries.isEmpty() && mustNotQueries.isEmpty() && mustQueries.isEmpty()) {
return null;
}
Query query = new Query.Builder().bool(boolQueryBuilder -> {
if (!shouldQueries.isEmpty()) {
boolQueryBuilder.should(shouldQueries);
}
if (!mustNotQueries.isEmpty()) {
boolQueryBuilder.mustNot(mustNotQueries);
}
if (!mustQueries.isEmpty()) {
boolQueryBuilder.must(mustQueries);
}
return boolQueryBuilder;
}).build();
return query;
}
@Nullable
private static Query queryForEntries(Criteria criteria) {
Field field = criteria.getField();
if (field == null || criteria.getQueryCriteriaEntries().isEmpty())
return null;
String fieldName = field.getName();
Assert.notNull(fieldName, "Unknown field " + fieldName);
Iterator<Criteria.CriteriaEntry> it = criteria.getQueryCriteriaEntries().iterator();
Float boost = Float.isNaN(criteria.getBoost()) ? null : criteria.getBoost();
Query.Builder queryBuilder;
if (criteria.getQueryCriteriaEntries().size() == 1) {
queryBuilder = queryFor(it.next(), field, boost);
} else {
queryBuilder = new Query.Builder();
queryBuilder.bool(boolQueryBuilder -> {
while (it.hasNext()) {
Criteria.CriteriaEntry entry = it.next();
boolQueryBuilder.must(queryFor(entry, field, null).build());
}
boolQueryBuilder.boost(boost);
return boolQueryBuilder;
});
}
if (hasText(field.getPath())) {
final Query query = queryBuilder.build();
queryBuilder = new Query.Builder();
queryBuilder.nested(nqb -> nqb //
.path(field.getPath()) //
.query(query) //
.scoreMode(ChildScoreMode.Avg));
}
return queryBuilder.build();
}
private static Query.Builder queryFor(Criteria.CriteriaEntry entry, Field field, @Nullable Float boost) {
String fieldName = field.getName();
boolean isKeywordField = FieldType.Keyword == field.getFieldType();
Criteria.OperationKey key = entry.getKey();
Object value = key.hasValue() ? entry.getValue() : null;
String searchText = value != null ? QueryParserUtil.escape(value.toString()) : "UNKNOWN_VALUE";
Query.Builder queryBuilder = new Query.Builder();
switch (key) {
case EXISTS:
queryBuilder //
.exists(eb -> eb //
.field(fieldName) //
.boost(boost));
break;
case EMPTY:
queryBuilder //
.bool(bb -> bb //
.must(mb -> mb //
.exists(eb -> eb //
.field(fieldName) //
)) //
.mustNot(mnb -> mnb //
.wildcard(wb -> wb //
.field(fieldName) //
.wildcard("*"))) //
.boost(boost));
break;
case NOT_EMPTY:
queryBuilder //
.wildcard(wb -> wb //
.field(fieldName) //
.wildcard("*") //
.boost(boost));
break;
case EQUALS:
queryBuilder.queryString(queryStringQuery(fieldName, searchText, Operator.And, boost));
break;
case CONTAINS:
queryBuilder.queryString(queryStringQuery(fieldName, '*' + searchText + '*', true, boost));
break;
case STARTS_WITH:
queryBuilder.queryString(queryStringQuery(fieldName, searchText + '*', true, boost));
break;
case ENDS_WITH:
queryBuilder.queryString(queryStringQuery(fieldName, '*' + searchText, true, boost));
break;
case EXPRESSION:
queryBuilder.queryString(queryStringQuery(fieldName, value.toString(), boost));
break;
case LESS:
queryBuilder //
.range(rb -> rb //
.field(fieldName) //
.lt(JsonData.of(value)) //
.boost(boost)); //
break;
case LESS_EQUAL:
queryBuilder //
.range(rb -> rb //
.field(fieldName) //
.lte(JsonData.of(value)) //
.boost(boost)); //
break;
case GREATER:
queryBuilder //
.range(rb -> rb //
.field(fieldName) //
.gt(JsonData.of(value)) //
.boost(boost)); //
break;
case GREATER_EQUAL:
queryBuilder //
.range(rb -> rb //
.field(fieldName) //
.gte(JsonData.of(value)) //
.boost(boost)); //
break;
case BETWEEN:
Object[] ranges = (Object[]) value;
queryBuilder //
.range(rb -> {
rb.field(fieldName);
if (ranges[0] != null) {
rb.gte(JsonData.of(ranges[0]));
}
if (ranges[1] != null) {
rb.lte(JsonData.of(ranges[1]));
}
rb.boost(boost); //
return rb;
}); //
break;
case FUZZY:
queryBuilder //
.fuzzy(fb -> fb //
.field(fieldName) //
.value(FieldValue.of(searchText)) //
.boost(boost)); //
break;
case MATCHES:
queryBuilder.match(matchQuery(fieldName, value.toString(), Operator.Or, boost));
break;
case MATCHES_ALL:
queryBuilder.match(matchQuery(fieldName, value.toString(), Operator.And, boost));
break;
case IN:
if (value instanceof Iterable) {
Iterable<?> iterable = (Iterable<?>) value;
if (isKeywordField) {
queryBuilder.bool(bb -> bb //
.must(mb -> mb //
.terms(tb -> tb //
.field(fieldName) //
.terms(tsb -> tsb //
.value(toFieldValueList(iterable))))) //
.boost(boost)); //
} else {
queryBuilder //
.queryString(qsb -> qsb //
.fields(fieldName) //
.query(orQueryString(iterable)) //
.boost(boost)); //
}
} else {
throw new CriteriaQueryException("value for " + fieldName + " is not an Iterable");
}
break;
case NOT_IN:
if (value instanceof Iterable) {
Iterable<?> iterable = (Iterable<?>) value;
if (isKeywordField) {
queryBuilder.bool(bb -> bb //
.mustNot(mnb -> mnb //
.terms(tb -> tb //
.field(fieldName) //
.terms(tsb -> tsb //
.value(toFieldValueList(iterable))))) //
.boost(boost)); //
} else {
queryBuilder //
.queryString(qsb -> qsb //
.fields(fieldName) //
.query("NOT(" + orQueryString(iterable) + ')') //
.boost(boost)); //
}
} else {
throw new CriteriaQueryException("value for " + fieldName + " is not an Iterable");
}
break;
default:
throw new CriteriaQueryException("Could not build query for " + entry);
}
return queryBuilder;
}
private static List<FieldValue> toFieldValueList(Iterable<?> iterable) {
List<FieldValue> list = new ArrayList<>();
for (Object item : iterable) {
list.add(item != null ? FieldValue.of(item.toString()) : null);
}
return list;
}
private static String orQueryString(Iterable<?> iterable) {
StringBuilder sb = new StringBuilder();
for (Object item : iterable) {
if (item != null) {
if (sb.length() > 0) {
sb.append(' ');
}
sb.append('"');
sb.append(QueryParserUtil.escape(item.toString()));
sb.append('"');
}
}
return sb.toString();
}
}

View File

@ -0,0 +1,212 @@
/*
* Copyright 2021-2022 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.core.GetResponse;
import co.elastic.clients.elasticsearch.core.MgetResponse;
import co.elastic.clients.elasticsearch.core.explain.ExplanationDetail;
import co.elastic.clients.elasticsearch.core.get.GetResult;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.NestedIdentity;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.json.JsonpMapper;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.data.elasticsearch.core.MultiGetItem;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.document.Explanation;
import org.springframework.data.elasticsearch.core.document.NestedMetaData;
import org.springframework.data.elasticsearch.core.document.SearchDocument;
import org.springframework.data.elasticsearch.core.document.SearchDocumentAdapter;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Utility class to adapt different Elasticsearch responses to a
* {@link org.springframework.data.elasticsearch.core.document.Document}
*
* @author Peter-Josef Meisch
* @since 4.4
*/
final class DocumentAdapters {
private static final Log LOGGER = LogFactory.getLog(DocumentAdapters.class);
private DocumentAdapters() {}
/**
* Creates a {@link SearchDocument} from a {@link Hit} returned by the Elasticsearch client.
*
* @param hit the hit object
* @param jsonpMapper to map JsonData objects
* @return the created {@link SearchDocument}
*/
public static SearchDocument from(Hit<?> hit, JsonpMapper jsonpMapper) {
Assert.notNull(hit, "hit must not be null");
Map<String, List<String>> highlightFields = hit.highlight();
Map<String, SearchDocumentResponse> innerHits = new LinkedHashMap<>();
hit.innerHits().forEach((name, innerHitsResult) -> {
// noinspection ReturnOfNull
innerHits.put(name, SearchDocumentResponseBuilder.from(innerHitsResult.hits(), null, null, null,
searchDocument -> null, jsonpMapper));
});
NestedMetaData nestedMetaData = from(hit.nested());
// todo #1973 explanation
Explanation explanation = from(hit.explanation());
// todo #1973 matchedQueries
List<String> matchedQueries = null;
// todo #1973 documentFields
Map<String, List<Object>> documentFields = Collections.emptyMap();
Document document;
Object source = hit.source();
if (source == null) {
// Elasticsearch provides raw JsonData, so we build the fields into a JSON string
StringBuilder sb = new StringBuilder("{");
final boolean[] firstField = { true };
hit.fields().forEach((key, jsonData) -> {
if (!firstField[0]) {
sb.append(',');
}
sb.append('"').append(key).append("\":") //
.append(jsonData.toJson(jsonpMapper).toString());
firstField[0] = false;
});
sb.append('}');
document = Document.parse(sb.toString());
} else {
if (source instanceof EntityAsMap) {
document = Document.from((EntityAsMap) source);
} else if (source instanceof JsonData) {
JsonData jsonData = (JsonData) source;
document = Document.from(jsonData.to(EntityAsMap.class));
} else {
if (LOGGER.isWarnEnabled()) {
LOGGER.warn(String.format("Cannot map from type " + source.getClass().getName()));
}
document = Document.create();
}
}
document.setIndex(hit.index());
document.setId(hit.id());
if (hit.version() != null) {
document.setVersion(hit.version());
}
document.setSeqNo(hit.seqNo() != null && hit.seqNo() >= 0 ? hit.seqNo() : -2); // -2 was the default value in the
// old client
document.setPrimaryTerm(hit.primaryTerm() != null && hit.primaryTerm() > 0 ? hit.primaryTerm() : 0);
float score = hit.score() != null ? hit.score().floatValue() : Float.NaN;
return new SearchDocumentAdapter(document, score, hit.sort().toArray(new String[0]), documentFields,
highlightFields, innerHits, nestedMetaData, explanation, matchedQueries, hit.routing());
}
@Nullable
private static Explanation from(@Nullable co.elastic.clients.elasticsearch.core.explain.Explanation explanation) {
if (explanation == null) {
return null;
}
List<Explanation> details = explanation.details().stream().map(DocumentAdapters::from).collect(Collectors.toList());
return new Explanation(true, (double) explanation.value(), explanation.description(), details);
}
private static Explanation from(ExplanationDetail explanationDetail) {
List<Explanation> details = explanationDetail.details().stream().map(DocumentAdapters::from)
.collect(Collectors.toList());
return new Explanation(null, (double) explanationDetail.value(), explanationDetail.description(), details);
}
@Nullable
private static NestedMetaData from(@Nullable NestedIdentity nestedIdentity) {
if (nestedIdentity == null) {
return null;
}
NestedMetaData child = from(nestedIdentity.nested());
return NestedMetaData.of(nestedIdentity.field(), nestedIdentity.offset(), child);
}
/**
* Creates a {@link Document} from a {@link GetResponse} where the found document is contained as {@link EntityAsMap}.
*
* @param getResponse the response instance
* @return the Document
*/
@Nullable
public static Document from(GetResult<EntityAsMap> getResponse) {
Assert.notNull(getResponse, "getResponse must not be null");
if (!getResponse.found()) {
return null;
}
Document document = getResponse.source() != null ? Document.from(getResponse.source()) : Document.create();
document.setIndex(getResponse.index());
document.setId(getResponse.id());
if (getResponse.version() != null) {
document.setVersion(getResponse.version());
}
if (getResponse.seqNo() != null) {
document.setSeqNo(getResponse.seqNo());
}
if (getResponse.primaryTerm() != null) {
document.setPrimaryTerm(getResponse.primaryTerm());
}
return document;
}
/**
* Creates a list of {@link MultiGetItem}s from a {@link MgetResponse} where the data is contained as
* {@link EntityAsMap} instances.
*
* @param mgetResponse the response instance
* @return list of multiget items
*/
public static List<MultiGetItem<Document>> from(MgetResponse<EntityAsMap> mgetResponse) {
Assert.notNull(mgetResponse, "mgetResponse must not be null");
return mgetResponse.docs().stream() //
.map(itemResponse -> MultiGetItem.of( //
itemResponse.isFailure() ? null : from(itemResponse.result()), //
ResponseConverter.getFailure(itemResponse)))
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2021-2022 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._types.aggregations.Aggregate;
import java.util.Map;
import org.springframework.data.elasticsearch.core.AggregationsContainer;
/**
* AggregationsContainer implementation for the Elasticsearch aggregations.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public class ElasticsearchAggregations implements AggregationsContainer<Map<String, Aggregate>> {
private final Map<String, Aggregate> aggregations;
public ElasticsearchAggregations(Map<String, Aggregate> aggregations) {
this.aggregations = aggregations;
}
@Override
public Map<String, Aggregate> aggregations() {
return aggregations;
}
}

View File

@ -0,0 +1,381 @@
/*
* Copyright 2021-2022 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.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.rest_client.RestClientTransport;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
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 java.util.stream.Collectors;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.entity.ByteArrayEntity;
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.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.ClientLogger;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.WebClient;
/**
* Utility class to create the different Elasticsearch clients
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public final class ElasticsearchClients {
/**
* Name of whose value can be used to correlate log messages for this request.
*/
private static final String LOG_ID_ATTRIBUTE = ElasticsearchClients.class.getName() + ".LOG_ID";
private static final String X_SPRING_DATA_ELASTICSEARCH_CLIENT = "X-SpringDataElasticsearch-Client";
private static final String IMPERATIVE_CLIENT = "imperative";
private static final String REACTIVE_CLIENT = "reactive";
/**
* Creates a new {@link ReactiveElasticsearchClient}
*
* @param clientConfiguration configuration options, must not be {@literal null}.
* @return the {@link ReactiveElasticsearchClient}
*/
public static ReactiveElasticsearchClient createReactive(ClientConfiguration clientConfiguration) {
Assert.notNull(clientConfiguration, "clientConfiguration must not be null");
return createReactive(getRestClient(clientConfiguration), null);
}
/**
* Creates a new {@link ReactiveElasticsearchClient}
*
* @param clientConfiguration configuration options, must not be {@literal null}.
* @param transportOptions options to be added to each request.
* @return the {@link ReactiveElasticsearchClient}
*/
public static ReactiveElasticsearchClient createReactive(ClientConfiguration clientConfiguration,
@Nullable TransportOptions transportOptions) {
Assert.notNull(clientConfiguration, "ClientConfiguration must not be null!");
return createReactive(getRestClient(clientConfiguration), transportOptions);
}
/**
* Creates a new {@link ReactiveElasticsearchClient}.
*
* @param restClient the underlying {@link RestClient}
* @return the {@link ReactiveElasticsearchClient}
*/
public static ReactiveElasticsearchClient createReactive(RestClient restClient) {
return createReactive(restClient, null);
}
/**
* Creates a new {@link ReactiveElasticsearchClient}.
*
* @param restClient the underlying {@link RestClient}
* @param transportOptions options to be added to each request.
* @return the {@link ReactiveElasticsearchClient}
*/
public static ReactiveElasticsearchClient createReactive(RestClient restClient,
@Nullable TransportOptions transportOptions) {
return new ReactiveElasticsearchClient(getElasticsearchTransport(restClient, REACTIVE_CLIENT, transportOptions));
}
/**
* Creates a new imperative {@link ElasticsearchClient}
*
* @param clientConfiguration configuration options, must not be {@literal null}.
* @return the {@link ElasticsearchClient}
*/
public static ElasticsearchClient createImperative(ClientConfiguration clientConfiguration) {
return createImperative(getRestClient(clientConfiguration), null);
}
/**
* Creates a new imperative {@link ElasticsearchClient}
*
* @param clientConfiguration configuration options, must not be {@literal null}.
* @param transportOptions options to be added to each request.
* @return the {@link ElasticsearchClient}
*/
public static ElasticsearchClient createImperative(ClientConfiguration clientConfiguration,
TransportOptions transportOptions) {
return createImperative(getRestClient(clientConfiguration), transportOptions);
}
/**
* Creates a new imperative {@link ElasticsearchClient}
*
* @param restClient the RestClient to use
* @return the {@link ElasticsearchClient}
*/
public static ElasticsearchClient createImperative(RestClient restClient) {
return createImperative(restClient, null);
}
/**
* Creates a new imperative {@link ElasticsearchClient}
*
* @param restClient the RestClient to use
* @param transportOptions options to be added to each request.
* @return the {@link ElasticsearchClient}
*/
public static ElasticsearchClient createImperative(RestClient restClient,
@Nullable TransportOptions transportOptions) {
Assert.notNull(restClient, "restClient must not be null");
ElasticsearchTransport transport = getElasticsearchTransport(restClient, IMPERATIVE_CLIENT, transportOptions);
return new AutoCloseableElasticsearchClient(transport);
}
/**
* 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 -> {
clientConfiguration.getSslContext().ifPresent(clientBuilder::setSSLContext);
clientConfiguration.getHostNameVerifier().ifPresent(clientBuilder::setSSLHostnameVerifier);
clientBuilder.addInterceptorLast(new CustomHeaderInjector(clientConfiguration.getHeadersSupplier()));
if (ClientLogger.isEnabled()) {
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
clientBuilder.addInterceptorLast((HttpRequestInterceptor) interceptor);
clientBuilder.addInterceptorLast((HttpResponseInterceptor) interceptor);
}
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 ElasticsearchClientConfigurationCallback) {
ElasticsearchClientConfigurationCallback restClientConfigurationCallback = (ElasticsearchClientConfigurationCallback) clientConfigurer;
clientBuilder = restClientConfigurationCallback.configure(clientBuilder);
}
}
return clientBuilder;
});
return builder;
}
private static ElasticsearchTransport getElasticsearchTransport(RestClient restClient, String clientType,
@Nullable TransportOptions transportOptions) {
TransportOptions.Builder transportOptionsBuilder = transportOptions != null ? transportOptions.toBuilder()
: new RestClientOptions(RequestOptions.DEFAULT).toBuilder();
TransportOptions transportOptionsWithHeader = transportOptionsBuilder
.addHeader(X_SPRING_DATA_ELASTICSEARCH_CLIENT, clientType).build();
ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper(),
transportOptionsWithHeader);
return transport;
}
private static List<String> formattedHosts(List<InetSocketAddress> hosts, boolean useSsl) {
return hosts.stream().map(it -> (useSsl ? "https" : "http") + "://" + it.getHostString() + ":" + it.getPort())
.collect(Collectors.toList());
}
private static org.apache.http.Header[] toHeaderArray(HttpHeaders headers) {
return headers.entrySet().stream() //
.flatMap(entry -> entry.getValue().stream() //
.map(value -> new BasicHeader(entry.getKey(), value))) //
.toArray(org.apache.http.Header[]::new);
}
/**
* Logging interceptors for Elasticsearch client logging.
*
* @see ClientLogger
* @since 4.4
*/
private static class HttpLoggingInterceptor implements HttpResponseInterceptor, HttpRequestInterceptor {
@Override
public void process(HttpRequest request, HttpContext context) throws IOException {
String logId = (String) context.getAttribute(LOG_ID_ATTRIBUTE);
if (logId == null) {
logId = ClientLogger.newLogId();
context.setAttribute(LOG_ID_ATTRIBUTE, logId);
}
String headers = Arrays.stream(request.getAllHeaders())
.map(header -> header.getName()
+ ((header.getName().equals("Authorization")) ? ": *****" : ": " + header.getValue()))
.collect(Collectors.joining(", ", "[", "]"));
if (request instanceof HttpEntityEnclosingRequest && ((HttpEntityEnclosingRequest) request).getEntity() != null) {
HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest) request;
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
entity.writeTo(buffer);
if (!entity.isRepeatable()) {
entityRequest.setEntity(new ByteArrayEntity(buffer.toByteArray()));
}
ClientLogger.logRequest(logId, request.getRequestLine().getMethod(), request.getRequestLine().getUri(), "",
headers, buffer::toString);
} else {
ClientLogger.logRequest(logId, request.getRequestLine().getMethod(), request.getRequestLine().getUri(), "",
headers);
}
}
@Override
public void process(HttpResponse response, HttpContext context) throws IOException {
String logId = (String) context.getAttribute(LOG_ID_ATTRIBUTE);
String headers = Arrays.stream(response.getAllHeaders())
.map(header -> header.getName()
+ ((header.getName().equals("Authorization")) ? ": *****" : ": " + header.getValue()))
.collect(Collectors.joining(", ", "[", "]"));
// no way of logging the body, in this callback, it is not read yset, later there is no callback possibility in
// RestClient or RestClientTransport
ClientLogger.logRawResponse(logId, HttpStatus.resolve(response.getStatusLine().getStatusCode()), headers);
}
}
/**
* Interceptor to inject custom supplied headers.
*
* @since 4.4
*/
private static class CustomHeaderInjector implements HttpRequestInterceptor {
public CustomHeaderInjector(Supplier<HttpHeaders> headersSupplier) {
this.headersSupplier = headersSupplier;
}
private final Supplier<HttpHeaders> headersSupplier;
@Override
public void process(HttpRequest request, HttpContext context) {
HttpHeaders httpHeaders = headersSupplier.get();
if (httpHeaders != null && httpHeaders != HttpHeaders.EMPTY) {
Arrays.stream(toHeaderArray(httpHeaders)).forEach(request::addHeader);
}
}
}
/**
* {@link org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationCallback} to configure
* the RestClient with a {@link HttpAsyncClientBuilder}
*
* @since 4.4
*/
public interface ElasticsearchClientConfigurationCallback
extends ClientConfiguration.ClientConfigurationCallback<HttpAsyncClientBuilder> {
static ElasticsearchClientConfigurationCallback from(
Function<HttpAsyncClientBuilder, HttpAsyncClientBuilder> clientBuilderCallback) {
Assert.notNull(clientBuilderCallback, "clientBuilderCallback must not be null");
// noinspection NullableProblems
return clientBuilderCallback::apply;
}
}
/**
* {@link org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationCallback} to configure
* the ReactiveElasticsearchClient with a {@link WebClient}
*
* @since 4.4
*/
public interface WebClientConfigurationCallback extends ClientConfiguration.ClientConfigurationCallback<WebClient> {
static WebClientConfigurationCallback from(Function<WebClient, WebClient> webClientCallback) {
Assert.notNull(webClientCallback, "webClientCallback must not be null");
// noinspection NullableProblems
return webClientCallback::apply;
}
}
}

View File

@ -0,0 +1,98 @@
/*
* Copyright 2021-2022 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.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.config.ElasticsearchConfigurationSupport;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
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 Elasticsearch Client.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public abstract class ElasticsearchConfiguration extends ElasticsearchConfigurationSupport {
/**
* Must be implemented by deriving classes to provide the {@link ClientConfiguration}.
*
* @return configuration, must not be {@literal null}
*/
@Bean
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 restClient(ClientConfiguration clientConfiguration) {
Assert.notNull(clientConfiguration, "clientConfiguration must not be null");
return ElasticsearchClients.getRestClient(clientConfiguration);
}
/**
* Provides the {@link ElasticsearchClient} to be used.
*
* @param restClient the low level RestClient to use
* @return ElasticsearchClient instance
*/
@Bean
public ElasticsearchClient elasticsearchClient(RestClient restClient) {
Assert.notNull(restClient, "restClient must not be null");
return ElasticsearchClients.createImperative(restClient, transportOptions());
}
/**
* Creates a {@link ElasticsearchOperations} implementation using an
* {@link co.elastic.clients.elasticsearch.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;
}
/**
* @return the options that should be added to every request. Must not be {@literal null}
*/
public TransportOptions transportOptions() {
return new RestClientOptions(RequestOptions.DEFAULT);
}
}

View File

@ -0,0 +1,108 @@
/*
* Copyright 2021-2022 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._types.ElasticsearchException;
import co.elastic.clients.elasticsearch._types.ErrorResponse;
import co.elastic.clients.json.JsonpMapper;
import java.io.IOException;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.elasticsearch.RestStatusException;
import org.springframework.data.elasticsearch.UncategorizedElasticsearchException;
/**
* Simple {@link PersistenceExceptionTranslator} for Elasticsearch. Convert the given runtime exception to an
* appropriate exception from the {@code org.springframework.dao} hierarchy. Return {@literal null} if no translation is
* appropriate: any other exception may have resulted from user code, and should not be translated.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public class ElasticsearchExceptionTranslator implements PersistenceExceptionTranslator {
private final JsonpMapper jsonpMapper;
public ElasticsearchExceptionTranslator(JsonpMapper jsonpMapper) {
this.jsonpMapper = jsonpMapper;
}
/**
* translates an Exception if possible. Exceptions that are no {@link RuntimeException}s are wrapped in a
* RuntimeException
*
* @param throwable the Exception to map
* @return the potentially translated RuntimeException.
*/
public RuntimeException translateException(Throwable throwable) {
RuntimeException runtimeException = throwable instanceof RuntimeException ? (RuntimeException) throwable
: new RuntimeException(throwable.getMessage(), throwable);
RuntimeException potentiallyTranslatedException = translateExceptionIfPossible(runtimeException);
return potentiallyTranslatedException != null ? potentiallyTranslatedException : runtimeException;
}
@Override
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
if (isSeqNoConflict(ex)) {
return new OptimisticLockingFailureException("Cannot index a document due to seq_no+primary_term conflict", ex);
}
// todo #1973 index unavailable?
if (ex instanceof ElasticsearchException) {
ElasticsearchException elasticsearchException = (ElasticsearchException) ex;
ErrorResponse response = elasticsearchException.response();
String body = JsonUtils.toJson(response, jsonpMapper);
return new UncategorizedElasticsearchException(ex.getMessage(), response.status(), body, ex);
}
Throwable cause = ex.getCause();
if (cause instanceof IOException) {
return new DataAccessResourceFailureException(ex.getMessage(), ex);
}
return null;
}
private boolean isSeqNoConflict(Exception exception) {
// todo #1973 check if this works
Integer status = null;
String message = null;
if (exception instanceof RestStatusException) {
RestStatusException statusException = (RestStatusException) exception;
status = statusException.getStatus();
message = statusException.getMessage();
}
if (status != null && message != null) {
return status == 409 && message.contains("type=version_conflict_engine_exception")
&& message.contains("version conflict, required seqNo");
}
return false;
}
}

View File

@ -0,0 +1,496 @@
/*
* Copyright 2021-2022 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.elasticsearch._types.Time;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.transport.Version;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.data.elasticsearch.BulkFailureException;
import org.springframework.data.elasticsearch.client.UnsupportedBackendOperation;
import org.springframework.data.elasticsearch.core.AbstractElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.IndexOperations;
import org.springframework.data.elasticsearch.core.IndexedObjectInformation;
import org.springframework.data.elasticsearch.core.MultiGetItem;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.SearchScrollHits;
import org.springframework.data.elasticsearch.core.cluster.ClusterOperations;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.BulkOptions;
import org.springframework.data.elasticsearch.core.query.ByQueryResponse;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.MoreLikeThisQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
import org.springframework.data.elasticsearch.core.query.UpdateResponse;
import org.springframework.data.elasticsearch.core.reindex.ReindexRequest;
import org.springframework.data.elasticsearch.core.reindex.ReindexResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Implementation of {@link org.springframework.data.elasticsearch.core.ElasticsearchOperations} using the new
* Elasticsearch client.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
private final ElasticsearchClient client;
private final RequestConverter requestConverter;
private final ResponseConverter responseConverter;
private final JsonpMapper jsonpMapper;
private final ElasticsearchExceptionTranslator exceptionTranslator;
// region _initialization
public ElasticsearchTemplate(ElasticsearchClient client, ElasticsearchConverter elasticsearchConverter) {
super(elasticsearchConverter);
Assert.notNull(client, "client must not be null");
this.client = client;
this.jsonpMapper = client._transport().jsonpMapper();
requestConverter = new RequestConverter(elasticsearchConverter, jsonpMapper);
responseConverter = new ResponseConverter(jsonpMapper);
exceptionTranslator = new ElasticsearchExceptionTranslator(jsonpMapper);
}
@Override
protected AbstractElasticsearchTemplate doCopy() {
return new ElasticsearchTemplate(client, elasticsearchConverter);
}
// endregion
// region child templates
@Override
public IndexOperations indexOps(Class<?> clazz) {
return new IndicesTemplate(client.indices(), elasticsearchConverter, clazz);
}
@Override
public IndexOperations indexOps(IndexCoordinates index) {
return new IndicesTemplate(client.indices(), elasticsearchConverter, index);
}
@Override
public ClusterOperations cluster() {
return new ClusterTemplate(client.cluster(), elasticsearchConverter);
}
// endregion
// region document operations
@Override
@Nullable
public <T> T get(String id, Class<T> clazz, IndexCoordinates index) {
GetRequest getRequest = requestConverter.documentGetRequest(elasticsearchConverter.convertId(id),
routingResolver.getRouting(), index, false);
GetResponse<EntityAsMap> getResponse = execute(client -> client.get(getRequest, EntityAsMap.class));
ReadDocumentCallback<T> callback = new ReadDocumentCallback<>(elasticsearchConverter, clazz, index);
return callback.doWith(DocumentAdapters.from(getResponse));
}
@Override
public <T> List<MultiGetItem<T>> multiGet(Query query, Class<T> clazz, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
Assert.notNull(clazz, "clazz must not be null");
MgetRequest request = requestConverter.documentMgetRequest(query, clazz, index);
MgetResponse<EntityAsMap> result = execute(client -> client.mget(request, EntityAsMap.class));
ReadDocumentCallback<T> callback = new ReadDocumentCallback<>(elasticsearchConverter, clazz, index);
return DocumentAdapters.from(result).stream() //
.map(multiGetItem -> MultiGetItem.of( //
multiGetItem.isFailed() ? null : callback.doWith(multiGetItem.getItem()), multiGetItem.getFailure())) //
.collect(Collectors.toList());
}
@Override
public void bulkUpdate(List<UpdateQuery> queries, BulkOptions bulkOptions, IndexCoordinates index) {
throw new UnsupportedOperationException("not implemented");
}
@Override
public ByQueryResponse delete(Query query, Class<?> clazz, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
DeleteByQueryRequest request = requestConverter.documentDeleteByQueryRequest(query, clazz, index,
getRefreshPolicy());
DeleteByQueryResponse response = execute(client -> client.deleteByQuery(request));
return responseConverter.byQueryResponse(response);
}
@Override
public UpdateResponse update(UpdateQuery updateQuery, IndexCoordinates index) {
throw new UnsupportedOperationException("not implemented");
}
@Override
public ByQueryResponse updateByQuery(UpdateQuery updateQuery, IndexCoordinates index) {
throw new UnsupportedOperationException("not implemented");
}
@Override
public String doIndex(IndexQuery query, IndexCoordinates indexCoordinates) {
Assert.notNull(query, "query must not be null");
Assert.notNull(indexCoordinates, "indexCoordinates must not be null");
IndexRequest<?> indexRequest = requestConverter.documentIndexRequest(query, indexCoordinates, refreshPolicy);
IndexResponse indexResponse = execute(client -> client.index(indexRequest));
Object queryObject = query.getObject();
if (queryObject != null) {
query.setObject(updateIndexedObject(queryObject, IndexedObjectInformation.of(indexResponse.id(),
indexResponse.seqNo(), indexResponse.primaryTerm(), indexResponse.version())));
}
return indexResponse.id();
}
@Override
protected boolean doExists(String id, IndexCoordinates index) {
Assert.notNull(id, "id must not be null");
Assert.notNull(index, "index must not be null");
GetRequest request = requestConverter.documentGetRequest(id, routingResolver.getRouting(), index, true);
return execute(client -> client.get(request, EntityAsMap.class)).found();
}
@Override
protected String doDelete(String id, @Nullable String routing, IndexCoordinates index) {
Assert.notNull(id, "id must not be null");
Assert.notNull(index, "index must not be null");
DeleteRequest request = requestConverter.documentDeleteRequest(elasticsearchConverter.convertId(id), routing, index,
getRefreshPolicy());
return execute(client -> client.delete(request)).id();
}
@Override
public ReindexResponse reindex(ReindexRequest reindexRequest) {
Assert.notNull(reindexRequest, "reindexRequest must not be null");
co.elastic.clients.elasticsearch.core.ReindexRequest reindexRequestES = requestConverter.reindex(reindexRequest,
true);
co.elastic.clients.elasticsearch.core.ReindexResponse reindexResponse = execute(
client -> client.reindex(reindexRequestES));
return responseConverter.reindexResponse(reindexResponse);
}
@Override
public String submitReindex(ReindexRequest reindexRequest) {
co.elastic.clients.elasticsearch.core.ReindexRequest reindexRequestES = requestConverter.reindex(reindexRequest,
false);
co.elastic.clients.elasticsearch.core.ReindexResponse reindexResponse = execute(
client -> client.reindex(reindexRequestES));
if (reindexResponse.task() == null) {
// todo #1973 check behaviour and create issue in ES if necessary
throw new UnsupportedBackendOperation("ElasticsearchClient did not return a task id on submit request");
}
return reindexResponse.task();
}
@Override
public List<IndexedObjectInformation> doBulkOperation(List<?> queries, BulkOptions bulkOptions,
IndexCoordinates index) {
BulkRequest bulkRequest = requestConverter.documentBulkRequest(queries, bulkOptions, index, refreshPolicy);
BulkResponse bulkResponse = execute(client -> client.bulk(bulkRequest));
List<IndexedObjectInformation> indexedObjectInformationList = checkForBulkOperationFailure(bulkResponse);
updateIndexedObjectsWithQueries(queries, indexedObjectInformationList);
return indexedObjectInformationList;
}
// endregion
@Override
protected String getClusterVersion() {
return execute(client -> client.info().version().number());
}
@Override
protected String getVendor() {
return "Elasticsearch";
}
@Override
protected String getRuntimeLibraryVersion() {
return Version.VERSION.toString();
}
// region search operations
@Override
public long count(Query query, @Nullable Class<?> clazz, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
Assert.notNull(index, "index must not be null");
SearchRequest searchRequest = requestConverter.searchRequest(query, clazz, index, true, false);
SearchResponse<EntityAsMap> searchResponse = execute(client -> client.search(searchRequest, EntityAsMap.class));
return searchResponse.hits().total().value();
}
@Override
public <T> SearchHits<T> search(Query query, Class<T> clazz, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
Assert.notNull(index, "index must not be null");
SearchRequest searchRequest = requestConverter.searchRequest(query, clazz, index, false, false);
SearchResponse<EntityAsMap> searchResponse = execute(client -> client.search(searchRequest, EntityAsMap.class));
ReadDocumentCallback<T> readDocumentCallback = new ReadDocumentCallback<>(elasticsearchConverter, clazz, index);
SearchDocumentResponse.EntityCreator<T> entityCreator = getEntityCreator(readDocumentCallback);
SearchDocumentResponseCallback<SearchHits<T>> callback = new ReadSearchDocumentResponseCallback<>(clazz, index);
return callback.doWith(SearchDocumentResponseBuilder.from(searchResponse, entityCreator, jsonpMapper));
}
@Override
protected <T> SearchHits<T> doSearch(MoreLikeThisQuery query, Class<T> clazz, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
Assert.notNull(clazz, "clazz must not be null");
Assert.notNull(index, "index must not be null");
return search(NativeQuery.builder() //
.withQuery(q -> q.moreLikeThis(requestConverter.moreLikeThisQuery(query, index)))//
.withPageable(query.getPageable()) //
.build(), clazz, index);
}
@Override
protected <T> SearchScrollHits<T> searchScrollStart(long scrollTimeInMillis, Query query, Class<T> clazz,
IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
Assert.notNull(query.getPageable(), "pageable of query must not be null.");
SearchRequest request = requestConverter.searchRequest(query, clazz, index, false, scrollTimeInMillis);
SearchResponse<EntityAsMap> response = execute(client -> client.search(request, EntityAsMap.class));
return getSearchScrollHits(clazz, index, response);
}
@Override
protected <T> SearchScrollHits<T> searchScrollContinue(String scrollId, long scrollTimeInMillis, Class<T> clazz,
IndexCoordinates index) {
Assert.notNull(scrollId, "scrollId must not be null");
ScrollRequest request = ScrollRequest
.of(sr -> sr.scrollId(scrollId).scroll(Time.of(t -> t.time(scrollTimeInMillis + "ms"))));
ScrollResponse<EntityAsMap> response = execute(client -> client.scroll(request, EntityAsMap.class));
return getSearchScrollHits(clazz, index, response);
}
private <T, R extends SearchResponse<EntityAsMap>> SearchScrollHits<T> getSearchScrollHits(Class<T> clazz,
IndexCoordinates index, R response) {
ReadDocumentCallback<T> documentCallback = new ReadDocumentCallback<>(elasticsearchConverter, clazz, index);
SearchDocumentResponseCallback<SearchScrollHits<T>> callback = new ReadSearchScrollDocumentResponseCallback<>(clazz,
index);
return callback
.doWith(SearchDocumentResponseBuilder.from(response, getEntityCreator(documentCallback), jsonpMapper));
}
@Override
protected void searchScrollClear(List<String> scrollIds) {
Assert.notNull(scrollIds, "scrollIds must not be null");
if (!scrollIds.isEmpty()) {
ClearScrollRequest request = ClearScrollRequest.of(csr -> csr.scrollId(scrollIds));
execute(client -> client.clearScroll(request));
}
}
@Override
public <T> List<SearchHits<T>> multiSearch(List<? extends Query> queries, Class<T> clazz, IndexCoordinates index) {
Assert.notNull(queries, "queries must not be null");
Assert.notNull(clazz, "clazz must not be null");
List<MultiSearchQueryParameter> multiSearchQueryParameters = new ArrayList<>(queries.size());
for (Query query : queries) {
multiSearchQueryParameters.add(new MultiSearchQueryParameter(query, clazz, getIndexCoordinatesFor(clazz)));
}
// noinspection unchecked
return doMultiSearch(multiSearchQueryParameters).stream().map(searchHits -> (SearchHits<T>) searchHits)
.collect(Collectors.toList());
}
@Override
public List<SearchHits<?>> multiSearch(List<? extends Query> queries, List<Class<?>> classes) {
Assert.notNull(queries, "queries must not be null");
Assert.notNull(classes, "classes must not be null");
Assert.isTrue(queries.size() == classes.size(), "queries and classes must have the same size");
List<MultiSearchQueryParameter> multiSearchQueryParameters = new ArrayList<>(queries.size());
Iterator<Class<?>> it = classes.iterator();
for (Query query : queries) {
Class<?> clazz = it.next();
multiSearchQueryParameters.add(new MultiSearchQueryParameter(query, clazz, getIndexCoordinatesFor(clazz)));
}
return doMultiSearch(multiSearchQueryParameters);
}
@Override
public List<SearchHits<?>> multiSearch(List<? extends Query> queries, List<Class<?>> classes,
IndexCoordinates index) {
Assert.notNull(queries, "queries must not be null");
Assert.notNull(classes, "classes must not be null");
Assert.notNull(index, "index must not be null");
Assert.isTrue(queries.size() == classes.size(), "queries and classes must have the same size");
List<MultiSearchQueryParameter> multiSearchQueryParameters = new ArrayList<>(queries.size());
Iterator<Class<?>> it = classes.iterator();
for (Query query : queries) {
Class<?> clazz = it.next();
multiSearchQueryParameters.add(new MultiSearchQueryParameter(query, clazz, index));
}
return doMultiSearch(multiSearchQueryParameters);
}
private List<SearchHits<?>> doMultiSearch(List<MultiSearchQueryParameter> multiSearchQueryParameters) {
throw new UnsupportedOperationException("not implemented");
}
/**
* value class combining the information needed for a single query in a multisearch request.
*/
private static class MultiSearchQueryParameter {
final Query query;
final Class<?> clazz;
final IndexCoordinates index;
public MultiSearchQueryParameter(Query query, Class<?> clazz, IndexCoordinates index) {
this.query = query;
this.clazz = clazz;
this.index = index;
}
}
// endregion
// region client callback
/**
* Callback interface to be used with {@link #execute(ElasticsearchTemplate.ClientCallback)} for operating directly on
* the {@link ElasticsearchClient}.
*/
@FunctionalInterface
public interface ClientCallback<T> {
T doWithClient(ElasticsearchClient client) throws IOException;
}
/**
* Execute a callback with the {@link ElasticsearchClient} and provide exception translation.
*
* @param callback the callback to execute, must not be {@literal null}
* @param <T> the type returned from the callback
* @return the callback result
*/
public <T> T execute(ElasticsearchTemplate.ClientCallback<T> callback) {
Assert.notNull(callback, "callback must not be null");
try {
return callback.doWithClient(client);
} catch (IOException | RuntimeException e) {
throw exceptionTranslator.translateException(e);
}
}
// endregion
// region helper methods
@Override
public Query matchAllQuery() {
return NativeQuery.builder().withQuery(qb -> qb.matchAll(mab -> mab)).build();
}
@Override
public Query idsQuery(List<String> ids) {
return NativeQuery.builder().withQuery(qb -> qb.ids(iq -> iq.values(ids))).build();
}
/**
* extract the list of {@link IndexedObjectInformation} from a {@link BulkResponse}.
*
* @param bulkResponse the response to evaluate
* @return the list of the {@link IndexedObjectInformation}s
*/
protected List<IndexedObjectInformation> checkForBulkOperationFailure(BulkResponse bulkResponse) {
if (bulkResponse.errors()) {
Map<String, String> failedDocuments = new HashMap<>();
for (BulkResponseItem item : bulkResponse.items()) {
if (item.error() != null) {
failedDocuments.put(item.id(), item.error().reason());
}
}
throw new BulkFailureException(
"Bulk operation has failures. Use ElasticsearchException.getFailedDocuments() for detailed messages ["
+ failedDocuments + ']',
failedDocuments);
}
return bulkResponse.items().stream()
.map(item -> IndexedObjectInformation.of(item.id(), item.seqNo(), item.primaryTerm(), item.version()))
.collect(Collectors.toList());
}
// endregion
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2021-2022 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 org.springframework.data.elasticsearch.support.DefaultStringObjectMap;
/**
* A Map&lt;String,Object> to represent any entity as it's returned from Elasticsearch and before it is converted to a
* {@link org.springframework.data.elasticsearch.core.document.Document}.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public class EntityAsMap extends DefaultStringObjectMap<EntityAsMap> {}

View File

@ -0,0 +1,236 @@
/*
* Copyright 2022 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 static org.springframework.data.elasticsearch.client.elc.TypeUtils.*;
import java.util.Arrays;
import java.util.stream.Collectors;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.query.highlight.Highlight;
import org.springframework.data.elasticsearch.core.query.highlight.HighlightField;
import org.springframework.data.elasticsearch.core.query.highlight.HighlightFieldParameters;
import org.springframework.data.elasticsearch.core.query.highlight.HighlightParameters;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
* Converts the {@link Highlight} annotation from a method to an ElasticsearchClient
* {@link co.elastic.clients.elasticsearch.core.search.Highlight}.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
class HighlightQueryBuilder {
private final MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext;
HighlightQueryBuilder(
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext) {
this.mappingContext = mappingContext;
}
public co.elastic.clients.elasticsearch.core.search.Highlight getHighlight(Highlight highlight,
@Nullable Class<?> type) {
co.elastic.clients.elasticsearch.core.search.Highlight.Builder highlightBuilder = new co.elastic.clients.elasticsearch.core.search.Highlight.Builder();
// in the old implementation we could use one addParameters method, but in the new Elasticsearch client
// the builder for highlight and highlightfield share no code
addParameters(highlight.getParameters(), highlightBuilder);
for (HighlightField highlightField : highlight.getFields()) {
String mappedName = mapFieldName(highlightField.getName(), type);
highlightBuilder.fields(mappedName, hf -> {
addParameters(highlightField.getParameters(), hf, type);
return hf;
});
}
return highlightBuilder.build();
}
/*
* the builder for highlight and highlight fields don't share code, so we have these two methods here that basically are almost copies
*/
private void addParameters(HighlightParameters parameters,
co.elastic.clients.elasticsearch.core.search.Highlight.Builder builder) {
if (StringUtils.hasLength(parameters.getBoundaryChars())) {
builder.boundaryChars(parameters.getBoundaryChars());
}
if (parameters.getBoundaryMaxScan() > -1) {
builder.boundaryMaxScan(parameters.getBoundaryMaxScan());
}
if (StringUtils.hasLength(parameters.getBoundaryScanner())) {
builder.boundaryScanner(boundaryScanner(parameters.getBoundaryScanner()));
}
if (StringUtils.hasLength(parameters.getBoundaryScannerLocale())) {
builder.boundaryScannerLocale(parameters.getBoundaryScannerLocale());
}
if (parameters.getForceSource()) { // default is false
// todo #1973 parameter missing in new client
}
if (StringUtils.hasLength(parameters.getFragmenter())) {
builder.fragmenter(highlighterFragmenter(parameters.getFragmenter()));
}
if (parameters.getFragmentSize() > -1) {
builder.fragmentSize(parameters.getFragmentSize());
}
if (parameters.getNoMatchSize() > -1) {
builder.noMatchSize(parameters.getNoMatchSize());
}
if (parameters.getNumberOfFragments() > -1) {
builder.numberOfFragments(parameters.getNumberOfFragments());
}
if (StringUtils.hasLength(parameters.getOrder())) {
builder.order(highlighterOrder(parameters.getOrder()));
}
if (parameters.getPhraseLimit() > -1) {
// todo #1973 parameter missing in new client
}
if (parameters.getPreTags().length > 0) {
builder.preTags(Arrays.asList(parameters.getPreTags()));
}
if (parameters.getPostTags().length > 0) {
builder.postTags(Arrays.asList(parameters.getPostTags()));
}
if (!parameters.getRequireFieldMatch()) { // default is true
builder.requireFieldMatch(false);
}
if (StringUtils.hasLength(parameters.getType())) {
builder.type(highlighterType(parameters.getType()));
}
if (StringUtils.hasLength(parameters.getEncoder())) {
builder.encoder(highlighterEncoder(parameters.getEncoder()));
}
if (StringUtils.hasLength(parameters.getTagsSchema())) {
builder.tagsSchema(highlighterTagsSchema(parameters.getTagsSchema()));
}
}
/*
* the builder for highlight and highlight fields don't share code, so we have these two methods here that basically are almost copies
*/
private void addParameters(HighlightFieldParameters parameters,
co.elastic.clients.elasticsearch.core.search.HighlightField.Builder builder, Class<?> type) {
if (StringUtils.hasLength(parameters.getBoundaryChars())) {
builder.boundaryChars(parameters.getBoundaryChars());
}
if (parameters.getBoundaryMaxScan() > -1) {
builder.boundaryMaxScan(parameters.getBoundaryMaxScan());
}
if (StringUtils.hasLength(parameters.getBoundaryScanner())) {
builder.boundaryScanner(boundaryScanner(parameters.getBoundaryScanner()));
}
if (StringUtils.hasLength(parameters.getBoundaryScannerLocale())) {
builder.boundaryScannerLocale(parameters.getBoundaryScannerLocale());
}
if (parameters.getForceSource()) { // default is false
builder.forceSource(parameters.getForceSource());
}
if (StringUtils.hasLength(parameters.getFragmenter())) {
builder.fragmenter(highlighterFragmenter(parameters.getFragmenter()));
}
if (parameters.getFragmentSize() > -1) {
builder.fragmentSize(parameters.getFragmentSize());
}
if (parameters.getNoMatchSize() > -1) {
builder.noMatchSize(parameters.getNoMatchSize());
}
if (parameters.getNumberOfFragments() > -1) {
builder.numberOfFragments(parameters.getNumberOfFragments());
}
if (StringUtils.hasLength(parameters.getOrder())) {
builder.order(highlighterOrder(parameters.getOrder()));
}
if (parameters.getPhraseLimit() > -1) {
builder.phraseLimit(parameters.getPhraseLimit());
}
if (parameters.getPreTags().length > 0) {
builder.preTags(Arrays.asList(parameters.getPreTags()));
}
if (parameters.getPostTags().length > 0) {
builder.postTags(Arrays.asList(parameters.getPostTags()));
}
if (!parameters.getRequireFieldMatch()) { // default is true
builder.requireFieldMatch(false);
}
if (StringUtils.hasLength(parameters.getType())) {
builder.type(highlighterType(parameters.getType()));
}
if ((parameters).getFragmentOffset() > -1) {
builder.fragmentOffset(parameters.getFragmentOffset());
}
if (parameters.getMatchedFields().length > 0) {
builder.matchedFields(Arrays.stream(parameters.getMatchedFields()).map(fieldName -> mapFieldName(fieldName, type)) //
.collect(Collectors.toList()));
}
}
private String mapFieldName(String fieldName, @Nullable Class<?> type) {
if (type != null) {
ElasticsearchPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(type);
if (persistentEntity != null) {
ElasticsearchPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(fieldName);
if (persistentProperty != null) {
return persistentProperty.getFieldName();
}
}
}
return fieldName;
}
}

View File

@ -0,0 +1,354 @@
/*
* Copyright 2021-2022 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 static org.springframework.util.StringUtils.*;
import co.elastic.clients.elasticsearch.indices.*;
import co.elastic.clients.transport.endpoints.BooleanResponse;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.elasticsearch.UncategorizedElasticsearchException;
import org.springframework.data.elasticsearch.annotations.Mapping;
import org.springframework.data.elasticsearch.core.IndexInformation;
import org.springframework.data.elasticsearch.core.IndexOperations;
import org.springframework.data.elasticsearch.core.ResourceUtil;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.index.AliasActions;
import org.springframework.data.elasticsearch.core.index.AliasData;
import org.springframework.data.elasticsearch.core.index.DeleteTemplateRequest;
import org.springframework.data.elasticsearch.core.index.ExistsTemplateRequest;
import org.springframework.data.elasticsearch.core.index.GetTemplateRequest;
import org.springframework.data.elasticsearch.core.index.MappingBuilder;
import org.springframework.data.elasticsearch.core.index.PutTemplateRequest;
import org.springframework.data.elasticsearch.core.index.Settings;
import org.springframework.data.elasticsearch.core.index.TemplateData;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Implementation of the {@link IndexOperations} interface using en {@link ElasticsearchIndicesClient}.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public class IndicesTemplate extends ChildTemplate<ElasticsearchIndicesClient> implements IndexOperations {
private static final Logger LOGGER = LoggerFactory.getLogger(IndicesTemplate.class);
protected final ElasticsearchConverter elasticsearchConverter;
@Nullable protected final Class<?> boundClass;
@Nullable protected final IndexCoordinates boundIndex;
public IndicesTemplate(ElasticsearchIndicesClient client, ElasticsearchConverter elasticsearchConverter,
Class<?> boundClass) {
super(client, elasticsearchConverter);
Assert.notNull(elasticsearchConverter, "elasticsearchConverter must not be null");
Assert.notNull(boundClass, "boundClass may not be null");
this.elasticsearchConverter = elasticsearchConverter;
this.boundClass = boundClass;
this.boundIndex = null;
}
public IndicesTemplate(ElasticsearchIndicesClient client, ElasticsearchConverter elasticsearchConverter,
IndexCoordinates boundIndex) {
super(client, elasticsearchConverter);
Assert.notNull(elasticsearchConverter, "elasticsearchConverter must not be null");
Assert.notNull(boundIndex, "boundIndex must not be null");
this.elasticsearchConverter = elasticsearchConverter;
this.boundClass = null;
this.boundIndex = boundIndex;
}
protected Class<?> checkForBoundClass() {
if (boundClass == null) {
throw new InvalidDataAccessApiUsageException("IndexOperations are not bound");
}
return boundClass;
}
@Override
public boolean create() {
Settings settings = boundClass != null ? createSettings(boundClass) : new Settings();
return doCreate(getIndexCoordinates(), settings, null);
}
@Override
public boolean create(Map<String, Object> settings) {
Assert.notNull(settings, "settings must not be null");
return doCreate(getIndexCoordinates(), settings, null);
}
@Override
public boolean create(Map<String, Object> settings, Document mapping) {
Assert.notNull(settings, "settings must not be null");
Assert.notNull(mapping, "mapping must not be null");
return doCreate(getIndexCoordinates(), settings, mapping);
}
@Override
public boolean createWithMapping() {
return doCreate(getIndexCoordinates(), createSettings(), createMapping());
}
protected boolean doCreate(IndexCoordinates indexCoordinates, Map<String, Object> settings,
@Nullable Document mapping) {
Assert.notNull(indexCoordinates, "indexCoordinates must not be null");
Assert.notNull(settings, "settings must not be null");
CreateIndexRequest createIndexRequest = requestConverter.indicesCreateRequest(indexCoordinates, settings, mapping);
CreateIndexResponse createIndexResponse = execute(client -> client.create(createIndexRequest));
return Boolean.TRUE.equals(createIndexResponse.acknowledged());
}
@Override
public boolean delete() {
return doDelete(getIndexCoordinates());
}
private boolean doDelete(IndexCoordinates indexCoordinates) {
Assert.notNull(indexCoordinates, "indexCoordinates must not be null");
if (doExists(indexCoordinates)) {
DeleteIndexRequest deleteIndexRequest = requestConverter.indicesDeleteRequest(indexCoordinates);
DeleteIndexResponse deleteIndexResponse = execute(client -> client.delete(deleteIndexRequest));
return deleteIndexResponse.acknowledged();
}
return false;
}
@Override
public boolean exists() {
return doExists(getIndexCoordinates());
}
private boolean doExists(IndexCoordinates indexCoordinates) {
Assert.notNull(indexCoordinates, "indexCoordinates must not be null");
ExistsRequest existsRequest = requestConverter.indicesExistsRequest(indexCoordinates);
BooleanResponse existsResponse = execute(client -> client.exists(existsRequest));
return existsResponse.value();
}
@Override
public void refresh() {
RefreshRequest refreshRequest = requestConverter.indicesRefreshRequest(getIndexCoordinates());
execute(client -> client.refresh(refreshRequest));
}
@Override
public Document createMapping() {
return createMapping(checkForBoundClass());
}
@Override
public Document createMapping(Class<?> clazz) {
Assert.notNull(clazz, "clazz must not be null");
// load mapping specified in Mapping annotation if present
// noinspection DuplicatedCode
Mapping mappingAnnotation = AnnotatedElementUtils.findMergedAnnotation(clazz, Mapping.class);
if (mappingAnnotation != null) {
String mappingPath = mappingAnnotation.mappingPath();
if (hasText(mappingPath)) {
String mappings = ResourceUtil.readFileFromClasspath(mappingPath);
if (hasText(mappings)) {
return Document.parse(mappings);
}
}
}
// build mapping from field annotations
try {
String mapping = new MappingBuilder(elasticsearchConverter).buildPropertyMapping(clazz);
return Document.parse(mapping);
} catch (Exception e) {
throw new UncategorizedElasticsearchException("Failed to build mapping for " + clazz.getSimpleName(), e);
}
}
@Override
public boolean putMapping(Document mapping) {
Assert.notNull(mapping, "mapping must not be null");
PutMappingRequest putMappingRequest = requestConverter.indicesPutMappingRequest(getIndexCoordinates(), mapping);
PutMappingResponse putMappingResponse = execute(client -> client.putMapping(putMappingRequest));
return putMappingResponse.acknowledged();
}
@Override
public Map<String, Object> getMapping() {
IndexCoordinates indexCoordinates = getIndexCoordinates();
GetMappingRequest getMappingRequest = requestConverter.indicesGetMappingRequest(indexCoordinates);
GetMappingResponse getMappingResponse = execute(client -> client.getMapping(getMappingRequest));
Document mappingResponse = responseConverter.indicesGetMapping(getMappingResponse, indexCoordinates);
return mappingResponse;
}
@Override
public Settings createSettings() {
return createSettings(checkForBoundClass());
}
@Override
public Settings createSettings(Class<?> clazz) {
Assert.notNull(clazz, "clazz must not be null");
ElasticsearchPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(clazz);
String settingPath = persistentEntity.settingPath();
return hasText(settingPath) //
? Settings.parse(ResourceUtil.readFileFromClasspath(settingPath)) //
: persistentEntity.getDefaultSettings();
}
@Override
public Settings getSettings() {
return getSettings(false);
}
@Override
public Settings getSettings(boolean includeDefaults) {
GetIndicesSettingsRequest getIndicesSettingsRequest = requestConverter
.indicesGetSettingsRequest(getIndexCoordinates(), includeDefaults);
GetIndicesSettingsResponse getIndicesSettingsResponse = execute(
client -> client.getSettings(getIndicesSettingsRequest));
return responseConverter.indicesGetSettings(getIndicesSettingsResponse, getIndexCoordinates().getIndexName());
}
@Override
public boolean alias(AliasActions aliasActions) {
Assert.notNull(aliasActions, "aliasActions must not be null");
UpdateAliasesRequest updateAliasesRequest = requestConverter.indicesUpdateAliasesRequest(aliasActions);
UpdateAliasesResponse updateAliasesResponse = execute(client -> client.updateAliases(updateAliasesRequest));
return updateAliasesResponse.acknowledged();
}
@Override
public Map<String, Set<AliasData>> getAliases(String... aliasNames) {
throw new UnsupportedOperationException("not implemented");
}
@Override
public Map<String, Set<AliasData>> getAliasesForIndex(String... indexNames) {
throw new UnsupportedOperationException("not implemented");
}
@Override
public boolean putTemplate(PutTemplateRequest putTemplateRequest) {
Assert.notNull(putTemplateRequest, "putTemplateRequest must not be null");
co.elastic.clients.elasticsearch.indices.PutTemplateRequest putTemplateRequestES = requestConverter
.indicesPutTemplateRequest(putTemplateRequest);
return execute(client -> client.putTemplate(putTemplateRequestES)).acknowledged();
}
@Override
public TemplateData getTemplate(GetTemplateRequest getTemplateRequest) {
Assert.notNull(getTemplateRequest, "getTemplateRequest must not be null");
co.elastic.clients.elasticsearch.indices.GetTemplateRequest getTemplateRequestES = requestConverter
.indicesGetTemplateRequest(getTemplateRequest);
GetTemplateResponse getTemplateResponse = execute(client -> client.getTemplate(getTemplateRequestES));
return responseConverter.indicesGetTemplateData(getTemplateResponse, getTemplateRequest.getTemplateName());
}
@Override
public boolean existsTemplate(ExistsTemplateRequest existsTemplateRequest) {
Assert.notNull(existsTemplateRequest, "existsTemplateRequest must not be null");
co.elastic.clients.elasticsearch.indices.ExistsTemplateRequest existsTemplateRequestSO = requestConverter
.indicesExistsTemplateRequest(existsTemplateRequest);
return execute(client -> client.existsTemplate(existsTemplateRequestSO)).value();
}
@Override
public boolean deleteTemplate(DeleteTemplateRequest deleteTemplateRequest) {
Assert.notNull(deleteTemplateRequest, "deleteTemplateRequest must not be null");
co.elastic.clients.elasticsearch.indices.DeleteTemplateRequest deleteTemplateRequestES = requestConverter
.indicesDeleteTemplateRequest(deleteTemplateRequest);
return execute(client -> client.deleteTemplate(deleteTemplateRequestES)).acknowledged();
}
@Override
public List<IndexInformation> getInformation(IndexCoordinates indexCoordinates) {
Assert.notNull(indexCoordinates, "indexCoordinates must not be null");
GetIndexRequest getIndexRequest = requestConverter.indicesGetIndexRequest(indexCoordinates);
GetIndexResponse getIndexResponse = execute(client -> client.get(getIndexRequest));
return responseConverter.indicesGetIndexInformations(getIndexResponse);
}
// region Helper functions
ElasticsearchPersistentEntity<?> getRequiredPersistentEntity(Class<?> clazz) {
return elasticsearchConverter.getMappingContext().getRequiredPersistentEntity(clazz);
}
@Override
public IndexCoordinates getIndexCoordinates() {
return (boundClass != null) ? getIndexCoordinatesFor(boundClass) : Objects.requireNonNull(boundIndex);
}
public IndexCoordinates getIndexCoordinatesFor(Class<?> clazz) {
return getRequiredPersistentEntity(clazz).getIndexCoordinates();
}
// endregion
}

View File

@ -0,0 +1,53 @@
/*
* Copyright 2021-2022 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 jakarta.json.stream.JsonGenerator;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* @author Peter-Josef Meisch
* @since 4.4
*/
final class JsonUtils {
private static final Log LOGGER = LogFactory.getLog(JsonUtils.class);
private JsonUtils() {}
public static String toJson(Object object, JsonpMapper mapper) {
// noinspection SpellCheckingInspection
ByteArrayOutputStream baos = new ByteArrayOutputStream();
JsonGenerator generator = mapper.jsonProvider().createGenerator(baos);
mapper.serialize(object, generator);
generator.close();
String jsonMapping = "{}";
try {
jsonMapping = baos.toString("UTF-8");
} catch (UnsupportedEncodingException e) {
LOGGER.warn("could not read json", e);
}
return jsonMapping;
}
}

View File

@ -0,0 +1,90 @@
/*
* Copyright 2021-2022 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._types.aggregations.Aggregation;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch.core.search.Suggester;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* A {@link org.springframework.data.elasticsearch.core.query.Query} implementation using query builders from the new
* Elasticsearch Client library.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public class NativeQuery extends BaseQuery {
@Nullable private final Query query;
// note: the new client does not have pipeline aggs, these are just set up as normal aggs
private final Map<String, Aggregation> aggregations = new LinkedHashMap<>();
@Nullable private Suggester suggester;
public NativeQuery(NativeQueryBuilder builder) {
super(builder);
this.query = builder.getQuery();
}
public NativeQuery(@Nullable Query query) {
this.query = query;
}
public static NativeQueryBuilder builder() {
return new NativeQueryBuilder();
}
@Nullable
public Query getQuery() {
return query;
}
public void addAggregation(String name, Aggregation aggregation) {
Assert.notNull(name, "name must not be null");
Assert.notNull(aggregation, "aggregation must not be null");
aggregations.put(name, aggregation);
}
public void setAggregations(Map<String, Aggregation> aggregations) {
Assert.notNull(aggregations, "aggregations must not be null");
this.aggregations.clear();
this.aggregations.putAll(aggregations);
}
public Map<String, Aggregation> getAggregations() {
return aggregations;
}
@Nullable
public Suggester getSuggester() {
return suggester;
}
public void setSuggester(@Nullable Suggester suggester) {
this.suggester = suggester;
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright 2022 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._types.aggregations.Aggregation;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch.core.search.Suggester;
import co.elastic.clients.util.ObjectBuilder;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import org.springframework.data.elasticsearch.core.query.BaseQueryBuilder;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* @author Peter-Josef Meisch
* @since 4.4
*/
public class NativeQueryBuilder extends BaseQueryBuilder<NativeQuery, NativeQueryBuilder> {
@Nullable private Query query;
private final Map<String, Aggregation> aggregations = new LinkedHashMap<>();
@Nullable private Suggester suggester;
public NativeQueryBuilder() {
}
@Nullable
public Query getQuery() {
return query;
}
public NativeQueryBuilder withQuery(Query query) {
Assert.notNull(query, "query must not be null");
this.query = query;
return this;
}
public NativeQueryBuilder withQuery(Function<Query.Builder, ObjectBuilder<Query>> fn) {
Assert.notNull(fn, "fn must not be null");
return withQuery(fn.apply(new Query.Builder()).build());
}
public NativeQueryBuilder withAggregation(String name, Aggregation aggregation) {
Assert.notNull(name, "name must not be null");
Assert.notNull(aggregation, "aggregation must not be null");
this.aggregations.put(name, aggregation);
return this;
}
public NativeQueryBuilder withSuggester(@Nullable Suggester suggester) {
this.suggester = suggester;
return this;
}
public NativeQuery build() {
NativeQuery nativeQuery = new NativeQuery(this);
nativeQuery.setAggregations(aggregations);
nativeQuery.setSuggester(suggester);
return nativeQuery;
}
}

View File

@ -0,0 +1,154 @@
/*
* Copyright 2022 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._types.FieldValue;
import co.elastic.clients.elasticsearch._types.LatLonGeoLocation;
import co.elastic.clients.elasticsearch._types.query_dsl.MatchAllQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.MatchQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.Operator;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch._types.query_dsl.QueryStringQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.TermQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.WildcardQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.WrapperQuery;
import co.elastic.clients.util.ObjectBuilder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.function.Function;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Utility class simplifying the creation of some more complex queries and type.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public final class QueryBuilders {
private QueryBuilders() {}
public static MatchQuery matchQuery(String fieldName, String query, @Nullable Operator operator,
@Nullable Float boost) {
Assert.notNull(fieldName, "fieldName must not be null");
Assert.notNull(query, "query must not be null");
return MatchQuery.of(mb -> mb.field(fieldName).query(FieldValue.of(query)).operator(operator).boost(boost));
}
public static Query matchQueryAsQuery(String fieldName, String query, @Nullable Operator operator,
@Nullable Float boost) {
Function<Query.Builder, ObjectBuilder<Query>> builder = b -> b.match(matchQuery(fieldName, query, operator, boost));
return builder.apply(new Query.Builder()).build();
}
public static MatchAllQuery matchAllQuery() {
return MatchAllQuery.of(b -> b);
}
public static Query matchAllQueryAsQuery() {
Function<Query.Builder, ObjectBuilder<Query>> builder = b -> b.matchAll(matchAllQuery());
return builder.apply(new Query.Builder()).build();
}
public static QueryStringQuery queryStringQuery(String fieldName, String query, @Nullable Float boost) {
return queryStringQuery(fieldName, query, null, null, boost);
}
public static QueryStringQuery queryStringQuery(String fieldName, String query, Operator defaultOperator,
@Nullable Float boost) {
return queryStringQuery(fieldName, query, null, defaultOperator, boost);
}
public static QueryStringQuery queryStringQuery(String fieldName, String query, @Nullable Boolean analyzeWildcard,
@Nullable Float boost) {
return queryStringQuery(fieldName, query, analyzeWildcard, null, boost);
}
public static QueryStringQuery queryStringQuery(String fieldName, String query, @Nullable Boolean analyzeWildcard,
@Nullable Operator defaultOperator, @Nullable Float boost) {
Assert.notNull(fieldName, "fieldName must not be null");
Assert.notNull(query, "query must not be null");
return QueryStringQuery.of(qs -> qs.fields(fieldName).query(query).analyzeWildcard(analyzeWildcard)
.defaultOperator(defaultOperator).boost(boost));
}
public static TermQuery termQuery(String fieldName, String value) {
Assert.notNull(fieldName, "fieldName must not be null");
Assert.notNull(value, "value must not be null");
return TermQuery.of(t -> t.field(fieldName).value(FieldValue.of(value)));
}
public static Query termQueryAsQuery(String fieldName, String value) {
Function<Query.Builder, ObjectBuilder<Query>> builder = q -> q.term(termQuery(fieldName, value));
return builder.apply(new Query.Builder()).build();
}
public static WildcardQuery wildcardQuery(String field, String value) {
Assert.notNull(field, "field must not be null");
Assert.notNull(value, "value must not be null");
return WildcardQuery.of(w -> w.field(field).wildcard(value));
}
public static Query wildcardQueryAsQuery(String field, String value) {
Function<Query.Builder, ObjectBuilder<Query>> builder = q -> q.wildcard(wildcardQuery(field, value));
return builder.apply(new Query.Builder()).build();
}
public static Query wrapperQueryAsQuery(String query) {
Function<Query.Builder, ObjectBuilder<Query>> builder = q -> q.wrapper(wrapperQuery(query));
return builder.apply(new Query.Builder()).build();
}
public static WrapperQuery wrapperQuery(String query) {
Assert.notNull(query, "query must not be null");
String encodedValue = Base64.getEncoder().encodeToString(query.getBytes(StandardCharsets.UTF_8));
return WrapperQuery.of(wq -> wq.query(encodedValue));
}
public static LatLonGeoLocation latLon(GeoPoint geoPoint) {
Assert.notNull(geoPoint, "geoPoint must not be null");
return latLon(geoPoint.getLat(), geoPoint.getLon());
}
public static LatLonGeoLocation latLon(double lat, double lon) {
return LatLonGeoLocation.of(_0 -> _0.lat(lat).lon(lon));
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2021-2022 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.ApiClient;
import co.elastic.clients.json.JsonpMapper;
import reactor.core.publisher.Flux;
import org.reactivestreams.Publisher;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.util.Assert;
/**
* base class for a reactive template that uses on of the {@link ReactiveElasticsearchClient}'s child clients.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public class ReactiveChildTemplate<CLIENT extends ApiClient> {
protected final CLIENT client;
protected final ElasticsearchConverter elasticsearchConverter;
protected final RequestConverter requestConverter;
protected final ResponseConverter responseConverter;
private final JsonpMapper jsonpMapper;
protected final ElasticsearchExceptionTranslator exceptionTranslator;
public ReactiveChildTemplate(CLIENT client, ElasticsearchConverter elasticsearchConverter) {
this.client = client;
this.elasticsearchConverter = elasticsearchConverter;
jsonpMapper = client._transport().jsonpMapper();
requestConverter = new RequestConverter(elasticsearchConverter, jsonpMapper);
responseConverter = new ResponseConverter(jsonpMapper);
exceptionTranslator = new ElasticsearchExceptionTranslator(jsonpMapper);
}
/**
* Callback interface to be used with {@link #execute(ClientCallback)} for operating directly on the client.
*/
@FunctionalInterface
public interface ClientCallback<CLIENT, RESULT extends Publisher<?>> {
RESULT doWithClient(CLIENT client);
}
/**
* Execute a callback with the client and provide exception translation.
*
* @param callback the callback to execute, must not be {@literal null}
* @param <RESULT> the type returned from the callback
* @return the callback result
*/
public <RESULT> Publisher<RESULT> execute(ClientCallback<CLIENT, Publisher<RESULT>> callback) {
Assert.notNull(callback, "callback must not be null");
return Flux.defer(() -> callback.doWithClient(client)).onErrorMap(exceptionTranslator::translateException);
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2021-2022 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.cluster.HealthRequest;
import co.elastic.clients.elasticsearch.cluster.HealthResponse;
import reactor.core.publisher.Mono;
import org.springframework.data.elasticsearch.core.cluster.ClusterHealth;
import org.springframework.data.elasticsearch.core.cluster.ReactiveClusterOperations;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
/**
* @author Peter-Josef Meisch
* @since 4.4
*/
public class ReactiveClusterTemplate extends ReactiveChildTemplate<ReactiveElasticsearchClusterClient>
implements ReactiveClusterOperations {
public ReactiveClusterTemplate(ReactiveElasticsearchClusterClient client,
ElasticsearchConverter elasticsearchConverter) {
super(client, elasticsearchConverter);
}
@Override
public Mono<ClusterHealth> health() {
HealthRequest healthRequest = requestConverter.clusterHealthRequest();
Mono<HealthResponse> healthResponse = Mono.from(execute(client -> client.health(healthRequest)));
return healthResponse.map(responseConverter::clusterHealth);
}
}

View File

@ -0,0 +1,226 @@
/*
* Copyright 2021-2022 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.ApiClient;
import co.elastic.clients.elasticsearch._types.ErrorResponse;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.JsonEndpoint;
import co.elastic.clients.transport.TransportOptions;
import co.elastic.clients.transport.endpoints.BooleanResponse;
import co.elastic.clients.transport.endpoints.EndpointWithResponseMapperAttr;
import co.elastic.clients.util.ObjectBuilder;
import reactor.core.publisher.Mono;
import java.util.function.Function;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Reactive version of {@link co.elastic.clients.elasticsearch.ElasticsearchClient}.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public class ReactiveElasticsearchClient extends ApiClient<ElasticsearchTransport, ReactiveElasticsearchClient>
implements AutoCloseable {
public ReactiveElasticsearchClient(ElasticsearchTransport transport) {
super(transport, null);
}
public ReactiveElasticsearchClient(ElasticsearchTransport transport, @Nullable TransportOptions transportOptions) {
super(transport, transportOptions);
}
@Override
public ReactiveElasticsearchClient withTransportOptions(@Nullable TransportOptions transportOptions) {
return new ReactiveElasticsearchClient(transport, transportOptions);
}
@Override
public void close() throws Exception {
transport.close();
}
// region child clients
public ReactiveElasticsearchClusterClient cluster() {
return new ReactiveElasticsearchClusterClient(transport, transportOptions);
}
public ReactiveElasticsearchIndicesClient indices() {
return new ReactiveElasticsearchIndicesClient(transport, transportOptions);
}
// endregion
// region info
public Mono<InfoResponse> info() {
return Mono
.fromFuture(transport.performRequestAsync(InfoRequest._INSTANCE, InfoRequest._ENDPOINT, transportOptions));
}
public Mono<BooleanResponse> ping() {
return Mono
.fromFuture(transport.performRequestAsync(PingRequest._INSTANCE, PingRequest._ENDPOINT, transportOptions));
}
// endregion
// region document
public <T> Mono<IndexResponse> index(IndexRequest<T> request) {
Assert.notNull(request, "request must not be null");
return Mono.fromFuture(transport.performRequestAsync(request, IndexRequest._ENDPOINT, transportOptions));
}
public <T> Mono<IndexResponse> index(Function<IndexRequest.Builder<T>, ObjectBuilder<IndexRequest<T>>> fn) {
Assert.notNull(fn, "fn must not be null");
return index(fn.apply(new IndexRequest.Builder<>()).build());
}
public Mono<BulkResponse> bulk(BulkRequest request) {
Assert.notNull(request, "request must not be null");
return Mono.fromFuture(transport.performRequestAsync(request, BulkRequest._ENDPOINT, transportOptions));
}
public Mono<BulkResponse> bulk(Function<BulkRequest.Builder, ObjectBuilder<BulkRequest>> fn) {
Assert.notNull(fn, "fn must not be null");
return bulk(fn.apply(new BulkRequest.Builder()).build());
}
public <T> Mono<GetResponse<T>> get(GetRequest request, Class<T> tClass) {
Assert.notNull(request, "request must not be null");
// code adapted from
// co.elastic.clients.elasticsearch.ElasticsearchClient.get(co.elastic.clients.elasticsearch.core.GetRequest,
// java.lang.Class<TDocument>)
// noinspection unchecked
JsonEndpoint<GetRequest, GetResponse<T>, ErrorResponse> endpoint = (JsonEndpoint<GetRequest, GetResponse<T>, ErrorResponse>) GetRequest._ENDPOINT;
endpoint = new EndpointWithResponseMapperAttr<>(endpoint, "co.elastic.clients:Deserializer:_global.get.TDocument",
getDeserializer(tClass));
return Mono.fromFuture(transport.performRequestAsync(request, endpoint, transportOptions));
}
public <T> Mono<GetResponse<T>> get(Function<GetRequest.Builder, ObjectBuilder<GetRequest>> fn, Class<T> tClass) {
Assert.notNull(fn, "fn must not be null");
return get(fn.apply(new GetRequest.Builder()).build(), tClass);
}
public Mono<ReindexResponse> reindex(ReindexRequest request) {
Assert.notNull(request, "request must not be null");
return Mono.fromFuture(transport.performRequestAsync(request, ReindexRequest._ENDPOINT, transportOptions));
}
public Mono<ReindexResponse> reindex(Function<ReindexRequest.Builder, ObjectBuilder<ReindexRequest>> fn) {
Assert.notNull(fn, "fn must not be null");
return reindex(fn.apply(new ReindexRequest.Builder()).build());
}
public Mono<DeleteResponse> delete(DeleteRequest request) {
Assert.notNull(request, "request must not be null");
return Mono.fromFuture(transport.performRequestAsync(request, DeleteRequest._ENDPOINT, transportOptions));
}
public Mono<DeleteResponse> delete(Function<DeleteRequest.Builder, ObjectBuilder<DeleteRequest>> fn) {
Assert.notNull(fn, "fn must not be null");
return delete(fn.apply(new DeleteRequest.Builder()).build());
}
// endregion
// region search
public <T> Mono<SearchResponse<T>> search(SearchRequest request, Class<T> tDocumentClass) {
Assert.notNull(request, "request must not be null");
Assert.notNull(tDocumentClass, "tDocumentClass must not be null");
return Mono.fromFuture(transport.performRequestAsync(request,
SearchRequest.createSearchEndpoint(this.getDeserializer(tDocumentClass)), transportOptions));
}
public <T> Mono<SearchResponse<T>> search(Function<SearchRequest.Builder, ObjectBuilder<SearchRequest>> fn,
Class<T> tDocumentClass) {
Assert.notNull(fn, "fn must not be null");
Assert.notNull(tDocumentClass, "tDocumentClass must not be null");
return search(fn.apply(new SearchRequest.Builder()).build(), tDocumentClass);
}
public <T> Mono<ScrollResponse<T>> scroll(ScrollRequest request, Class<T> tDocumentClass) {
Assert.notNull(request, "request must not be null");
Assert.notNull(tDocumentClass, "tDocumentClass must not be null");
// code adapted from
// co.elastic.clients.elasticsearch.ElasticsearchClient.scroll(co.elastic.clients.elasticsearch.core.ScrollRequest,
// java.lang.Class<TDocument>)
// noinspection unchecked
JsonEndpoint<ScrollRequest, ScrollResponse<T>, ErrorResponse> endpoint = (JsonEndpoint<ScrollRequest, ScrollResponse<T>, ErrorResponse>) ScrollRequest._ENDPOINT;
endpoint = new EndpointWithResponseMapperAttr<>(endpoint,
"co.elastic.clients:Deserializer:_global.scroll.TDocument", getDeserializer(tDocumentClass));
return Mono.fromFuture(transport.performRequestAsync(request, endpoint, transportOptions));
}
public <T> Mono<ScrollResponse<T>> scroll(Function<ScrollRequest.Builder, ObjectBuilder<ScrollRequest>> fn,
Class<T> tDocumentClass) {
Assert.notNull(fn, "fn must not be null");
Assert.notNull(tDocumentClass, "tDocumentClass must not be null");
return scroll(fn.apply(new ScrollRequest.Builder()).build(), tDocumentClass);
}
public Mono<ClearScrollResponse> clearScroll(ClearScrollRequest request) {
Assert.notNull(request, "request must not be null");
return Mono.fromFuture(transport.performRequestAsync(request, ClearScrollRequest._ENDPOINT, transportOptions));
}
public Mono<ClearScrollResponse> clearScroll(
Function<ClearScrollRequest.Builder, ObjectBuilder<ClearScrollRequest>> fn) {
Assert.notNull(fn, "fn must not be null");
return clearScroll(fn.apply(new ClearScrollRequest.Builder()).build());
}
// endregion
}

View File

@ -0,0 +1,56 @@
/*
* Copyright 2021-2022 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.ApiClient;
import co.elastic.clients.elasticsearch.cluster.HealthRequest;
import co.elastic.clients.elasticsearch.cluster.HealthResponse;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.TransportOptions;
import co.elastic.clients.util.ObjectBuilder;
import reactor.core.publisher.Mono;
import java.util.function.Function;
import org.springframework.lang.Nullable;
/**
* Reactive version of the {@link co.elastic.clients.elasticsearch.cluster.ElasticsearchClusterClient}
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public class ReactiveElasticsearchClusterClient
extends ApiClient<ElasticsearchTransport, ReactiveElasticsearchClusterClient> {
public ReactiveElasticsearchClusterClient(ElasticsearchTransport transport,
@Nullable TransportOptions transportOptions) {
super(transport, transportOptions);
}
@Override
public ReactiveElasticsearchClusterClient withTransportOptions(@Nullable TransportOptions transportOptions) {
return new ReactiveElasticsearchClusterClient(transport, transportOptions);
}
public Mono<HealthResponse> health(HealthRequest healthRequest) {
return Mono.fromFuture(transport.performRequestAsync(healthRequest, HealthRequest._ENDPOINT, transportOptions));
}
public Mono<HealthResponse> health(Function<HealthRequest.Builder, ObjectBuilder<HealthRequest>> fn) {
return health(fn.apply(new HealthRequest.Builder()).build());
}
}

View File

@ -0,0 +1,97 @@
/*
* Copyright 2021-2022 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.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.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}.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public abstract class ReactiveElasticsearchConfiguration extends ElasticsearchConfigurationSupport {
/**
* Must be implemented by deriving classes to provide the {@link ClientConfiguration}.
*
* @return configuration, must not be {@literal null}
*/
@Bean
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 restClient(ClientConfiguration clientConfiguration) {
Assert.notNull(clientConfiguration, "clientConfiguration must not be null");
return ElasticsearchClients.getRestClient(clientConfiguration);
}
/**
* Provides the {@link ReactiveElasticsearchClient} instance used.
*
* @param restClient the low level RestClient to use
* @return ReactiveElasticsearchClient instance.
*/
@Bean
public ReactiveElasticsearchClient reactiveElasticsearchClient(RestClient restClient) {
Assert.notNull(restClient, "restClient must not be null");
return ElasticsearchClients.createReactive(restClient, transportOptions());
}
/**
* 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;
}
/**
* @return the options that should be added to every request. Must not be {@literal null}
*/
public TransportOptions transportOptions() {
return new RestClientOptions(RequestOptions.DEFAULT).toBuilder().build();
}
}

View File

@ -0,0 +1,629 @@
/*
* Copyright 2021-2022 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.ApiClient;
import co.elastic.clients.elasticsearch.indices.*;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.TransportOptions;
import co.elastic.clients.transport.endpoints.BooleanResponse;
import co.elastic.clients.util.ObjectBuilder;
import reactor.core.publisher.Mono;
import java.util.function.Function;
import org.springframework.lang.Nullable;
/**
* Reactive version of the {@link co.elastic.clients.elasticsearch.indices.ElasticsearchIndicesClient}
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public class ReactiveElasticsearchIndicesClient
extends ApiClient<ElasticsearchTransport, ReactiveElasticsearchIndicesClient> {
public ReactiveElasticsearchIndicesClient(ElasticsearchTransport transport,
@Nullable TransportOptions transportOptions) {
super(transport, transportOptions);
}
@Override
public ReactiveElasticsearchIndicesClient withTransportOptions(@Nullable TransportOptions transportOptions) {
return new ReactiveElasticsearchIndicesClient(transport, transportOptions);
}
public Mono<AddBlockResponse> addBlock(AddBlockRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, AddBlockRequest._ENDPOINT, transportOptions));
}
public Mono<AddBlockResponse> addBlock(Function<AddBlockRequest.Builder, ObjectBuilder<AddBlockRequest>> fn) {
return addBlock(fn.apply(new AddBlockRequest.Builder()).build());
}
public Mono<AnalyzeResponse> analyze(AnalyzeRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, AnalyzeRequest._ENDPOINT, transportOptions));
}
public Mono<AnalyzeResponse> analyze(Function<AnalyzeRequest.Builder, ObjectBuilder<AnalyzeRequest>> fn) {
return analyze(fn.apply(new AnalyzeRequest.Builder()).build());
}
public Mono<AnalyzeResponse> analyze() {
return analyze(builder -> builder);
}
public Mono<ClearCacheResponse> clearCache(ClearCacheRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, ClearCacheRequest._ENDPOINT, transportOptions));
}
public Mono<ClearCacheResponse> clearCache(Function<ClearCacheRequest.Builder, ObjectBuilder<ClearCacheRequest>> fn) {
return clearCache(fn.apply(new ClearCacheRequest.Builder()).build());
}
public Mono<ClearCacheResponse> clearCache() {
return clearCache(builder -> builder);
}
public Mono<CloneIndexResponse> clone(CloneIndexRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, CloneIndexRequest._ENDPOINT, transportOptions));
}
public Mono<CloneIndexResponse> clone(Function<CloneIndexRequest.Builder, ObjectBuilder<CloneIndexRequest>> fn) {
return clone(fn.apply(new CloneIndexRequest.Builder()).build());
}
public Mono<CloseIndexResponse> close(CloseIndexRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, CloseIndexRequest._ENDPOINT, transportOptions));
}
public Mono<CloseIndexResponse> close(Function<CloseIndexRequest.Builder, ObjectBuilder<CloseIndexRequest>> fn) {
return close(fn.apply(new CloseIndexRequest.Builder()).build());
}
public Mono<CreateIndexResponse> create(CreateIndexRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, CreateIndexRequest._ENDPOINT, transportOptions));
}
public Mono<CreateIndexResponse> create(Function<CreateIndexRequest.Builder, ObjectBuilder<CreateIndexRequest>> fn) {
return create(fn.apply(new CreateIndexRequest.Builder()).build());
}
public Mono<CreateDataStreamResponse> createDataStream(CreateDataStreamRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, CreateDataStreamRequest._ENDPOINT, transportOptions));
}
public Mono<CreateDataStreamResponse> createDataStream(
Function<CreateDataStreamRequest.Builder, ObjectBuilder<CreateDataStreamRequest>> fn) {
return createDataStream(fn.apply(new CreateDataStreamRequest.Builder()).build());
}
public Mono<DataStreamsStatsResponse> dataStreamsStats(DataStreamsStatsRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, DataStreamsStatsRequest._ENDPOINT, transportOptions));
}
public Mono<DataStreamsStatsResponse> dataStreamsStats(
Function<DataStreamsStatsRequest.Builder, ObjectBuilder<DataStreamsStatsRequest>> fn) {
return dataStreamsStats(fn.apply(new DataStreamsStatsRequest.Builder()).build());
}
public Mono<DataStreamsStatsResponse> dataStreamsStats() {
return dataStreamsStats(builder -> builder);
}
public Mono<DeleteIndexResponse> delete(DeleteIndexRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, DeleteIndexRequest._ENDPOINT, transportOptions));
}
public Mono<DeleteIndexResponse> delete(Function<DeleteIndexRequest.Builder, ObjectBuilder<DeleteIndexRequest>> fn) {
return delete(fn.apply(new DeleteIndexRequest.Builder()).build());
}
public Mono<DeleteAliasResponse> deleteAlias(DeleteAliasRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, DeleteAliasRequest._ENDPOINT, transportOptions));
}
public Mono<DeleteAliasResponse> deleteAlias(
Function<DeleteAliasRequest.Builder, ObjectBuilder<DeleteAliasRequest>> fn) {
return deleteAlias(fn.apply(new DeleteAliasRequest.Builder()).build());
}
public Mono<DeleteDataStreamResponse> deleteDataStream(DeleteDataStreamRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, DeleteDataStreamRequest._ENDPOINT, transportOptions));
}
public Mono<DeleteDataStreamResponse> deleteDataStream(
Function<DeleteDataStreamRequest.Builder, ObjectBuilder<DeleteDataStreamRequest>> fn) {
return deleteDataStream(fn.apply(new DeleteDataStreamRequest.Builder()).build());
}
public Mono<DeleteIndexTemplateResponse> deleteIndexTemplate(DeleteIndexTemplateRequest request) {
return Mono
.fromFuture(transport.performRequestAsync(request, DeleteIndexTemplateRequest._ENDPOINT, transportOptions));
}
public Mono<DeleteIndexTemplateResponse> deleteIndexTemplate(
Function<DeleteIndexTemplateRequest.Builder, ObjectBuilder<DeleteIndexTemplateRequest>> fn) {
return deleteIndexTemplate(fn.apply(new DeleteIndexTemplateRequest.Builder()).build());
}
public Mono<DeleteTemplateResponse> deleteTemplate(DeleteTemplateRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, DeleteTemplateRequest._ENDPOINT, transportOptions));
}
public Mono<DeleteTemplateResponse> deleteTemplate(
Function<DeleteTemplateRequest.Builder, ObjectBuilder<DeleteTemplateRequest>> fn) {
return deleteTemplate(fn.apply(new DeleteTemplateRequest.Builder()).build());
}
public Mono<DiskUsageResponse> diskUsage(DiskUsageRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, DiskUsageRequest._ENDPOINT, transportOptions));
}
public Mono<DiskUsageResponse> diskUsage(Function<DiskUsageRequest.Builder, ObjectBuilder<DiskUsageRequest>> fn) {
return diskUsage(fn.apply(new DiskUsageRequest.Builder()).build());
}
public Mono<BooleanResponse> exists(ExistsRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, ExistsRequest._ENDPOINT, transportOptions));
}
public Mono<BooleanResponse> exists(Function<ExistsRequest.Builder, ObjectBuilder<ExistsRequest>> fn) {
return exists(fn.apply(new ExistsRequest.Builder()).build());
}
public Mono<BooleanResponse> existsAlias(ExistsAliasRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, ExistsAliasRequest._ENDPOINT, transportOptions));
}
public Mono<BooleanResponse> existsAlias(Function<ExistsAliasRequest.Builder, ObjectBuilder<ExistsAliasRequest>> fn) {
return existsAlias(fn.apply(new ExistsAliasRequest.Builder()).build());
}
public Mono<BooleanResponse> existsIndexTemplate(ExistsIndexTemplateRequest request) {
return Mono
.fromFuture(transport.performRequestAsync(request, ExistsIndexTemplateRequest._ENDPOINT, transportOptions));
}
public Mono<BooleanResponse> existsIndexTemplate(
Function<ExistsIndexTemplateRequest.Builder, ObjectBuilder<ExistsIndexTemplateRequest>> fn) {
return existsIndexTemplate(fn.apply(new ExistsIndexTemplateRequest.Builder()).build());
}
public Mono<BooleanResponse> existsTemplate(ExistsTemplateRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, ExistsTemplateRequest._ENDPOINT, transportOptions));
}
public Mono<BooleanResponse> existsTemplate(
Function<ExistsTemplateRequest.Builder, ObjectBuilder<ExistsTemplateRequest>> fn) {
return existsTemplate(fn.apply(new ExistsTemplateRequest.Builder()).build());
}
public Mono<BooleanResponse> existsType(ExistsTypeRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, ExistsTypeRequest._ENDPOINT, transportOptions));
}
public Mono<BooleanResponse> existsType(Function<ExistsTypeRequest.Builder, ObjectBuilder<ExistsTypeRequest>> fn) {
return existsType(fn.apply(new ExistsTypeRequest.Builder()).build());
}
public Mono<FlushResponse> flush(FlushRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, FlushRequest._ENDPOINT, transportOptions));
}
public Mono<FlushResponse> flush(Function<FlushRequest.Builder, ObjectBuilder<FlushRequest>> fn) {
return flush(fn.apply(new FlushRequest.Builder()).build());
}
public Mono<FlushResponse> flush() {
return flush(builder -> builder);
}
public Mono<FlushSyncedResponse> flushSynced(FlushSyncedRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, FlushSyncedRequest._ENDPOINT, transportOptions));
}
public Mono<FlushSyncedResponse> flushSynced(
Function<FlushSyncedRequest.Builder, ObjectBuilder<FlushSyncedRequest>> fn) {
return flushSynced(fn.apply(new FlushSyncedRequest.Builder()).build());
}
public Mono<FlushSyncedResponse> flushSynced() {
return flushSynced(builder -> builder);
}
@SuppressWarnings("SpellCheckingInspection")
public Mono<ForcemergeResponse> forcemerge(ForcemergeRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, ForcemergeRequest._ENDPOINT, transportOptions));
}
@SuppressWarnings("SpellCheckingInspection")
public Mono<ForcemergeResponse> forcemerge(Function<ForcemergeRequest.Builder, ObjectBuilder<ForcemergeRequest>> fn) {
return forcemerge(fn.apply(new ForcemergeRequest.Builder()).build());
}
@SuppressWarnings("SpellCheckingInspection")
public Mono<ForcemergeResponse> forcemerge() {
return forcemerge(builder -> builder);
}
public Mono<FreezeResponse> freeze(FreezeRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, FreezeRequest._ENDPOINT, transportOptions));
}
public Mono<FreezeResponse> freeze(Function<FreezeRequest.Builder, ObjectBuilder<FreezeRequest>> fn) {
return freeze(fn.apply(new FreezeRequest.Builder()).build());
}
public Mono<GetIndexResponse> get(GetIndexRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, GetIndexRequest._ENDPOINT, transportOptions));
}
public Mono<GetIndexResponse> get(Function<GetIndexRequest.Builder, ObjectBuilder<GetIndexRequest>> fn) {
return get(fn.apply(new GetIndexRequest.Builder()).build());
}
public Mono<GetAliasResponse> getAlias(GetAliasRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, GetAliasRequest._ENDPOINT, transportOptions));
}
public Mono<GetAliasResponse> getAlias(Function<GetAliasRequest.Builder, ObjectBuilder<GetAliasRequest>> fn) {
return getAlias(fn.apply(new GetAliasRequest.Builder()).build());
}
public Mono<GetAliasResponse> getAlias() {
return getAlias(builder -> builder);
}
public Mono<GetDataStreamResponse> getDataStream(GetDataStreamRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, GetDataStreamRequest._ENDPOINT, transportOptions));
}
public Mono<GetDataStreamResponse> getDataStream(
Function<GetDataStreamRequest.Builder, ObjectBuilder<GetDataStreamRequest>> fn) {
return getDataStream(fn.apply(new GetDataStreamRequest.Builder()).build());
}
public Mono<GetDataStreamResponse> getDataStream() {
return getDataStream(builder -> builder);
}
public Mono<GetFieldMappingResponse> getFieldMapping(GetFieldMappingRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, GetFieldMappingRequest._ENDPOINT, transportOptions));
}
public Mono<GetFieldMappingResponse> getFieldMapping(
Function<GetFieldMappingRequest.Builder, ObjectBuilder<GetFieldMappingRequest>> fn) {
return getFieldMapping(fn.apply(new GetFieldMappingRequest.Builder()).build());
}
public Mono<GetIndexTemplateResponse> getIndexTemplate(GetIndexTemplateRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, GetIndexTemplateRequest._ENDPOINT, transportOptions));
}
public Mono<GetIndexTemplateResponse> getIndexTemplate(
Function<GetIndexTemplateRequest.Builder, ObjectBuilder<GetIndexTemplateRequest>> fn) {
return getIndexTemplate(fn.apply(new GetIndexTemplateRequest.Builder()).build());
}
public Mono<GetIndexTemplateResponse> getIndexTemplate() {
return getIndexTemplate(builder -> builder);
}
public Mono<GetMappingResponse> getMapping(GetMappingRequest getMappingRequest) {
return Mono
.fromFuture(transport.performRequestAsync(getMappingRequest, GetMappingRequest._ENDPOINT, transportOptions));
}
public Mono<GetMappingResponse> getMapping(Function<GetMappingRequest.Builder, ObjectBuilder<GetMappingRequest>> fn) {
return getMapping(fn.apply(new GetMappingRequest.Builder()).build());
}
public Mono<GetMappingResponse> getMapping() {
return getMapping(builder -> builder);
}
public Mono<GetIndicesSettingsResponse> getSettings(GetIndicesSettingsRequest request) {
return Mono
.fromFuture(transport.performRequestAsync(request, GetIndicesSettingsRequest._ENDPOINT, transportOptions));
}
public Mono<GetIndicesSettingsResponse> getSettings(
Function<GetIndicesSettingsRequest.Builder, ObjectBuilder<GetIndicesSettingsRequest>> fn) {
return getSettings(fn.apply(new GetIndicesSettingsRequest.Builder()).build());
}
public Mono<GetIndicesSettingsResponse> getSettings() {
return getSettings(builder -> builder);
}
public Mono<GetTemplateResponse> getTemplate(GetTemplateRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, GetTemplateRequest._ENDPOINT, transportOptions));
}
public Mono<GetTemplateResponse> getTemplate(
Function<GetTemplateRequest.Builder, ObjectBuilder<GetTemplateRequest>> fn) {
return getTemplate(fn.apply(new GetTemplateRequest.Builder()).build());
}
public Mono<GetTemplateResponse> getTemplate() {
return getTemplate(builder -> builder);
}
public Mono<GetUpgradeResponse> getUpgrade(GetUpgradeRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, GetUpgradeRequest._ENDPOINT, transportOptions));
}
public Mono<GetUpgradeResponse> getUpgrade(Function<GetUpgradeRequest.Builder, ObjectBuilder<GetUpgradeRequest>> fn) {
return getUpgrade(fn.apply(new GetUpgradeRequest.Builder()).build());
}
public Mono<GetUpgradeResponse> getUpgrade() {
return getUpgrade(builder -> builder);
}
public Mono<MigrateToDataStreamResponse> migrateToDataStream(MigrateToDataStreamRequest request) {
return Mono
.fromFuture(transport.performRequestAsync(request, MigrateToDataStreamRequest._ENDPOINT, transportOptions));
}
public Mono<MigrateToDataStreamResponse> migrateToDataStream(
Function<MigrateToDataStreamRequest.Builder, ObjectBuilder<MigrateToDataStreamRequest>> fn) {
return migrateToDataStream(fn.apply(new MigrateToDataStreamRequest.Builder()).build());
}
public Mono<OpenResponse> open(OpenRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, OpenRequest._ENDPOINT, transportOptions));
}
public Mono<OpenResponse> open(Function<OpenRequest.Builder, ObjectBuilder<OpenRequest>> fn) {
return open(fn.apply(new OpenRequest.Builder()).build());
}
public Mono<PromoteDataStreamResponse> promoteDataStream(PromoteDataStreamRequest request) {
return Mono
.fromFuture(transport.performRequestAsync(request, PromoteDataStreamRequest._ENDPOINT, transportOptions));
}
public Mono<PromoteDataStreamResponse> promoteDataStream(
Function<PromoteDataStreamRequest.Builder, ObjectBuilder<PromoteDataStreamRequest>> fn) {
return promoteDataStream(fn.apply(new PromoteDataStreamRequest.Builder()).build());
}
public Mono<PutAliasResponse> putAlias(PutAliasRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, PutAliasRequest._ENDPOINT, transportOptions));
}
public Mono<PutAliasResponse> putAlias(Function<PutAliasRequest.Builder, ObjectBuilder<PutAliasRequest>> fn) {
return putAlias(fn.apply(new PutAliasRequest.Builder()).build());
}
public Mono<PutIndexTemplateResponse> putIndexTemplate(PutIndexTemplateRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, PutIndexTemplateRequest._ENDPOINT, transportOptions));
}
public Mono<PutIndexTemplateResponse> putIndexTemplate(
Function<PutIndexTemplateRequest.Builder, ObjectBuilder<PutIndexTemplateRequest>> fn) {
return putIndexTemplate(fn.apply(new PutIndexTemplateRequest.Builder()).build());
}
public Mono<PutMappingResponse> putMapping(PutMappingRequest putMappingRequest) {
return Mono
.fromFuture(transport.performRequestAsync(putMappingRequest, PutMappingRequest._ENDPOINT, transportOptions));
}
public Mono<PutMappingResponse> putMapping(Function<PutMappingRequest.Builder, ObjectBuilder<PutMappingRequest>> fn) {
return putMapping(fn.apply(new PutMappingRequest.Builder()).build());
}
public Mono<PutIndicesSettingsResponse> putSettings(PutIndicesSettingsRequest request) {
return Mono
.fromFuture(transport.performRequestAsync(request, PutIndicesSettingsRequest._ENDPOINT, transportOptions));
}
public Mono<PutIndicesSettingsResponse> putSettings(
Function<PutIndicesSettingsRequest.Builder, ObjectBuilder<PutIndicesSettingsRequest>> fn) {
return putSettings(fn.apply(new PutIndicesSettingsRequest.Builder()).build());
}
public Mono<PutIndicesSettingsResponse> putSettings() {
return putSettings(builder -> builder);
}
public Mono<PutTemplateResponse> putTemplate(PutTemplateRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, PutTemplateRequest._ENDPOINT, transportOptions));
}
public Mono<PutTemplateResponse> putTemplate(
Function<PutTemplateRequest.Builder, ObjectBuilder<PutTemplateRequest>> fn) {
return putTemplate(fn.apply(new PutTemplateRequest.Builder()).build());
}
public Mono<RecoveryResponse> recovery(RecoveryRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, RecoveryRequest._ENDPOINT, transportOptions));
}
public Mono<RecoveryResponse> recovery(Function<RecoveryRequest.Builder, ObjectBuilder<RecoveryRequest>> fn) {
return recovery(fn.apply(new RecoveryRequest.Builder()).build());
}
public Mono<RecoveryResponse> recovery() {
return recovery(builder -> builder);
}
public Mono<RefreshResponse> refresh(RefreshRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, RefreshRequest._ENDPOINT, transportOptions));
}
public Mono<RefreshResponse> refresh(Function<RefreshRequest.Builder, ObjectBuilder<RefreshRequest>> fn) {
return refresh(fn.apply(new RefreshRequest.Builder()).build());
}
public Mono<RefreshResponse> refresh() {
return refresh(builder -> builder);
}
public Mono<ReloadSearchAnalyzersResponse> reloadSearchAnalyzers(ReloadSearchAnalyzersRequest request) {
return Mono
.fromFuture(transport.performRequestAsync(request, ReloadSearchAnalyzersRequest._ENDPOINT, transportOptions));
}
public Mono<ReloadSearchAnalyzersResponse> reloadSearchAnalyzers(
Function<ReloadSearchAnalyzersRequest.Builder, ObjectBuilder<ReloadSearchAnalyzersRequest>> fn) {
return reloadSearchAnalyzers(fn.apply(new ReloadSearchAnalyzersRequest.Builder()).build());
}
public Mono<ResolveIndexResponse> resolveIndex(ResolveIndexRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, ResolveIndexRequest._ENDPOINT, transportOptions));
}
public Mono<ResolveIndexResponse> resolveIndex(
Function<ResolveIndexRequest.Builder, ObjectBuilder<ResolveIndexRequest>> fn) {
return resolveIndex(fn.apply(new ResolveIndexRequest.Builder()).build());
}
public Mono<RolloverResponse> rollover(RolloverRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, RolloverRequest._ENDPOINT, transportOptions));
}
public Mono<RolloverResponse> rollover(Function<RolloverRequest.Builder, ObjectBuilder<RolloverRequest>> fn) {
return rollover(fn.apply(new RolloverRequest.Builder()).build());
}
public Mono<SegmentsResponse> segments(SegmentsRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, SegmentsRequest._ENDPOINT, transportOptions));
}
public Mono<SegmentsResponse> segments(Function<SegmentsRequest.Builder, ObjectBuilder<SegmentsRequest>> fn) {
return segments(fn.apply(new SegmentsRequest.Builder()).build());
}
public Mono<SegmentsResponse> segments() {
return segments(builder -> builder);
}
public Mono<ShardStoresResponse> shardStores(ShardStoresRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, ShardStoresRequest._ENDPOINT, transportOptions));
}
public Mono<ShardStoresResponse> shardStores(
Function<ShardStoresRequest.Builder, ObjectBuilder<ShardStoresRequest>> fn) {
return shardStores(fn.apply(new ShardStoresRequest.Builder()).build());
}
public Mono<ShardStoresResponse> shardStores() {
return shardStores(builder -> builder);
}
public Mono<ShrinkResponse> shrink(ShrinkRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, ShrinkRequest._ENDPOINT, transportOptions));
}
public Mono<ShrinkResponse> shrink(Function<ShrinkRequest.Builder, ObjectBuilder<ShrinkRequest>> fn) {
return shrink(fn.apply(new ShrinkRequest.Builder()).build());
}
public Mono<SimulateIndexTemplateResponse> simulateIndexTemplate(SimulateIndexTemplateRequest request) {
return Mono
.fromFuture(transport.performRequestAsync(request, SimulateIndexTemplateRequest._ENDPOINT, transportOptions));
}
public Mono<SimulateIndexTemplateResponse> simulateIndexTemplate(
Function<SimulateIndexTemplateRequest.Builder, ObjectBuilder<SimulateIndexTemplateRequest>> fn) {
return simulateIndexTemplate(fn.apply(new SimulateIndexTemplateRequest.Builder()).build());
}
public Mono<SimulateTemplateResponse> simulateTemplate(SimulateTemplateRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, SimulateTemplateRequest._ENDPOINT, transportOptions));
}
public Mono<SimulateTemplateResponse> simulateTemplate(
Function<SimulateTemplateRequest.Builder, ObjectBuilder<SimulateTemplateRequest>> fn) {
return simulateTemplate(fn.apply(new SimulateTemplateRequest.Builder()).build());
}
public Mono<SimulateTemplateResponse> simulateTemplate() {
return simulateTemplate(builder -> builder);
}
public Mono<SplitResponse> split(SplitRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, SplitRequest._ENDPOINT, transportOptions));
}
public Mono<SplitResponse> split(Function<SplitRequest.Builder, ObjectBuilder<SplitRequest>> fn) {
return split(fn.apply(new SplitRequest.Builder()).build());
}
public Mono<IndicesStatsResponse> stats(IndicesStatsRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, IndicesStatsRequest._ENDPOINT, transportOptions));
}
public Mono<IndicesStatsResponse> stats(
Function<IndicesStatsRequest.Builder, ObjectBuilder<IndicesStatsRequest>> fn) {
return stats(fn.apply(new IndicesStatsRequest.Builder()).build());
}
public Mono<IndicesStatsResponse> stats() {
return stats(builder -> builder);
}
public Mono<UnfreezeResponse> unfreeze(UnfreezeRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, UnfreezeRequest._ENDPOINT, transportOptions));
}
public Mono<UnfreezeResponse> unfreeze(Function<UnfreezeRequest.Builder, ObjectBuilder<UnfreezeRequest>> fn) {
return unfreeze(fn.apply(new UnfreezeRequest.Builder()).build());
}
public Mono<UpdateAliasesResponse> updateAliases(UpdateAliasesRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, UpdateAliasesRequest._ENDPOINT, transportOptions));
}
public Mono<UpdateAliasesResponse> updateAliases(
Function<UpdateAliasesRequest.Builder, ObjectBuilder<UpdateAliasesRequest>> fn) {
return updateAliases(fn.apply(new UpdateAliasesRequest.Builder()).build());
}
public Mono<UpdateAliasesResponse> updateAliases() {
return updateAliases(builder -> builder);
}
public Mono<UpgradeResponse> upgrade(UpgradeRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, UpgradeRequest._ENDPOINT, transportOptions));
}
public Mono<UpgradeResponse> upgrade(Function<UpgradeRequest.Builder, ObjectBuilder<UpgradeRequest>> fn) {
return upgrade(fn.apply(new UpgradeRequest.Builder()).build());
}
public Mono<UpgradeResponse> upgrade() {
return upgrade(builder -> builder);
}
public Mono<ValidateQueryResponse> validateQuery(ValidateQueryRequest request) {
return Mono.fromFuture(transport.performRequestAsync(request, ValidateQueryRequest._ENDPOINT, transportOptions));
}
public Mono<ValidateQueryResponse> validateQuery(
Function<ValidateQueryRequest.Builder, ObjectBuilder<ValidateQueryRequest>> fn) {
return validateQuery(fn.apply(new ValidateQueryRequest.Builder()).build());
}
public Mono<ValidateQueryResponse> validateQuery() {
return validateQuery(builder -> builder);
}
}

View File

@ -0,0 +1,480 @@
/*
* Copyright 2021-2022 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._types.Result;
import co.elastic.clients.elasticsearch._types.Time;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.transport.Version;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.reactivestreams.Publisher;
import org.springframework.data.elasticsearch.BulkFailureException;
import org.springframework.data.elasticsearch.NoSuchIndexException;
import org.springframework.data.elasticsearch.UncategorizedElasticsearchException;
import org.springframework.data.elasticsearch.client.UnsupportedBackendOperation;
import org.springframework.data.elasticsearch.client.util.ScrollState;
import org.springframework.data.elasticsearch.core.AbstractReactiveElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.AggregationContainer;
import org.springframework.data.elasticsearch.core.IndexedObjectInformation;
import org.springframework.data.elasticsearch.core.MultiGetItem;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.core.ReactiveIndexOperations;
import org.springframework.data.elasticsearch.core.cluster.ReactiveClusterOperations;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.document.SearchDocument;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.BulkOptions;
import org.springframework.data.elasticsearch.core.query.ByQueryResponse;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
import org.springframework.data.elasticsearch.core.query.UpdateResponse;
import org.springframework.data.elasticsearch.core.reindex.ReindexRequest;
import org.springframework.data.elasticsearch.core.reindex.ReindexResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* Implementation of {@link org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations} using the new
* Elasticsearch client.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearchTemplate {
private final ReactiveElasticsearchClient client;
private final RequestConverter requestConverter;
private final ResponseConverter responseConverter;
private final JsonpMapper jsonpMapper;
private final ElasticsearchExceptionTranslator exceptionTranslator;
protected ReactiveElasticsearchTemplate(ReactiveElasticsearchClient client, ElasticsearchConverter converter) {
super(converter);
Assert.notNull(client, "client must not be null");
this.client = client;
this.jsonpMapper = client._transport().jsonpMapper();
requestConverter = new RequestConverter(converter, jsonpMapper);
responseConverter = new ResponseConverter(jsonpMapper);
exceptionTranslator = new ElasticsearchExceptionTranslator(jsonpMapper);
}
// region Document operations
@Override
protected <T> Mono<Tuple2<T, IndexResponseMetaData>> doIndex(T entity, IndexCoordinates index) {
IndexRequest<?> indexRequest = requestConverter.documentIndexRequest(getIndexQuery(entity), index,
getRefreshPolicy());
return Mono.just(entity) //
.zipWith(//
Mono.from(execute((ClientCallback<Publisher<IndexResponse>>) client -> client.index(indexRequest))) //
.map(indexResponse -> new IndexResponseMetaData(indexResponse.id(), //
indexResponse.seqNo(), //
indexResponse.primaryTerm(), //
indexResponse.version() //
)));
}
@Override
public <T> Mono<T> get(String id, Class<T> entityType, IndexCoordinates index) {
Assert.notNull(id, "id must not be null");
Assert.notNull(entityType, "entityType must not be null");
Assert.notNull(index, "index must not be null");
GetRequest getRequest = requestConverter.documentGetRequest(id, routingResolver.getRouting(), index, false);
Mono<GetResponse<EntityAsMap>> getResponse = Mono.from(execute(
(ClientCallback<Publisher<GetResponse<EntityAsMap>>>) client -> client.get(getRequest, EntityAsMap.class)));
ReadDocumentCallback<T> callback = new ReadDocumentCallback<>(converter, entityType, index);
return getResponse.flatMap(response -> callback.toEntity(DocumentAdapters.from(response)));
}
@Override
public Mono<ReindexResponse> reindex(ReindexRequest reindexRequest) {
Assert.notNull(reindexRequest, "reindexRequest must not be null");
co.elastic.clients.elasticsearch.core.ReindexRequest reindexRequestES = requestConverter.reindex(reindexRequest,
true);
return Mono.from(execute( //
(ClientCallback<Publisher<co.elastic.clients.elasticsearch.core.ReindexResponse>>) client -> client
.reindex(reindexRequestES)))
.map(responseConverter::reindexResponse);
}
@Override
public Mono<String> submitReindex(ReindexRequest reindexRequest) {
Assert.notNull(reindexRequest, "reindexRequest must not be null");
co.elastic.clients.elasticsearch.core.ReindexRequest reindexRequestES = requestConverter.reindex(reindexRequest,
false);
return Mono.from(execute( //
(ClientCallback<Publisher<co.elastic.clients.elasticsearch.core.ReindexResponse>>) client -> client
.reindex(reindexRequestES)))
.flatMap(response -> (response.task() == null)
? Mono.error( // todo #1973 check behaviour and create issue in ES if necessary
new UnsupportedBackendOperation("ElasticsearchClient did not return a task id on submit request"))
: Mono.just(response.task()));
}
@Override
public Mono<Void> bulkUpdate(List<UpdateQuery> queries, BulkOptions bulkOptions, IndexCoordinates index) {
Assert.notNull(queries, "List of UpdateQuery must not be null");
Assert.notNull(bulkOptions, "BulkOptions must not be null");
Assert.notNull(index, "Index must not be null");
return doBulkOperation(queries, bulkOptions, index).then();
}
private Flux<BulkResponseItem> doBulkOperation(List<?> queries, BulkOptions bulkOptions, IndexCoordinates index) {
BulkRequest bulkRequest = requestConverter.documentBulkRequest(queries, bulkOptions, index, getRefreshPolicy());
return client.bulk(bulkRequest)
.onErrorMap(e -> new UncategorizedElasticsearchException("Error executing bulk request", e))
.flatMap(this::checkForBulkOperationFailure) //
.flatMapMany(response -> Flux.fromIterable(response.items()));
}
private <R> Mono<BulkResponse> checkForBulkOperationFailure(BulkResponse bulkResponse) {
if (bulkResponse.errors()) {
Map<String, String> failedDocuments = new HashMap<>();
for (BulkResponseItem item : bulkResponse.items()) {
if (item.error() != null) {
failedDocuments.put(item.id(), item.error().reason());
}
}
BulkFailureException exception = new BulkFailureException(
"Bulk operation has failures. Use ElasticsearchException.getFailedDocuments() for detailed messages ["
+ failedDocuments + ']',
failedDocuments);
return Mono.error(exception);
} else {
return Mono.just(bulkResponse);
}
}
@Override
protected Mono<String> doDeleteById(String id, @Nullable String routing, IndexCoordinates index) {
Assert.notNull(id, "id must not be null");
Assert.notNull(index, "index must not be null");
return Mono.defer(() -> {
DeleteRequest deleteRequest = requestConverter.documentDeleteRequest(id, routing, index, getRefreshPolicy());
return doDelete(deleteRequest);
});
}
private Mono<String> doDelete(DeleteRequest request) {
return Mono.from(execute((ClientCallback<Publisher<DeleteResponse>>) client -> client.delete(request))) //
.flatMap(deleteResponse -> {
if (deleteResponse.result() == Result.NotFound) {
return Mono.empty();
}
return Mono.just(deleteResponse.id());
}).onErrorResume(NoSuchIndexException.class, it -> Mono.empty());
}
// endregion
@Override
protected ReactiveElasticsearchTemplate doCopy() {
return new ReactiveElasticsearchTemplate(client, converter);
}
@Override
protected Mono<Boolean> doExists(String id, IndexCoordinates index) {
throw new UnsupportedOperationException("not implemented");
}
// region search operations
@Override
protected Flux<SearchDocument> doFind(Query query, Class<?> clazz, IndexCoordinates index) {
return Flux.defer(() -> {
boolean useScroll = !(query.getPageable().isPaged() || query.isLimiting());
SearchRequest searchRequest = requestConverter.searchRequest(query, clazz, index, false, useScroll);
if (useScroll) {
return doScroll(searchRequest);
} else {
return doFind(searchRequest);
}
});
}
private Flux<SearchDocument> doScroll(SearchRequest searchRequest) {
Time scrollTimeout = searchRequest.scroll() != null ? searchRequest.scroll() : Time.of(t -> t.time("1m"));
Flux<SearchResponse<EntityAsMap>> searchResponses = Flux.usingWhen(Mono.fromSupplier(ScrollState::new), //
state -> Mono
.from(execute((ClientCallback<Publisher<SearchResponse<EntityAsMap>>>) client -> client
.search(searchRequest, EntityAsMap.class))) //
.expand(entityAsMapSearchResponse -> {
state.updateScrollId(entityAsMapSearchResponse.scrollId());
if (entityAsMapSearchResponse.hits() == null
|| CollectionUtils.isEmpty(entityAsMapSearchResponse.hits().hits())) {
return Mono.empty();
}
return Mono.from(execute((ClientCallback<Publisher<ScrollResponse<EntityAsMap>>>) client -> client.scroll(
ScrollRequest.of(sr -> sr.scrollId(state.getScrollId()).scroll(scrollTimeout)), EntityAsMap.class)));
}),
this::cleanupScroll, (state, ex) -> cleanupScroll(state), this::cleanupScroll);
return searchResponses.flatMapIterable(entityAsMapSearchResponse -> entityAsMapSearchResponse.hits().hits())
.map(entityAsMapHit -> DocumentAdapters.from(entityAsMapHit, jsonpMapper));
}
private Publisher<?> cleanupScroll(ScrollState state) {
return execute((ClientCallback<Publisher<ClearScrollResponse>>) client -> client
.clearScroll(ClearScrollRequest.of(csr -> csr.scrollId(state.getScrollIds()))));
}
@Override
protected Mono<Long> doCount(Query query, Class<?> entityType, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
Assert.notNull(index, "index must not be null");
SearchRequest searchRequest = requestConverter.searchRequest(query, entityType, index, true, false);
return Mono
.from(execute((ClientCallback<Publisher<SearchResponse<EntityAsMap>>>) client -> client.search(searchRequest,
EntityAsMap.class)))
.map(searchResponse -> searchResponse.hits().total() != null ? searchResponse.hits().total().value() : 0L);
}
private Flux<SearchDocument> doFind(SearchRequest searchRequest) {
return Mono
.from(execute((ClientCallback<Publisher<SearchResponse<EntityAsMap>>>) client -> client.search(searchRequest,
EntityAsMap.class))) //
.flatMapIterable(entityAsMapSearchResponse -> entityAsMapSearchResponse.hits().hits()) //
.map(entityAsMapHit -> DocumentAdapters.from(entityAsMapHit, jsonpMapper));
}
@Override
protected <T> Mono<SearchDocumentResponse> doFindForResponse(Query query, Class<?> clazz, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
Assert.notNull(index, "index must not be null");
SearchRequest searchRequest = requestConverter.searchRequest(query, clazz, index, false, false);
// noinspection unchecked
SearchDocumentCallback<T> callback = new ReadSearchDocumentCallback<T>((Class<T>) clazz, index);
SearchDocumentResponse.EntityCreator<T> entityCreator = searchDocument -> callback.toEntity(searchDocument)
.toFuture();
return Mono
.from(execute((ClientCallback<Publisher<SearchResponse<EntityAsMap>>>) client -> client.search(searchRequest,
EntityAsMap.class)))
.map(searchResponse -> SearchDocumentResponseBuilder.from(searchResponse, entityCreator, jsonpMapper));
}
// endregion
@Override
protected Mono<String> getVendor() {
return Mono.just("Elasticsearch");
}
@Override
protected Mono<String> getRuntimeLibraryVersion() {
return Mono.just(Version.VERSION.toString());
}
@Override
protected Mono<String> getClusterVersion() {
return Mono.from(execute(ReactiveElasticsearchClient::info)).map(infoResponse -> infoResponse.version().number());
}
@Override
public <T> Flux<T> saveAll(Mono<? extends Collection<? extends T>> entitiesPublisher, IndexCoordinates index) {
Assert.notNull(entitiesPublisher, "entitiesPublisher must not be null!");
return entitiesPublisher //
.flatMapMany(entities -> Flux.fromIterable(entities) //
.concatMap(entity -> maybeCallBeforeConvert(entity, index)) //
).collectList() //
.map(Entities::new) //
.flatMapMany(entities -> {
if (entities.isEmpty()) {
return Flux.empty();
}
return doBulkOperation(entities.indexQueries(), BulkOptions.defaultOptions(), index)//
.index() //
.flatMap(indexAndResponse -> {
T savedEntity = entities.entityAt(indexAndResponse.getT1());
BulkResponseItem response = indexAndResponse.getT2();
updateIndexedObject(savedEntity, IndexedObjectInformation.of(response.id(), response.seqNo(),
response.primaryTerm(), response.version()));
return maybeCallAfterSave(savedEntity, index);
});
});
}
@Override
public <T> Flux<MultiGetItem<T>> multiGet(Query query, Class<T> clazz, IndexCoordinates index) {
throw new UnsupportedOperationException("not implemented");
}
@Override
public Mono<ByQueryResponse> delete(Query query, Class<?> entityType, IndexCoordinates index) {
throw new UnsupportedOperationException("not implemented");
}
@Override
public Mono<UpdateResponse> update(UpdateQuery updateQuery, IndexCoordinates index) {
throw new UnsupportedOperationException("not implemented");
}
@Override
public Mono<ByQueryResponse> updateByQuery(UpdateQuery updateQuery, IndexCoordinates index) {
throw new UnsupportedOperationException("not implemented");
}
@Override
@Deprecated
public <T> Publisher<T> execute(ReactiveElasticsearchOperations.ClientCallback<Publisher<T>> callback) {
throw new UnsupportedBackendOperation("direct execution on the WebClient is not supported for this class");
}
@Override
public <T> Publisher<T> executeWithIndicesClient(IndicesClientCallback<Publisher<T>> callback) {
throw new UnsupportedOperationException("not implemented");
}
@Override
public <T> Publisher<T> executeWithClusterClient(ClusterClientCallback<Publisher<T>> callback) {
throw new UnsupportedOperationException("not implemented");
}
@Override
public ReactiveIndexOperations indexOps(IndexCoordinates index) {
return new ReactiveIndicesTemplate(client.indices(), converter, index);
}
@Override
public ReactiveIndexOperations indexOps(Class<?> clazz) {
return new ReactiveIndicesTemplate(client.indices(), converter, clazz);
}
@Override
public ReactiveClusterOperations cluster() {
return new ReactiveClusterTemplate(client.cluster(), converter);
}
@Override
public Flux<AggregationContainer<?>> aggregate(Query query, Class<?> entityType, IndexCoordinates index) {
throw new UnsupportedOperationException("not implemented");
}
@Override
public Flux<Suggest> suggest(SuggestBuilder suggestion, Class<?> entityType) {
throw new UnsupportedOperationException("not implemented");
}
@Override
public Flux<Suggest> suggest(SuggestBuilder suggestion, IndexCoordinates index) {
throw new UnsupportedOperationException("not implemented");
}
@Override
public Query matchAllQuery() {
return NativeQuery.builder().withQuery(QueryBuilders.matchAllQueryAsQuery()).build();
}
@Override
public Query idsQuery(List<String> ids) {
throw new UnsupportedOperationException("not implemented");
}
/**
* Callback interface to be used with {@link #execute(ReactiveElasticsearchOperations.ClientCallback)} for operating
* directly on {@link ReactiveElasticsearchClient}.
*
* @param <T>
*/
interface ClientCallback<T extends Publisher<?>> {
T doWithClient(ReactiveElasticsearchClient client);
}
/**
* Execute a callback with the {@link ReactiveElasticsearchClient} and provide exception translation.
*
* @param callback the callback to execute, must not be {@literal null}
* @param <T> the type returned from the callback
* @return the callback result
*/
public <T> Publisher<T> execute(ReactiveElasticsearchTemplate.ClientCallback<Publisher<T>> callback) {
return Flux.defer(() -> callback.doWithClient(client)).onErrorMap(this::translateException);
}
/**
* translates an Exception if possible. Exceptions that are no {@link RuntimeException}s are wrapped in a
* RuntimeException
*
* @param throwable the Exception to map
* @return the potentially translated RuntimeException.
*/
private RuntimeException translateException(Throwable throwable) {
RuntimeException runtimeException = throwable instanceof RuntimeException ? (RuntimeException) throwable
: new RuntimeException(throwable.getMessage(), throwable);
RuntimeException potentiallyTranslatedException = exceptionTranslator
.translateExceptionIfPossible(runtimeException);
return potentiallyTranslatedException != null ? potentiallyTranslatedException : runtimeException;
}
}

View File

@ -0,0 +1,337 @@
/*
* Copyright 2021-2022 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 static org.springframework.util.StringUtils.*;
import co.elastic.clients.elasticsearch._types.AcknowledgedResponseBase;
import co.elastic.clients.elasticsearch.indices.*;
import co.elastic.clients.transport.endpoints.BooleanResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.elasticsearch.NoSuchIndexException;
import org.springframework.data.elasticsearch.annotations.Mapping;
import org.springframework.data.elasticsearch.core.IndexInformation;
import org.springframework.data.elasticsearch.core.ReactiveIndexOperations;
import org.springframework.data.elasticsearch.core.ReactiveResourceUtil;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.index.AliasActions;
import org.springframework.data.elasticsearch.core.index.AliasData;
import org.springframework.data.elasticsearch.core.index.DeleteTemplateRequest;
import org.springframework.data.elasticsearch.core.index.ExistsTemplateRequest;
import org.springframework.data.elasticsearch.core.index.GetTemplateRequest;
import org.springframework.data.elasticsearch.core.index.PutTemplateRequest;
import org.springframework.data.elasticsearch.core.index.ReactiveMappingBuilder;
import org.springframework.data.elasticsearch.core.index.Settings;
import org.springframework.data.elasticsearch.core.index.TemplateData;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* @author Peter-Josef Meisch
*/
public class ReactiveIndicesTemplate extends ReactiveChildTemplate<ReactiveElasticsearchIndicesClient>
implements ReactiveIndexOperations {
@Nullable private final Class<?> boundClass;
private final IndexCoordinates boundIndexCoordinates;
public ReactiveIndicesTemplate(ReactiveElasticsearchIndicesClient client,
ElasticsearchConverter elasticsearchConverter, IndexCoordinates index) {
super(client, elasticsearchConverter);
Assert.notNull(index, "index must not be null");
this.boundClass = null;
this.boundIndexCoordinates = index;
}
public ReactiveIndicesTemplate(ReactiveElasticsearchIndicesClient client,
ElasticsearchConverter elasticsearchConverter, Class<?> clazz) {
super(client, elasticsearchConverter);
Assert.notNull(clazz, "clazz must not be null");
this.boundClass = clazz;
this.boundIndexCoordinates = getIndexCoordinatesFor(clazz);
}
@Override
public Mono<Boolean> create() {
IndexCoordinates indexCoordinates = getIndexCoordinates();
if (boundClass != null) {
return createSettings(boundClass).flatMap(settings -> doCreate(indexCoordinates, settings, null));
} else {
return doCreate(indexCoordinates, new Settings(), null);
}
}
@Override
public Mono<Boolean> create(Map<String, Object> settings) {
Assert.notNull(settings, "settings must not be null");
return doCreate(getIndexCoordinates(), settings, null);
}
@Override
public Mono<Boolean> create(Map<String, Object> settings, Document mapping) {
Assert.notNull(settings, "settings must not be null");
Assert.notNull(mapping, "mapping must not be null");
return doCreate(getIndexCoordinates(), settings, mapping);
}
@Override
public Mono<Boolean> createWithMapping() {
return createSettings() //
.flatMap(settings -> //
createMapping().flatMap(mapping -> //
doCreate(getIndexCoordinates(), settings, mapping))); //
}
private Mono<Boolean> doCreate(IndexCoordinates indexCoordinates, Map<String, Object> settings,
@Nullable Document mapping) {
CreateIndexRequest createIndexRequest = requestConverter.indicesCreateRequest(indexCoordinates, settings, mapping);
Mono<CreateIndexResponse> createIndexResponse = Mono.from(execute(client -> client.create(createIndexRequest)));
return createIndexResponse.map(CreateIndexResponse::acknowledged);
}
@Override
public Mono<Boolean> delete() {
return exists().flatMap(exists -> {
if (exists) {
DeleteIndexRequest deleteIndexRequest = requestConverter.indicesDeleteRequest(getIndexCoordinates());
return Mono.from(execute(client -> client.delete(deleteIndexRequest))) //
.map(DeleteIndexResponse::acknowledged) //
.onErrorResume(NoSuchIndexException.class, e -> Mono.just(false));
} else {
return Mono.just(false);
}
});
}
@Override
public Mono<Boolean> exists() {
ExistsRequest existsRequest = requestConverter.indicesExistsRequest(getIndexCoordinates());
Mono<BooleanResponse> existsResponse = Mono.from(execute(client -> client.exists(existsRequest)));
return existsResponse.map(BooleanResponse::value);
}
@Override
public Mono<Void> refresh() {
return Mono.from(execute(client1 -> client1.refresh())).then();
}
@Override
public Mono<Document> createMapping() {
return createMapping(checkForBoundClass());
}
@Override
public Mono<Document> createMapping(Class<?> clazz) {
Assert.notNull(clazz, "clazz must not be null");
Mapping mappingAnnotation = AnnotatedElementUtils.findMergedAnnotation(clazz, Mapping.class);
if (mappingAnnotation != null) {
String mappingPath = mappingAnnotation.mappingPath();
if (hasText(mappingPath)) {
return ReactiveResourceUtil.loadDocument(mappingAnnotation.mappingPath(), "@Mapping");
}
}
return new ReactiveMappingBuilder(elasticsearchConverter).buildReactivePropertyMapping(clazz).map(Document::parse);
}
@Override
public Mono<Boolean> putMapping(Mono<Document> mapping) {
Assert.notNull(mapping, "mapping must not be null");
Mono<PutMappingResponse> putMappingResponse = mapping
.map(document -> requestConverter.indicesPutMappingRequest(getIndexCoordinates(), document)) //
.flatMap(putMappingRequest -> Mono.from(client.putMapping(putMappingRequest)));
return putMappingResponse.map(PutMappingResponse::acknowledged);
}
@Override
public Mono<Document> getMapping() {
IndexCoordinates indexCoordinates = getIndexCoordinates();
GetMappingRequest getMappingRequest = requestConverter.indicesGetMappingRequest(indexCoordinates);
Mono<GetMappingResponse> getMappingResponse = Mono.from(execute(client -> client.getMapping(getMappingRequest)));
return getMappingResponse.map(response -> responseConverter.indicesGetMapping(response, indexCoordinates));
}
@Override
public Mono<Settings> createSettings() {
return createSettings(checkForBoundClass());
}
@Override
public Mono<Settings> createSettings(Class<?> clazz) {
Assert.notNull(clazz, "clazz must not be null");
ElasticsearchPersistentEntity<?> persistentEntity = elasticsearchConverter.getMappingContext()
.getRequiredPersistentEntity(clazz);
String settingPath = persistentEntity.settingPath();
return hasText(settingPath) //
? ReactiveResourceUtil.loadDocument(settingPath, "@Setting") //
.map(Settings::new) //
: Mono.just(persistentEntity.getDefaultSettings());
}
@Override
public Mono<Settings> getSettings(boolean includeDefaults) {
GetIndicesSettingsRequest getSettingsRequest = requestConverter.indicesGetSettingsRequest(getIndexCoordinates(),
includeDefaults);
Mono<GetIndicesSettingsResponse> getSettingsResponse = Mono
.from(execute(client -> client.getSettings(getSettingsRequest)));
return getSettingsResponse
.map(response -> responseConverter.indicesGetSettings(response, getIndexCoordinates().getIndexName()));
}
@Override
public Mono<Boolean> alias(AliasActions aliasActions) {
Assert.notNull(aliasActions, "aliasActions must not be null");
UpdateAliasesRequest updateAliasesRequest = requestConverter.indicesUpdateAliasesRequest(aliasActions);
Mono<UpdateAliasesResponse> updateAliasesResponse = Mono
.from(execute(client -> client.updateAliases(updateAliasesRequest)));
return updateAliasesResponse.map(AcknowledgedResponseBase::acknowledged);
}
@Override
public Mono<Map<String, Set<AliasData>>> getAliases(String... aliasNames) {
return getAliases(aliasNames, null);
}
@Override
public Mono<Map<String, Set<AliasData>>> getAliasesForIndex(String... indexNames) {
return getAliases(null, indexNames);
}
private Mono<Map<String, Set<AliasData>>> getAliases(@Nullable String[] aliasNames, @Nullable String[] indexNames) {
GetAliasRequest getAliasRequest = requestConverter.indicesGetAliasRequest(aliasNames, indexNames);
Mono<GetAliasResponse> getAliasResponse = Mono.from(execute(client -> client.getAlias(getAliasRequest)));
return getAliasResponse.map(responseConverter::indicesGetAliasData);
}
@Override
public Mono<Boolean> putTemplate(PutTemplateRequest putTemplateRequest) {
Assert.notNull(putTemplateRequest, "putTemplateRequest must not be null");
co.elastic.clients.elasticsearch.indices.PutTemplateRequest putTemplateRequestES = requestConverter
.indicesPutTemplateRequest(putTemplateRequest);
Mono<PutTemplateResponse> putTemplateResponse = Mono
.from(execute(client -> client.putTemplate(putTemplateRequestES)));
return putTemplateResponse.map(PutTemplateResponse::acknowledged);
}
@Override
public Mono<TemplateData> getTemplate(GetTemplateRequest getTemplateRequest) {
Assert.notNull(getTemplateRequest, "getTemplateRequest must not be null");
co.elastic.clients.elasticsearch.indices.GetTemplateRequest getTemplateRequestES = requestConverter
.indicesGetTemplateRequest(getTemplateRequest);
Mono<GetTemplateResponse> getTemplateResponse = Mono
.from(execute(client -> client.getTemplate(getTemplateRequestES)));
return getTemplateResponse.flatMap(response -> {
if (response != null) {
TemplateData templateData = responseConverter.indicesGetTemplateData(response,
getTemplateRequest.getTemplateName());
if (templateData != null) {
return Mono.just(templateData);
}
}
return Mono.empty();
});
}
@Override
public Mono<Boolean> existsTemplate(ExistsTemplateRequest existsTemplateRequest) {
Assert.notNull(existsTemplateRequest, "existsTemplateRequest must not be null");
co.elastic.clients.elasticsearch.indices.ExistsTemplateRequest existsTemplateRequestES = requestConverter
.indicesExistsTemplateRequest(existsTemplateRequest);
return Mono.from(execute(client1 -> client1.existsTemplate(existsTemplateRequestES))).map(BooleanResponse::value);
}
@Override
public Mono<Boolean> deleteTemplate(DeleteTemplateRequest deleteTemplateRequest) {
Assert.notNull(deleteTemplateRequest, "deleteTemplateRequest must not be null");
co.elastic.clients.elasticsearch.indices.DeleteTemplateRequest deleteTemplateRequestES = requestConverter
.indicesDeleteTemplateRequest(deleteTemplateRequest);
return Mono.from(execute(client1 -> client1.deleteTemplate(deleteTemplateRequestES)))
.map(DeleteTemplateResponse::acknowledged);
}
@Override
public Flux<IndexInformation> getInformation(IndexCoordinates index) {
throw new UnsupportedOperationException("not implemented");
}
@Override
public IndexCoordinates getIndexCoordinates() {
return (boundClass != null) ? getIndexCoordinatesFor(boundClass) : Objects.requireNonNull(boundIndexCoordinates);
}
// region helper functions
private IndexCoordinates getIndexCoordinatesFor(Class<?> clazz) {
return elasticsearchConverter.getMappingContext().getRequiredPersistentEntity(clazz).getIndexCoordinates();
}
private Class<?> checkForBoundClass() {
if (boundClass == null) {
throw new InvalidDataAccessApiUsageException("IndexOperations are not bound");
}
return boundClass;
}
// endregion
}

View File

@ -0,0 +1,389 @@
/*
* Copyright 2021-2022 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 static org.springframework.data.elasticsearch.client.elc.JsonUtils.*;
import co.elastic.clients.elasticsearch._types.BulkIndexByScrollFailure;
import co.elastic.clients.elasticsearch._types.ErrorCause;
import co.elastic.clients.elasticsearch._types.Time;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch.cluster.HealthResponse;
import co.elastic.clients.elasticsearch.core.DeleteByQueryResponse;
import co.elastic.clients.elasticsearch.core.mget.MultiGetError;
import co.elastic.clients.elasticsearch.core.mget.MultiGetResponseItem;
import co.elastic.clients.elasticsearch.indices.*;
import co.elastic.clients.elasticsearch.indices.get_mapping.IndexMappingRecord;
import co.elastic.clients.json.JsonpMapper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.elasticsearch.ElasticsearchErrorCause;
import org.springframework.data.elasticsearch.core.IndexInformation;
import org.springframework.data.elasticsearch.core.MultiGetItem;
import org.springframework.data.elasticsearch.core.cluster.ClusterHealth;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.index.AliasData;
import org.springframework.data.elasticsearch.core.index.Settings;
import org.springframework.data.elasticsearch.core.index.TemplateData;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.ByQueryResponse;
import org.springframework.data.elasticsearch.core.reindex.ReindexResponse;
import org.springframework.data.elasticsearch.support.DefaultStringObjectMap;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Class to convert Elasticsearch responses into Spring Data Elasticsearch classes.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
class ResponseConverter {
private static final Logger LOGGER = LoggerFactory.getLogger(ResponseConverter.class);
private final JsonpMapper jsonpMapper;
public ResponseConverter(JsonpMapper jsonpMapper) {
this.jsonpMapper = jsonpMapper;
}
// region cluster client
public ClusterHealth clusterHealth(HealthResponse healthResponse) {
Assert.notNull(healthResponse, "healthResponse must not be null");
return ClusterHealth.builder() //
.withActivePrimaryShards(healthResponse.activePrimaryShards()) //
.withActiveShards(healthResponse.activeShards()) //
.withActiveShardsPercent(Double.parseDouble(healthResponse.activeShardsPercentAsNumber()))//
.withClusterName(healthResponse.clusterName()) //
.withDelayedUnassignedShards(healthResponse.delayedUnassignedShards()) //
.withInitializingShards(healthResponse.initializingShards()) //
.withNumberOfDataNodes(healthResponse.numberOfDataNodes()) //
.withNumberOfInFlightFetch(healthResponse.numberOfInFlightFetch()) //
.withNumberOfNodes(healthResponse.numberOfNodes()) //
.withNumberOfPendingTasks(healthResponse.numberOfPendingTasks()) //
.withRelocatingShards(healthResponse.relocatingShards()) //
.withStatus(healthResponse.status().toString()) //
.withTaskMaxWaitingTimeMillis(Long.parseLong(healthResponse.taskMaxWaitingInQueueMillis())) //
.withTimedOut(healthResponse.timedOut()) //
.withUnassignedShards(healthResponse.unassignedShards()) //
.build(); //
}
// endregion
// region indices client
public Settings indicesGetSettings(GetIndicesSettingsResponse getIndicesSettingsResponse, String indexName) {
Assert.notNull(getIndicesSettingsResponse, "getIndicesSettingsResponse must not be null");
Assert.notNull(indexName, "indexName must not be null");
Settings settings = new Settings();
IndexState indexState = getIndicesSettingsResponse.get(indexName);
if (indexState != null) {
Function<IndexSettings, Settings> indexSettingsToSettings = indexSettings -> {
Settings parsedSettings = Settings.parse(toJson(indexSettings, jsonpMapper));
return (indexSettings.index() != null) ? parsedSettings : new Settings().append("index", parsedSettings);
};
if (indexState.defaults() != null) {
Settings defaultSettings = indexSettingsToSettings.apply(indexState.defaults());
settings.merge(defaultSettings);
}
if (indexState.settings() != null) {
Settings nonDefaultSettings = indexSettingsToSettings.apply(indexState.settings());
settings.merge(nonDefaultSettings);
}
}
return settings;
}
public Document indicesGetMapping(GetMappingResponse getMappingResponse, IndexCoordinates indexCoordinates) {
Assert.notNull(getMappingResponse, "getMappingResponse must not be null");
Assert.notNull(indexCoordinates, "indexCoordinates must not be null");
Map<String, IndexMappingRecord> mappings = getMappingResponse.result();
if (mappings == null || mappings.size() == 0) {
return Document.create();
}
IndexMappingRecord indexMappingRecord = mappings.get(indexCoordinates.getIndexName());
// this can happen when the mapping was requested with an alias
if (indexMappingRecord == null) {
if (mappings.size() != 1) {
LOGGER.warn("no mapping returned for index {}", indexCoordinates.getIndexName());
return Document.create();
}
String index = mappings.keySet().iterator().next();
indexMappingRecord = mappings.get(index);
}
return Document.parse(toJson(indexMappingRecord.mappings(), jsonpMapper));
}
public List<IndexInformation> indicesGetIndexInformations(GetIndexResponse getIndexResponse) {
Assert.notNull(getIndexResponse, "getIndexResponse must not be null");
List<IndexInformation> indexInformationList = new ArrayList<>();
getIndexResponse.result().forEach((indexName, indexState) -> {
Settings settings = indexState.settings() != null ? Settings.parse(toJson(indexState.settings(), jsonpMapper))
: new Settings();
Document mappings = indexState.mappings() != null ? Document.parse(toJson(indexState.mappings(), jsonpMapper))
: Document.create();
List<AliasData> aliasDataList = new ArrayList<>();
indexState.aliases().forEach((aliasName, alias) -> aliasDataList.add(indicesGetAliasData(aliasName, alias)));
indexInformationList.add(IndexInformation.of(indexName, settings, mappings, aliasDataList));
});
return indexInformationList;
}
public Map<String, Set<AliasData>> indicesGetAliasData(GetAliasResponse getAliasResponse) {
Assert.notNull(getAliasResponse, "getAliasResponse must not be null");
Map<String, Set<AliasData>> aliasDataMap = new HashMap<>();
getAliasResponse.result().forEach((indexName, alias) -> {
Set<AliasData> aliasDataSet = new HashSet<>();
alias.aliases()
.forEach((aliasName, aliasDefinition) -> aliasDataSet.add(indicesGetAliasData(aliasName, aliasDefinition)));
aliasDataMap.put(indexName, aliasDataSet);
});
return aliasDataMap;
}
private AliasData indicesGetAliasData(String aliasName, Alias alias) {
Query filter = alias.filter();
String filterJson = filter != null ? toJson(filter, jsonpMapper) : null;
Document filterDocument = filterJson != null ? Document.parse(filterJson) : null;
return AliasData.of(aliasName, filterDocument, alias.indexRouting(), alias.searchRouting(), alias.isWriteIndex(),
alias.isHidden());
}
private AliasData indicesGetAliasData(String aliasName, AliasDefinition alias) {
Query filter = alias.filter();
String filterJson = filter != null ? toJson(filter, jsonpMapper) : null;
Document filterDocument = filterJson != null ? Document.parse(filterJson) : null;
return AliasData.of(aliasName, filterDocument, alias.indexRouting(), alias.searchRouting(), alias.isWriteIndex(),
null);
}
@Nullable
public TemplateData indicesGetTemplateData(GetTemplateResponse getTemplateResponse, String templateName) {
Assert.notNull(getTemplateResponse, "getTemplateResponse must not be null");
Assert.notNull(templateName, "templateName must not be null");
TemplateMapping templateMapping = getTemplateResponse.get(templateName);
if (templateMapping != null) {
Settings settings = new Settings();
templateMapping.settings().forEach((key, jsonData) -> {
if (key.contains(".")) {
// returned string contains " quotes
settings.put(key, jsonData.toJson().toString().replaceAll("^\"|\"$", ""));
} else {
settings.put(key, new DefaultStringObjectMap<>().fromJson(jsonData.toJson().toString()));
}
});
Function<? super Map.Entry<String, Alias>, String> keyMapper = Map.Entry::getKey;
Function<? super Map.Entry<String, Alias>, AliasData> valueMapper = entry -> indicesGetAliasData(entry.getKey(),
entry.getValue());
Map<String, AliasData> aliases = templateMapping.aliases().entrySet().stream()
.collect(Collectors.toMap(keyMapper, valueMapper));
Document mapping = Document.parse(toJson(templateMapping.mappings(), jsonpMapper));
TemplateData.TemplateDataBuilder builder = TemplateData.builder() //
.withIndexPatterns(templateMapping.indexPatterns().toArray(new String[0])) //
.withOrder(templateMapping.order()) //
.withSettings(settings) //
.withMapping(mapping) //
.withAliases(aliases) //
;
if (templateMapping.version() != null) {
builder.withVersion(templateMapping.version().intValue());
}
return builder.build();
}
return null;
}
// endregion
// region document operations
public ReindexResponse reindexResponse(co.elastic.clients.elasticsearch.core.ReindexResponse reindexResponse) {
Assert.notNull(reindexResponse, "reindexResponse must not be null");
List<ReindexResponse.Failure> failures = reindexResponse.failures() //
.stream() //
.map(this::reindexResponseFailureOf) //
.collect(Collectors.toList());
// noinspection ConstantConditions
return ReindexResponse.builder() //
.withTook(timeToLong(reindexResponse.took())) //
.withTimedOut(reindexResponse.timedOut()) //
.withTotal(reindexResponse.total()) //
.withCreated(reindexResponse.created()) //
.withUpdated(reindexResponse.updated()) //
.withDeleted(reindexResponse.deleted()) //
.withBatches(reindexResponse.batches()) //
.withVersionConflicts(reindexResponse.versionConflicts()) //
.withNoops(reindexResponse.noops()) //
.withBulkRetries(reindexResponse.retries().bulk()) //
.withSearchRetries(reindexResponse.retries().search()) //
.withThrottledMillis(Long.parseLong(reindexResponse.throttledMillis())) //
.withRequestsPerSecond(reindexResponse.requestsPerSecond()) //
.withThrottledUntilMillis(Long.parseLong(reindexResponse.throttledUntilMillis())).withFailures(failures) //
.build();
}
private ReindexResponse.Failure reindexResponseFailureOf(BulkIndexByScrollFailure failure) {
return ReindexResponse.Failure.builder() //
.withIndex(failure.index()) //
.withType(failure.type()) //
.withId(failure.id()) //
.withStatus(failure.status())//
.withErrorCause(toErrorCause(failure.cause())) //
// seqno, term, aborted are not available in the new client
.build();
}
private ByQueryResponse.Failure byQueryResponseFailureOf(BulkIndexByScrollFailure failure) {
return ByQueryResponse.Failure.builder() //
.withIndex(failure.index()) //
.withType(failure.type()) //
.withId(failure.id()) //
.withStatus(failure.status())//
.withErrorCause(toErrorCause(failure.cause())).build();
}
@Nullable
public static MultiGetItem.Failure getFailure(MultiGetResponseItem<EntityAsMap> itemResponse) {
MultiGetError responseFailure = itemResponse.isFailure() ? itemResponse.failure() : null;
return responseFailure != null
? MultiGetItem.Failure.of(responseFailure.index(), null, responseFailure.id(), null,
toErrorCause(responseFailure.error()))
: null;
}
public ByQueryResponse byQueryResponse(DeleteByQueryResponse response) {
List<ByQueryResponse.Failure> failures = response.failures().stream().map(this::byQueryResponseFailureOf)
.collect(Collectors.toList());
ByQueryResponse.ByQueryResponseBuilder builder = ByQueryResponse.builder();
if (response.took() != null) {
builder.withTook(response.took());
}
if (response.timedOut() != null) {
builder.withTimedOut(response.timedOut());
}
if (response.total() != null) {
builder.withTotal(response.total());
}
if (response.deleted() != null) {
builder.withDeleted(response.deleted());
}
if (response.batches() != null) {
builder.withBatches(Math.toIntExact(response.batches()));
}
if (response.versionConflicts() != null) {
builder.withVersionConflicts(response.versionConflicts());
}
if (response.noops() != null) {
builder.withNoops(response.noops());
}
if (response.retries() != null) {
builder.withBulkRetries(response.retries().bulk());
builder.withSearchRetries(response.retries().search());
}
builder.withFailures(failures);
return builder.build();
}
// endregion
// region helper functions
private long timeToLong(Time time) {
if (time.isTime()) {
return Long.parseLong(time.time());
} else {
return time.offset();
}
}
@Nullable
private static ElasticsearchErrorCause toErrorCause(@Nullable ErrorCause errorCause) {
if (errorCause != null) {
return new ElasticsearchErrorCause( //
errorCause.type(), //
errorCause.reason(), //
errorCause.stackTrace(), //
toErrorCause(errorCause.causedBy()), //
errorCause.rootCause().stream().map(ResponseConverter::toErrorCause).collect(Collectors.toList()), //
errorCause.suppressed().stream().map(ResponseConverter::toErrorCause).collect(Collectors.toList()));
} else {
return null;
}
}
// endregion
}

View File

@ -0,0 +1,123 @@
/*
* Copyright 2021-2022 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._types.aggregations.Aggregate;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.HitsMetadata;
import co.elastic.clients.elasticsearch.core.search.Suggestion;
import co.elastic.clients.elasticsearch.core.search.TotalHits;
import co.elastic.clients.json.JsonpMapper;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.elasticsearch.search.SearchHits;
import org.springframework.data.elasticsearch.core.TotalHitsRelation;
import org.springframework.data.elasticsearch.core.document.SearchDocument;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
import org.springframework.data.elasticsearch.core.suggest.response.Suggest;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Factory class to create {@link SearchDocumentResponse} instances.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
class SearchDocumentResponseBuilder {
/**
* creates a SearchDocumentResponse from the {@link SearchResponse}
*
* @param searchResponse the Elasticsearch search response
* @param entityCreator function to create an entity from a {@link SearchDocument}
* @param jsonpMapper to map JsonData objects
* @return the SearchDocumentResponse
*/
public static <T> SearchDocumentResponse from(SearchResponse<EntityAsMap> searchResponse,
SearchDocumentResponse.EntityCreator<T> entityCreator, JsonpMapper jsonpMapper) {
Assert.notNull(searchResponse, "searchResponse must not be null");
Assert.notNull(entityCreator, "entityCreator must not be null");
HitsMetadata<EntityAsMap> hitsMetadata = searchResponse.hits();
String scrollId = searchResponse.scrollId();
Map<String, Aggregate> aggregations = searchResponse.aggregations();
Map<String, List<Suggestion<EntityAsMap>>> suggest = searchResponse.suggest();
return from(hitsMetadata, scrollId, aggregations, suggest, entityCreator, jsonpMapper);
}
/**
* creates a {@link SearchDocumentResponseBuilder} from {@link SearchHits} with the given scrollId aggregations and
* suggestES
*
* @param <T> entity type
* @param hitsMetadata the {@link SearchHits} to process
* @param scrollId scrollId
* @param aggregations aggregations
* @param suggestES the suggestion response from Elasticsearch
* @param entityCreator function to create an entity from a {@link SearchDocument}, needed in mapping the suggest data
* @param jsonpMapper to map JsonData objects
* @return the {@link SearchDocumentResponse}
*/
public static <T> SearchDocumentResponse from(HitsMetadata<?> hitsMetadata, @Nullable String scrollId,
Map<String, Aggregate> aggregations, Map<String, List<Suggestion<EntityAsMap>>> suggestES,
SearchDocumentResponse.EntityCreator<T> entityCreator, JsonpMapper jsonpMapper) {
Assert.notNull(hitsMetadata, "hitsMetadata must not be null");
long totalHits;
String totalHitsRelation;
TotalHits responseTotalHits = hitsMetadata.total();
if (responseTotalHits != null) {
totalHits = responseTotalHits.value();
switch (responseTotalHits.relation().jsonValue()) {
case "eq":
totalHitsRelation = TotalHitsRelation.EQUAL_TO.name();
break;
case "gte":
totalHitsRelation = TotalHitsRelation.GREATER_THAN_OR_EQUAL_TO.name();
break;
default:
totalHitsRelation = TotalHitsRelation.OFF.name();
}
} else {
totalHits = hitsMetadata.hits().size();
totalHitsRelation = "OFF";
}
float maxScore = hitsMetadata.maxScore() != null ? hitsMetadata.maxScore().floatValue() : Float.NaN;
List<SearchDocument> searchDocuments = new ArrayList<>();
for (Hit<?> hit : hitsMetadata.hits()) {
searchDocuments.add(DocumentAdapters.from(hit, jsonpMapper));
}
ElasticsearchAggregations aggregationsContainer = aggregations != null ? new ElasticsearchAggregations(aggregations)
: null;
// todo #1973
Suggest suggest = null;
return new SearchDocumentResponse(totalHits, totalHitsRelation, maxScore, scrollId, searchDocuments,
aggregationsContainer, suggest);
}
}

View File

@ -0,0 +1,298 @@
/*
* Copyright 2022 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._types.Conflicts;
import co.elastic.clients.elasticsearch._types.DistanceUnit;
import co.elastic.clients.elasticsearch._types.GeoDistanceType;
import co.elastic.clients.elasticsearch._types.OpType;
import co.elastic.clients.elasticsearch._types.Refresh;
import co.elastic.clients.elasticsearch._types.SortMode;
import co.elastic.clients.elasticsearch._types.VersionType;
import co.elastic.clients.elasticsearch._types.mapping.FieldType;
import co.elastic.clients.elasticsearch.core.search.BoundaryScanner;
import co.elastic.clients.elasticsearch.core.search.BuiltinHighlighterType;
import co.elastic.clients.elasticsearch.core.search.HighlighterEncoder;
import co.elastic.clients.elasticsearch.core.search.HighlighterFragmenter;
import co.elastic.clients.elasticsearch.core.search.HighlighterOrder;
import co.elastic.clients.elasticsearch.core.search.HighlighterTagsSchema;
import co.elastic.clients.elasticsearch.core.search.HighlighterType;
import org.springframework.data.elasticsearch.core.RefreshPolicy;
import org.springframework.data.elasticsearch.core.query.GeoDistanceOrder;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.Order;
import org.springframework.data.elasticsearch.core.reindex.ReindexRequest;
import org.springframework.lang.Nullable;
/**
* Utility to handle new Elasticsearch client type values.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
final class TypeUtils {
@Nullable
static BoundaryScanner boundaryScanner(@Nullable String value) {
if (value != null) {
switch (value.toLowerCase()) {
case "chars":
return BoundaryScanner.Chars;
case "sentence":
return BoundaryScanner.Sentence;
case "word":
return BoundaryScanner.Word;
default:
return null;
}
}
return null;
}
static Conflicts conflicts(ReindexRequest.Conflicts conflicts) {
switch (conflicts) {
case ABORT:
return Conflicts.Abort;
case PROCEED:
return Conflicts.Proceed;
}
throw new IllegalArgumentException("Cannot map conflicts value " + conflicts.name());
}
@Nullable
static DistanceUnit distanceUnit(String unit) {
switch (unit.toLowerCase()) {
case "in":
case "inch":
return DistanceUnit.Inches;
case "yd":
case "yards":
return DistanceUnit.Yards;
case "ft":
case "feet":
return DistanceUnit.Feet;
case "km":
case "kilometers":
return DistanceUnit.Kilometers;
case "nm":
case "nmi":
return DistanceUnit.NauticMiles;
case "mm":
case "millimeters":
return DistanceUnit.Millimeters;
case "cm":
case "centimeters":
return DistanceUnit.Centimeters;
case "mi":
case "miles":
return DistanceUnit.Miles;
case "m":
case "meters":
return DistanceUnit.Meters;
}
return null;
}
@Nullable
static FieldType fieldType(String type) {
for (FieldType fieldType : FieldType.values()) {
if (fieldType.jsonValue().equals(type)) {
return fieldType;
}
}
return null;
}
@Nullable
static GeoDistanceType geoDistanceType(GeoDistanceOrder.DistanceType distanceType) {
switch (distanceType) {
case arc:
return GeoDistanceType.Arc;
case plane:
return GeoDistanceType.Plane;
}
return null;
}
@Nullable
static HighlighterFragmenter highlighterFragmenter(@Nullable String value) {
if (value != null) {
switch (value.toLowerCase()) {
case "simple":
return HighlighterFragmenter.Simple;
case "span":
return HighlighterFragmenter.Span;
default:
return null;
}
}
return null;
}
@Nullable
static HighlighterOrder highlighterOrder(@Nullable String value) {
if (value != null) {
if ("score".equals(value.toLowerCase())) {
return HighlighterOrder.Score;
}
}
return null;
}
@Nullable
static HighlighterType highlighterType(@Nullable String value) {
if (value != null) {
switch (value.toLowerCase()) {
case "unified":
return HighlighterType.of(b -> b.builtin(BuiltinHighlighterType.Unified));
case "plain":
return HighlighterType.of(b -> b.builtin(BuiltinHighlighterType.Plain));
case "fvh":
return HighlighterType.of(b -> b.builtin(BuiltinHighlighterType.FastVector));
default:
return null;
}
}
return null;
}
@Nullable
static HighlighterEncoder highlighterEncoder(@Nullable String value) {
if (value != null) {
switch (value.toLowerCase()) {
case "default":
return HighlighterEncoder.Default;
case "html":
return HighlighterEncoder.Html;
default:
return null;
}
}
return null;
}
@Nullable
static HighlighterTagsSchema highlighterTagsSchema(@Nullable String value) {
if (value != null) {
if ("styled".equals(value.toLowerCase())) {
return HighlighterTagsSchema.Styled;
}
}
return null;
}
@Nullable
static OpType opType(@Nullable IndexQuery.OpType opType) {
if (opType != null) {
switch (opType) {
case INDEX:
return OpType.Index;
case CREATE:
return OpType.Create;
}
}
return null;
}
static Refresh refresh(@Nullable RefreshPolicy refreshPolicy) {
if (refreshPolicy == null) {
return Refresh.False;
}
switch (refreshPolicy) {
case IMMEDIATE:
return Refresh.True;
case WAIT_UNTIL:
return Refresh.WaitFor;
case NONE:
default:
return Refresh.False;
}
}
@Nullable
static SortMode sortMode(Order.Mode mode) {
switch (mode) {
case min:
return SortMode.Min;
case max:
return SortMode.Max;
case median:
return SortMode.Median;
case avg:
return SortMode.Avg;
}
return null;
}
@Nullable
static VersionType versionType(
@Nullable org.springframework.data.elasticsearch.annotations.Document.VersionType versionType) {
if (versionType != null) {
switch (versionType) {
case INTERNAL:
return VersionType.Internal;
case EXTERNAL:
return VersionType.External;
case EXTERNAL_GTE:
return VersionType.ExternalGte;
case FORCE:
return VersionType.Force;
}
}
return null;
}
static Integer waitForActiveShardsCount(@Nullable String value) {
// values taken from the RHLC implementation
if (value == null) {
return -2;
} else if ("all".equals(value.toUpperCase())) {
return -1;
} else {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Illegale value for waitForActiveShards" + value);
}
}
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2022 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 classes that use the new Elasticsearch client library (co.elastic.clients:elasticsearch-java)
* to access Elasticsearch.
*/
@org.springframework.lang.NonNullApi
@org.springframework.lang.NonNullFields
package org.springframework.data.elasticsearch.client.elc;

View File

@ -15,33 +15,19 @@
*/
package org.springframework.data.elasticsearch.client.reactive;
import io.netty.channel.ChannelOption;
import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ClientAuth;
import io.netty.handler.ssl.IdentityCipherSuiteFilter;
import io.netty.handler.ssl.JdkSslContext;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;
import reactor.netty.transport.ProxyProvider;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Collection;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.net.ssl.SSLContext;
import org.apache.http.util.EntityUtils;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ElasticsearchStatusException;
@ -120,7 +106,6 @@ import org.springframework.data.util.Lazy;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@ -229,7 +214,7 @@ public class DefaultReactiveElasticsearchClient implements ReactiveElasticsearch
Assert.notNull(clientConfiguration, "ClientConfiguration must not be null");
Assert.notNull(requestCreator, "RequestCreator must not be null");
WebClientProvider provider = getWebClientProvider(clientConfiguration);
WebClientProvider provider = WebClientProvider.getWebClientProvider(clientConfiguration);
HostProvider<?> hostProvider = HostProvider.provider(provider, clientConfiguration.getHeadersSupplier(),
clientConfiguration.getEndpoints().toArray(new InetSocketAddress[0]));
@ -241,83 +226,6 @@ public class DefaultReactiveElasticsearchClient implements ReactiveElasticsearch
return client;
}
private static WebClientProvider getWebClientProvider(ClientConfiguration clientConfiguration) {
Duration connectTimeout = clientConfiguration.getConnectTimeout();
Duration soTimeout = clientConfiguration.getSocketTimeout();
HttpClient httpClient = HttpClient.create().compress(true);
if (!connectTimeout.isNegative()) {
httpClient = httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, Math.toIntExact(connectTimeout.toMillis()));
}
if (!soTimeout.isNegative()) {
httpClient = httpClient.doOnConnected(connection -> connection //
.addHandlerLast(new ReadTimeoutHandler(soTimeout.toMillis(), TimeUnit.MILLISECONDS))
.addHandlerLast(new WriteTimeoutHandler(soTimeout.toMillis(), TimeUnit.MILLISECONDS)));
}
if (clientConfiguration.getProxy().isPresent()) {
String proxy = clientConfiguration.getProxy().get();
String[] hostPort = proxy.split(":");
if (hostPort.length != 2) {
throw new IllegalArgumentException("invalid proxy configuration " + proxy + ", should be \"host:port\"");
}
httpClient = httpClient.proxy(proxyOptions -> proxyOptions.type(ProxyProvider.Proxy.HTTP).host(hostPort[0])
.port(Integer.parseInt(hostPort[1])));
}
String scheme = "http";
if (clientConfiguration.useSsl()) {
Optional<SSLContext> sslContext = clientConfiguration.getSslContext();
if (sslContext.isPresent()) {
httpClient = httpClient
.secure(sslContextSpec -> sslContextSpec.sslContext(new JdkSslContext(sslContext.get(), true, null,
IdentityCipherSuiteFilter.INSTANCE, ApplicationProtocolConfig.DISABLED, ClientAuth.NONE, null, false)));
} else {
httpClient = httpClient.secure();
}
scheme = "https";
}
WebClientProvider provider = WebClientProvider.create(scheme, new ReactorClientHttpConnector(httpClient));
if (clientConfiguration.getPathPrefix() != null) {
provider = provider.withPathPrefix(clientConfiguration.getPathPrefix());
}
Function<WebClient, WebClient> webClientConfigurer = webClient -> {
for (ClientConfiguration.ClientConfigurationCallback<?> clientConfigurer : clientConfiguration
.getClientConfigurers()) {
if (clientConfigurer instanceof ReactiveRestClients.WebClientConfigurationCallback) {
ReactiveRestClients.WebClientConfigurationCallback webClientConfigurationCallback = (ReactiveRestClients.WebClientConfigurationCallback) clientConfigurer;
webClient = webClientConfigurationCallback.configure(webClient);
}
}
return webClient;
};
provider = provider //
.withDefaultHeaders(clientConfiguration.getDefaultHeaders()) //
.withWebClientConfigurer(webClientConfigurer) //
.withRequestConfigurer(requestHeadersSpec -> requestHeadersSpec.headers(httpHeaders -> {
HttpHeaders suppliedHeaders = clientConfiguration.getHeadersSupplier().get();
if (suppliedHeaders != null && suppliedHeaders != HttpHeaders.EMPTY) {
httpHeaders.addAll(suppliedHeaders);
}
}));
return provider;
}
public void setHeadersSupplier(Supplier<HttpHeaders> headersSupplier) {
Assert.notNull(headersSupplier, "headersSupplier must not be null");

View File

@ -99,11 +99,34 @@ public interface HostProvider<T extends HostProvider<T>> {
return lookupActiveHost(verification).map(this::createWebClient);
}
/**
* Get the {@link WebClient} connecting to an active host utilizing cached {@link ElasticsearchHost}.
*
* @return the {@link Mono} emitting the client for an active host or {@link Mono#error(Throwable) an error} if none
* found.
* @since 4.4
*/
default Mono<WebClient> getWebClient() {
return getWebClient(Verification.LAZY);
}
/**
* Get the {@link WebClient} connecting to an active host.
*
* @param verification must not be {@literal null}.
* @return the {@link Mono} emitting the client for an active host or {@link Mono#error(Throwable) an error} if none
* found.
* @since 4.4
*/
default Mono<WebClient> getWebClient(Verification verification) {
return lookupActiveHost(verification).map(this::createWebClient);
}
/**
* Creates a {@link WebClient} for {@link InetSocketAddress endpoint}.
*
* @param endpoint must not be {@literal null}.
* @return a {@link WebClient} using the the given endpoint as {@literal base url}.
* @return a {@link WebClient} using the the given endpoint as {@literal transport url}.
*/
WebClient createWebClient(InetSocketAddress endpoint);

View File

@ -15,12 +15,30 @@
*/
package org.springframework.data.elasticsearch.client.reactive;
import io.netty.channel.ChannelOption;
import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ClientAuth;
import io.netty.handler.ssl.IdentityCipherSuiteFilter;
import io.netty.handler.ssl.JdkSslContext;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import reactor.netty.http.client.HttpClient;
import reactor.netty.transport.ProxyProvider;
import java.net.InetSocketAddress;
import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.net.ssl.SSLContext;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchClients;
import org.springframework.http.HttpHeaders;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.WebClient;
@ -152,4 +170,96 @@ public interface WebClientProvider {
* @since 4.3
*/
WebClientProvider withRequestConfigurer(Consumer<WebClient.RequestHeadersSpec<?>> requestConfigurer);
/**
* Creates a {@link WebClientProvider} for the given configuration
*
* @param clientConfiguration must not be {@literal} null
* @return the {@link WebClientProvider}
* @since 4.3
*/
static WebClientProvider getWebClientProvider(ClientConfiguration clientConfiguration) {
Assert.notNull(clientConfiguration, "clientConfiguration must not be null");
Duration connectTimeout = clientConfiguration.getConnectTimeout();
Duration soTimeout = clientConfiguration.getSocketTimeout();
HttpClient httpClient = HttpClient.create().compress(true);
if (!connectTimeout.isNegative()) {
httpClient = httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, Math.toIntExact(connectTimeout.toMillis()));
}
if (!soTimeout.isNegative()) {
httpClient = httpClient.doOnConnected(connection -> connection //
.addHandlerLast(new ReadTimeoutHandler(soTimeout.toMillis(), TimeUnit.MILLISECONDS))
.addHandlerLast(new WriteTimeoutHandler(soTimeout.toMillis(), TimeUnit.MILLISECONDS)));
}
if (clientConfiguration.getProxy().isPresent()) {
String proxy = clientConfiguration.getProxy().get();
String[] hostPort = proxy.split(":");
if (hostPort.length != 2) {
throw new IllegalArgumentException("invalid proxy configuration " + proxy + ", should be \"host:port\"");
}
httpClient = httpClient.proxy(proxyOptions -> proxyOptions.type(ProxyProvider.Proxy.HTTP).host(hostPort[0])
.port(Integer.parseInt(hostPort[1])));
}
String scheme = "http";
if (clientConfiguration.useSsl()) {
Optional<SSLContext> sslContext = clientConfiguration.getSslContext();
if (sslContext.isPresent()) {
httpClient = httpClient
.secure(sslContextSpec -> sslContextSpec.sslContext(new JdkSslContext(sslContext.get(), true, null,
IdentityCipherSuiteFilter.INSTANCE, ApplicationProtocolConfig.DISABLED, ClientAuth.NONE, null, false)));
} else {
httpClient = httpClient.secure();
}
scheme = "https";
}
WebClientProvider provider = WebClientProvider.create(scheme, new ReactorClientHttpConnector(httpClient));
if (clientConfiguration.getPathPrefix() != null) {
provider = provider.withPathPrefix(clientConfiguration.getPathPrefix());
}
Function<WebClient, WebClient> webClientConfigurer = webClient -> {
for (ClientConfiguration.ClientConfigurationCallback<?> clientConfigurer : clientConfiguration
.getClientConfigurers()) {
if (clientConfigurer instanceof ReactiveRestClients.WebClientConfigurationCallback) {
ReactiveRestClients.WebClientConfigurationCallback webClientConfigurationCallback = (ReactiveRestClients.WebClientConfigurationCallback) clientConfigurer;
webClient = webClientConfigurationCallback.configure(webClient);
}
if (clientConfigurer instanceof ElasticsearchClients.WebClientConfigurationCallback) {
ElasticsearchClients.WebClientConfigurationCallback webClientConfigurationCallback = (ElasticsearchClients.WebClientConfigurationCallback) clientConfigurer;
webClient = webClientConfigurationCallback.configure(webClient);
}
}
return webClient;
};
provider = provider //
.withDefaultHeaders(clientConfiguration.getDefaultHeaders()) //
.withWebClientConfigurer(webClientConfigurer) //
.withRequestConfigurer(requestHeadersSpec -> requestHeadersSpec //
.headers(httpHeaders -> {
HttpHeaders suppliedHeaders = clientConfiguration.getHeadersSupplier().get();
if (suppliedHeaders != null && suppliedHeaders != HttpHeaders.EMPTY) {
httpHeaders.addAll(suppliedHeaders);
}
}));
return provider;
}
}

View File

@ -21,16 +21,17 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.convert.EntityReader;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
import org.springframework.data.elasticsearch.core.event.AfterConvertCallback;
import org.springframework.data.elasticsearch.core.event.AfterLoadCallback;
import org.springframework.data.elasticsearch.core.event.AfterSaveCallback;
@ -77,7 +78,6 @@ import org.springframework.util.Assert;
public abstract class AbstractElasticsearchTemplate implements ElasticsearchOperations, ApplicationContextAware {
protected ElasticsearchConverter elasticsearchConverter;
protected RequestFactory requestFactory;
protected EntityOperations entityOperations;
@Nullable protected EntityCallbacks entityCallbacks;
@Nullable protected RefreshPolicy refreshPolicy;
@ -96,8 +96,6 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
this.entityOperations = new EntityOperations(mappingContext);
this.routingResolver = new DefaultRoutingResolver(mappingContext);
requestFactory = new RequestFactory(this.elasticsearchConverter);
// initialize the VersionInfo class in the initialization phase
// noinspection ResultOfMethodCallIgnored
VersionInfo.versionProperties();
@ -429,7 +427,7 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
/*
* internal use only, not for public API
*/
abstract protected <T> SearchScrollHits<T> searchScrollContinue(@Nullable String scrollId, long scrollTimeInMillis,
abstract protected <T> SearchScrollHits<T> searchScrollContinue(String scrollId, long scrollTimeInMillis,
Class<T> clazz, IndexCoordinates index);
/*
@ -455,13 +453,6 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
return elasticsearchConverter;
}
/**
* @since 4.0
*/
public RequestFactory getRequestFactory() {
return requestFactory;
}
protected static String[] toArray(List<String> values) {
String[] valuesAsArray = new String[values.size()];
return values.toArray(valuesAsArray);
@ -551,9 +542,9 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
@Nullable
private SeqNoPrimaryTerm getEntitySeqNoPrimaryTerm(Object entity) {
EntityOperations.AdaptibleEntity<Object> adaptibleEntity = entityOperations.forEntity(entity,
EntityOperations.AdaptableEntity<Object> adaptableEntity = entityOperations.forEntity(entity,
elasticsearchConverter.getConversionService(), routingResolver);
return adaptibleEntity.hasSeqNoPrimaryTerm() ? adaptibleEntity.getSeqNoPrimaryTerm() : null;
return adaptableEntity.hasSeqNoPrimaryTerm() ? adaptableEntity.getSeqNoPrimaryTerm() : null;
}
private <T> IndexQuery getIndexQuery(T entity) {
@ -588,6 +579,10 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
return builder.build();
}
protected <T> SearchDocumentResponse.EntityCreator<T> getEntityCreator(ReadDocumentCallback<T> documentCallback) {
return searchDocument -> CompletableFuture.completedFuture(documentCallback.doWith(searchDocument));
}
/**
* tries to extract the version of the Elasticsearch cluster
*

View File

@ -45,8 +45,6 @@ import org.springframework.util.Assert;
public abstract class AbstractIndexTemplate implements IndexOperations {
protected final ElasticsearchConverter elasticsearchConverter;
protected final RequestFactory requestFactory;
@Nullable protected final Class<?> boundClass;
@Nullable private final IndexCoordinates boundIndex;
@ -55,7 +53,6 @@ public abstract class AbstractIndexTemplate implements IndexOperations {
Assert.notNull(boundClass, "boundClass may not be null");
this.elasticsearchConverter = elasticsearchConverter;
requestFactory = new RequestFactory(elasticsearchConverter);
this.boundClass = boundClass;
this.boundIndex = null;
}
@ -65,7 +62,6 @@ public abstract class AbstractIndexTemplate implements IndexOperations {
Assert.notNull(boundIndex, "boundIndex may not be null");
this.elasticsearchConverter = elasticsearchConverter;
requestFactory = new RequestFactory(elasticsearchConverter);
this.boundClass = null;
this.boundIndex = boundIndex;
}
@ -195,12 +191,9 @@ public abstract class AbstractIndexTemplate implements IndexOperations {
@Override
public Document createMapping(Class<?> clazz) {
return buildMapping(clazz);
}
protected Document buildMapping(Class<?> clazz) {
// load mapping specified in Mapping annotation if present
// noinspection DuplicatedCode
Mapping mappingAnnotation = AnnotatedElementUtils.findMergedAnnotation(clazz, Mapping.class);
if (mappingAnnotation != null) {

View File

@ -0,0 +1,685 @@
/*
* Copyright 2021-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.convert.EntityReader;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.document.SearchDocument;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
import org.springframework.data.elasticsearch.core.event.ReactiveAfterConvertCallback;
import org.springframework.data.elasticsearch.core.event.ReactiveAfterLoadCallback;
import org.springframework.data.elasticsearch.core.event.ReactiveAfterSaveCallback;
import org.springframework.data.elasticsearch.core.event.ReactiveBeforeConvertCallback;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.elasticsearch.core.query.ByQueryResponse;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.data.elasticsearch.core.routing.DefaultRoutingResolver;
import org.springframework.data.elasticsearch.core.routing.RoutingResolver;
import org.springframework.data.elasticsearch.core.suggest.response.Suggest;
import org.springframework.data.elasticsearch.support.VersionInfo;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.callback.ReactiveEntityCallbacks;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Base class keeping common code for implementations of the {@link ReactiveElasticsearchOperations} interface
* independent of the used client.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
abstract public class AbstractReactiveElasticsearchTemplate
implements ReactiveElasticsearchOperations, ApplicationContextAware {
protected static final Logger QUERY_LOGGER = LoggerFactory
.getLogger("org.springframework.data.elasticsearch.core.QUERY");
protected final ElasticsearchConverter converter;
protected final SimpleElasticsearchMappingContext mappingContext;
protected final EntityOperations entityOperations;
protected @Nullable RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE;
protected RoutingResolver routingResolver;
protected @Nullable ReactiveEntityCallbacks entityCallbacks;
// region Initialization
protected AbstractReactiveElasticsearchTemplate(@Nullable ElasticsearchConverter converter) {
this.converter = converter != null ? converter : createElasticsearchConverter();
this.mappingContext = (SimpleElasticsearchMappingContext) this.converter.getMappingContext();
this.entityOperations = new EntityOperations(this.mappingContext);
this.routingResolver = new DefaultRoutingResolver(this.mappingContext);
// initialize the VersionInfo class in the initialization phase
// noinspection ResultOfMethodCallIgnored
VersionInfo.versionProperties();
}
@Override
public ElasticsearchConverter getElasticsearchConverter() {
return converter;
}
/**
* @return copy of this instance.
*/
private AbstractReactiveElasticsearchTemplate copy() {
AbstractReactiveElasticsearchTemplate copy = doCopy();
copy.setRefreshPolicy(refreshPolicy);
if (entityCallbacks != null) {
copy.setEntityCallbacks(entityCallbacks);
}
copy.setRoutingResolver(routingResolver);
return copy;
}
abstract protected AbstractReactiveElasticsearchTemplate doCopy();
private ElasticsearchConverter createElasticsearchConverter() {
MappingElasticsearchConverter mappingElasticsearchConverter = new MappingElasticsearchConverter(
new SimpleElasticsearchMappingContext());
mappingElasticsearchConverter.afterPropertiesSet();
return mappingElasticsearchConverter;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (entityCallbacks == null) {
setEntityCallbacks(ReactiveEntityCallbacks.create(applicationContext));
}
}
/**
* Set the default {@link RefreshPolicy} to apply when writing to Elasticsearch.
*
* @param refreshPolicy can be {@literal null}.
*/
public void setRefreshPolicy(@Nullable RefreshPolicy refreshPolicy) {
this.refreshPolicy = refreshPolicy;
}
/**
* @return the current {@link RefreshPolicy}.
*/
@Nullable
public RefreshPolicy getRefreshPolicy() {
return refreshPolicy;
}
/**
* Set the {@link ReactiveEntityCallbacks} instance to use when invoking {@link ReactiveEntityCallbacks callbacks}
* like the {@link ReactiveBeforeConvertCallback}.
* <p />
* Overrides potentially existing {@link ReactiveEntityCallbacks}.
*
* @param entityCallbacks must not be {@literal null}.
* @throws IllegalArgumentException if the given instance is {@literal null}.
* @since 4.0
*/
public void setEntityCallbacks(ReactiveEntityCallbacks entityCallbacks) {
Assert.notNull(entityCallbacks, "EntityCallbacks must not be null!");
this.entityCallbacks = entityCallbacks;
}
/**
* logs the versions of the different Elasticsearch components.
*
* @return a Mono signalling finished execution
* @since 4.3
*/
public Mono<Void> logVersions() {
return getVendor() //
.zipWith(getRuntimeLibraryVersion()) //
.zipWith(getClusterVersion()) //
.doOnNext(objects -> {
VersionInfo.logVersions(objects.getT1().getT1(), objects.getT1().getT2(), objects.getT2());
}).then();
}
// endregion
// region routing
private void setRoutingResolver(RoutingResolver routingResolver) {
Assert.notNull(routingResolver, "routingResolver must not be null");
this.routingResolver = routingResolver;
}
@Override
public ReactiveElasticsearchOperations withRouting(RoutingResolver routingResolver) {
Assert.notNull(routingResolver, "routingResolver must not be null");
AbstractReactiveElasticsearchTemplate copy = copy();
copy.setRoutingResolver(routingResolver);
return copy;
}
// endregion
// region DocumentOperations
@Override
public <T> Mono<T> save(T entity) {
return save(entity, getIndexCoordinatesFor(entity.getClass()));
}
@Override
public <T> Flux<T> saveAll(Mono<? extends Collection<? extends T>> entities, Class<T> clazz) {
return saveAll(entities, getIndexCoordinatesFor(clazz));
}
protected IndexQuery getIndexQuery(Object value) {
EntityOperations.AdaptableEntity<?> entity = entityOperations.forEntity(value, converter.getConversionService(),
routingResolver);
Object id = entity.getId();
IndexQuery query = new IndexQuery();
if (id != null) {
query.setId(id.toString());
}
query.setObject(value);
boolean usingSeqNo = false;
if (entity.hasSeqNoPrimaryTerm()) {
SeqNoPrimaryTerm seqNoPrimaryTerm = entity.getSeqNoPrimaryTerm();
if (seqNoPrimaryTerm != null) {
query.setSeqNo(seqNoPrimaryTerm.getSequenceNumber());
query.setPrimaryTerm(seqNoPrimaryTerm.getPrimaryTerm());
usingSeqNo = true;
}
}
// seq_no and version are incompatible in the same request
if (!usingSeqNo && entity.isVersionedEntity()) {
Number version = entity.getVersion();
if (version != null) {
query.setVersion(version.longValue());
}
}
query.setRouting(entity.getRouting());
return query;
}
protected <T> T updateIndexedObject(T entity, IndexedObjectInformation indexedObjectInformation) {
ElasticsearchPersistentEntity<?> persistentEntity = converter.getMappingContext()
.getPersistentEntity(entity.getClass());
if (persistentEntity != null) {
PersistentPropertyAccessor<Object> propertyAccessor = persistentEntity.getPropertyAccessor(entity);
ElasticsearchPersistentProperty idProperty = persistentEntity.getIdProperty();
// Only deal with text because ES generated Ids are strings!
if (indexedObjectInformation.getId() != null && idProperty != null
&& idProperty.getType().isAssignableFrom(String.class)) {
propertyAccessor.setProperty(idProperty, indexedObjectInformation.getId());
}
if (indexedObjectInformation.getSeqNo() != null && indexedObjectInformation.getPrimaryTerm() != null
&& persistentEntity.hasSeqNoPrimaryTermProperty()) {
ElasticsearchPersistentProperty seqNoPrimaryTermProperty = persistentEntity.getSeqNoPrimaryTermProperty();
// noinspection ConstantConditions
propertyAccessor.setProperty(seqNoPrimaryTermProperty,
new SeqNoPrimaryTerm(indexedObjectInformation.getSeqNo(), indexedObjectInformation.getPrimaryTerm()));
}
if (indexedObjectInformation.getVersion() != null && persistentEntity.hasVersionProperty()) {
ElasticsearchPersistentProperty versionProperty = persistentEntity.getVersionProperty();
// noinspection ConstantConditions
propertyAccessor.setProperty(versionProperty, indexedObjectInformation.getVersion());
}
// noinspection unchecked
T updatedEntity = (T) propertyAccessor.getBean();
return updatedEntity;
} else {
EntityOperations.AdaptableEntity<T> adaptableEntity = entityOperations.forEntity(entity,
converter.getConversionService(), routingResolver);
adaptableEntity.populateIdIfNecessary(indexedObjectInformation.getId());
}
return entity;
}
@Override
public <T> Flux<MultiGetItem<T>> multiGet(Query query, Class<T> clazz) {
return multiGet(query, clazz, getIndexCoordinatesFor(clazz));
}
@Override
public Mono<Boolean> exists(String id, Class<?> entityType) {
return doExists(id, getIndexCoordinatesFor(entityType));
}
@Override
public Mono<Boolean> exists(String id, IndexCoordinates index) {
return doExists(id, index);
}
@Override
public <T> Mono<T> save(T entity, IndexCoordinates index) {
Assert.notNull(entity, "Entity must not be null!");
Assert.notNull(index, "index must not be null");
return maybeCallBeforeConvert(entity, index)
.flatMap(entityAfterBeforeConversionCallback -> doIndex(entityAfterBeforeConversionCallback, index)) //
.map(it -> {
T savedEntity = it.getT1();
IndexResponseMetaData indexResponseMetaData = it.getT2();
return updateIndexedObject(savedEntity, IndexedObjectInformation.of( //
indexResponseMetaData.getId(), //
indexResponseMetaData.getSeqNo(), //
indexResponseMetaData.getPrimaryTerm(), //
indexResponseMetaData.getVersion()));
}).flatMap(saved -> maybeCallAfterSave(saved, index));
}
abstract protected <T> Mono<Tuple2<T, IndexResponseMetaData>> doIndex(T entity, IndexCoordinates index);
abstract protected Mono<Boolean> doExists(String id, IndexCoordinates index);
@Override
public <T> Mono<T> get(String id, Class<T> entityType) {
return get(id, entityType, getIndexCoordinatesFor(entityType));
}
@Override
public Mono<String> delete(Object entity, IndexCoordinates index) {
EntityOperations.AdaptableEntity<?> elasticsearchEntity = entityOperations.forEntity(entity,
converter.getConversionService(), routingResolver);
if (elasticsearchEntity.getId() == null) {
return Mono.error(new IllegalArgumentException("entity must have an id"));
}
return Mono.defer(() -> {
String id = converter.convertId(elasticsearchEntity.getId());
String routing = elasticsearchEntity.getRouting();
return doDeleteById(id, routing, index);
});
}
@Override
public Mono<String> delete(Object entity) {
return delete(entity, getIndexCoordinatesFor(entity.getClass()));
}
@Override
public Mono<String> delete(String id, Class<?> entityType) {
Assert.notNull(id, "id must not be null");
Assert.notNull(entityType, "entityType must not be null");
return delete(id, getIndexCoordinatesFor(entityType));
}
@Override
public Mono<String> delete(String id, IndexCoordinates index) {
Assert.notNull(id, "id must not be null");
Assert.notNull(index, "index must not be null");
return doDeleteById(id, routingResolver.getRouting(), index);
}
abstract protected Mono<String> doDeleteById(String id, @Nullable String routing, IndexCoordinates index);
@Override
public Mono<ByQueryResponse> delete(Query query, Class<?> entityType) {
return delete(query, entityType, getIndexCoordinatesFor(entityType));
}
// endregion
// region SearchDocument
@Override
public <T> Flux<SearchHit<T>> search(Query query, Class<?> entityType, Class<T> resultType, IndexCoordinates index) {
SearchDocumentCallback<T> callback = new ReadSearchDocumentCallback<>(resultType, index);
return doFind(query, entityType, index).concatMap(callback::toSearchHit);
}
@Override
public <T> Flux<SearchHit<T>> search(Query query, Class<?> entityType, Class<T> returnType) {
return search(query, entityType, returnType, getIndexCoordinatesFor(entityType));
}
@Override
public <T> Mono<SearchPage<T>> searchForPage(Query query, Class<?> entityType, Class<T> resultType) {
return searchForPage(query, entityType, resultType, getIndexCoordinatesFor(entityType));
}
@Override
public <T> Mono<SearchPage<T>> searchForPage(Query query, Class<?> entityType, Class<T> resultType,
IndexCoordinates index) {
SearchDocumentCallback<T> callback = new ReadSearchDocumentCallback<>(resultType, index);
return doFindForResponse(query, entityType, index) //
.flatMap(searchDocumentResponse -> Flux.fromIterable(searchDocumentResponse.getSearchDocuments()) //
.flatMap(callback::toEntity) //
.collectList() //
.map(entities -> SearchHitMapping.mappingFor(resultType, converter) //
.mapHits(searchDocumentResponse, entities))) //
.map(searchHits -> SearchHitSupport.searchPageFor(searchHits, query.getPageable()));
}
@Override
public <T> Mono<ReactiveSearchHits<T>> searchForHits(Query query, Class<?> entityType, Class<T> resultType) {
return searchForHits(query, entityType, resultType, getIndexCoordinatesFor(entityType));
}
@Override
public <T> Mono<ReactiveSearchHits<T>> searchForHits(Query query, Class<?> entityType, Class<T> resultType,
IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
Assert.notNull(entityType, "entityType must not be null");
Assert.notNull(resultType, "resultType must not be null");
Assert.notNull(index, "index must not be null");
SearchDocumentCallback<T> callback = new ReadSearchDocumentCallback<>(resultType, index);
return doFindForResponse(query, entityType, index) //
.flatMap(searchDocumentResponse -> Flux.fromIterable(searchDocumentResponse.getSearchDocuments()) //
.flatMap(callback::toEntity) //
.collectList() //
.map(entities -> SearchHitMapping.mappingFor(resultType, converter) //
.mapHits(searchDocumentResponse, entities))) //
.map(ReactiveSearchHitSupport::searchHitsFor);
}
abstract protected Flux<SearchDocument> doFind(Query query, Class<?> clazz, IndexCoordinates index);
abstract protected <T> Mono<SearchDocumentResponse> doFindForResponse(Query query, Class<?> clazz,
IndexCoordinates index);
@Override
public Flux<AggregationContainer<?>> aggregate(Query query, Class<?> entityType) {
return aggregate(query, entityType, getIndexCoordinatesFor(entityType));
}
@Override
public Mono<Suggest> suggest(Query query, Class<?> entityType) {
return suggest(query, entityType, getIndexCoordinatesFor(entityType));
}
@Override
public Mono<Suggest> suggest(Query query, Class<?> entityType, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
Assert.notNull(entityType, "entityType must not be null");
Assert.notNull(index, "index must not be null");
return doFindForResponse(query, entityType, index).mapNotNull(searchDocumentResponse -> {
Suggest suggest = searchDocumentResponse.getSuggest();
SearchHitMapping.mappingFor(entityType, converter).mapHitsInCompletionSuggestion(suggest);
return suggest;
});
}
@Override
public Mono<Long> count(Query query, Class<?> entityType) {
return count(query, entityType, getIndexCoordinatesFor(entityType));
}
@Override
public Mono<Long> count(Query query, Class<?> entityType, IndexCoordinates index) {
return doCount(query, entityType, index);
}
abstract protected Mono<Long> doCount(Query query, Class<?> entityType, IndexCoordinates index);
// endregion
// region callbacks
protected <T> Mono<T> maybeCallBeforeConvert(T entity, IndexCoordinates index) {
if (null != entityCallbacks) {
return entityCallbacks.callback(ReactiveBeforeConvertCallback.class, entity, index);
}
return Mono.just(entity);
}
protected <T> Mono<T> maybeCallAfterSave(T entity, IndexCoordinates index) {
if (null != entityCallbacks) {
return entityCallbacks.callback(ReactiveAfterSaveCallback.class, entity, index);
}
return Mono.just(entity);
}
protected <T> Mono<T> maybeCallAfterConvert(T entity, Document document, IndexCoordinates index) {
if (null != entityCallbacks) {
return entityCallbacks.callback(ReactiveAfterConvertCallback.class, entity, document, index);
}
return Mono.just(entity);
}
protected <T> Mono<Document> maybeCallbackAfterLoad(Document document, Class<T> type, IndexCoordinates index) {
if (entityCallbacks != null) {
return entityCallbacks.callback(ReactiveAfterLoadCallback.class, document, type, index);
}
return Mono.just(document);
}
protected interface DocumentCallback<T> {
@NonNull
Mono<T> toEntity(@Nullable Document document);
}
protected class ReadDocumentCallback<T> implements DocumentCallback<T> {
private final EntityReader<? super T, Document> reader;
private final Class<T> type;
private final IndexCoordinates index;
public ReadDocumentCallback(EntityReader<? super T, Document> reader, Class<T> type, IndexCoordinates index) {
Assert.notNull(reader, "reader is null");
Assert.notNull(type, "type is null");
this.reader = reader;
this.type = type;
this.index = index;
}
@NonNull
public Mono<T> toEntity(@Nullable Document document) {
if (document == null) {
return Mono.empty();
}
return maybeCallbackAfterLoad(document, type, index) //
.flatMap(documentAfterLoad -> {
T entity = reader.read(type, documentAfterLoad);
IndexedObjectInformation indexedObjectInformation = IndexedObjectInformation.of( //
documentAfterLoad.hasId() ? documentAfterLoad.getId() : null, //
documentAfterLoad.getSeqNo(), //
documentAfterLoad.getPrimaryTerm(), //
documentAfterLoad.getVersion()); //
entity = updateIndexedObject(entity, indexedObjectInformation);
return maybeCallAfterConvert(entity, documentAfterLoad, index);
});
}
}
protected interface SearchDocumentCallback<T> {
Mono<T> toEntity(SearchDocument response);
Mono<SearchHit<T>> toSearchHit(SearchDocument response);
}
protected class ReadSearchDocumentCallback<T> implements SearchDocumentCallback<T> {
private final DocumentCallback<T> delegate;
private final Class<T> type;
public ReadSearchDocumentCallback(Class<T> type, IndexCoordinates index) {
Assert.notNull(type, "type is null");
this.delegate = new ReadDocumentCallback<>(converter, type, index);
this.type = type;
}
@Override
public Mono<T> toEntity(SearchDocument response) {
return delegate.toEntity(response);
}
@Override
public Mono<SearchHit<T>> toSearchHit(SearchDocument response) {
return toEntity(response).map(entity -> SearchHitMapping.mappingFor(type, converter).mapHit(response, entity));
}
}
// endregion
// region Helper methods
@Override
public IndexCoordinates getIndexCoordinatesFor(Class<?> clazz) {
ElasticsearchPersistentEntity<?> persistentEntity = getPersistentEntityFor(clazz);
Assert.notNull(persistentEntity, "could not get indexCoordinates for class " + clazz.getName());
return persistentEntity.getIndexCoordinates();
}
@Override
@Nullable
public ElasticsearchPersistentEntity<?> getPersistentEntityFor(@Nullable Class<?> type) {
return type != null ? mappingContext.getPersistentEntity(type) : null;
}
/**
* @return the vendor name of the used cluster and client library
* @since 4.3
*/
abstract protected Mono<String> getVendor();
/**
* @return the version of the used client runtime library.
* @since 4.3
*/
abstract protected Mono<String> getRuntimeLibraryVersion();
abstract protected Mono<String> getClusterVersion();
/**
* Value class to capture client independent information from a response to an index request.
*/
public static class IndexResponseMetaData {
private final String id;
private final long seqNo;
private final long primaryTerm;
private final long version;
public IndexResponseMetaData(String id, long seqNo, long primaryTerm, long version) {
this.id = id;
this.seqNo = seqNo;
this.primaryTerm = primaryTerm;
this.version = version;
}
public String getId() {
return id;
}
public long getSeqNo() {
return seqNo;
}
public long getPrimaryTerm() {
return primaryTerm;
}
public long getVersion() {
return version;
}
}
// endregion
protected class Entities<T> {
private final List<T> entities;
public Entities(List<T> entities) {
Assert.notNull(entities, "entities cannot be null");
this.entities = entities;
}
public boolean isEmpty() {
return entities.isEmpty();
}
public List<IndexQuery> indexQueries() {
return entities.stream().map(value -> getIndexQuery(value)).collect(Collectors.toList());
}
public T entityAt(long index) {
// it's safe to cast to int because the original indexed collection was fitting in memory
int intIndex = (int) index;
return entities.get(intIndex);
}
}
}

View File

@ -44,7 +44,6 @@ import org.springframework.util.Assert;
* @author Rasmus Faber-Espensen
* @author James Bodkin
* @author Peter-Josef Meisch
* @since 4.4
*/
class CriteriaQueryProcessor {

View File

@ -12,7 +12,7 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
*/
package org.springframework.data.elasticsearch.core;
import java.io.IOException;
@ -21,7 +21,6 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -62,7 +61,7 @@ import org.springframework.data.elasticsearch.core.cluster.ClusterOperations;
import org.springframework.data.elasticsearch.core.cluster.ElasticsearchClusterOperations;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.document.DocumentAdapters;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponseBuilder;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.BulkOptions;
import org.springframework.data.elasticsearch.core.query.ByQueryResponse;
@ -118,13 +117,16 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate {
private final RestHighLevelClient client;
private final ElasticsearchExceptionTranslator exceptionTranslator = new ElasticsearchExceptionTranslator();
protected RequestFactory requestFactory;
// region Initialization
// region _initialization
public ElasticsearchRestTemplate(RestHighLevelClient client) {
Assert.notNull(client, "Client must not be null!");
this.client = client;
requestFactory = new RequestFactory(this.elasticsearchConverter);
}
public ElasticsearchRestTemplate(RestHighLevelClient client, ElasticsearchConverter elasticsearchConverter) {
@ -134,12 +136,23 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate {
Assert.notNull(client, "Client must not be null!");
this.client = client;
requestFactory = new RequestFactory(this.elasticsearchConverter);
}
@Override
protected AbstractElasticsearchTemplate doCopy() {
return new ElasticsearchRestTemplate(client, elasticsearchConverter);
ElasticsearchRestTemplate copy = new ElasticsearchRestTemplate(client, elasticsearchConverter);
copy.requestFactory = this.requestFactory;
return copy;
}
/**
* @since 4.0
*/
public RequestFactory getRequestFactory() {
return requestFactory;
}
// endregion
// region IndexOperations
@ -282,22 +295,23 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate {
}
@Override
public ReindexResponse reindex(ReindexRequest postReindexRequest) {
public ReindexResponse reindex(ReindexRequest reindexRequest) {
Assert.notNull(postReindexRequest, "postReindexRequest must not be null");
Assert.notNull(reindexRequest, "reindexRequest must not be null");
org.elasticsearch.index.reindex.ReindexRequest reindexRequest = requestFactory.reindexRequest(postReindexRequest);
org.elasticsearch.index.reindex.ReindexRequest reindexRequestES = requestFactory.reindexRequest(reindexRequest);
BulkByScrollResponse bulkByScrollResponse = execute(
client -> client.reindex(reindexRequest, RequestOptions.DEFAULT));
client -> client.reindex(reindexRequestES, RequestOptions.DEFAULT));
return ResponseConverter.reindexResponseOf(bulkByScrollResponse);
}
@Override
public String submitReindex(ReindexRequest postReindexRequest) {
Assert.notNull(postReindexRequest, "postReindexRequest must not be null");
public String submitReindex(ReindexRequest reindexRequest) {
org.elasticsearch.index.reindex.ReindexRequest reindexRequest = requestFactory.reindexRequest(postReindexRequest);
return execute(client -> client.submitReindexTask(reindexRequest, RequestOptions.DEFAULT).getTask());
Assert.notNull(reindexRequest, "reindexRequest must not be null");
org.elasticsearch.index.reindex.ReindexRequest reindexRequestES = requestFactory.reindexRequest(reindexRequest);
return execute(client -> client.submitReindexTask(reindexRequestES, RequestOptions.DEFAULT).getTask());
}
public List<IndexedObjectInformation> doBulkOperation(List<?> queries, BulkOptions bulkOptions,
@ -387,7 +401,7 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate {
ReadDocumentCallback<T> documentCallback = new ReadDocumentCallback<>(elasticsearchConverter, clazz, index);
SearchDocumentResponseCallback<SearchHits<T>> callback = new ReadSearchDocumentResponseCallback<>(clazz, index);
return callback.doWith(SearchDocumentResponse.from(response, getEntityCreator(documentCallback)));
return callback.doWith(SearchDocumentResponseBuilder.from(response, getEntityCreator(documentCallback)));
}
protected <T> SearchHits<T> doSearch(MoreLikeThisQuery query, Class<T> clazz, IndexCoordinates index) {
@ -411,12 +425,12 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate {
ReadDocumentCallback<T> documentCallback = new ReadDocumentCallback<>(elasticsearchConverter, clazz, index);
SearchDocumentResponseCallback<SearchScrollHits<T>> callback = new ReadSearchScrollDocumentResponseCallback<>(clazz,
index);
return callback.doWith(SearchDocumentResponse.from(response, getEntityCreator(documentCallback)));
return callback.doWith(SearchDocumentResponseBuilder.from(response, getEntityCreator(documentCallback)));
}
@Override
public <T> SearchScrollHits<T> searchScrollContinue(@Nullable String scrollId, long scrollTimeInMillis,
Class<T> clazz, IndexCoordinates index) {
public <T> SearchScrollHits<T> searchScrollContinue(String scrollId, long scrollTimeInMillis, Class<T> clazz,
IndexCoordinates index) {
SearchScrollRequest request = new SearchScrollRequest(scrollId);
request.scroll(TimeValue.timeValueMillis(scrollTimeInMillis));
@ -426,7 +440,7 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate {
ReadDocumentCallback<T> documentCallback = new ReadDocumentCallback<>(elasticsearchConverter, clazz, index);
SearchDocumentResponseCallback<SearchScrollHits<T>> callback = new ReadSearchScrollDocumentResponseCallback<>(clazz,
index);
return callback.doWith(SearchDocumentResponse.from(response, getEntityCreator(documentCallback)));
return callback.doWith(SearchDocumentResponseBuilder.from(response, getEntityCreator(documentCallback)));
}
@Override
@ -459,7 +473,8 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate {
SearchDocumentResponseCallback<SearchHits<T>> callback = new ReadSearchDocumentResponseCallback<>(clazz, index);
List<SearchHits<T>> res = new ArrayList<>(queries.size());
for (int i = 0; i < queries.size(); i++) {
res.add(callback.doWith(SearchDocumentResponse.from(items[i].getResponse(), getEntityCreator(documentCallback))));
res.add(callback
.doWith(SearchDocumentResponseBuilder.from(items[i].getResponse(), getEntityCreator(documentCallback))));
}
return res;
}
@ -492,7 +507,7 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate {
index);
SearchResponse response = items[i].getResponse();
res.add(callback.doWith(SearchDocumentResponse.from(response, getEntityCreator(documentCallback))));
res.add(callback.doWith(SearchDocumentResponseBuilder.from(response, getEntityCreator(documentCallback))));
}
return res;
}
@ -525,7 +540,7 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate {
index);
SearchResponse response = items[i].getResponse();
res.add(callback.doWith(SearchDocumentResponse.from(response, getEntityCreator(documentCallback))));
res.add(callback.doWith(SearchDocumentResponseBuilder.from(response, getEntityCreator(documentCallback))));
}
return res;
}
@ -537,11 +552,8 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate {
return items;
}
private <T> SearchDocumentResponse.EntityCreator<T> getEntityCreator(ReadDocumentCallback<T> documentCallback) {
return searchDocument -> CompletableFuture.completedFuture(documentCallback.doWith(searchDocument));
}
// endregion
// region ClientCallback
/**
* Callback interface to be used with {@link #execute(ClientCallback)} for operating directly on

View File

@ -73,15 +73,15 @@ public class EntityOperations {
}
/**
* Creates a new {@link AdaptibleEntity} for the given bean and {@link ConversionService} and {@link RoutingResolver}.
* Creates a new {@link AdaptableEntity} for the given bean and {@link ConversionService} and {@link RoutingResolver}.
*
* @param entity must not be {@literal null}.
* @param conversionService must not be {@literal null}.
* @param routingResolver the {@link RoutingResolver}, must not be {@literal null}
* @return the {@link AdaptibleEntity}
* @return the {@link AdaptableEntity}
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public <T> AdaptibleEntity<T> forEntity(T entity, ConversionService conversionService,
public <T> AdaptableEntity<T> forEntity(T entity, ConversionService conversionService,
RoutingResolver routingResolver) {
Assert.notNull(entity, "Bean must not be null!");
@ -91,7 +91,7 @@ public class EntityOperations {
return new SimpleMappedEntity((Map<String, Object>) entity);
}
return AdaptibleMappedEntity.of(entity, context, conversionService, routingResolver);
return AdaptableMappedEntity.of(entity, context, conversionService, routingResolver);
}
/**
@ -199,7 +199,7 @@ public class EntityOperations {
* @author Mark Paluch
* @author Christoph Strobl
*/
public interface AdaptibleEntity<T> extends Entity<T> {
public interface AdaptableEntity<T> extends Entity<T> {
/**
* Populates the identifier of the backing entity if it has an identifier property and there's no identifier
@ -267,7 +267,7 @@ public class EntityOperations {
* @author Christoph Strobl
* @since 3.2
*/
private static class MapBackedEntity<T extends Map<String, Object>> implements AdaptibleEntity<T> {
private static class MapBackedEntity<T extends Map<String, Object>> implements AdaptableEntity<T> {
public MapBackedEntity(T map) {
@ -289,7 +289,7 @@ public class EntityOperations {
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.core.EntityOperations.AdaptibleEntity#populateIdIfNecessary(java.lang.Object)
* @see org.springframework.data.elasticsearch.core.EntityOperations.AdaptableEntity#populateIdIfNecessary(java.lang.Object)
*/
@Nullable
@Override
@ -302,7 +302,7 @@ public class EntityOperations {
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.core.EntityOperations.AdaptibleEntity#initializeVersionProperty()
* @see org.springframework.data.elasticsearch.core.EntityOperations.AdaptableEntity#initializeVersionProperty()
*/
@Override
public T initializeVersionProperty() {
@ -311,7 +311,7 @@ public class EntityOperations {
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.core.EntityOperations.AdaptibleEntity#getVersion()
* @see org.springframework.data.elasticsearch.core.EntityOperations.AdaptableEntity#getVersion()
*/
@Override
@Nullable
@ -331,7 +331,7 @@ public class EntityOperations {
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.core.EntityOperations.AdaptibleEntity#incrementVersion()
* @see org.springframework.data.elasticsearch.core.EntityOperations.AdaptableEntity#incrementVersion()
*/
@Override
public T incrementVersion() {
@ -476,7 +476,7 @@ public class EntityOperations {
* @param <T>
* @since 3.2
*/
private static class AdaptibleMappedEntity<T> extends MappedEntity<T> implements AdaptibleEntity<T> {
private static class AdaptableMappedEntity<T> extends MappedEntity<T> implements AdaptableEntity<T> {
private final ElasticsearchPersistentEntity<?> entity;
private final ConvertingPropertyAccessor<T> propertyAccessor;
@ -484,7 +484,7 @@ public class EntityOperations {
private final ConversionService conversionService;
private final RoutingResolver routingResolver;
private AdaptibleMappedEntity(T bean, ElasticsearchPersistentEntity<?> entity,
private AdaptableMappedEntity(T bean, ElasticsearchPersistentEntity<?> entity,
IdentifierAccessor identifierAccessor, ConvertingPropertyAccessor<T> propertyAccessor,
ConversionService conversionService, RoutingResolver routingResolver) {
@ -497,7 +497,7 @@ public class EntityOperations {
this.routingResolver = routingResolver;
}
static <T> AdaptibleEntity<T> of(T bean,
static <T> AdaptableEntity<T> of(T bean,
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> context,
ConversionService conversionService, RoutingResolver routingResolver) {
@ -505,7 +505,7 @@ public class EntityOperations {
IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(bean);
PersistentPropertyAccessor<T> propertyAccessor = entity.getPropertyAccessor(bean);
return new AdaptibleMappedEntity<>(bean, entity, identifierAccessor,
return new AdaptableMappedEntity<>(bean, entity, identifierAccessor,
new ConvertingPropertyAccessor<>(propertyAccessor, conversionService), conversionService, routingResolver);
}

View File

@ -228,8 +228,7 @@ public interface IndexOperations {
boolean putTemplate(PutTemplateRequest putTemplateRequest);
/**
* gets an index template using the legacy Elasticsearch
* interface/Users/peter/Entwicklung/Projekte/spring-data-elasticsearch/src/main/java/org/springframework/data/elasticsearch/core/IndexOperations.java.
* gets an index template using the legacy Elasticsearch interface.
*
* @param templateName the template name
* @return TemplateData, {@literal null} if no template with the given name exists.
@ -241,8 +240,7 @@ public interface IndexOperations {
}
/**
* gets an index template using the legacy Elasticsearch
* interface/Users/peter/Entwicklung/Projekte/spring-data-elasticsearch/src/main/java/org/springframework/data/elasticsearch/core/IndexOperations.java.
* gets an index template using the legacy Elasticsearch interface.
*
* @param getTemplateRequest the request parameters
* @return TemplateData, {@literal null} if no template with the given name exists.
@ -252,8 +250,7 @@ public interface IndexOperations {
TemplateData getTemplate(GetTemplateRequest getTemplateRequest);
/**
* check if an index template exists using the legacy Elasticsearch
* interface/Users/peter/Entwicklung/Projekte/spring-data-elasticsearch/src/main/java/org/springframework/data/elasticsearch/core/IndexOperations.java.
* check if an index template exists using the legacy Elasticsearch interface.
*
* @param templateName the template name
* @return {@literal true} if the index exists
@ -264,8 +261,7 @@ public interface IndexOperations {
}
/**
* check if an index template exists using the legacy Elasticsearch
* interface/Users/peter/Entwicklung/Projekte/spring-data-elasticsearch/src/main/java/org/springframework/data/elasticsearch/core/IndexOperations.java.
* check if an index template exists using the legacy Elasticsearch interface.
*
* @param existsTemplateRequest the request parameters
* @return {@literal true} if the index exists

View File

@ -15,12 +15,13 @@
*/
package org.springframework.data.elasticsearch.core;
import org.springframework.data.elasticsearch.ElasticsearchErrorCause;
import org.springframework.lang.Nullable;
/**
* Response object for items returned from multiget requests, encapsulating the returned data and potential error
* information.
*
*
* @param <T> the entity type
* @author Peter-Josef Meisch
* @since 4.2
@ -61,16 +62,20 @@ public class MultiGetItem<T> {
@Nullable private final String type;
@Nullable private final String id;
@Nullable private final Exception exception;
@Nullable private final ElasticsearchErrorCause elasticsearchErrorCause;
private Failure(@Nullable String index, @Nullable String type, @Nullable String id, @Nullable Exception exception) {
private Failure(@Nullable String index, @Nullable String type, @Nullable String id, @Nullable Exception exception,
@Nullable ElasticsearchErrorCause elasticsearchErrorCause) {
this.index = index;
this.type = type;
this.id = id;
this.exception = exception;
this.elasticsearchErrorCause = elasticsearchErrorCause;
}
public static Failure of(String index, String type, String id, Exception exception) {
return new Failure(index, type, id, exception);
public static Failure of(String index, @Nullable String type, String id, @Nullable Exception exception,
@Nullable ElasticsearchErrorCause elasticsearchErrorCause) {
return new Failure(index, type, id, exception, elasticsearchErrorCause);
}
@Nullable
@ -92,5 +97,10 @@ public class MultiGetItem<T> {
public Exception getException() {
return exception;
}
@Nullable
public ElasticsearchErrorCause getElasticsearchErrorCause() {
return elasticsearchErrorCause;
}
}
}

View File

@ -44,7 +44,9 @@ public interface ReactiveElasticsearchOperations extends ReactiveDocumentOperati
* @param callback must not be {@literal null}.
* @param <T> the type the Publisher emits
* @return the {@link Publisher} emitting results.
* @deprecated since 4.4, use the execute methods from the implementing classes (they are client specific)
*/
@Deprecated
<T> Publisher<T> execute(ClientCallback<Publisher<T>> callback);
/**
@ -126,7 +128,7 @@ public interface ReactiveElasticsearchOperations extends ReactiveDocumentOperati
* Callback interface to be used with {@link #execute(ClientCallback)} for operating directly on
* {@link ReactiveElasticsearchClient}.
*
* @param <T>
* @param <T> result type of the callback
* @author Christoph Strobl
* @since 3.2
*/

View File

@ -23,10 +23,7 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.elasticsearch.Version;
import org.elasticsearch.action.DocWriteResponse;
import org.elasticsearch.action.bulk.BulkItemResponse;
@ -48,49 +45,27 @@ import org.elasticsearch.index.reindex.DeleteByQueryRequest;
import org.elasticsearch.index.reindex.UpdateByQueryRequest;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.reactivestreams.Publisher;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.convert.EntityReader;
import org.springframework.data.elasticsearch.BulkFailureException;
import org.springframework.data.elasticsearch.NoSuchIndexException;
import org.springframework.data.elasticsearch.UncategorizedElasticsearchException;
import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient;
import org.springframework.data.elasticsearch.core.EntityOperations.AdaptibleEntity;
import org.springframework.data.elasticsearch.core.cluster.DefaultReactiveClusterOperations;
import org.springframework.data.elasticsearch.core.cluster.ReactiveClusterOperations;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.document.DocumentAdapters;
import org.springframework.data.elasticsearch.core.document.SearchDocument;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
import org.springframework.data.elasticsearch.core.event.ReactiveAfterConvertCallback;
import org.springframework.data.elasticsearch.core.event.ReactiveAfterLoadCallback;
import org.springframework.data.elasticsearch.core.event.ReactiveAfterSaveCallback;
import org.springframework.data.elasticsearch.core.event.ReactiveBeforeConvertCallback;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponseBuilder;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.elasticsearch.core.query.BulkOptions;
import org.springframework.data.elasticsearch.core.query.ByQueryResponse;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
import org.springframework.data.elasticsearch.core.query.UpdateResponse;
import org.springframework.data.elasticsearch.core.reindex.ReindexRequest;
import org.springframework.data.elasticsearch.core.reindex.ReindexResponse;
import org.springframework.data.elasticsearch.core.routing.DefaultRoutingResolver;
import org.springframework.data.elasticsearch.core.routing.RoutingResolver;
import org.springframework.data.elasticsearch.core.suggest.response.Suggest;
import org.springframework.data.elasticsearch.support.VersionInfo;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.callback.ReactiveEntityCallbacks;
import org.springframework.http.HttpStatus;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@ -109,98 +84,37 @@ import org.springframework.util.Assert;
* @author Sijia Liu
* @since 3.2
*/
public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOperations, ApplicationContextAware {
private static final Log QUERY_LOGGER = LogFactory.getLog("org.springframework.data.elasticsearch.core.QUERY");
public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearchTemplate {
private final ReactiveElasticsearchClient client;
private final ElasticsearchConverter converter;
private final SimpleElasticsearchMappingContext mappingContext;
private final ElasticsearchExceptionTranslator exceptionTranslator;
private final EntityOperations operations;
protected RequestFactory requestFactory;
private @Nullable RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE;
private @Nullable IndicesOptions indicesOptions = IndicesOptions.strictExpandOpenAndForbidClosedIgnoreThrottled();
private @Nullable ReactiveEntityCallbacks entityCallbacks;
private RoutingResolver routingResolver;
// region Initialization
public ReactiveElasticsearchTemplate(ReactiveElasticsearchClient client) {
this(client, new MappingElasticsearchConverter(new SimpleElasticsearchMappingContext()));
this(client, null);
}
public ReactiveElasticsearchTemplate(ReactiveElasticsearchClient client, ElasticsearchConverter converter) {
public ReactiveElasticsearchTemplate(ReactiveElasticsearchClient client, @Nullable ElasticsearchConverter converter) {
super(converter);
Assert.notNull(client, "client must not be null");
Assert.notNull(converter, "converter must not be null");
this.client = client;
this.converter = converter;
this.mappingContext = (SimpleElasticsearchMappingContext) converter.getMappingContext();
this.routingResolver = new DefaultRoutingResolver(this.mappingContext);
this.exceptionTranslator = new ElasticsearchExceptionTranslator();
this.operations = new EntityOperations(this.mappingContext);
this.requestFactory = new RequestFactory(converter);
// initialize the VersionInfo class in the initialization phase
// noinspection ResultOfMethodCallIgnored
VersionInfo.versionProperties();
this.requestFactory = new RequestFactory(this.converter);
}
private ReactiveElasticsearchTemplate copy() {
protected ReactiveElasticsearchTemplate doCopy() {
ReactiveElasticsearchTemplate copy = new ReactiveElasticsearchTemplate(client, converter);
copy.setRefreshPolicy(refreshPolicy);
copy.setIndicesOptions(indicesOptions);
copy.setEntityCallbacks(entityCallbacks);
copy.setRoutingResolver(routingResolver);
return copy;
}
/**
* logs the versions of the different Elasticsearch components.
*
* @return a Mono signalling finished execution
* @since 4.3
*/
public Mono<Void> logVersions() {
return getVendor() //
.doOnNext(vendor -> getRuntimeLibraryVersion() //
.doOnNext(runtimeLibraryVersion -> getClusterVersion() //
.doOnNext(clusterVersion -> VersionInfo.logVersions(vendor, runtimeLibraryVersion, clusterVersion)))) //
.then(); //
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (entityCallbacks == null) {
setEntityCallbacks(ReactiveEntityCallbacks.create(applicationContext));
}
}
/**
* Set the default {@link RefreshPolicy} to apply when writing to Elasticsearch.
*
* @param refreshPolicy can be {@literal null}.
*/
public void setRefreshPolicy(@Nullable RefreshPolicy refreshPolicy) {
this.refreshPolicy = refreshPolicy;
}
/**
* @return the current {@link RefreshPolicy}.
*/
@Nullable
public RefreshPolicy getRefreshPolicy() {
return refreshPolicy;
}
/**
* Set the default {@link IndicesOptions} for {@link SearchRequest search requests}.
*
@ -210,58 +124,14 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
this.indicesOptions = indicesOptions;
}
/**
* Set the {@link ReactiveEntityCallbacks} instance to use when invoking {@link ReactiveEntityCallbacks callbacks}
* like the {@link ReactiveBeforeConvertCallback}.
* <p />
* Overrides potentially existing {@link ReactiveEntityCallbacks}.
*
* @param entityCallbacks must not be {@literal null}.
* @throws IllegalArgumentException if the given instance is {@literal null}.
* @since 4.0
*/
public void setEntityCallbacks(ReactiveEntityCallbacks entityCallbacks) {
Assert.notNull(entityCallbacks, "EntityCallbacks must not be null!");
this.entityCallbacks = entityCallbacks;
}
// endregion
// region DocumentOperations
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.core.ReactiveDElasticsearchOperations#index(Object, IndexCoordinates)
*/
@Override
public <T> Mono<T> save(T entity, IndexCoordinates index) {
Assert.notNull(entity, "Entity must not be null!");
return maybeCallBeforeConvert(entity, index)
.flatMap(entityAfterBeforeConversionCallback -> doIndex(entityAfterBeforeConversionCallback, index)) //
.map(it -> {
T savedEntity = it.getT1();
IndexResponse indexResponse = it.getT2();
return updateIndexedObject(savedEntity, IndexedObjectInformation.of(indexResponse.getId(),
indexResponse.getSeqNo(), indexResponse.getPrimaryTerm(), indexResponse.getVersion()));
}).flatMap(saved -> maybeCallAfterSave(saved, index));
}
@Override
public <T> Mono<T> save(T entity) {
return save(entity, getIndexCoordinatesFor(entity.getClass()));
}
@Override
public <T> Flux<T> saveAll(Mono<? extends Collection<? extends T>> entities, Class<T> clazz) {
return saveAll(entities, getIndexCoordinatesFor(clazz));
}
@Override
public <T> Flux<T> saveAll(Mono<? extends Collection<? extends T>> entitiesPublisher, IndexCoordinates index) {
Assert.notNull(entitiesPublisher, "Entities must not be null!");
Assert.notNull(entitiesPublisher, "entitiesPublisher must not be null!");
return entitiesPublisher //
.flatMapMany(entities -> Flux.fromIterable(entities) //
@ -275,7 +145,8 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
}
return doBulkOperation(entities.indexQueries(), BulkOptions.defaultOptions(), index) //
.index().flatMap(indexAndResponse -> {
.index() //
.flatMap(indexAndResponse -> {
T savedEntity = entities.entityAt(indexAndResponse.getT1());
BulkItemResponse bulkItemResponse = indexAndResponse.getT2();
@ -288,51 +159,6 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
});
}
private <T> T updateIndexedObject(T entity, IndexedObjectInformation indexedObjectInformation) {
ElasticsearchPersistentEntity<?> persistentEntity = converter.getMappingContext()
.getPersistentEntity(entity.getClass());
if (persistentEntity != null) {
PersistentPropertyAccessor<Object> propertyAccessor = persistentEntity.getPropertyAccessor(entity);
ElasticsearchPersistentProperty idProperty = persistentEntity.getIdProperty();
// Only deal with text because ES generated Ids are strings!
if (indexedObjectInformation.getId() != null && idProperty != null
&& idProperty.getType().isAssignableFrom(String.class)) {
propertyAccessor.setProperty(idProperty, indexedObjectInformation.getId());
}
if (indexedObjectInformation.getSeqNo() != null && indexedObjectInformation.getPrimaryTerm() != null
&& persistentEntity.hasSeqNoPrimaryTermProperty()) {
ElasticsearchPersistentProperty seqNoPrimaryTermProperty = persistentEntity.getSeqNoPrimaryTermProperty();
// noinspection ConstantConditions
propertyAccessor.setProperty(seqNoPrimaryTermProperty,
new SeqNoPrimaryTerm(indexedObjectInformation.getSeqNo(), indexedObjectInformation.getPrimaryTerm()));
}
if (indexedObjectInformation.getVersion() != null && persistentEntity.hasVersionProperty()) {
ElasticsearchPersistentProperty versionProperty = persistentEntity.getVersionProperty();
// noinspection ConstantConditions
propertyAccessor.setProperty(versionProperty, indexedObjectInformation.getVersion());
}
// noinspection unchecked
T updatedEntity = (T) propertyAccessor.getBean();
return updatedEntity;
} else {
AdaptibleEntity<T> adaptibleEntity = operations.forEntity(entity, converter.getConversionService(),
routingResolver);
adaptibleEntity.populateIdIfNecessary(indexedObjectInformation.getId());
}
return entity;
}
@Override
public <T> Flux<MultiGetItem<T>> multiGet(Query query, Class<T> clazz) {
return multiGet(query, clazz, getIndexCoordinatesFor(clazz));
}
@Override
public <T> Flux<MultiGetItem<T>> multiGet(Query query, Class<T> clazz, IndexCoordinates index) {
@ -352,16 +178,6 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
);
}
@Override
public Mono<Void> bulkUpdate(List<UpdateQuery> queries, BulkOptions bulkOptions, IndexCoordinates index) {
Assert.notNull(queries, "List of UpdateQuery must not be null");
Assert.notNull(bulkOptions, "BulkOptions must not be null");
Assert.notNull(index, "Index must not be null");
return doBulkOperation(queries, bulkOptions, index).then();
}
/**
* Customization hook on the actual execution result {@link Publisher}. <br />
* You know what you're doing here? Well fair enough, go ahead on your own risk.
@ -373,7 +189,18 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
return Mono.from(execute(client -> client.index(request)));
}
@Override
public Mono<Void> bulkUpdate(List<UpdateQuery> queries, BulkOptions bulkOptions, IndexCoordinates index) {
Assert.notNull(queries, "List of UpdateQuery must not be null");
Assert.notNull(bulkOptions, "BulkOptions must not be null");
Assert.notNull(index, "Index must not be null");
return doBulkOperation(queries, bulkOptions, index).then();
}
protected Flux<BulkItemResponse> doBulkOperation(List<?> queries, BulkOptions bulkOptions, IndexCoordinates index) {
BulkRequest bulkRequest = prepareWriteRequest(requestFactory.bulkRequest(queries, bulkOptions, index));
return client.bulk(bulkRequest) //
.onErrorMap(e -> new UncategorizedElasticsearchException("Error while bulk for request: " + bulkRequest, e)) //
@ -401,17 +228,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
}
}
@Override
public Mono<Boolean> exists(String id, Class<?> entityType) {
return doExists(id, getIndexCoordinatesFor(entityType));
}
@Override
public Mono<Boolean> exists(String id, IndexCoordinates index) {
return doExists(id, index);
}
private Mono<Boolean> doExists(String id, IndexCoordinates index) {
protected Mono<Boolean> doExists(String id, IndexCoordinates index) {
return Mono.defer(() -> doExists(requestFactory.getRequest(id, routingResolver.getRouting(), index)));
}
@ -427,54 +244,17 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
.onErrorReturn(NoSuchIndexException.class, false);
}
private <T> Mono<Tuple2<T, IndexResponse>> doIndex(T entity, IndexCoordinates index) {
protected <T> Mono<Tuple2<T, IndexResponseMetaData>> doIndex(T entity, IndexCoordinates index) {
IndexRequest request = requestFactory.indexRequest(getIndexQuery(entity), index);
request = prepareIndexRequest(entity, request);
return Mono.just(entity).zipWith(doIndex(request));
}
private IndexQuery getIndexQuery(Object value) {
AdaptibleEntity<?> entity = operations.forEntity(value, converter.getConversionService(), routingResolver);
Object id = entity.getId();
IndexQuery query = new IndexQuery();
if (id != null) {
query.setId(id.toString());
}
query.setObject(value);
boolean usingSeqNo = false;
if (entity.hasSeqNoPrimaryTerm()) {
SeqNoPrimaryTerm seqNoPrimaryTerm = entity.getSeqNoPrimaryTerm();
if (seqNoPrimaryTerm != null) {
query.setSeqNo(seqNoPrimaryTerm.getSequenceNumber());
query.setPrimaryTerm(seqNoPrimaryTerm.getPrimaryTerm());
usingSeqNo = true;
}
}
// seq_no and version are incompatible in the same request
if (!usingSeqNo && entity.isVersionedEntity()) {
Number version = entity.getVersion();
if (version != null) {
query.setVersion(version.longValue());
}
}
query.setRouting(entity.getRouting());
return query;
}
@Override
public <T> Mono<T> get(String id, Class<T> entityType) {
return get(id, entityType, getIndexCoordinatesFor(entityType));
return Mono.just(entity).zipWith(doIndex(request) //
.map(indexResponse -> new IndexResponseMetaData( //
indexResponse.getId(), //
indexResponse.getSeqNo(), //
indexResponse.getPrimaryTerm(), //
indexResponse.getVersion() //
))); //
}
@Override
@ -482,13 +262,11 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
Assert.notNull(id, "Id must not be null!");
GetRequest request = requestFactory.getRequest(id, routingResolver.getRouting(), index);
Mono<GetResult> getResult = doGet(request);
DocumentCallback<T> callback = new ReadDocumentCallback<>(converter, entityType, index);
return doGet(id, index).flatMap(response -> callback.toEntity(DocumentAdapters.from(response)));
}
private Mono<GetResult> doGet(String id, IndexCoordinates index) {
return Mono.defer(() -> doGet(requestFactory.getRequest(id, routingResolver.getRouting(), index)));
return getResult.flatMap(response -> callback.toEntity(DocumentAdapters.from(response)));
}
/**
@ -503,51 +281,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
.onErrorResume(NoSuchIndexException.class, it -> Mono.empty());
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations#delete(Object, String, String)
*/
@Override
public Mono<String> delete(Object entity, IndexCoordinates index) {
AdaptibleEntity<?> elasticsearchEntity = operations.forEntity(entity, converter.getConversionService(),
routingResolver);
if (elasticsearchEntity.getId() == null) {
return Mono.error(new IllegalArgumentException("entity must have an id"));
}
return Mono.defer(() -> {
String id = converter.convertId(elasticsearchEntity.getId());
String routing = elasticsearchEntity.getRouting();
return doDeleteById(id, routing, index);
});
}
@Override
public Mono<String> delete(Object entity) {
return delete(entity, getIndexCoordinatesFor(entity.getClass()));
}
@Override
public Mono<String> delete(String id, Class<?> entityType) {
Assert.notNull(id, "id must not be null");
Assert.notNull(entityType, "entityType must not be null");
return delete(id, getIndexCoordinatesFor(entityType));
}
@Override
public Mono<String> delete(String id, IndexCoordinates index) {
Assert.notNull(id, "id must not be null");
Assert.notNull(index, "index must not be null");
return doDeleteById(id, routingResolver.getRouting(), index);
}
private Mono<String> doDeleteById(String id, @Nullable String routing, IndexCoordinates index) {
protected Mono<String> doDeleteById(String id, @Nullable String routing, IndexCoordinates index) {
return Mono.defer(() -> {
DeleteRequest request = requestFactory.deleteRequest(id, routing, index);
@ -633,12 +367,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
});
}
@Override
public Mono<ByQueryResponse> delete(Query query, Class<?> entityType) {
return delete(query, entityType, getIndexCoordinatesFor(entityType));
}
private Mono<BulkByScrollResponse> doDeleteBy(Query query, Class<?> entityType, IndexCoordinates index) {
protected Mono<BulkByScrollResponse> doDeleteBy(Query query, Class<?> entityType, IndexCoordinates index) {
return Mono.defer(() -> {
DeleteByQueryRequest request = requestFactory.deleteByQueryRequest(query, entityType, index);
@ -747,16 +476,6 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
// endregion
// region SearchOperations
@Override
public <T> Flux<SearchHit<T>> search(Query query, Class<?> entityType, Class<T> resultType, IndexCoordinates index) {
SearchDocumentCallback<T> callback = new ReadSearchDocumentCallback<>(resultType, index);
return doFind(query, entityType, index).concatMap(callback::toSearchHit);
}
@Override
public <T> Flux<SearchHit<T>> search(Query query, Class<?> entityType, Class<T> returnType) {
return search(query, entityType, returnType, getIndexCoordinatesFor(entityType));
}
@Override
public <T> Mono<SearchPage<T>> searchForPage(Query query, Class<?> entityType, Class<T> resultType) {
@ -778,32 +497,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
.map(searchHits -> SearchHitSupport.searchPageFor(searchHits, query.getPageable()));
}
@Override
public <T> Mono<ReactiveSearchHits<T>> searchForHits(Query query, Class<?> entityType, Class<T> resultType) {
return searchForHits(query, entityType, resultType, getIndexCoordinatesFor(entityType));
}
@Override
public <T> Mono<ReactiveSearchHits<T>> searchForHits(Query query, Class<?> entityType, Class<T> resultType,
IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
Assert.notNull(entityType, "entityType must not be null");
Assert.notNull(resultType, "resultType must not be null");
Assert.notNull(index, "index must not be null");
SearchDocumentCallback<T> callback = new ReadSearchDocumentCallback<>(resultType, index);
return doFindForResponse(query, entityType, index) //
.flatMap(searchDocumentResponse -> Flux.fromIterable(searchDocumentResponse.getSearchDocuments()) //
.flatMap(callback::toEntity) //
.collectList() //
.map(entities -> SearchHitMapping.mappingFor(resultType, converter) //
.mapHits(searchDocumentResponse, entities))) //
.map(ReactiveSearchHitSupport::searchHitsFor);
}
private Flux<SearchDocument> doFind(Query query, Class<?> clazz, IndexCoordinates index) {
protected Flux<SearchDocument> doFind(Query query, Class<?> clazz, IndexCoordinates index) {
return Flux.defer(() -> {
@ -819,7 +513,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
});
}
private <T> Mono<SearchDocumentResponse> doFindForResponse(Query query, Class<?> clazz, IndexCoordinates index) {
protected <T> Mono<SearchDocumentResponse> doFindForResponse(Query query, Class<?> clazz, IndexCoordinates index) {
return Mono.defer(() -> {
SearchRequest request = requestFactory.searchRequest(query, clazz, index);
@ -834,11 +528,6 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
});
}
@Override
public Flux<AggregationContainer<?>> aggregate(Query query, Class<?> entityType) {
return aggregate(query, entityType, getIndexCoordinatesFor(entityType));
}
@Override
public Flux<AggregationContainer<?>> aggregate(Query query, Class<?> entityType, IndexCoordinates index) {
@ -869,25 +558,6 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
.onErrorResume(NoSuchIndexException.class, it -> Flux.empty()).map(ElasticsearchAggregation::new);
}
@Override
public Mono<Suggest> suggest(Query query, Class<?> entityType) {
return suggest(query, entityType, getIndexCoordinatesFor(entityType));
}
@Override
public Mono<Suggest> suggest(Query query, Class<?> entityType, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
Assert.notNull(entityType, "entityType must not be null");
Assert.notNull(index, "index must not be null");
return doFindForResponse(query, entityType, index).mapNotNull(searchDocumentResponse -> {
Suggest suggest = searchDocumentResponse.getSuggest();
SearchHitMapping.mappingFor(entityType, converter).mapHitsInCompletionSuggestion(suggest);
return suggest;
});
}
@Override
@Deprecated
public Flux<org.elasticsearch.search.suggest.Suggest> suggest(SuggestBuilder suggestion, Class<?> entityType) {
@ -908,17 +578,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
});
}
@Override
public Mono<Long> count(Query query, Class<?> entityType) {
return count(query, entityType, getIndexCoordinatesFor(entityType));
}
@Override
public Mono<Long> count(Query query, Class<?> entityType, IndexCoordinates index) {
return doCount(query, entityType, index);
}
private Mono<Long> doCount(Query query, Class<?> entityType, IndexCoordinates index) {
protected Mono<Long> doCount(Query query, Class<?> entityType, IndexCoordinates index) {
return Mono.defer(() -> {
SearchRequest request = requestFactory.searchRequest(query, entityType, index);
@ -958,7 +618,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
}
return Mono.from(execute(client -> client.searchForResponse(request)))
.map(searchResponse -> SearchDocumentResponse.from(searchResponse, entityCreator));
.map(searchResponse -> SearchDocumentResponseBuilder.from(searchResponse, entityCreator));
}
/**
@ -1014,10 +674,10 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
return request;
}
// endregion
// region Helper methods
@Override
protected Mono<String> getClusterVersion() {
try {
return Mono.from(execute(ReactiveElasticsearchClient::info))
@ -1030,6 +690,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
* @return the vendor name of the used cluster and client library
* @since 4.3
*/
@Override
protected Mono<String> getVendor() {
return Mono.just("Elasticsearch");
}
@ -1038,6 +699,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
* @return the version of the used client runtime library.
* @since 4.3
*/
@Override
protected Mono<String> getRuntimeLibraryVersion() {
return Mono.just(Version.CURRENT.toString());
}
@ -1072,11 +734,6 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
return Flux.defer(() -> callback.doWithClient(getClusterClient())).onErrorMap(this::translateException);
}
@Override
public ElasticsearchConverter getElasticsearchConverter() {
return converter;
}
@Override
public ReactiveIndexOperations indexOps(IndexCoordinates index) {
return new ReactiveIndexTemplate(this, index);
@ -1092,17 +749,6 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
return new DefaultReactiveClusterOperations(this);
}
@Override
public IndexCoordinates getIndexCoordinatesFor(Class<?> clazz) {
return getPersistentEntityFor(clazz).getIndexCoordinates();
}
@Override
@Nullable
public ElasticsearchPersistentEntity<?> getPersistentEntityFor(@Nullable Class<?> type) {
return type != null ? mappingContext.getPersistentEntity(type) : null;
}
/**
* Obtain the {@link ReactiveElasticsearchClient} to operate upon.
*
@ -1157,159 +803,4 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
return potentiallyTranslatedException != null ? potentiallyTranslatedException : runtimeException;
}
// region callbacks
protected <T> Mono<T> maybeCallBeforeConvert(T entity, IndexCoordinates index) {
if (null != entityCallbacks) {
return entityCallbacks.callback(ReactiveBeforeConvertCallback.class, entity, index);
}
return Mono.just(entity);
}
protected <T> Mono<T> maybeCallAfterSave(T entity, IndexCoordinates index) {
if (null != entityCallbacks) {
return entityCallbacks.callback(ReactiveAfterSaveCallback.class, entity, index);
}
return Mono.just(entity);
}
protected <T> Mono<T> maybeCallAfterConvert(T entity, Document document, IndexCoordinates index) {
if (null != entityCallbacks) {
return entityCallbacks.callback(ReactiveAfterConvertCallback.class, entity, document, index);
}
return Mono.just(entity);
}
protected <T> Mono<Document> maybeCallbackAfterLoad(Document document, Class<T> type,
IndexCoordinates indexCoordinates) {
if (entityCallbacks != null) {
return entityCallbacks.callback(ReactiveAfterLoadCallback.class, document, type, indexCoordinates);
}
return Mono.just(document);
}
// endregion
// region routing
private void setRoutingResolver(RoutingResolver routingResolver) {
Assert.notNull(routingResolver, "routingResolver must not be null");
this.routingResolver = routingResolver;
}
@Override
public ReactiveElasticsearchOperations withRouting(RoutingResolver routingResolver) {
Assert.notNull(routingResolver, "routingResolver must not be null");
ReactiveElasticsearchTemplate copy = copy();
copy.setRoutingResolver(routingResolver);
return copy;
}
// endregion
protected interface DocumentCallback<T> {
@NonNull
Mono<T> toEntity(@Nullable Document document);
}
protected class ReadDocumentCallback<T> implements DocumentCallback<T> {
private final EntityReader<? super T, Document> reader;
private final Class<T> type;
private final IndexCoordinates index;
public ReadDocumentCallback(EntityReader<? super T, Document> reader, Class<T> type, IndexCoordinates index) {
Assert.notNull(reader, "reader is null");
Assert.notNull(type, "type is null");
this.reader = reader;
this.type = type;
this.index = index;
}
@NonNull
public Mono<T> toEntity(@Nullable Document document) {
if (document == null) {
return Mono.empty();
}
return maybeCallbackAfterLoad(document, type, index) //
.flatMap(documentAfterLoad -> {
T entity = reader.read(type, documentAfterLoad);
IndexedObjectInformation indexedObjectInformation = IndexedObjectInformation.of( //
documentAfterLoad.hasId() ? documentAfterLoad.getId() : null, //
documentAfterLoad.getSeqNo(), //
documentAfterLoad.getPrimaryTerm(), //
documentAfterLoad.getVersion()); //
entity = updateIndexedObject(entity, indexedObjectInformation);
return maybeCallAfterConvert(entity, documentAfterLoad, index);
});
}
}
protected interface SearchDocumentCallback<T> {
Mono<T> toEntity(SearchDocument response);
Mono<SearchHit<T>> toSearchHit(SearchDocument response);
}
protected class ReadSearchDocumentCallback<T> implements SearchDocumentCallback<T> {
private final DocumentCallback<T> delegate;
private final Class<T> type;
public ReadSearchDocumentCallback(Class<T> type, IndexCoordinates index) {
Assert.notNull(type, "type is null");
this.delegate = new ReadDocumentCallback<>(converter, type, index);
this.type = type;
}
@Override
public Mono<T> toEntity(SearchDocument response) {
return delegate.toEntity(response);
}
@Override
public Mono<SearchHit<T>> toSearchHit(SearchDocument response) {
return toEntity(response).map(entity -> SearchHitMapping.mappingFor(type, converter).mapHit(response, entity));
}
}
private class Entities<T> {
private final List<T> entities;
private Entities(List<T> entities) {
Assert.notNull(entities, "entities cannot be null");
this.entities = entities;
}
private boolean isEmpty() {
return entities.isEmpty();
}
private List<IndexQuery> indexQueries() {
return entities.stream().map(ReactiveElasticsearchTemplate.this::getIndexQuery).collect(Collectors.toList());
}
private T entityAt(long index) {
// it's safe to cast to int because the original indexed collection was fitting in memory
int intIndex = (int) index;
return entities.get(intIndex);
}
}
}

View File

@ -230,8 +230,7 @@ public interface ReactiveIndexOperations {
Mono<Boolean> putTemplate(PutTemplateRequest putTemplateRequest);
/**
* gets an index template using the legacy Elasticsearch
* interface/Users/peter/Entwicklung/Projekte/spring-data-elasticsearch/src/main/java/org/springframework/data/elasticsearch/core/IndexOperations.java.
* gets an index template using the legacy Elasticsearch interface.
*
* @param templateName the template name
* @return Mono of TemplateData, {@literal Mono.empty()} if no template with the given name exists.
@ -242,8 +241,7 @@ public interface ReactiveIndexOperations {
}
/**
* gets an index template using the legacy Elasticsearch
* interface/Users/peter/Entwicklung/Projekte/spring-data-elasticsearch/src/main/java/org/springframework/data/elasticsearch/core/IndexOperations.java.
* gets an index template using the legacy Elasticsearch interface.
*
* @param getTemplateRequest the request parameters
* @return Mono of TemplateData, {@literal Mono.empty()} if no template with the given name exists.

View File

@ -180,13 +180,14 @@ class ReactiveIndexTemplate implements ReactiveIndexOperations {
@Override
public Mono<Document> createMapping(Class<?> clazz) {
// noinspection DuplicatedCode
Mapping mappingAnnotation = AnnotatedElementUtils.findMergedAnnotation(clazz, Mapping.class);
if (mappingAnnotation != null) {
String mappingPath = mappingAnnotation.mappingPath();
if (hasText(mappingPath)) {
return loadDocument(mappingAnnotation.mappingPath(), "@Mapping");
return ReactiveResourceUtil.loadDocument(mappingAnnotation.mappingPath(), "@Mapping");
}
}
@ -230,7 +231,7 @@ class ReactiveIndexTemplate implements ReactiveIndexOperations {
ElasticsearchPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(clazz);
String settingPath = persistentEntity.settingPath();
return hasText(settingPath) //
? loadDocument(settingPath, "@Setting") //
? ReactiveResourceUtil.loadDocument(settingPath, "@Setting") //
.map(Settings::new) //
: Mono.just(persistentEntity.getDefaultSettings());
}
@ -351,23 +352,6 @@ class ReactiveIndexTemplate implements ReactiveIndexOperations {
return converter.getMappingContext().getRequiredPersistentEntity(clazz);
}
private Mono<Document> loadDocument(String path, String annotation) {
if (hasText(path)) {
return ReactiveResourceUtil.readFileFromClasspath(path).flatMap(s -> {
if (hasText(s)) {
return Mono.just(Document.parse(s));
} else {
return Mono.just(Document.create());
}
});
} else {
LOGGER.info(String.format("path in %s has to be defined. Using default empty Document instead.", annotation));
}
return Mono.just(Document.create());
}
private Class<?> checkForBoundClass() {
if (boundClass == null) {
throw new InvalidDataAccessApiUsageException("IndexOperations are not bound");

View File

@ -15,6 +15,8 @@
*/
package org.springframework.data.elasticsearch.core;
import static org.springframework.util.StringUtils.*;
import reactor.core.publisher.Mono;
import java.io.BufferedReader;
@ -22,10 +24,13 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.data.elasticsearch.ResourceFailureException;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.util.Assert;
/**
@ -36,6 +41,8 @@ import org.springframework.util.Assert;
*/
public abstract class ReactiveResourceUtil {
private static final Log LOGGER = LogFactory.getLog(ReactiveResourceUtil.class);
private static final int BUFFER_SIZE = 8_192;
/**
@ -74,6 +81,33 @@ public abstract class ReactiveResourceUtil {
throwable -> Mono.error(new ResourceFailureException("Could not load resource from " + url, throwable)));
}
/**
* loads a Document initialized with data from a given resource path.
*
* @param path the path to load data from
* @param annotation the annotation that had the resource path defined
* @return the parsed document
* @since 4.4
*/
public static Mono<Document> loadDocument(String path, String annotation) {
if (hasText(path)) {
return readFileFromClasspath(path).flatMap(s -> {
if (hasText(s)) {
return Mono.just(Document.parse(s));
} else {
return Mono.just(Document.create());
}
});
} else {
if (LOGGER.isInfoEnabled()) {
LOGGER.info(String.format("path in %s has to be defined. Using default instead.", annotation));
}
}
return Mono.just(Document.create());
}
// Utility constructor
private ReactiveResourceUtil() {}
}

View File

@ -23,7 +23,15 @@ import static org.springframework.util.CollectionUtils.*;
import java.io.IOException;
import java.time.Duration;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.elasticsearch.action.DocWriteRequest;
@ -41,7 +49,6 @@ import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.MultiGetRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.action.search.SearchType;
import org.elasticsearch.action.support.ActiveShardCount;
import org.elasticsearch.action.support.WriteRequest;
@ -270,7 +277,7 @@ class RequestFactory {
}
public GetIndexRequest getIndexRequest(IndexCoordinates index) {
return new GetIndexRequest(index.getIndexNames());
return new GetIndexRequest(index.getIndexNames()).humanReadable(false);
}
public IndicesExistsRequest indicesExistsRequest(IndexCoordinates index) {
@ -405,7 +412,7 @@ class RequestFactory {
}
if (reindexRequest.getMaxDocs() != null) {
request.setMaxDocs(reindexRequest.getMaxDocs());
request.setMaxDocs(Math.toIntExact(reindexRequest.getMaxDocs()));
}
// region source build
final Source source = reindexRequest.getSource();
@ -464,12 +471,12 @@ class RequestFactory {
final org.springframework.data.elasticsearch.annotations.Document.VersionType versionType = dest.getVersionType();
if (versionType != null) {
request.setDestVersionType(VersionType.fromString(versionType.name().toLowerCase(Locale.ROOT)));
request.setDestVersionType(VersionType.fromString(versionType.getEsName()));
}
final IndexQuery.OpType opType = dest.getOpType();
if (opType != null) {
request.setDestOpType(opType.name().toLowerCase(Locale.ROOT));
request.setDestOpType(opType.getEsName());
}
// endregion
@ -508,7 +515,7 @@ class RequestFactory {
}
if (reindexRequest.getSlices() != null) {
request.setSlices(reindexRequest.getSlices());
request.setSlices(Math.toIntExact(reindexRequest.getSlices()));
}
// endregion
return request;
@ -923,22 +930,6 @@ class RequestFactory {
}
}
@SuppressWarnings("rawtypes")
private void prepareSort(Query query, SearchRequestBuilder searchRequestBuilder,
@Nullable ElasticsearchPersistentEntity<?> entity) {
if (query.getSort() != null) {
query.getSort().forEach(order -> searchRequestBuilder.addSort(getSortBuilder(order, entity)));
}
if (query instanceof NativeSearchQuery) {
NativeSearchQuery nativeSearchQuery = (NativeSearchQuery) query;
List<SortBuilder<?>> sorts = nativeSearchQuery.getElasticsearchSorts();
if (sorts != null) {
sorts.forEach(searchRequestBuilder::addSort);
}
}
}
private SortBuilder<?> getSortBuilder(Sort.Order order, @Nullable ElasticsearchPersistentEntity<?> entity) {
SortOrder sortOrder = order.getDirection().isDescending() ? SortOrder.DESC : SortOrder.ASC;

View File

@ -293,7 +293,7 @@ public class ResponseConverter {
MultiGetResponse.Failure responseFailure = itemResponse.getFailure();
return responseFailure != null ? MultiGetItem.Failure.of(responseFailure.getIndex(), responseFailure.getType(),
responseFailure.getId(), responseFailure.getFailure()) : null;
responseFailure.getId(), responseFailure.getFailure(), null) : null;
}
// endregion
@ -387,8 +387,7 @@ public class ResponseConverter {
// endregion
// region postReindexResponse
// region reindex
/**
* @since 4.4
*/
@ -402,6 +401,7 @@ public class ResponseConverter {
.withTook(bulkByScrollResponse.getTook().getMillis()) //
.withTimedOut(bulkByScrollResponse.isTimedOut()) //
.withTotal(bulkByScrollResponse.getTotal()) //
.withCreated(bulkByScrollResponse.getCreated()) //
.withUpdated(bulkByScrollResponse.getUpdated()) //
.withDeleted(bulkByScrollResponse.getDeleted()) //
.withBatches(bulkByScrollResponse.getBatches()) //

View File

@ -66,15 +66,18 @@ class RestIndexTemplate extends AbstractIndexTemplate implements IndexOperations
private static final Log LOGGER = LogFactory.getLog(RestIndexTemplate.class);
private final ElasticsearchRestTemplate restTemplate;
protected final RequestFactory requestFactory;
public RestIndexTemplate(ElasticsearchRestTemplate restTemplate, Class<?> boundClass) {
super(restTemplate.getElasticsearchConverter(), boundClass);
this.restTemplate = restTemplate;
requestFactory = new RequestFactory(elasticsearchConverter);
}
public RestIndexTemplate(ElasticsearchRestTemplate restTemplate, IndexCoordinates boundIndex) {
super(restTemplate.getElasticsearchConverter(), boundIndex);
this.restTemplate = restTemplate;
requestFactory = new RequestFactory(elasticsearchConverter);
}
@Override
@ -116,7 +119,7 @@ class RestIndexTemplate extends AbstractIndexTemplate implements IndexOperations
@Override
protected Map<String, Object> doGetMapping(IndexCoordinates index) {
Assert.notNull(index, "No index defined for getMapping()");
Assert.notNull(index, "No index defined for doGetMapping()");
GetMappingsRequest mappingsRequest = requestFactory.getMappingsRequest(index);

View File

@ -57,4 +57,18 @@ public class RuntimeField {
map.put("script", script);
return map;
}
/**
* @since 4.4
*/
public String getType() {
return type;
}
/**
* @since 4.4
*/
public String getScript() {
return script;
}
}

View File

@ -23,11 +23,11 @@ import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.data.elasticsearch.UncategorizedElasticsearchException;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.document.NestedMetaData;
import org.springframework.data.elasticsearch.core.document.SearchDocument;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.suggest.response.CompletionSuggestion;

View File

@ -19,9 +19,10 @@ import java.util.List;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.MoreLikeThisQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.lang.Nullable;
@ -77,7 +78,9 @@ public interface SearchOperations {
* and get the suggest from {@link SearchHits#getSuggest()}
*/
@Deprecated
SearchResponse suggest(SuggestBuilder suggestion, Class<?> clazz);
default SearchResponse suggest(SuggestBuilder suggestion, Class<?> clazz) {
throw new InvalidDataAccessApiUsageException("Unsupported operation");
}
/**
* Does a suggest query
@ -90,7 +93,9 @@ public interface SearchOperations {
* and get the suggest from {@link SearchHits#getSuggest()}
*/
@Deprecated
SearchResponse suggest(SuggestBuilder suggestion, IndexCoordinates index);
default SearchResponse suggest(SuggestBuilder suggestion, IndexCoordinates index) {
throw new InvalidDataAccessApiUsageException("Unsupported operation");
}
/**
* Execute the query against elasticsearch and return the first returned object.

View File

@ -122,13 +122,23 @@ public class ClusterHealth {
@Override
public String toString() {
return "ClusterHealth{" + "clusterName='" + clusterName + '\'' + ", status='" + status + '\'' + ", numberOfNodes="
+ numberOfNodes + ", numberOfDataNodes=" + numberOfDataNodes + ", activeShards=" + activeShards
+ ", relocatingShards=" + relocatingShards + ", activePrimaryShards=" + activePrimaryShards
+ ", initializingShards=" + initializingShards + ", unassignedShards=" + unassignedShards
+ ", activeShardsPercent=" + activeShardsPercent + ", numberOfPendingTasks=" + numberOfPendingTasks
+ ", timedOut=" + timedOut + ", numberOfInFlightFetch=" + numberOfInFlightFetch + ", delayedUnassignedShards="
+ delayedUnassignedShards + ", taskMaxWaitingTimeMillis=" + taskMaxWaitingTimeMillis + '}';
return "ClusterHealth{" + //
"clusterName='" + clusterName + '\'' + //
", status='" + status + '\'' + //
", numberOfNodes=" + numberOfNodes + //
", numberOfDataNodes=" + numberOfDataNodes + //
", activeShards=" + activeShards + //
", relocatingShards=" + relocatingShards + //
", activePrimaryShards=" + activePrimaryShards + //
", initializingShards=" + initializingShards + //
", unassignedShards=" + unassignedShards + //
", activeShardsPercent=" + activeShardsPercent + //
", numberOfPendingTasks=" + numberOfPendingTasks + //
", timedOut=" + timedOut + //
", numberOfInFlightFetch=" + numberOfInFlightFetch + //
", delayedUnassignedShards=" + delayedUnassignedShards + //
", taskMaxWaitingTimeMillis=" + taskMaxWaitingTimeMillis + //
'}'; //
}
public static ClusterHealthBuilder builder() {
@ -160,7 +170,7 @@ public class ClusterHealth {
}
public ClusterHealthBuilder withStatus(String status) {
this.status = status;
this.status = status.toUpperCase();
return this;
}

View File

@ -27,6 +27,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
@ -177,8 +178,8 @@ public final class DocumentAdapters {
Map<String, SearchHits> sourceInnerHits = source.getInnerHits();
if (sourceInnerHits != null) {
sourceInnerHits.forEach((name, searchHits) -> innerHits.put(name,
SearchDocumentResponse.from(searchHits, null, null, null, searchDocument -> null)));
sourceInnerHits.forEach((name, searchHits) -> innerHits.put(name, SearchDocumentResponseBuilder.from(searchHits,
null, null, null, searchDocument -> CompletableFuture.completedFuture(null))));
}
NestedMetaData nestedMetaData = from(source.getNestedIdentity());

View File

@ -23,16 +23,16 @@ import org.springframework.util.Assert;
/**
* class that holds explanations returned from an Elasticsearch search.
*
*
* @author Peter-Josef Meisch
*/
public class Explanation {
private final boolean match;
@Nullable private final Boolean match;
private final Double value;
@Nullable private final String description;
private final List<Explanation> details;
public Explanation(boolean match, Double value, @Nullable String description, List<Explanation> details) {
public Explanation(@Nullable Boolean match, Double value, @Nullable String description, List<Explanation> details) {
Assert.notNull(value, "value must not be null");
Assert.notNull(details, "details must not be null");
@ -44,7 +44,7 @@ public class Explanation {
}
public boolean isMatch() {
return match;
return match != null && match;
}
public Double getValue() {

View File

@ -0,0 +1,296 @@
/*
* Copyright 2021-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.document;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import org.springframework.lang.Nullable;
/**
* {@link SearchDocument} implementation using a {@link Document} delegate.
*
* @author Mark Paluch
* @author Peter-Josef Meisch
* @since 4.4
*/
public class SearchDocumentAdapter implements SearchDocument {
private final float score;
private final Object[] sortValues;
private final Map<String, List<Object>> fields = new HashMap<>();
private final Document delegate;
private final Map<String, List<String>> highlightFields = new HashMap<>();
private final Map<String, SearchDocumentResponse> innerHits = new HashMap<>();
@Nullable private final NestedMetaData nestedMetaData;
@Nullable private final Explanation explanation;
@Nullable private final List<String> matchedQueries;
@Nullable private final String routing;
public SearchDocumentAdapter(Document delegate, float score, Object[] sortValues, Map<String, List<Object>> fields,
Map<String, List<String>> highlightFields, Map<String, SearchDocumentResponse> innerHits,
@Nullable NestedMetaData nestedMetaData, @Nullable Explanation explanation, @Nullable List<String> matchedQueries,
@Nullable String routing) {
this.delegate = delegate;
this.score = score;
this.sortValues = sortValues;
this.fields.putAll(fields);
this.highlightFields.putAll(highlightFields);
this.innerHits.putAll(innerHits);
this.nestedMetaData = nestedMetaData;
this.explanation = explanation;
this.matchedQueries = matchedQueries;
this.routing = routing;
}
@Override
public SearchDocument append(String key, Object value) {
delegate.append(key, value);
return this;
}
@Override
public float getScore() {
return score;
}
@Override
public Map<String, List<Object>> getFields() {
return fields;
}
@Override
public Object[] getSortValues() {
return sortValues;
}
@Override
public Map<String, List<String>> getHighlightFields() {
return highlightFields;
}
@Override
public String getIndex() {
return delegate.getIndex();
}
@Override
public boolean hasId() {
return delegate.hasId();
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public void setId(String id) {
delegate.setId(id);
}
@Override
public boolean hasVersion() {
return delegate.hasVersion();
}
@Override
public long getVersion() {
return delegate.getVersion();
}
@Override
public void setVersion(long version) {
delegate.setVersion(version);
}
@Override
public boolean hasSeqNo() {
return delegate.hasSeqNo();
}
@Override
public long getSeqNo() {
return delegate.getSeqNo();
}
@Override
public void setSeqNo(long seqNo) {
delegate.setSeqNo(seqNo);
}
@Override
public boolean hasPrimaryTerm() {
return delegate.hasPrimaryTerm();
}
@Override
public long getPrimaryTerm() {
return delegate.getPrimaryTerm();
}
@Override
public void setPrimaryTerm(long primaryTerm) {
delegate.setPrimaryTerm(primaryTerm);
}
@Override
public Map<String, SearchDocumentResponse> getInnerHits() {
return innerHits;
}
@Override
@Nullable
public NestedMetaData getNestedMetaData() {
return nestedMetaData;
}
@Override
@Nullable
public <T> T get(Object key, Class<T> type) {
return delegate.get(key, type);
}
@Override
public String toJson() {
return delegate.toJson();
}
@Override
public int size() {
return delegate.size();
}
@Override
public boolean isEmpty() {
return delegate.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return delegate.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return delegate.containsValue(value);
}
@Override
public Object get(Object key) {
if (delegate.containsKey(key)) {
return delegate.get(key);
}
// fallback to fields
return fields.get(key);
}
@Override
public Object put(String key, Object value) {
return delegate.put(key, value);
}
@Override
public Object remove(Object key) {
return delegate.remove(key);
}
@Override
public void putAll(Map<? extends String, ?> m) {
delegate.putAll(m);
}
@Override
public void clear() {
delegate.clear();
}
@Override
public Set<String> keySet() {
return delegate.keySet();
}
@Override
public Collection<Object> values() {
return delegate.values();
}
@Override
public Set<Entry<String, Object>> entrySet() {
return delegate.entrySet();
}
@Override
@Nullable
public Explanation getExplanation() {
return explanation;
}
@Override
@Nullable
public List<String> getMatchedQueries() {
return matchedQueries;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof SearchDocumentAdapter)) {
return false;
}
SearchDocumentAdapter that = (SearchDocumentAdapter) o;
return Float.compare(that.score, score) == 0 && delegate.equals(that.delegate);
}
@Override
public int hashCode() {
return delegate.hashCode();
}
@Override
public void forEach(BiConsumer<? super String, ? super Object> action) {
delegate.forEach(action);
}
@Override
public boolean remove(Object key, Object value) {
return delegate.remove(key, value);
}
@Override
public String getRouting() {
return routing;
}
@Override
public String toString() {
String id = hasId() ? getId() : "?";
String version = hasVersion() ? Long.toString(getVersion()) : "?";
return getClass().getSimpleName() + '@' + id + '#' + version + ' ' + toJson();
}
}

View File

@ -15,57 +15,39 @@
*/
package org.springframework.data.elasticsearch.core.document;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.search.TotalHits;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregations;
import org.springframework.data.elasticsearch.core.AggregationsContainer;
import org.springframework.data.elasticsearch.core.ElasticsearchAggregations;
import org.springframework.data.elasticsearch.core.suggest.response.CompletionSuggestion;
import org.springframework.data.elasticsearch.core.suggest.response.PhraseSuggestion;
import org.springframework.data.elasticsearch.core.suggest.response.SortBy;
import org.springframework.data.elasticsearch.core.suggest.response.Suggest;
import org.springframework.data.elasticsearch.core.suggest.response.TermSuggestion;
import org.springframework.data.elasticsearch.support.ScoreDoc;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* This represents the complete search response from Elasticsearch, including the returned documents. Instances must be
* created with the {@link #from(SearchResponse, EntityCreator)} method.
* This represents the complete search response from Elasticsearch, including the returned documents.
*
* @author Peter-Josef Meisch
* @since 4.0
*/
public class SearchDocumentResponse {
private static final Log LOGGER = LogFactory.getLog(SearchDocumentResponse.class);
private final long totalHits;
private final String totalHitsRelation;
private final float maxScore;
private final String scrollId;
@Nullable private final String scrollId;
private final List<SearchDocument> searchDocuments;
@Nullable private final AggregationsContainer<?> aggregations;
@Nullable private final Suggest suggest;
private SearchDocumentResponse(long totalHits, String totalHitsRelation, float maxScore, String scrollId,
List<SearchDocument> searchDocuments, @Nullable Aggregations aggregations, @Nullable Suggest suggest) {
public SearchDocumentResponse(long totalHits, String totalHitsRelation, float maxScore, @Nullable String scrollId,
List<SearchDocument> searchDocuments, @Nullable AggregationsContainer<?> aggregationsContainer,
@Nullable Suggest suggest) {
this.totalHits = totalHits;
this.totalHitsRelation = totalHitsRelation;
this.maxScore = maxScore;
this.scrollId = scrollId;
this.searchDocuments = searchDocuments;
this.aggregations = aggregations != null ? new ElasticsearchAggregations(aggregations) : null;
this.aggregations = aggregationsContainer;
this.suggest = suggest;
}
@ -81,6 +63,7 @@ public class SearchDocumentResponse {
return maxScore;
}
@Nullable
public String getScrollId() {
return scrollId;
}
@ -99,178 +82,6 @@ public class SearchDocumentResponse {
return suggest;
}
/**
* creates a SearchDocumentResponse from the {@link SearchResponse}
*
* @param searchResponse must not be {@literal null}
* @param entityCreator function to create an entity from a {@link SearchDocument}
* @param <T> entity type
* @return the SearchDocumentResponse
*/
public static <T> SearchDocumentResponse from(SearchResponse searchResponse, EntityCreator<T> entityCreator) {
Assert.notNull(searchResponse, "searchResponse must not be null");
SearchHits searchHits = searchResponse.getHits();
String scrollId = searchResponse.getScrollId();
Aggregations aggregations = searchResponse.getAggregations();
org.elasticsearch.search.suggest.Suggest suggest = searchResponse.getSuggest();
return from(searchHits, scrollId, aggregations, suggest, entityCreator);
}
/**
* creates a {@link SearchDocumentResponse} from {@link SearchHits} with the given scrollId aggregations and suggest
*
* @param searchHits the {@link SearchHits} to process
* @param scrollId scrollId
* @param aggregations aggregations
* @param suggestES the suggestion response from Elasticsearch
* @param entityCreator function to create an entity from a {@link SearchDocument}
* @param <T> entity type
* @return the {@link SearchDocumentResponse}
* @since 4.3
*/
public static <T> SearchDocumentResponse from(SearchHits searchHits, @Nullable String scrollId,
@Nullable Aggregations aggregations, @Nullable org.elasticsearch.search.suggest.Suggest suggestES,
EntityCreator<T> entityCreator) {
TotalHits responseTotalHits = searchHits.getTotalHits();
long totalHits;
String totalHitsRelation;
if (responseTotalHits != null) {
totalHits = responseTotalHits.value;
totalHitsRelation = responseTotalHits.relation.name();
} else {
totalHits = searchHits.getHits().length;
totalHitsRelation = "OFF";
}
float maxScore = searchHits.getMaxScore();
List<SearchDocument> searchDocuments = new ArrayList<>();
for (SearchHit searchHit : searchHits) {
if (searchHit != null) {
searchDocuments.add(DocumentAdapters.from(searchHit));
}
}
Suggest suggest = suggestFrom(suggestES, entityCreator);
return new SearchDocumentResponse(totalHits, totalHitsRelation, maxScore, scrollId, searchDocuments, aggregations,
suggest);
}
@Nullable
private static <T> Suggest suggestFrom(@Nullable org.elasticsearch.search.suggest.Suggest suggestES,
EntityCreator<T> entityCreator) {
if (suggestES == null) {
return null;
}
List<Suggest.Suggestion<? extends Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option>>> suggestions = new ArrayList<>();
for (org.elasticsearch.search.suggest.Suggest.Suggestion<? extends org.elasticsearch.search.suggest.Suggest.Suggestion.Entry<? extends org.elasticsearch.search.suggest.Suggest.Suggestion.Entry.Option>> suggestionES : suggestES) {
if (suggestionES instanceof org.elasticsearch.search.suggest.term.TermSuggestion) {
org.elasticsearch.search.suggest.term.TermSuggestion termSuggestionES = (org.elasticsearch.search.suggest.term.TermSuggestion) suggestionES;
List<TermSuggestion.Entry> entries = new ArrayList<>();
for (org.elasticsearch.search.suggest.term.TermSuggestion.Entry entryES : termSuggestionES) {
List<TermSuggestion.Entry.Option> options = new ArrayList<>();
for (org.elasticsearch.search.suggest.term.TermSuggestion.Entry.Option optionES : entryES) {
options.add(new TermSuggestion.Entry.Option(textToString(optionES.getText()),
textToString(optionES.getHighlighted()), optionES.getScore(), optionES.collateMatch(),
optionES.getFreq()));
}
entries.add(new TermSuggestion.Entry(textToString(entryES.getText()), entryES.getOffset(),
entryES.getLength(), options));
}
suggestions.add(new TermSuggestion(termSuggestionES.getName(), termSuggestionES.getSize(), entries,
suggestFrom(termSuggestionES.getSort())));
}
if (suggestionES instanceof org.elasticsearch.search.suggest.phrase.PhraseSuggestion) {
org.elasticsearch.search.suggest.phrase.PhraseSuggestion phraseSuggestionES = (org.elasticsearch.search.suggest.phrase.PhraseSuggestion) suggestionES;
List<PhraseSuggestion.Entry> entries = new ArrayList<>();
for (org.elasticsearch.search.suggest.phrase.PhraseSuggestion.Entry entryES : phraseSuggestionES) {
List<PhraseSuggestion.Entry.Option> options = new ArrayList<>();
for (org.elasticsearch.search.suggest.phrase.PhraseSuggestion.Entry.Option optionES : entryES) {
options.add(new PhraseSuggestion.Entry.Option(textToString(optionES.getText()),
textToString(optionES.getHighlighted()), optionES.getScore(), optionES.collateMatch()));
}
entries.add(new PhraseSuggestion.Entry(textToString(entryES.getText()), entryES.getOffset(),
entryES.getLength(), options, entryES.getCutoffScore()));
}
suggestions.add(new PhraseSuggestion(phraseSuggestionES.getName(), phraseSuggestionES.getSize(), entries));
}
if (suggestionES instanceof org.elasticsearch.search.suggest.completion.CompletionSuggestion) {
org.elasticsearch.search.suggest.completion.CompletionSuggestion completionSuggestionES = (org.elasticsearch.search.suggest.completion.CompletionSuggestion) suggestionES;
List<CompletionSuggestion.Entry<T>> entries = new ArrayList<>();
for (org.elasticsearch.search.suggest.completion.CompletionSuggestion.Entry entryES : completionSuggestionES) {
List<CompletionSuggestion.Entry.Option<T>> options = new ArrayList<>();
for (org.elasticsearch.search.suggest.completion.CompletionSuggestion.Entry.Option optionES : entryES) {
SearchDocument searchDocument = optionES.getHit() != null ? DocumentAdapters.from(optionES.getHit()) : null;
T hitEntity = null;
if (searchDocument != null) {
try {
hitEntity = entityCreator.apply(searchDocument).get();
} catch (Exception e) {
if (LOGGER.isWarnEnabled()) {
LOGGER.warn("Error creating entity from SearchDocument");
}
}
}
options.add(new CompletionSuggestion.Entry.Option<T>(textToString(optionES.getText()),
textToString(optionES.getHighlighted()), optionES.getScore(), optionES.collateMatch(),
optionES.getContexts(), scoreDocFrom(optionES.getDoc()), searchDocument, hitEntity));
}
entries.add(new CompletionSuggestion.Entry<T>(textToString(entryES.getText()), entryES.getOffset(),
entryES.getLength(), options));
}
suggestions.add(
new CompletionSuggestion<T>(completionSuggestionES.getName(), completionSuggestionES.getSize(), entries));
}
}
return new Suggest(suggestions, suggestES.hasScoreDocs());
}
private static SortBy suggestFrom(org.elasticsearch.search.suggest.SortBy sort) {
return SortBy.valueOf(sort.name().toUpperCase());
}
@Nullable
private static ScoreDoc scoreDocFrom(@Nullable org.apache.lucene.search.ScoreDoc scoreDoc) {
if (scoreDoc == null) {
return null;
}
return new ScoreDoc(scoreDoc.score, scoreDoc.doc, scoreDoc.shardIndex);
}
private static String textToString(@Nullable Text text) {
return text != null ? text.string() : "";
}
/**
* A function to convert a {@link SearchDocument} async into an entity. Asynchronous so that it can be used from the
* imperative and the reactive code.

View File

@ -0,0 +1,224 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.document;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.search.TotalHits;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregations;
import org.springframework.data.elasticsearch.core.ElasticsearchAggregations;
import org.springframework.data.elasticsearch.core.suggest.response.CompletionSuggestion;
import org.springframework.data.elasticsearch.core.suggest.response.PhraseSuggestion;
import org.springframework.data.elasticsearch.core.suggest.response.SortBy;
import org.springframework.data.elasticsearch.core.suggest.response.Suggest;
import org.springframework.data.elasticsearch.core.suggest.response.TermSuggestion;
import org.springframework.data.elasticsearch.support.ScoreDoc;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Factory class to create {@link SearchDocumentResponse} instances.
*
* @author Peter-Josef Meisch
* @since 4.0
*/
public class SearchDocumentResponseBuilder {
private static final Log LOGGER = LogFactory.getLog(SearchDocumentResponse.class);
/**
* creates a SearchDocumentResponse from the {@link SearchResponse}
*
* @param searchResponse must not be {@literal null}
* @param entityCreator function to create an entity from a {@link SearchDocument}
* @param <T> entity type
* @return the SearchDocumentResponse
*/
public static <T> SearchDocumentResponse from(SearchResponse searchResponse,
SearchDocumentResponse.EntityCreator<T> entityCreator) {
Assert.notNull(searchResponse, "searchResponse must not be null");
SearchHits searchHits = searchResponse.getHits();
String scrollId = searchResponse.getScrollId();
Aggregations aggregations = searchResponse.getAggregations();
org.elasticsearch.search.suggest.Suggest suggest = searchResponse.getSuggest();
return from(searchHits, scrollId, aggregations, suggest, entityCreator);
}
/**
* creates a {@link SearchDocumentResponseBuilder} from {@link SearchHits} with the given scrollId aggregations and
* suggest
*
* @param searchHits the {@link SearchHits} to process
* @param scrollId scrollId
* @param aggregations aggregations
* @param suggestES the suggestion response from Elasticsearch
* @param entityCreator function to create an entity from a {@link SearchDocument}
* @param <T> entity type
* @return the {@link SearchDocumentResponse}
* @since 4.3
*/
public static <T> SearchDocumentResponse from(SearchHits searchHits, @Nullable String scrollId,
@Nullable Aggregations aggregations, @Nullable org.elasticsearch.search.suggest.Suggest suggestES,
SearchDocumentResponse.EntityCreator<T> entityCreator) {
TotalHits responseTotalHits = searchHits.getTotalHits();
long totalHits;
String totalHitsRelation;
if (responseTotalHits != null) {
totalHits = responseTotalHits.value;
totalHitsRelation = responseTotalHits.relation.name();
} else {
totalHits = searchHits.getHits().length;
totalHitsRelation = "OFF";
}
float maxScore = searchHits.getMaxScore();
List<SearchDocument> searchDocuments = new ArrayList<>();
for (SearchHit searchHit : searchHits) {
if (searchHit != null) {
searchDocuments.add(DocumentAdapters.from(searchHit));
}
}
ElasticsearchAggregations aggregationsContainer = aggregations != null ? new ElasticsearchAggregations(aggregations)
: null;
Suggest suggest = suggestFrom(suggestES, entityCreator);
return new SearchDocumentResponse(totalHits, totalHitsRelation, maxScore, scrollId, searchDocuments,
aggregationsContainer, suggest);
}
@Nullable
private static <T> Suggest suggestFrom(@Nullable org.elasticsearch.search.suggest.Suggest suggestES,
SearchDocumentResponse.EntityCreator<T> entityCreator) {
if (suggestES == null) {
return null;
}
List<Suggest.Suggestion<? extends Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option>>> suggestions = new ArrayList<>();
for (org.elasticsearch.search.suggest.Suggest.Suggestion<? extends org.elasticsearch.search.suggest.Suggest.Suggestion.Entry<? extends org.elasticsearch.search.suggest.Suggest.Suggestion.Entry.Option>> suggestionES : suggestES) {
if (suggestionES instanceof org.elasticsearch.search.suggest.term.TermSuggestion) {
org.elasticsearch.search.suggest.term.TermSuggestion termSuggestionES = (org.elasticsearch.search.suggest.term.TermSuggestion) suggestionES;
List<TermSuggestion.Entry> entries = new ArrayList<>();
for (org.elasticsearch.search.suggest.term.TermSuggestion.Entry entryES : termSuggestionES) {
List<TermSuggestion.Entry.Option> options = new ArrayList<>();
for (org.elasticsearch.search.suggest.term.TermSuggestion.Entry.Option optionES : entryES) {
options.add(new TermSuggestion.Entry.Option(textToString(optionES.getText()),
textToString(optionES.getHighlighted()), optionES.getScore(), optionES.collateMatch(),
optionES.getFreq()));
}
entries.add(new TermSuggestion.Entry(textToString(entryES.getText()), entryES.getOffset(),
entryES.getLength(), options));
}
suggestions.add(new TermSuggestion(termSuggestionES.getName(), termSuggestionES.getSize(), entries,
suggestFrom(termSuggestionES.getSort())));
}
if (suggestionES instanceof org.elasticsearch.search.suggest.phrase.PhraseSuggestion) {
org.elasticsearch.search.suggest.phrase.PhraseSuggestion phraseSuggestionES = (org.elasticsearch.search.suggest.phrase.PhraseSuggestion) suggestionES;
List<PhraseSuggestion.Entry> entries = new ArrayList<>();
for (org.elasticsearch.search.suggest.phrase.PhraseSuggestion.Entry entryES : phraseSuggestionES) {
List<PhraseSuggestion.Entry.Option> options = new ArrayList<>();
for (org.elasticsearch.search.suggest.phrase.PhraseSuggestion.Entry.Option optionES : entryES) {
options.add(new PhraseSuggestion.Entry.Option(textToString(optionES.getText()),
textToString(optionES.getHighlighted()), optionES.getScore(), optionES.collateMatch()));
}
entries.add(new PhraseSuggestion.Entry(textToString(entryES.getText()), entryES.getOffset(),
entryES.getLength(), options, entryES.getCutoffScore()));
}
suggestions.add(new PhraseSuggestion(phraseSuggestionES.getName(), phraseSuggestionES.getSize(), entries));
}
if (suggestionES instanceof org.elasticsearch.search.suggest.completion.CompletionSuggestion) {
org.elasticsearch.search.suggest.completion.CompletionSuggestion completionSuggestionES = (org.elasticsearch.search.suggest.completion.CompletionSuggestion) suggestionES;
List<CompletionSuggestion.Entry<T>> entries = new ArrayList<>();
for (org.elasticsearch.search.suggest.completion.CompletionSuggestion.Entry entryES : completionSuggestionES) {
List<CompletionSuggestion.Entry.Option<T>> options = new ArrayList<>();
for (org.elasticsearch.search.suggest.completion.CompletionSuggestion.Entry.Option optionES : entryES) {
SearchDocument searchDocument = optionES.getHit() != null ? DocumentAdapters.from(optionES.getHit()) : null;
T hitEntity = null;
if (searchDocument != null) {
try {
hitEntity = entityCreator.apply(searchDocument).get();
} catch (Exception e) {
if (LOGGER.isWarnEnabled()) {
LOGGER.warn("Error creating entity from SearchDocument");
}
}
}
options.add(new CompletionSuggestion.Entry.Option<>(textToString(optionES.getText()),
textToString(optionES.getHighlighted()), optionES.getScore(), optionES.collateMatch(),
optionES.getContexts(), scoreDocFrom(optionES.getDoc()), searchDocument, hitEntity));
}
entries.add(new CompletionSuggestion.Entry<>(textToString(entryES.getText()), entryES.getOffset(),
entryES.getLength(), options));
}
suggestions.add(
new CompletionSuggestion<>(completionSuggestionES.getName(), completionSuggestionES.getSize(), entries));
}
}
return new Suggest(suggestions, suggestES.hasScoreDocs());
}
private static SortBy suggestFrom(org.elasticsearch.search.suggest.SortBy sort) {
return SortBy.valueOf(sort.name().toUpperCase());
}
@Nullable
private static ScoreDoc scoreDocFrom(@Nullable org.apache.lucene.search.ScoreDoc scoreDoc) {
if (scoreDoc == null) {
return null;
}
return new ScoreDoc(scoreDoc.score, scoreDoc.doc, scoreDoc.shardIndex);
}
private static String textToString(@Nullable Text text) {
return text != null ? text.string() : "";
}
}

View File

@ -73,7 +73,7 @@ import com.fasterxml.jackson.databind.util.RawValue;
*/
public class MappingBuilder {
private static final Log LOGGER = LogFactory.getLog(ElasticsearchRestTemplate.class);
private static final Log LOGGER = LogFactory.getLog(MappingBuilder.class);
private static final String FIELD_INDEX = "index";
private static final String FIELD_PROPERTIES = "properties";

View File

@ -15,9 +15,14 @@
*/
package org.springframework.data.elasticsearch.core.index;
import org.springframework.data.elasticsearch.support.DefaultStringObjectMap;
import java.util.AbstractMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.data.elasticsearch.support.DefaultStringObjectMap;
import org.springframework.util.Assert;
/**
* class defining the settings for an index.
@ -47,4 +52,77 @@ public class Settings extends DefaultStringObjectMap<Settings> {
public String toString() {
return "Settings: " + toJson();
}
@Override
public Object get(Object key) {
return containsKey(key) ? super.get(key) : path(key.toString());
}
/**
* Merges some other settings onto this one. Other has higher priority on same keys.
*
* @param other the other settings. Must not be {@literal null}
* @since 4.4
*/
public void merge(Settings other) {
Assert.notNull(other, "other must not be null");
deepMerge(this, other);
}
/*
* taken from https://stackoverflow.com/a/29698326/4393565
*/
@SuppressWarnings("unchecked")
private static Map<?, ?> deepMerge(Map<String, Object> original, Map<String, Object> newMap) {
for (Object key : newMap.keySet()) {
if (newMap.get(key) instanceof Map && original.get(key) instanceof Map) {
Map<String, Object> originalChild = (Map<String, Object>) original.get(key);
Map<String, Object> newChild = (Map<String, Object>) newMap.get(key);
original.put(key.toString(), deepMerge(originalChild, newChild));
} else if (newMap.get(key) instanceof List && original.get(key) instanceof List) {
List<Object> originalChild = (List<Object>) original.get(key);
List<Object> newChild = (List<Object>) newMap.get(key);
for (Object each : newChild) {
if (!originalChild.contains(each)) {
originalChild.add(each);
}
}
} else {
original.put(key.toString(), newMap.get(key));
}
}
return original;
}
/**
* flattens the nested structure (JSON fields index/foo/bar/: value) into a flat structure (index.foo.bar: value)
*
* @return Settings with the flattened elements.
*/
public Settings flatten() {
return new Settings( //
entrySet().stream() //
.flatMap(Settings::doFlatten) //
.collect(Collectors.toMap(Entry::getKey, Entry::getValue))); //
}
/**
* flattens a Map<String, Object> to a stream of Map.Entry objects where the keys are the dot separated concatenated
* keys of sub map entries
*/
static private Stream<Map.Entry<String, Object>> doFlatten(Map.Entry<String, Object> entry) {
if (entry.getValue() instanceof Map<?, ?>) {
Map<?, ?> nested = (Map<?, ?>) entry.getValue();
// noinspection unchecked
return nested.entrySet().stream() //
.map(e -> new AbstractMap.SimpleEntry<>(entry.getKey() + "." + e.getKey(), e.getValue()))
.flatMap(e2 -> doFlatten((Entry<String, Object>) e2));
} else {
return Stream.of(entry);
}
}
}

View File

@ -72,6 +72,23 @@ public class BaseQuery implements Query {
private List<IdWithRouting> idsWithRouting = Collections.emptyList();
private final List<RuntimeField> runtimeFields = new ArrayList<>();
public BaseQuery() {}
public <Q extends BaseQuery, B extends BaseQueryBuilder<Q, B>> BaseQuery(BaseQueryBuilder<Q, B> builder) {
this.sort = builder.getSort();
// do a setPageable after setting the sort, because the pageable may contain an additional sort
this.setPageable(builder.getPageable() != null ? builder.getPageable() : DEFAULT_PAGE);
this.ids = builder.getIds();
this.trackScores = builder.getTrackScores();
this.maxResults = builder.getMaxResults();
this.indicesOptions = builder.getIndicesOptions();
this.minScore = builder.getMinScore();
this.preference = builder.getPreference();
this.sourceFilter = builder.getSourceFilter();
this.fields = builder.getFields();
// #1973 add the other fields to the builder
}
@Override
@Nullable
public Sort getSort() {

View File

@ -0,0 +1,165 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.query;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
/**
* base class for query builders. The different implementations of {@link Query} should derive from this class and then
* offer a constructor that takes their builder as argument and passes this on to the super class.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
public abstract class BaseQueryBuilder<Q extends BaseQuery, SELF extends BaseQueryBuilder<Q, SELF>> {
@Nullable private Pageable pageable;
@Nullable private Sort sort;
@Nullable private Integer maxResults;
@Nullable private Collection<String> ids;
private boolean trackScores;
@Nullable protected IndicesOptions indicesOptions;
private float minScore;
@Nullable private String preference;
@Nullable private SourceFilter sourceFilter;
private List<String> fields = new ArrayList<>();
@Nullable
public Pageable getPageable() {
return pageable;
}
@Nullable
public Sort getSort() {
return sort;
}
@Nullable
public Integer getMaxResults() {
return maxResults;
}
@Nullable
public Collection<String> getIds() {
return ids;
}
public boolean getTrackScores() {
return trackScores;
}
@Nullable
public IndicesOptions getIndicesOptions() {
return indicesOptions;
}
public float getMinScore() {
return minScore;
}
@Nullable
public String getPreference() {
return preference;
}
@Nullable
public SourceFilter getSourceFilter() {
return sourceFilter;
}
public List<String> getFields() {
return fields;
}
public SELF withPageable(Pageable pageable) {
this.pageable = pageable;
return self();
}
public SELF withSort(Sort sort) {
if (this.sort == null) {
this.sort = sort;
} else {
this.sort = this.sort.and(sort);
}
return self();
}
public SELF withMaxResults(Integer maxResults) {
this.maxResults = maxResults;
return self();
}
public SELF withIds(String... ids) {
this.ids = Arrays.asList(ids);
return self();
}
public SELF withIds(Collection<String> ids) {
this.ids = ids;
return self();
}
public SELF withTrackScores(boolean trackScores) {
this.trackScores = trackScores;
return self();
}
public SELF withIndicesOptions(IndicesOptions indicesOptions) {
this.indicesOptions = indicesOptions;
return self();
}
public SELF withMinScore(float minScore) {
this.minScore = minScore;
return self();
}
public SELF withPreference(String preference) {
this.preference = preference;
return self();
}
public SELF withSourceFilter(SourceFilter sourceFilter) {
this.sourceFilter = sourceFilter;
return self();
}
public SELF withFields(String... fields) {
Collections.addAll(this.fields, fields);
return self();
}
public SELF withFields(Collection<String> fields) {
this.fields.addAll(fields);
return self();
}
public abstract Q build();
private SELF self() {
// noinspection unchecked
return (SELF) this;
}
}

View File

@ -18,6 +18,7 @@ package org.springframework.data.elasticsearch.core.query;
import java.util.Collections;
import java.util.List;
import org.springframework.data.elasticsearch.ElasticsearchErrorCause;
import org.springframework.lang.Nullable;
/**
@ -173,9 +174,11 @@ public class ByQueryResponse {
@Nullable private final Long seqNo;
@Nullable private final Long term;
@Nullable private final Boolean aborted;
@Nullable private final ElasticsearchErrorCause elasticsearchErrorCause;
private Failure(@Nullable String index, @Nullable String type, @Nullable String id, @Nullable Exception cause,
@Nullable Integer status, @Nullable Long seqNo, @Nullable Long term, @Nullable Boolean aborted) {
@Nullable Integer status, @Nullable Long seqNo, @Nullable Long term, @Nullable Boolean aborted,
@Nullable ElasticsearchErrorCause elasticsearchErrorCause) {
this.index = index;
this.type = type;
this.id = id;
@ -184,6 +187,7 @@ public class ByQueryResponse {
this.seqNo = seqNo;
this.term = term;
this.aborted = aborted;
this.elasticsearchErrorCause = elasticsearchErrorCause;
}
@Nullable
@ -247,6 +251,7 @@ public class ByQueryResponse {
@Nullable private Long seqNo;
@Nullable private Long term;
@Nullable private Boolean aborted;
@Nullable private ElasticsearchErrorCause elasticsearchErrorCause;
private FailureBuilder() {}
@ -290,8 +295,13 @@ public class ByQueryResponse {
return this;
}
public FailureBuilder withErrorCause(ElasticsearchErrorCause elasticsearchErrorCause) {
this.elasticsearchErrorCause = elasticsearchErrorCause;
return this;
}
public Failure build() {
return new Failure(index, type, id, cause, status, seqNo, term, aborted);
return new Failure(index, type, id, cause, status, seqNo, term, aborted, elasticsearchErrorCause);
}
}
}

View File

@ -28,7 +28,19 @@ import org.springframework.util.Assert;
*/
public class CriteriaQuery extends BaseQuery {
private Criteria criteria;
private final Criteria criteria;
/**
* @since 4.4
*/
public CriteriaQuery(CriteriaQueryBuilder builder) {
super(builder);
this.criteria = builder.getCriteria();
}
public static CriteriaQueryBuilder builder(Criteria criteria) {
return new CriteriaQueryBuilder(criteria);
}
public CriteriaQuery(Criteria criteria) {
this(criteria, Pageable.unpaged());

View File

@ -0,0 +1,43 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.query;
import org.springframework.util.Assert;
/**
* @author Peter-Josef Meisch
* @since 4.4
*/
public class CriteriaQueryBuilder extends BaseQueryBuilder<CriteriaQuery, CriteriaQueryBuilder> {
private final Criteria criteria;
public CriteriaQueryBuilder(Criteria criteria) {
Assert.notNull(criteria, "criteria must not be null");
this.criteria = criteria;
}
public Criteria getCriteria() {
return criteria;
}
@Override
public CriteriaQuery build() {
return new CriteriaQuery(this);
}
}

View File

@ -32,7 +32,7 @@ import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
* Converts the {@link Highlight} annotation from a method to an Elasticsearch {@link HighlightBuilder}.
* Converts the {@link Highlight} annotation from a method to an Elasticsearch 7 {@link HighlightBuilder}.
*
* @author Peter-Josef Meisch
*/

View File

@ -168,6 +168,16 @@ public class IndexQuery {
* @since 4.2
*/
public enum OpType {
INDEX, CREATE
INDEX("index"), CREATE("create");
private final String esName;
OpType(String esName) {
this.esName = esName;
}
public String getEsName() {
return esName;
}
}
}

View File

@ -61,7 +61,6 @@ public class NativeSearchQuery extends BaseQuery {
@Nullable private List<SearchExtBuilder> searchExtBuilders;
public NativeSearchQuery(@Nullable QueryBuilder query) {
this.query = query;
}
@ -99,6 +98,17 @@ public class NativeSearchQuery extends BaseQuery {
this.highlightFields = highlightFields;
}
public NativeSearchQuery(NativeSearchQueryBuilder builder, @Nullable QueryBuilder query,
@Nullable QueryBuilder filter, @Nullable List<SortBuilder<?>> sorts, @Nullable HighlightBuilder highlightBuilder,
@Nullable HighlightBuilder.Field[] highlightFields) {
super(builder);
this.query = query;
this.filter = filter;
this.sorts = sorts;
this.highlightBuilder = highlightBuilder;
this.highlightFields = highlightFields;
}
@Nullable
public QueryBuilder getQuery() {
return query;

View File

@ -32,7 +32,6 @@ import org.elasticsearch.search.collapse.CollapseBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortBuilder;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.springframework.data.domain.Pageable;
import org.springframework.lang.Nullable;
/**
@ -52,7 +51,7 @@ import org.springframework.lang.Nullable;
* @author vdisk
* @author owen.qq
*/
public class NativeSearchQueryBuilder {
public class NativeSearchQueryBuilder extends BaseQueryBuilder<NativeSearchQuery, NativeSearchQueryBuilder> {
@Nullable private QueryBuilder queryBuilder;
@Nullable private QueryBuilder filterBuilder;
@ -62,21 +61,12 @@ public class NativeSearchQueryBuilder {
private final List<PipelineAggregationBuilder> pipelineAggregationBuilders = new ArrayList<>();
@Nullable private HighlightBuilder highlightBuilder;
@Nullable private List<HighlightBuilder.Field> highlightFields = new ArrayList<>();
private Pageable pageable = Pageable.unpaged();
@Nullable private List<String> fields = new ArrayList<>();
@Nullable protected List<String> storedFields;
@Nullable private SourceFilter sourceFilter;
@Nullable private CollapseBuilder collapseBuilder;
@Nullable private List<IndexBoost> indicesBoost = new ArrayList<>();
@Nullable private SearchTemplateRequestBuilder searchTemplateBuilder;
private float minScore;
private boolean trackScores;
@Nullable private List<String> ids = new ArrayList<>();
@Nullable private String route;
@Nullable private SearchType searchType;
@Nullable private IndicesOptions indicesOptions;
@Nullable private String preference;
@Nullable private Integer maxResults;
@Nullable private Boolean trackTotalHits;
@Nullable private Duration timeout;
private final List<RescorerQuery> rescorerQueries = new ArrayList<>();
@ -223,24 +213,6 @@ public class NativeSearchQueryBuilder {
return this;
}
public NativeSearchQueryBuilder withPageable(Pageable pageable) {
this.pageable = pageable;
return this;
}
/**
* @since 4.3
*/
public NativeSearchQueryBuilder withFields(Collection<String> fields) {
this.fields.addAll(fields);
return this;
}
public NativeSearchQueryBuilder withFields(String... fields) {
Collections.addAll(this.fields, fields);
return this;
}
public NativeSearchQueryBuilder withStoredFields(Collection<String> storedFields) {
if (this.storedFields == null) {
@ -260,39 +232,6 @@ public class NativeSearchQueryBuilder {
return this;
}
public NativeSearchQueryBuilder withSourceFilter(SourceFilter sourceFilter) {
this.sourceFilter = sourceFilter;
return this;
}
public NativeSearchQueryBuilder withMinScore(float minScore) {
this.minScore = minScore;
return this;
}
/**
* @param trackScores whether to track scores.
* @return this object
* @since 3.1
*/
public NativeSearchQueryBuilder withTrackScores(boolean trackScores) {
this.trackScores = trackScores;
return this;
}
public NativeSearchQueryBuilder withIds(Collection<String> ids) {
this.ids.addAll(ids);
return this;
}
/**
* @since 4.3
*/
public NativeSearchQueryBuilder withIds(String... ids) {
Collections.addAll(this.ids, ids);
return this;
}
public NativeSearchQueryBuilder withRoute(String route) {
this.route = route;
return this;
@ -308,16 +247,6 @@ public class NativeSearchQueryBuilder {
return this;
}
public NativeSearchQueryBuilder withPreference(String preference) {
this.preference = preference;
return this;
}
public NativeSearchQueryBuilder withMaxResults(Integer maxResults) {
this.maxResults = maxResults;
return this;
}
/**
* @since 4.2
*/
@ -356,27 +285,17 @@ public class NativeSearchQueryBuilder {
public NativeSearchQuery build() {
NativeSearchQuery nativeSearchQuery = new NativeSearchQuery( //
this, //
queryBuilder, //
filterBuilder, //
sortBuilders, //
highlightBuilder, //
highlightFields.toArray(new HighlightBuilder.Field[highlightFields.size()]));
nativeSearchQuery.setPageable(pageable);
nativeSearchQuery.setTrackScores(trackScores);
if (fields != null) {
nativeSearchQuery.setFields(fields);
}
if (storedFields != null) {
nativeSearchQuery.setStoredFields(storedFields);
}
if (sourceFilter != null) {
nativeSearchQuery.addSourceFilter(sourceFilter);
}
if (indicesBoost != null) {
nativeSearchQuery.setIndicesBoost(indicesBoost);
}
@ -401,14 +320,6 @@ public class NativeSearchQueryBuilder {
nativeSearchQuery.setPipelineAggregations(pipelineAggregationBuilders);
}
if (minScore > 0) {
nativeSearchQuery.setMinScore(minScore);
}
if (ids != null) {
nativeSearchQuery.setIds(ids);
}
if (route != null) {
nativeSearchQuery.setRoute(route);
}
@ -421,14 +332,6 @@ public class NativeSearchQueryBuilder {
nativeSearchQuery.setIndicesOptions(indicesOptions);
}
if (preference != null) {
nativeSearchQuery.setPreference(preference);
}
if (maxResults != null) {
nativeSearchQuery.setMaxResults(maxResults);
}
nativeSearchQuery.setTrackTotalHits(trackTotalHits);
if (timeout != null) {

View File

@ -339,7 +339,7 @@ public interface Query {
void setScrollTime(@Nullable Duration scrollTime);
/**
* @return {@literal true} if scrollTimeMillis is set.
* @return {@literal true} if a scroll time is set.
* @since 4.0
*/
default boolean hasScrollTime() {

View File

@ -23,10 +23,11 @@ import org.springframework.data.domain.Sort;
*
* @author Rizwan Idrees
* @author Mohsin Husen
* @author Peter-Josef Meisch
*/
public class StringQuery extends BaseQuery {
private String source;
private final String source;
public StringQuery(String source) {
this.source = source;
@ -43,6 +44,21 @@ public class StringQuery extends BaseQuery {
this.source = source;
}
/**
* @since 4.4
*/
public StringQuery(StringQueryBuilder builder) {
super(builder);
this.source = builder.getSource();
}
/**
* @since 4.4
*/
public static StringQueryBuilder builder(String source) {
return new StringQueryBuilder(source);
}
public String getSource() {
return source;
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.query;
import org.springframework.util.Assert;
/**
* @author Peter-Josef Meisch
* @since 4.4
*/
public class StringQueryBuilder extends BaseQueryBuilder<StringQuery, StringQueryBuilder> {
private final String source;
public StringQueryBuilder(String source) {
Assert.notNull(source, "source must not be null");
this.source = source;
}
public String getSource() {
return source;
}
@Override
public StringQuery build() {
return new StringQuery(this);
}
}

View File

@ -37,7 +37,7 @@ public class ReindexRequest {
// Request body
private final Source source;
private final Dest dest;
@Nullable private final Integer maxDocs;
@Nullable private final Long maxDocs;
@Nullable private final Conflicts conflicts;
@Nullable private final Script script;
@ -46,14 +46,14 @@ public class ReindexRequest {
@Nullable private final Boolean requireAlias;
@Nullable private final Boolean refresh;
@Nullable private final String waitForActiveShards;
@Nullable private final Integer requestsPerSecond;
@Nullable private final Long requestsPerSecond;
@Nullable private final Duration scroll;
@Nullable private final Integer slices;
@Nullable private final Long slices;
private ReindexRequest(Source source, Dest dest, @Nullable Integer maxDocs, @Nullable Conflicts conflicts,
private ReindexRequest(Source source, Dest dest, @Nullable Long maxDocs, @Nullable Conflicts conflicts,
@Nullable Script script, @Nullable Duration timeout, @Nullable Boolean requireAlias, @Nullable Boolean refresh,
@Nullable String waitForActiveShards, @Nullable Integer requestsPerSecond, @Nullable Duration scroll,
@Nullable Integer slices) {
@Nullable String waitForActiveShards, @Nullable Long requestsPerSecond, @Nullable Duration scroll,
@Nullable Long slices) {
Assert.notNull(source, "source must not be null");
Assert.notNull(dest, "dest must not be null");
@ -73,7 +73,7 @@ public class ReindexRequest {
}
@Nullable
public Integer getMaxDocs() {
public Long getMaxDocs() {
return maxDocs;
}
@ -116,7 +116,7 @@ public class ReindexRequest {
}
@Nullable
public Integer getRequestsPerSecond() {
public Long getRequestsPerSecond() {
return requestsPerSecond;
}
@ -126,7 +126,7 @@ public class ReindexRequest {
}
@Nullable
public Integer getSlices() {
public Long getSlices() {
return slices;
}
@ -275,16 +275,16 @@ public class ReindexRequest {
private final Source source;
private final Dest dest;
@Nullable private Integer maxDocs;
@Nullable private Long maxDocs;
@Nullable private Conflicts conflicts;
@Nullable private Script script;
@Nullable private Duration timeout;
@Nullable private Boolean requireAlias;
@Nullable private Boolean refresh;
@Nullable private String waitForActiveShards;
@Nullable private Integer requestsPerSecond;
@Nullable private Long requestsPerSecond;
@Nullable private Duration scroll;
@Nullable private Integer slices;
@Nullable private Long slices;
public ReindexRequestBuilder(IndexCoordinates sourceIndex, IndexCoordinates destIndex) {
@ -297,7 +297,7 @@ public class ReindexRequest {
// region setter
public ReindexRequestBuilder withMaxDocs(@Nullable Integer maxDocs) {
public ReindexRequestBuilder withMaxDocs(@Nullable Long maxDocs) {
this.maxDocs = maxDocs;
return this;
}
@ -377,7 +377,7 @@ public class ReindexRequest {
return this;
}
public ReindexRequestBuilder withRequestsPerSecond(int requestsPerSecond) {
public ReindexRequestBuilder withRequestsPerSecond(long requestsPerSecond) {
this.requestsPerSecond = requestsPerSecond;
return this;
}
@ -387,7 +387,7 @@ public class ReindexRequest {
return this;
}
public ReindexRequestBuilder withSlices(int slices) {
public ReindexRequestBuilder withSlices(long slices) {
this.slices = slices;
return this;
}

View File

@ -18,6 +18,7 @@ package org.springframework.data.elasticsearch.core.reindex;
import java.util.Collections;
import java.util.List;
import org.springframework.data.elasticsearch.ElasticsearchErrorCause;
import org.springframework.lang.Nullable;
/**
@ -32,9 +33,10 @@ public class ReindexResponse {
private final long took;
private final boolean timedOut;
private final long total;
private final long created;
private final long updated;
private final long deleted;
private final int batches;
private final long batches;
private final long versionConflicts;
private final long noops;
private final long bulkRetries;
@ -44,12 +46,13 @@ public class ReindexResponse {
private final long throttledUntilMillis;
private final List<Failure> failures;
private ReindexResponse(long took, boolean timedOut, long total, long updated, long deleted, int batches,
long versionConflicts, long noops, long bulkRetries, long searchRetries, long throttledMillis,
private ReindexResponse(long took, boolean timedOut, long total, long created, long updated, long deleted,
long batches, long versionConflicts, long noops, long bulkRetries, long searchRetries, long throttledMillis,
double requestsPerSecond, long throttledUntilMillis, List<Failure> failures) {
this.took = took;
this.timedOut = timedOut;
this.total = total;
this.created = created;
this.updated = updated;
this.deleted = deleted;
this.batches = batches;
@ -84,6 +87,13 @@ public class ReindexResponse {
return total;
}
/**
* The number of documents that were successfully created.
*/
public long getCreated() {
return created;
}
/**
* The number of documents that were successfully updated.
*/
@ -101,7 +111,7 @@ public class ReindexResponse {
/**
* The number of scroll responses pulled back by the update by query.
*/
public int getBatches() {
public long getBatches() {
return batches;
}
@ -184,9 +194,11 @@ public class ReindexResponse {
@Nullable private final Long seqNo;
@Nullable private final Long term;
@Nullable private final Boolean aborted;
@Nullable private final ElasticsearchErrorCause elasticsearchErrorCause;
private Failure(@Nullable String index, @Nullable String type, @Nullable String id, @Nullable Exception cause,
@Nullable Integer status, @Nullable Long seqNo, @Nullable Long term, @Nullable Boolean aborted) {
@Nullable Integer status, @Nullable Long seqNo, @Nullable Long term, @Nullable Boolean aborted,
@Nullable ElasticsearchErrorCause elasticsearchErrorCause) {
this.index = index;
this.type = type;
this.id = id;
@ -195,6 +207,7 @@ public class ReindexResponse {
this.seqNo = seqNo;
this.term = term;
this.aborted = aborted;
this.elasticsearchErrorCause = elasticsearchErrorCause;
}
@Nullable
@ -237,6 +250,11 @@ public class ReindexResponse {
return aborted;
}
@Nullable
public ElasticsearchErrorCause getElasticsearchErrorCause() {
return elasticsearchErrorCause;
}
/**
* Create a new {@link Failure.FailureBuilder} to build {@link Failure}
*
@ -258,6 +276,7 @@ public class ReindexResponse {
@Nullable private Long seqNo;
@Nullable private Long term;
@Nullable private Boolean aborted;
@Nullable private ElasticsearchErrorCause elasticsearchErrorCause;
private FailureBuilder() {}
@ -276,7 +295,7 @@ public class ReindexResponse {
return this;
}
public Failure.FailureBuilder withCause(Exception cause) {
public Failure.FailureBuilder withCause(@Nullable Exception cause) {
this.cause = cause;
return this;
}
@ -301,8 +320,13 @@ public class ReindexResponse {
return this;
}
public Failure.FailureBuilder withErrorCause(@Nullable ElasticsearchErrorCause elasticsearchErrorCause) {
this.elasticsearchErrorCause = elasticsearchErrorCause;
return this;
}
public Failure build() {
return new Failure(index, type, id, cause, status, seqNo, term, aborted);
return new Failure(index, type, id, cause, status, seqNo, term, aborted, elasticsearchErrorCause);
}
}
}
@ -311,9 +335,10 @@ public class ReindexResponse {
private long took;
private boolean timedOut;
private long total;
private long created;
private long updated;
private long deleted;
private int batches;
private long batches;
private long versionConflicts;
private long noops;
private long bulkRetries;
@ -340,6 +365,11 @@ public class ReindexResponse {
return this;
}
public ReindexResponseBuilder withCreated(long created) {
this.created = created;
return this;
}
public ReindexResponseBuilder withUpdated(long updated) {
this.updated = updated;
return this;
@ -350,7 +380,7 @@ public class ReindexResponse {
return this;
}
public ReindexResponseBuilder withBatches(int batches) {
public ReindexResponseBuilder withBatches(long batches) {
this.batches = batches;
return this;
}
@ -396,8 +426,8 @@ public class ReindexResponse {
}
public ReindexResponse build() {
return new ReindexResponse(took, timedOut, total, updated, deleted, batches, versionConflicts, noops, bulkRetries,
searchRetries, throttledMillis, requestsPerSecond, throttledUntilMillis, failures);
return new ReindexResponse(took, timedOut, total, created, updated, deleted, batches, versionConflicts, noops,
bulkRetries, searchRetries, throttledMillis, requestsPerSecond, throttledUntilMillis, failures);
}
}
}

View File

@ -15,7 +15,6 @@
*/
package org.springframework.data.elasticsearch.repository.support;
import static org.elasticsearch.index.query.QueryBuilders.*;
import static org.springframework.util.CollectionUtils.*;
import java.util.ArrayList;
@ -29,8 +28,6 @@ import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.AbstractElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.IndexOperations;
@ -42,7 +39,9 @@ import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.SearchPage;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.MoreLikeThisQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.data.util.StreamUtils;
@ -116,25 +115,34 @@ public class SimpleElasticsearchRepository<T, ID> implements ElasticsearchReposi
@SuppressWarnings("unchecked")
@Override
public Page<T> findAll(Pageable pageable) {
NativeSearchQuery query = new NativeSearchQueryBuilder().withQuery(matchAllQuery()).withPageable(pageable).build();
Assert.notNull(pageable, "pageable must not be null");
Query query = Query.findAll();
query.setPageable(pageable);
SearchHits<T> searchHits = execute(operations -> operations.search(query, entityClass, getIndexCoordinates()));
// noinspection ConstantConditions
SearchPage<T> page = SearchHitSupport.searchPageFor(searchHits, query.getPageable());
// noinspection ConstantConditions
return (Page<T>) SearchHitSupport.unwrapSearchHits(page);
}
@SuppressWarnings("unchecked")
@Override
public Iterable<T> findAll(Sort sort) {
Assert.notNull(sort, "sort must not be null");
int itemCount = (int) this.count();
if (itemCount == 0) {
return new PageImpl<>(Collections.emptyList());
}
NativeSearchQuery query = new NativeSearchQueryBuilder().withQuery(matchAllQuery())
.withPageable(PageRequest.of(0, itemCount, sort)).build();
Pageable pageable = PageRequest.of(0, itemCount, sort);
Query query = Query.findAll();
query.setPageable(pageable);
List<SearchHit<T>> searchHitList = execute(
operations -> operations.search(query, entityClass, getIndexCoordinates()).getSearchHits());
// noinspection ConstantConditions
return (List<T>) SearchHitSupport.unwrapSearchHits(searchHitList);
}
@ -166,8 +174,8 @@ public class SimpleElasticsearchRepository<T, ID> implements ElasticsearchReposi
@Override
public long count() {
NativeSearchQuery query = new NativeSearchQueryBuilder().withQuery(matchAllQuery()).build();
// noinspection ConstantConditions
Query query = Query.findAll();
((BaseQuery) query).setMaxResults(0);
return execute(operations -> operations.count(query, entityClass, getIndexCoordinates()));
}
@ -287,10 +295,9 @@ public class SimpleElasticsearchRepository<T, ID> implements ElasticsearchReposi
@Override
public void deleteAll() {
IndexCoordinates indexCoordinates = getIndexCoordinates();
Query query = new NativeSearchQueryBuilder().withQuery(matchAllQuery()).build();
executeAndRefresh((OperationsCallback<Void>) operations -> {
operations.delete(query, entityClass, indexCoordinates);
operations.delete(Query.findAll(), entityClass, indexCoordinates);
return null;
});
}

View File

@ -22,6 +22,7 @@ import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.fasterxml.jackson.core.JsonProcessingException;
@ -87,6 +88,7 @@ public class DefaultStringObjectMap<T extends StringObjectMap<T>> implements Str
}
@Override
@Nullable
public Object get(Object key) {
return delegate.get(key);
}
@ -150,4 +152,40 @@ public class DefaultStringObjectMap<T extends StringObjectMap<T>> implements Str
public String toString() {
return "DefaultStringObjectMap: " + toJson();
}
/**
* Gets the object at the given key path (dot separated) or null if no object exists with this path.
*
* @param path the key path, must not be {@literal null}
* @return the found object or null
*/
@Nullable
public Object path(String path) {
Assert.notNull(path, "path must not be null");
Map<String, Object> current = this;
String[] segments = path.split("\\.");
for (int i = 0; i < segments.length; i++) {
String segment = segments[i];
if (current.containsKey(segment)) {
Object currentObject = current.get(segment);
if (i == segments.length - 1) {
return currentObject;
}
if (currentObject instanceof Map) {
current = (Map<String, Object>) currentObject;
} else {
return null;
}
} else {
return null;
}
}
return null;
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright 2021-2022 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.support;
import java.net.ConnectException;
/**
* @author Peter-Josef Meisch
* @since 4.4
*/
public final class ExceptionUtils {
private ExceptionUtils() {}
/**
* checks if the given throwable is a {@link ConnectException} or has one in its cause chain
*
* @param throwable the throwable to check
* @return true if throwable is caused by a {@link ConnectException}
*/
public static boolean isCausedByConnectionException(Throwable throwable) {
Throwable t = throwable;
do {
if (t instanceof ConnectException) {
return true;
}
t = t.getCause();
} while (t != null);
return false;
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright 2021-2022 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;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.test.context.ContextConfiguration;
/**
* class demonstrating the setup of a JUnit 5 test in Spring Data Elasticsearch that uses the new rest client. The
* ContextConfiguration must include the {@link ElasticsearchTemplateConfiguration} class.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
@SpringIntegrationTest
@ContextConfiguration(classes = { ElasticsearchTemplateConfiguration.class })
@DisplayName("a sample JUnit 5 test with the new rest client")
public class JUnit5SampleElasticsearchTemplateTests {
@Autowired private ElasticsearchOperations elasticsearchOperations;
@Test
@DisplayName("should have an ElasticsearchTemplate")
void shouldHaveAElasticsearchTemplate() {
assertThat(elasticsearchOperations).isNotNull().isInstanceOf(ElasticsearchTemplate.class);
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright 2021-2022 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;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchTemplateConfiguration;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.test.context.ContextConfiguration;
/**
* class demonstrating the setup of a reactive JUnit 5 test in Spring Data Elasticsearch that uses the new rest client.
* The ContextConfiguration must include the {@link ReactiveElasticsearchTemplateConfiguration} class.
*
* @author Peter-Josef Meisch
* @since 4.4
*/
@SpringIntegrationTest
@ContextConfiguration(classes = { ReactiveElasticsearchTemplateConfiguration.class })
@DisplayName("a sample reactive JUnit 5 test with the new rest client")
public class JUnit5SampleReactiveELCTests {
@Autowired private ReactiveElasticsearchOperations elasticsearchOperations;
@Test
@DisplayName("should have an ReactiveElasticsearchTemplate")
void shouldHaveAElasticsearchTemplate() {
assertThat(elasticsearchOperations).isNotNull().isInstanceOf(ReactiveElasticsearchTemplate.class);
}
}

View File

@ -35,7 +35,7 @@ import org.springframework.test.context.ContextConfiguration;
@SpringIntegrationTest
@ContextConfiguration(classes = { ReactiveElasticsearchRestTemplateConfiguration.class })
@DisplayName("a sample JUnit 5 test with reactive rest client")
public class JUnit5SampleReactiveRestClientBasedTests {
public class JUnit5SampleReactiveERHLCTests {
@Autowired private ReactiveElasticsearchOperations elasticsearchOperations;

View File

@ -35,7 +35,7 @@ import org.springframework.test.context.ContextConfiguration;
@SpringIntegrationTest
@ContextConfiguration(classes = { ElasticsearchRestTemplateConfiguration.class })
@DisplayName("a sample JUnit 5 test with rest client")
public class JUnit5SampleRestTemplateBasedTests {
public class JUnit5SampleRestTemplateTests {
@Autowired private ElasticsearchOperations elasticsearchOperations;

View File

@ -0,0 +1,34 @@
/*
* Copyright 2021-2022 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;
/**
* TODO #1973 remove when the new Elasticsearch client is fully working
*
* @author Peter-Josef Meisch
*/
public interface NewElasticsearchClientDevelopment {
boolean forceEnable = false;
default boolean usesNewElasticsearchClient() {
return false;
}
default boolean newElasticsearchClient() {
return !forceEnable && usesNewElasticsearchClient();
}
}

View File

@ -28,11 +28,12 @@ public class BlockHoundIntegrationCustomizer implements BlockHoundIntegration {
public void applyTo(BlockHound.Builder builder) {
// Elasticsearch classes reading from the classpath on initialization, needed for parsing Elasticsearch responses
builder //
.allowBlockingCallsInside("org.elasticsearch.Build", "<clinit>") //
.allowBlockingCallsInside("org.elasticsearch.common.xcontent.XContentBuilder", "<clinit>") // pre 7.16
.allowBlockingCallsInside("org.elasticsearch.common.XContentBuilder", "<clinit>") // from 7.16 on
.allowBlockingCallsInside("org.elasticsearch.xcontent.json.JsonXContent", "contentBuilder") // from 7.16 on
.allowBlockingCallsInside("org.elasticsearch.Build", "<clinit>");
.allowBlockingCallsInside("jakarta.json.spi.JsonProvider", "provider") //
;
builder.blockingMethodCallback(it -> {
new Error(it.toString()).printStackTrace();
throw new BlockingOperationError(it);

View File

@ -21,6 +21,8 @@ import static io.specto.hoverfly.junit.dsl.HoverflyDsl.*;
import static io.specto.hoverfly.junit.verification.HoverflyVerifications.*;
import static org.assertj.core.api.Assertions.*;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.transport.endpoints.BooleanResponse;
import io.specto.hoverfly.junit.core.Hoverfly;
import io.specto.hoverfly.junit5.HoverflyExtension;
import io.specto.hoverfly.junit5.api.HoverflyCapture;
@ -40,7 +42,8 @@ import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchClients;
import org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchClient;
import org.springframework.data.elasticsearch.client.reactive.ReactiveRestClients;
import org.springframework.http.HttpHeaders;
@ -60,7 +63,7 @@ import com.github.tomakehurst.wiremock.stubbing.StubMapping;
@ExtendWith(HoverflyExtension.class)
public class RestClientsTest {
@ParameterizedTest // DATAES-700
@ParameterizedTest(name = "{0}") // DATAES-700
@MethodSource("clientUnderTestFactorySource")
@DisplayName("should use configured proxy")
void shouldUseConfiguredProxy(ClientUnderTestFactory clientUnderTestFactory, Hoverfly hoverfly) throws IOException {
@ -87,10 +90,11 @@ public class RestClientsTest {
});
}
@ParameterizedTest // DATAES-801, DATAES-588
@ParameterizedTest(name = "{0}") // DATAES-801, DATAES-588
@MethodSource("clientUnderTestFactorySource")
@DisplayName("should configure client and set all required headers")
void shouldConfigureClientAndSetAllRequiredHeaders(ClientUnderTestFactory clientUnderTestFactory) {
wireMockServer(server -> {
HttpHeaders defaultHeaders = new HttpHeaders();
@ -112,17 +116,29 @@ public class RestClientsTest {
return httpHeaders;
});
if (clientUnderTestFactory instanceof RestClientUnderTestFactory) {
if (clientUnderTestFactory instanceof ERHLCUnderTestFactory) {
configurationBuilder
.withClientConfigurer(RestClients.RestClientConfigurationCallback.from(httpClientBuilder -> {
clientConfigurerCount.incrementAndGet();
return httpClientBuilder;
}));
} else if (clientUnderTestFactory instanceof ReactiveElasticsearchClientUnderTestFactory) {
} else if (clientUnderTestFactory instanceof ReactiveERHLCUnderTestFactory) {
configurationBuilder.withClientConfigurer(ReactiveRestClients.WebClientConfigurationCallback.from(webClient -> {
clientConfigurerCount.incrementAndGet();
return webClient;
}));
} else if (clientUnderTestFactory instanceof ELCUnderTestFactory) {
configurationBuilder.withClientConfigurer(
ElasticsearchClients.ElasticsearchClientConfigurationCallback.from(httpClientBuilder -> {
clientConfigurerCount.incrementAndGet();
return httpClientBuilder;
}));
} else if (clientUnderTestFactory instanceof ReactiveELCUnderTestFactory) {
configurationBuilder
.withClientConfigurer(ElasticsearchClients.ElasticsearchClientConfigurationCallback.from(webClient -> {
clientConfigurerCount.incrementAndGet();
return webClient;
}));
}
ClientConfiguration clientConfiguration = configurationBuilder.build();
@ -130,7 +146,8 @@ public class RestClientsTest {
ClientUnderTest clientUnderTest = clientUnderTestFactory.create(clientConfiguration);
// do several calls to check that the headerSupplier provided values are set
for (int i = 1; i <= 3; i++) {
int startValue = clientUnderTest.usesInitialRequest() ? 2 : 1;
for (int i = startValue; i <= startValue + 2; i++) {
clientUnderTest.ping();
verify(headRequestedFor(urlEqualTo("/")) //
@ -140,7 +157,7 @@ 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 + 1))) //
.withHeader("supplied", new EqualToPattern("val" + (i))) //
);
}
@ -155,7 +172,8 @@ public class RestClientsTest {
wireMockServer(server -> {
stubFor(put(urlMatching("^/index/_doc/42(\\?.*)$?")) //
String urlPattern = "^/index/_doc/42(\\?.*)?$";
stubFor(put(urlMatching(urlPattern)) //
.willReturn(jsonResponse("{\n" + //
" \"_id\": \"42\",\n" + //
" \"_index\": \"test\",\n" + //
@ -196,9 +214,9 @@ public class RestClientsTest {
}
;
clientUnderTest.save(new Foo("42"));
clientUnderTest.index(new Foo("42"));
verify(putRequestedFor(urlMatching("^/index/_doc/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")) //
);
@ -286,7 +304,15 @@ public class RestClientsTest {
*/
boolean ping() throws Exception;
<T> void save(T entity) throws IOException;
/**
* @return true if the client send an initial request for discovery, version check or similar.
*/
boolean usesInitialRequest();
/**
* save an entity using an index request
*/
<T> void index(T entity) throws IOException;
}
/**
@ -304,9 +330,9 @@ public class RestClientsTest {
}
/**
* {@link ClientUnderTestFactory} implementation for the Standard {@link RestHighLevelClient}.
* {@link ClientUnderTestFactory} implementation for the old {@link RestHighLevelClient}.
*/
static class RestClientUnderTestFactory extends ClientUnderTestFactory {
static class ERHLCUnderTestFactory extends ClientUnderTestFactory {
@Override
protected String getDisplayName() {
@ -323,7 +349,12 @@ public class RestClientsTest {
}
@Override
public <T> void save(T entity) throws IOException {
public boolean usesInitialRequest() {
return true;
}
@Override
public <T> void index(T entity) throws IOException {
IndexRequest indexRequest = new IndexRequest("index");
indexRequest.id("42");
indexRequest.source(entity, XContentType.JSON);
@ -335,35 +366,108 @@ public class RestClientsTest {
}
/**
* {@link ClientUnderTestFactory} implementation for the {@link ReactiveElasticsearchClient}.
* {@link ClientUnderTestFactory} implementation for the {@link co.elastic.clients.elasticsearch.ElasticsearchClient}.
*/
static class ReactiveElasticsearchClientUnderTestFactory extends ClientUnderTestFactory {
static class ELCUnderTestFactory extends ClientUnderTestFactory {
@Override
protected String getDisplayName() {
return "ReactiveElasticsearchClient";
return "ElasticsearchClient";
}
@Override
ClientUnderTest create(ClientConfiguration clientConfiguration) {
ReactiveElasticsearchClient client = ReactiveRestClients.create(clientConfiguration);
ElasticsearchClient client = ElasticsearchClients.createImperative(clientConfiguration);
return new ClientUnderTest() {
@Override
public boolean ping() throws Exception {
return client.ping().block();
return client.ping().value();
}
@Override
public <T> void save(T entity) throws IOException {
public boolean usesInitialRequest() {
return false;
}
@Override
public <T> void index(T entity) throws IOException {
client.index(ir -> ir.index("index").id("42").document(entity));
}
};
}
}
/**
* {@link ClientUnderTestFactory} implementation for the
* {@link org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient}.
*/
static class ReactiveERHLCUnderTestFactory extends ClientUnderTestFactory {
@Override
protected String getDisplayName() {
return "ReactiveElasticsearchClient (RHLC based)";
}
@Override
ClientUnderTest create(ClientConfiguration clientConfiguration) {
org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient client = ReactiveRestClients
.create(clientConfiguration);
return new ClientUnderTest() {
@Override
public boolean ping() throws Exception {
return Boolean.TRUE.equals(client.ping().block());
}
@Override
public boolean usesInitialRequest() {
return true;
}
@Override
public <T> void index(T entity) throws IOException {
IndexRequest indexRequest = new IndexRequest("index");
indexRequest.id("42");
indexRequest.source("{}", XContentType.JSON);
indexRequest.source(entity, XContentType.JSON);
client.index(indexRequest).block();
}
};
}
}
/**
* {@link ClientUnderTestFactory} implementation for the {@link ReactiveElasticsearchClient}.
*/
static class ReactiveELCUnderTestFactory extends ClientUnderTestFactory {
@Override
protected String getDisplayName() {
return "ReactiveElasticsearchClient (ELC based)";
}
@Override
ClientUnderTest create(ClientConfiguration clientConfiguration) {
ReactiveElasticsearchClient client = ElasticsearchClients.createReactive(clientConfiguration);
return new ClientUnderTest() {
@Override
public boolean ping() throws Exception {
return Boolean.TRUE.equals(client.ping().map(BooleanResponse::value).block());
}
@Override
public boolean usesInitialRequest() {
return false;
}
@Override
public <T> void index(T entity) throws IOException {
client.index(ir -> ir.index("index").id("42").document(entity)).block();
}
};
}
}
/**
* Provides the factories to use in the parameterized tests
*
@ -371,8 +475,9 @@ public class RestClientsTest {
*/
static Stream<ClientUnderTestFactory> clientUnderTestFactorySource() {
return Stream.of( //
new RestClientUnderTestFactory(), //
new ReactiveElasticsearchClientUnderTestFactory() //
);
new ERHLCUnderTestFactory(), //
new ReactiveERHLCUnderTestFactory(), //
new ELCUnderTestFactory(), //
new ReactiveELCUnderTestFactory());
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 2021-2022 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 static org.mockito.Mockito.*;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.elasticsearch.client.RestClient;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
/**
* @author Peter-Josef Meisch
* @since 4.4
*/
@ExtendWith(MockitoExtension.class)
class AutoCloseableElasticsearchClientTest {
@Mock private RestClient restClient;
@Mock private JsonpMapper jsonMapper;
@Test // #1973
@DisplayName("should close the RestClient")
void shouldCloseTheRestClient() throws Exception {
ElasticsearchTransport transport = new RestClientTransport(restClient, jsonMapper);
// noinspection EmptyTryBlock
try (AutoCloseableElasticsearchClient ignored = new AutoCloseableElasticsearchClient(transport)) {}
verify(restClient).close();
}
}

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