diff --git a/.gitignore b/.gitignore index ea0475e148..449f58ea44 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,7 @@ target build/ node_modules node -package.json package-lock.json -.mvn/.gradle-enterprise +.mvn/.develocity +/src/test/resources/testcontainers-local.properties diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index ebd7610255..e0857eaa25 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -1,13 +1,8 @@ - com.gradle - gradle-enterprise-maven-extension - 1.19.2 - - - com.gradle - common-custom-user-data-maven-extension - 1.12.4 + io.spring.develocity.conventions + develocity-conventions-maven-extension + 0.0.22 diff --git a/.mvn/gradle-enterprise.xml b/.mvn/gradle-enterprise.xml deleted file mode 100644 index f88a885d06..0000000000 --- a/.mvn/gradle-enterprise.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - https://ge.spring.io - - - false - true - true - - #{{'0.0.0.0'}} - - - - - true - - - - - ${env.GRADLE_ENTERPRISE_CACHE_USERNAME} - ${env.GRADLE_ENTERPRISE_CACHE_PASSWORD} - - - true - #{env['GRADLE_ENTERPRISE_CACHE_USERNAME'] != null and env['GRADLE_ENTERPRISE_CACHE_PASSWORD'] != null} - - - diff --git a/.mvn/jvm.config b/.mvn/jvm.config new file mode 100644 index 0000000000..e27f6e8f5e --- /dev/null +++ b/.mvn/jvm.config @@ -0,0 +1,14 @@ +--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED +--add-opens=java.base/java.util=ALL-UNNAMED +--add-opens=java.base/java.lang.reflect=ALL-UNNAMED +--add-opens=java.base/java.text=ALL-UNNAMED +--add-opens=java.desktop/java.awt.font=ALL-UNNAMED diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index e6686c6c0c..e075a74d86 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,3 +1,3 @@ -#Thu Dec 14 08:40:44 CET 2023 +#Thu Nov 07 09:47:28 CET 2024 wrapperUrl=https\://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar -distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip +distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/Jenkinsfile b/Jenkinsfile index ade2e5a109..1d2500ed1e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -33,15 +33,16 @@ pipeline { environment { ARTIFACTORY = credentials("${p['artifactory.credentials']}") - DEVELOCITY_CACHE = credentials("${p['develocity.cache.credentials']}") DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") } steps { script { - docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.docker']) { - sh "PROFILE=none JENKINS_USER_NAME=${p['jenkins.user.name']} ci/verify.sh" - sh "JENKINS_USER_NAME=${p['jenkins.user.name']} ci/clean.sh" + docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) { + docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.docker']) { + sh "PROFILE=none JENKINS_USER_NAME=${p['jenkins.user.name']} ci/verify.sh" + sh "JENKINS_USER_NAME=${p['jenkins.user.name']} ci/clean.sh" + } } } } @@ -63,14 +64,15 @@ pipeline { options { timeout(time: 30, unit: 'MINUTES') } environment { ARTIFACTORY = credentials("${p['artifactory.credentials']}") - DEVELOCITY_CACHE = credentials("${p['develocity.cache.credentials']}") DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") } steps { script { - docker.image(p['docker.java.next.image']).inside(p['docker.java.inside.docker']) { - sh "PROFILE=none JENKINS_USER_NAME=${p['jenkins.user.name']} ci/verify.sh" - sh "JENKINS_USER_NAME=${p['jenkins.user.name']} ci/clean.sh" + docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) { + docker.image(p['docker.java.next.image']).inside(p['docker.java.inside.docker']) { + sh "PROFILE=none JENKINS_USER_NAME=${p['jenkins.user.name']} ci/verify.sh" + sh "JENKINS_USER_NAME=${p['jenkins.user.name']} ci/clean.sh" + } } } } @@ -92,24 +94,24 @@ pipeline { options { timeout(time: 20, unit: 'MINUTES') } environment { ARTIFACTORY = credentials("${p['artifactory.credentials']}") - DEVELOCITY_CACHE = credentials("${p['develocity.cache.credentials']}") DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") } steps { script { - docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.basic']) { - sh 'MAVEN_OPTS="-Duser.name=' + "${p['jenkins.user.name']}" + ' -Duser.home=/tmp/jenkins-home" ' + - "DEVELOCITY_CACHE_USERNAME=${DEVELOCITY_CACHE_USR} " + - "DEVELOCITY_CACHE_PASSWORD=${DEVELOCITY_CACHE_PSW} " + - "GRADLE_ENTERPRISE_ACCESS_KEY=${DEVELOCITY_ACCESS_KEY} " + - "./mvnw -s settings.xml -Pci,artifactory -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-elasticsearch-non-root " + - "-Dartifactory.server=${p['artifactory.url']} " + - "-Dartifactory.username=${ARTIFACTORY_USR} " + - "-Dartifactory.password=${ARTIFACTORY_PSW} " + - "-Dartifactory.staging-repository=${p['artifactory.repository.snapshot']} " + - "-Dartifactory.build-name=spring-data-elasticsearch " + - "-Dartifactory.build-number=spring-data-elasticsearch-${BRANCH_NAME}-build-${BUILD_NUMBER} " + - "-Dmaven.test.skip=true clean deploy -U -B" + docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) { + docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.docker']) { + sh 'MAVEN_OPTS="-Duser.name=' + "${p['jenkins.user.name']}" + ' -Duser.home=/tmp/jenkins-home" ' + + "./mvnw -s settings.xml -Pci,artifactory " + + "-Ddevelocity.storage.directory=/tmp/jenkins-home/.develocity-root " + + "-Dartifactory.server=${p['artifactory.url']} " + + "-Dartifactory.username=${ARTIFACTORY_USR} " + + "-Dartifactory.password=${ARTIFACTORY_PSW} " + + "-Dartifactory.staging-repository=${p['artifactory.repository.snapshot']} " + + "-Dartifactory.build-name=spring-data-elasticsearch " + + "-Dartifactory.build-number=spring-data-elasticsearch-${BRANCH_NAME}-build-${BUILD_NUMBER} " + + "-Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-elasticsearch " + + "-Dmaven.test.skip=true clean deploy -U -B" + } } } } diff --git a/README.adoc b/README.adoc index 72860e181b..0242089d82 100644 --- a/README.adoc +++ b/README.adoc @@ -1,5 +1,3 @@ -image:https://spring.io/badges/spring-data-elasticsearch/ga.svg[Spring Data Elasticsearch,link=https://projects.spring.io/spring-data-elasticsearch#quick-start] image:https://spring.io/badges/spring-data-elasticsearch/snapshot.svg[Spring Data Elasticsearch,link=https://projects.spring.io/spring-data-elasticsearch#quick-start] - = Spring Data for Elasticsearch image:https://jenkins.spring.io/buildStatus/icon?job=spring-data-elasticsearch%2Fmain&subject=Build[link=https://jenkins.spring.io/view/SpringData/job/spring-data-elasticsearch/] https://gitter.im/spring-projects/spring-data[image:https://badges.gitter.im/spring-projects/spring-data.svg[Gitter]] image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Develocity", link="https://ge.spring.io/scans?search.rootProjectNames=Spring%20Data%20Elasticsearch"] The primary goal of the https://projects.spring.io/spring-data[Spring Data] project is to make it easier to build Spring-powered applications that use new data access technologies such as non-relational databases, map-reduce frameworks, and cloud based data services. @@ -64,7 +62,7 @@ public class MyService { === Using the RestClient -Please check the [official documentation](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#elasticsearch.clients.configuration). +Please check the https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#elasticsearch.clients.configuration[official documentation]. === Maven configuration @@ -126,8 +124,7 @@ We’d love to help! https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/[reference documentation], and https://docs.spring.io/spring-data/elasticsearch/docs/current/api/[Javadocs]. * Learn the Spring basics – Spring Data builds on Spring Framework, check the https://spring.io[spring.io] web-site for a wealth of reference documentation. If you are just starting out with Spring, try one of the https://spring.io/guides[guides]. -* Ask a question - we monitor https://stackoverflow.com[stackoverflow.com] for questions tagged with https://stackoverflow.com/tags/spring-data[`spring-data-elasticsearch`]. -You can also chat with the community on https://gitter.im/spring-projects/spring-data[Gitter]. +* Ask a question or chat with the community on https://app.gitter.im/#/room/#spring-projects_spring-data:gitter.im[Gitter]. * Report bugs with Spring Data for Elasticsearch at https://github.com/spring-projects/spring-data-elasticsearch/issues[https://github.com/spring-projects/spring-data-elasticsearch/issues]. == Reporting Issues @@ -171,7 +168,7 @@ Building the documentation builds also the project without running tests. $ ./mvnw clean install -Pantora ---- -The generated documentation is available from `target/antora/site/index.html`. +The generated documentation is available from `target/site/index.html`. == Examples diff --git a/ci/clean.sh b/ci/clean.sh index 34ba4ffcc1..ca174330ee 100755 --- a/ci/clean.sh +++ b/ci/clean.sh @@ -2,12 +2,7 @@ set -euo pipefail -export DEVELOCITY_CACHE_USERNAME=${DEVELOCITY_CACHE_USR} -export DEVELOCITY_CACHE_PASSWORD=${DEVELOCITY_CACHE_PSW} export JENKINS_USER=${JENKINS_USER_NAME} -# The environment variable to configure access key is still GRADLE_ENTERPRISE_ACCESS_KEY -export GRADLE_ENTERPRISE_ACCESS_KEY=${DEVELOCITY_ACCESS_KEY} - MAVEN_OPTS="-Duser.name=${JENKINS_USER} -Duser.home=/tmp/jenkins-home" \ - ./mvnw -s settings.xml clean -Dscan=false -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-elasticsearch + ./mvnw -s settings.xml clean -Dscan=false -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-elasticsearch -Ddevelocity.storage.directory=/tmp/jenkins-home/.develocity-root diff --git a/ci/pipeline.properties b/ci/pipeline.properties index 60057f2659..cde4a8e881 100644 --- a/ci/pipeline.properties +++ b/ci/pipeline.properties @@ -1,22 +1,19 @@ # Java versions -java.main.tag=17.0.9_9-jdk-focal -java.next.tag=21.0.1_12-jdk-jammy +java.main.tag=24.0.1_9-jdk-noble +java.next.tag=24.0.1_9-jdk-noble # Docker container images - standard -docker.java.main.image=harbor-repo.vmware.com/dockerhub-proxy-cache/library/eclipse-temurin:${java.main.tag} -docker.java.next.image=harbor-repo.vmware.com/dockerhub-proxy-cache/library/eclipse-temurin:${java.next.tag} +docker.java.main.image=library/eclipse-temurin:${java.main.tag} +docker.java.next.image=library/eclipse-temurin:${java.next.tag} # Supported versions of MongoDB -docker.mongodb.4.4.version=4.4.25 -docker.mongodb.5.0.version=5.0.21 -docker.mongodb.6.0.version=6.0.10 -docker.mongodb.7.0.version=7.0.2 +docker.mongodb.6.0.version=6.0.23 +docker.mongodb.7.0.version=7.0.20 +docker.mongodb.8.0.version=8.0.9 # Supported versions of Redis docker.redis.6.version=6.2.13 - -# Supported versions of Cassandra -docker.cassandra.3.version=3.11.16 +docker.redis.7.version=7.2.4 # Docker environment settings docker.java.inside.basic=-v $HOME:/tmp/jenkins-home @@ -25,9 +22,10 @@ docker.java.inside.docker=-u root -v /var/run/docker.sock:/var/run/docker.sock - # Credentials docker.registry= docker.credentials=hub.docker.com-springbuildmaster +docker.proxy.registry=https://docker-hub.usw1.packages.broadcom.com +docker.proxy.credentials=usw1_packages_broadcom_com-jenkins-token artifactory.credentials=02bd1690-b54f-4c9f-819d-a77cb7a9822c artifactory.url=https://repo.spring.io artifactory.repository.snapshot=libs-snapshot-local -develocity.cache.credentials=gradle_enterprise_cache_user develocity.access-key=gradle_enterprise_secret_access_key jenkins.user.name=spring-builds+jenkins diff --git a/ci/verify.sh b/ci/verify.sh index 98b9c79cc6..46afc80280 100755 --- a/ci/verify.sh +++ b/ci/verify.sh @@ -3,15 +3,8 @@ set -euo pipefail mkdir -p /tmp/jenkins-home/.m2/spring-data-elasticsearch -chown -R 1001:1001 . - -export DEVELOCITY_CACHE_USERNAME=${DEVELOCITY_CACHE_USR} -export DEVELOCITY_CACHE_PASSWORD=${DEVELOCITY_CACHE_PSW} export JENKINS_USER=${JENKINS_USER_NAME} -# The environment variable to configure access key is still GRADLE_ENTERPRISE_ACCESS_KEY -export GRADLE_ENTERPRISE_ACCESS_KEY=${DEVELOCITY_ACCESS_KEY} - MAVEN_OPTS="-Duser.name=${JENKINS_USER} -Duser.home=/tmp/jenkins-home" \ ./mvnw -s settings.xml \ - -P${PROFILE} clean dependency:list verify -Dsort -U -B -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-elasticsearch + -P${PROFILE} clean dependency:list verify -Dsort -U -B -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-elasticsearch -Ddevelocity.storage.directory=/tmp/jenkins-home/.develocity-root diff --git a/package.json b/package.json new file mode 100644 index 0000000000..4689506b3f --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "antora": "3.2.0-alpha.6", + "@antora/atlas-extension": "1.0.0-alpha.2", + "@antora/collector-extension": "1.0.0-alpha.7", + "@asciidoctor/tabs": "1.0.0-beta.6", + "@springio/antora-extensions": "1.13.0", + "@springio/asciidoctor-extensions": "1.0.0-alpha.11" + } +} diff --git a/pom.xml b/pom.xml index 1e35a6d69f..4396ce5a62 100644 --- a/pom.xml +++ b/pom.xml @@ -5,12 +5,12 @@ org.springframework.data spring-data-elasticsearch - 5.3.0-SNAPSHOT + 6.0.0-SNAPSHOT org.springframework.data.build spring-data-parent - 3.3.0-SNAPSHOT + 4.0.0-SNAPSHOT Spring Data Elasticsearch @@ -18,17 +18,16 @@ https://github.com/spring-projects/spring-data-elasticsearch - 3.3.0-SNAPSHOT + 4.0.0-SNAPSHOT - 8.12.2 + 9.0.1 - 1.0.8.RELEASE - 0.14.4 - 2.18.0 - 1.5.1 - 1.18.0 - 2.35.1 + 0.19.0 + 2.23.1 + 1.5.3 + 1.20.0 + 3.9.1 spring.data.elasticsearch @@ -132,9 +131,10 @@ + org.elasticsearch.client - elasticsearch-rest-client + elasticsearch-rest-client ${elasticsearch-java} @@ -144,6 +144,13 @@ + + com.querydsl + querydsl-core + ${querydsl} + true + + com.fasterxml.jackson.core @@ -248,13 +255,6 @@ test - - io.projectreactor.tools - blockhound-junit-platform - ${blockhound-junit} - test - - org.skyscreamer jsonassert @@ -263,8 +263,8 @@ - com.github.tomakehurst - wiremock-jre8 + org.wiremock + wiremock ${wiremock} test @@ -443,25 +443,6 @@ - - jdk13+ - - - [13,) - - - - - org.apache.maven.plugins - maven-surefire-plugin - - -XX:+AllowRedefinitionToAddDeleteMethods - - - - - - antora-process-resources @@ -479,7 +460,7 @@ - io.spring.maven.antora + org.antora antora-maven-plugin diff --git a/src/main/antora/antora-playbook.yml b/src/main/antora/antora-playbook.yml index 27404c0c14..1a4f73c1e6 100644 --- a/src/main/antora/antora-playbook.yml +++ b/src/main/antora/antora-playbook.yml @@ -3,8 +3,7 @@ # The purpose of this Antora playbook is to build the docs in the current branch. antora: extensions: - - '@antora/collector-extension' - - require: '@springio/antora-extensions/root-component-extension' + - require: '@springio/antora-extensions' root_component_name: 'data-elasticsearch' site: title: Spring Data Elasticsearch @@ -18,17 +17,16 @@ content: - url: https://github.com/spring-projects/spring-data-commons # Refname matching: # https://docs.antora.org/antora/latest/playbook/content-refname-matching/ - branches: [ main, 3.2.x ] + branches: [ main, 3.4.x, 3.3.x ] start_path: src/main/antora asciidoc: attributes: - page-pagination: '' hide-uri-scheme: '@' tabs-sync-option: '@' - chomp: 'all' extensions: - '@asciidoctor/tabs' - '@springio/asciidoctor-extensions' + - '@springio/asciidoctor-extensions/javadoc-extension' sourcemap: true urls: latest_version_segment: '' @@ -38,5 +36,5 @@ runtime: format: pretty ui: bundle: - url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.3.5/ui-bundle.zip + url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.16/ui-bundle.zip snapshot: true diff --git a/src/main/antora/antora.yml b/src/main/antora/antora.yml index 70364a772e..2348fca613 100644 --- a/src/main/antora/antora.yml +++ b/src/main/antora/antora.yml @@ -10,3 +10,8 @@ ext: local: true scan: dir: target/classes/ + - run: + command: ./mvnw package -Pdistribute + local: true + scan: + dir: target/antora diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc index db6e1ca61b..fa1ee8110d 100644 --- a/src/main/antora/modules/ROOT/nav.adoc +++ b/src/main/antora/modules/ROOT/nav.adoc @@ -9,6 +9,10 @@ *** xref:migration-guides/migration-guide-4.4-5.0.adoc[] *** xref:migration-guides/migration-guide-5.0-5.1.adoc[] *** xref:migration-guides/migration-guide-5.1-5.2.adoc[] +*** xref:migration-guides/migration-guide-5.2-5.3.adoc[] +*** xref:migration-guides/migration-guide-5.3-5.4.adoc[] +*** xref:migration-guides/migration-guide-5.4-5.5.adoc[] +*** xref:migration-guides/migration-guide-5.5-6.0.adoc[] * xref:elasticsearch.adoc[] @@ -39,4 +43,5 @@ ** xref:repositories/query-keywords-reference.adoc[] ** xref:repositories/query-return-types-reference.adoc[] -* https://github.com/spring-projects/spring-data-commons/wiki[Wiki] +* xref:attachment$api/java/index.html[Javadoc,role=link-external,window=_blank] +* https://github.com/spring-projects/spring-data-commons/wiki[Wiki,role=link-external,window=_blank] diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/auditing.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/auditing.adoc index d02373f82b..f9633dec4f 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/auditing.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/auditing.adoc @@ -10,7 +10,7 @@ In order for the auditing code to be able to decide whether an entity instance i ---- package org.springframework.data.domain; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; public interface Persistable { @Nullable @@ -81,5 +81,5 @@ class MyConfiguration { } ---- -If your code contains more than one `AuditorAware` bean for different types, you must provide the name of the bean to use as an argument to the `auditorAwareRef` parameter of the - `@EnableElasticsearchAuditing` annotation. +If your code contains more than one `AuditorAware` bean for different types, you must provide the name of the bean to use as an argument to the `auditorAwareRef` parameter of the + `@EnableElasticsearchAuditing` annotation. diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/clients.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/clients.adoc index 0f8d8c1027..0cf7d5ea3c 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/clients.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/clients.adoc @@ -31,7 +31,7 @@ public class MyClientConfig extends ElasticsearchConfiguration { <.> for a detailed description of the builder methods see xref:elasticsearch/clients.adoc#elasticsearch.clients.configuration[Client Configuration] ==== -The `ElasticsearchConfiguration` class allows further configuration by overriding for example the `jsonpMapper()` or `transportOptions()` methods. +The javadoc:org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration[]] class allows further configuration by overriding for example the `jsonpMapper()` or `transportOptions()` methods. The following beans can then be injected in other Spring components: @@ -52,13 +52,13 @@ RestClient restClient; <.> JsonpMapper jsonpMapper; <.> ---- -<.> an implementation of `ElasticsearchOperations` +<.> an implementation of javadoc:org.springframework.data.elasticsearch.core.ElasticsearchOperations[] <.> the `co.elastic.clients.elasticsearch.ElasticsearchClient` that is used. <.> the low level `RestClient` from the Elasticsearch libraries <.> the `JsonpMapper` user by the Elasticsearch `Transport` ==== -Basically one should just use the `ElasticsearchOperations` to interact with the Elasticsearch cluster. +Basically one should just use the javadoc:org.springframework.data.elasticsearch.core.ElasticsearchOperations[] to interact with the Elasticsearch cluster. When using repositories, this instance is used under the hood as well. [[elasticsearch.clients.reactiverestclient]] @@ -86,7 +86,7 @@ public class MyClientConfig extends ReactiveElasticsearchConfiguration { <.> for a detailed description of the builder methods see xref:elasticsearch/clients.adoc#elasticsearch.clients.configuration[Client Configuration] ==== -The `ReactiveElasticsearchConfiguration` class allows further configuration by overriding for example the `jsonpMapper()` or `transportOptions()` methods. +The javadoc:org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchConfiguration[] class allows further configuration by overriding for example the `jsonpMapper()` or `transportOptions()` methods. The following beans can then be injected in other Spring components: @@ -108,20 +108,20 @@ JsonpMapper jsonpMapper; <.> the following can be injected: -<.> an implementation of `ReactiveElasticsearchOperations` +<.> an implementation of javadoc:org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations[] <.> the `org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchClient` that is used. This is a reactive implementation based on the Elasticsearch client implementation. <.> the low level `RestClient` from the Elasticsearch libraries <.> the `JsonpMapper` user by the Elasticsearch `Transport` ==== -Basically one should just use the `ReactiveElasticsearchOperations` to interact with the Elasticsearch cluster. +Basically one should just use the javadoc:org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations[] to interact with the Elasticsearch cluster. When using repositories, this instance is used under the hood as well. [[elasticsearch.clients.configuration]] == Client Configuration -Client behaviour can be changed via the `ClientConfiguration` that allows to set options for SSL, connect and socket timeouts, headers and other parameters. +Client behaviour can be changed via the javadoc:org.springframework.data.elasticsearch.client.ClientConfiguration[] that allows to set options for SSL, connect and socket timeouts, headers and other parameters. .Client Configuration ==== @@ -178,7 +178,7 @@ If this is used in the reactive setup, the supplier function *must not* block! [[elasticsearch.clients.configuration.callbacks]] === Client configuration callbacks -The `ClientConfiguration` class offers the most common parameters to configure the client. +The javadoc:org.springframework.data.elasticsearch.client.ClientConfiguration[] class offers the most common parameters to configure the client. In the case this is not enough, the user can add callback functions by using the `withClientConfigurer(ClientConfigurationCallback)` method. The following callbacks are provided: diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc index 17781c9946..f1e07dd195 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc @@ -1,15 +1,39 @@ [[new-features]] = What's new +[[new-features.6-0-0]] +== New in Spring Data Elasticsearch 6.6 + +* Upgarde to Spring 7 +* Switch to jspecify nullability annotations +* Upgrade to Elasticsearch 9.0.1 + + +[[new-features.5-5-0]] +== New in Spring Data Elasticsearch 5.5 + +* Upgrade to Elasticsearch 8.18.1. +* Add support for the `@SearchTemplateQuery` annotation on repository methods. +* Scripted field properties of type collection can be populated from scripts returning arrays. + +[[new-features.5-4-0]] +== New in Spring Data Elasticsearch 5.4 + +* Upgrade to Elasticsearch 8.15.3. +* Allow to customize the mapped type name for `@InnerField` and `@Field` annotations. +* Support for Elasticsearch SQL. +* Add support for retrieving request executionDuration. + [[new-features.5-3-0]] == New in Spring Data Elasticsearch 5.3 -* Upgrade to Elasticsearch 8.12.2. +* Upgrade to Elasticsearch 8.13.2. * Add support for highlight queries in highlighting. * Add shard statistics to the `SearchHit` class. * Add support for multi search template API. * Add support for SpEL in @Query. * Add support for field aliases in the index mapping. +* Add support for has_child and has_parent queries. [[new-features.5-2-0]] == New in Spring Data Elasticsearch 5.2 diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/join-types.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/join-types.adoc index c97dd46c62..a1bc3df192 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/join-types.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/join-types.adoc @@ -52,7 +52,7 @@ public class Statement { return routing; } - public void setRouting(Routing routing) { + public void setRouting(String routing) { this.routing = routing; } @@ -199,7 +199,7 @@ void init() { repository.save( Statement.builder() .withText("+1 for the sun") - ,withRouting(savedWeather.getId()) + .withRouting(savedWeather.getId()) .withRelation(new JoinField<>("vote", sunnyAnswer.getId())) <5> .build()); } @@ -226,6 +226,7 @@ SearchHits hasVotes() { Query query = NativeQuery.builder() .withQuery(co.elastic.clients.elasticsearch._types.query_dsl.Query.of(qb -> qb .hasChild(hc -> hc + .type("answer") .queryName("vote") .query(matchAllQueryAsQuery()) .scoreMode(ChildScoreMode.None) diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/misc.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/misc.adoc index 36567cda4a..7f3ac8f0ff 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/misc.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/misc.adoc @@ -365,6 +365,8 @@ operations.putScript( <.> To use a search template in a search query, Spring Data Elasticsearch provides the `SearchTemplateQuery`, an implementation of the `org.springframework.data.elasticsearch.core.query.Query` interface. +NOTE: Although `SearchTemplateQuery` is an implementation of the `Query` interface, not all of the functionality provided by the base class is available for a `SearchTemplateQuery` like setting a `Pageable` or a `Sort`. Values for this functionality must be added to the stored script like shown in the following example for paging parameters. If these values are set on the `Query` object, they will be ignored. + In the following code, we will add a call using a search template query to a custom repository implementation (see xref:repositories/custom-implementations.adoc[]) as an example how this can be integrated into a repository call. @@ -449,4 +451,3 @@ var query = Query.findAll().addSort(Sort.by(order)); About the filter query: It is not possible to use a `CriteriaQuery` here, as this query would be converted into a Elasticsearch nested query which does not work in the filter context. So only `StringQuery` or `NativeQuery` can be used here. When using one of these, like the term query above, the Elasticsearch field names must be used, so take care, when these are redefined with the `@Field(name="...")` definition. For the definition of the order path and the nested paths, the Java entity property names should be used. - diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/object-mapping.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/object-mapping.adoc index ab6e98d5c4..6ca12728c0 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/object-mapping.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/object-mapping.adoc @@ -192,8 +192,7 @@ public String getProperty() { This annotation can be set on a String property of an entity. This property will not be written to the mapping, it will not be stored in Elasticsearch and its value will not be read from an Elasticsearch document. -After an entity is persisted, for example with a call to `ElasticsearchOperations.save(T entity)`, the entity -returned from that call will contain the name of the index that an entity was saved to in that property. +After an entity is persisted, for example with a call to `ElasticsearchOperations.save(T entity)`, the entity returned from that call will contain the name of the index that an entity was saved to in that property. This is useful when the index name is dynamically set by a bean, or when writing to a write alias. Putting some value into such a property does not set the index into which an entity is stored! @@ -423,7 +422,6 @@ Looking at the `Configuration` from the xref:elasticsearch/object-mapping.adoc#e @Configuration public class Config extends ElasticsearchConfiguration { - @NonNull @Override public ClientConfiguration clientConfiguration() { return ClientConfiguration.builder() // diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repository-queries.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repository-queries.adoc index b558e13bba..b22e17522d 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repository-queries.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repository-queries.adoc @@ -10,7 +10,9 @@ The Elasticsearch module supports all basic query building feature as string que === Declared queries Deriving the query from the method name is not always sufficient and/or may result in unreadable method names. -In this case one might make use of the `@Query` annotation (see xref:elasticsearch/repositories/elasticsearch-repository-queries.adoc#elasticsearch.query-methods.at-query[Using @Query Annotation] ). +In this case one might make use of the `@Query` annotation (see xref:elasticsearch/repositories/elasticsearch-repository-queries.adoc#elasticsearch.query-methods.at-query[Using the @Query Annotation] ). + +Another possibility is the use of a search-template, (see xref:elasticsearch/repositories/elasticsearch-repository-queries.adoc#elasticsearch.query-methods.at-searchtemplate-query[Using the @SearchTemplateQuery Annotation] ). [[elasticsearch.query-methods.criterions]] == Query creation @@ -312,11 +314,13 @@ Repository methods can be defined to have the following return types for returni * `SearchPage` [[elasticsearch.query-methods.at-query]] -== Using @Query Annotation +== Using the @Query Annotation .Declare query on the method using the `@Query` annotation. ==== -The arguments passed to the method can be inserted into placeholders in the query string. The placeholders are of the form `?0`, `?1`, `?2` etc. for the first, second, third parameter and so on. +The arguments passed to the method can be inserted into placeholders in the query string. +The placeholders are of the form `?0`, `?1`, `?2` etc. for the first, second, third parameter and so on. + [source,java] ---- interface BookRepository extends ElasticsearchRepository { @@ -341,15 +345,20 @@ It will be sent to Easticsearch as value of the query element; if for example th } ---- ==== + .`@Query` annotation on a method taking a Collection argument ==== A repository method such as + [source,java] ---- @Query("{\"ids\": {\"values\": ?0 }}") List getByIds(Collection ids); ---- -would make an https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ids-query.html[IDs query] to return all the matching documents. So calling the method with a `List` of `["id1", "id2", "id3"]` would produce the query body + +would make an https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ids-query.html[IDs query] to return all the matching documents. +So calling the method with a `List` of `["id1", "id2", "id3"]` would produce the query body + [source,json] ---- { @@ -369,7 +378,6 @@ would make an https://www.elastic.co/guide/en/elasticsearch/reference/current/qu ==== https://docs.spring.io/spring-framework/reference/core/expressions.html[SpEL expression] is also supported when defining query in `@Query`. - [source,java] ---- interface BookRepository extends ElasticsearchRepository { @@ -411,6 +419,7 @@ If for example the function is called with the parameter _John_, it would produc .accessing parameter property. ==== Supposing that we have the following class as query parameter type: + [source,java] ---- public record QueryParameter(String value) { @@ -444,7 +453,9 @@ We can pass `new QueryParameter("John")` as the parameter now, and it will produ .accessing bean property. ==== -https://docs.spring.io/spring-framework/reference/core/expressions/language-ref/bean-references.html[Bean property] is also supported to access. Given that there is a bean named `queryParameter` of type `QueryParameter`, we can access the bean with symbol `@` rather than `#`, and there is no need to declare a parameter of type `QueryParameter` in the query method: +https://docs.spring.io/spring-framework/reference/core/expressions/language-ref/bean-references.html[Bean property] is also supported to access. +Given that there is a bean named `queryParameter` of type `QueryParameter`, we can access the bean with symbol `@` rather than `#`, and there is no need to declare a parameter of type `QueryParameter` in the query method: + [source,java] ---- interface BookRepository extends ElasticsearchRepository { @@ -493,6 +504,7 @@ interface BookRepository extends ElasticsearchRepository { NOTE: collection values should not be quoted when declaring the elasticsearch json query. A collection of `names` like `List.of("name1", "name2")` will produce the following terms query: + [source,json] ---- { @@ -532,6 +544,7 @@ interface BookRepository extends ElasticsearchRepository { Page findByName(Collection parameters, Pageable pageable); } ---- + This will extract all the `value` property values as a new `Collection` from `QueryParameter` collection, thus takes the same effect as above. ==== @@ -560,3 +573,20 @@ interface BookRepository extends ElasticsearchRepository { ---- ==== + +[[elasticsearch.query-methods.at-searchtemplate-query]] +== Using the @SearchTemplateQuery Annotation + +When using Elasticsearch search templates - (see xref:elasticsearch/misc.adoc#elasticsearch.misc.searchtemplates [Search Template support]) it is possible to specify that a repository method should use a template by adding the `@SearchTemplateQuery` annotation to that method. + +Let's assume that there is a search template stored with the name "book-by-title" and this template need a parameter named "title", then a repository method using that search template can be defined like this: + +[source,java] +---- +interface BookRepository extends ElasticsearchRepository { + @SearchTemplateQuery(id = "book-by-title") + SearchHits findByTitle(String title); +} +---- + +The parameters of the repository method are sent to the seacrh template as key/value pairs where the key is the parameter name and the value is taken from the actual value when the method is invoked. diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/scripted-and-runtime-fields.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/scripted-and-runtime-fields.adoc index 7c5d730e25..64d4a0c003 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/scripted-and-runtime-fields.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/scripted-and-runtime-fields.adoc @@ -20,12 +20,12 @@ Whereas the birthdate is fix, the age depends on the time when a query is issued ==== [source,java] ---- +import org.jspecify.annotations.Nullable; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.DateFormat; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.ScriptedField; -import org.springframework.lang.Nullable; import java.time.LocalDate; import java.time.format.DateTimeFormatter; @@ -194,7 +194,7 @@ In the following code this is used to run a query for a given gender and maximum var runtimeField = new RuntimeField("age", "long", """ <.> Instant currentDate = Instant.ofEpochMilli(new Date().getTime()); - Instant startDate = doc['birth-date'].value.toInstant(); + Instant startDate = doc['birthDate'].value.toInstant(); emit (ChronoUnit.DAYS.between(startDate, currentDate) / 365); """); diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/template.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/template.adoc index b893bb1a19..d5cbec5d09 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/template.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/template.adoc @@ -3,10 +3,10 @@ Spring Data Elasticsearch uses several interfaces to define the operations that can be called against an Elasticsearch index (for a description of the reactive interfaces see xref:elasticsearch/reactive-template.adoc[]). -* `IndexOperations` defines actions on index level like creating or deleting an index. -* `DocumentOperations` defines actions to store, update and retrieve entities based on their id. -* `SearchOperations` define the actions to search for multiple entities using queries -* `ElasticsearchOperations` combines the `DocumentOperations` and `SearchOperations` interfaces. +* javadoc:org.springframework.data.elasticsearch.core.IndexOperations[] defines actions on index level like creating or deleting an index. +* javadoc:org.springframework.data.elasticsearch.core.DocumentOperations[] defines actions to store, update and retrieve entities based on their id. +* javadoc:org.springframework.data.elasticsearch.core.SearchOperations[] define the actions to search for multiple entities using queries +* javadoc:org.springframework.data.elasticsearch.core.ElasticsearchOperations[] combines the `DocumentOperations` and `SearchOperations` interfaces. These interfaces correspond to the structuring of the https://www.elastic.co/guide/en/elasticsearch/reference/current/rest-apis.html[Elasticsearch API]. @@ -81,7 +81,7 @@ When a document is retrieved with the methods of the `DocumentOperations` inter When searching with the methods of the `SearchOperations` interface, additional information is available for each entity, for example the _score_ or the _sortValues_ of the found entity. In order to return this information, each entity is wrapped in a `SearchHit` object that contains this entity-specific additional information. -These `SearchHit` objects themselves are returned within a `SearchHits` object which additionally contains informations about the whole search like the _maxScore_ or requested aggregations. +These `SearchHit` objects themselves are returned within a `SearchHits` object which additionally contains informations about the whole search like the _maxScore_ or requested aggregations or the execution duration it took to complete the request. The following classes and interfaces are now available: .SearchHit diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/versions.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/versions.adoc index 9f88a81734..8fb6d72617 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/versions.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/versions.adoc @@ -6,10 +6,13 @@ The following table shows the Elasticsearch and Spring versions that are used by [cols="^,^,^,^",options="header"] |=== | Spring Data Release Train | Spring Data Elasticsearch | Elasticsearch | Spring Framework -| 2024.0 (?) | 5.3.x | 8.12.2 | ? -| 2023.1 (Vaughan) | 5.2.x | 8.11.1 | 6.1.x -| 2023.0 (Ullmann) | 5.1.x | 8.7.1 | 6.0.x -| 2022.0 (Turing) | 5.0.xfootnote:oom[Out of maintenance] | 8.5.3 | 6.0.x +| 2025.1 (in development) | 6.0.x | 9.0.1 | 7.0.x +| 2025.0 | 5.5.x | 8.18.1 | 6.2.x +| 2024.1 | 5.4.x | 8.15.5 | 6.1.x +| 2024.0 | 5.3.xfootnote:oom[Out of maintenance] | 8.13.4 | 6.1.x +| 2023.1 (Vaughan) | 5.2.xfootnote:oom[] | 8.11.1 | 6.1.x +| 2023.0 (Ullmann) | 5.1.xfootnote:oom[] | 8.7.1 | 6.0.x +| 2022.0 (Turing) | 5.0.xfootnote:oom[] | 8.5.3 | 6.0.x | 2021.2 (Raj) | 4.4.xfootnote:oom[] | 7.17.3 | 5.3.x | 2021.1 (Q) | 4.3.xfootnote:oom[] | 7.15.2 | 5.3.x | 2021.0 (Pascal) | 4.2.xfootnote:oom[] | 7.12.0 | 5.3.x diff --git a/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.2-5.3.adoc b/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.2-5.3.adoc index 17ab9063c5..808578cb59 100644 --- a/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.2-5.3.adoc +++ b/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.2-5.3.adoc @@ -6,10 +6,16 @@ This section describes breaking changes from version 5.2.x to 5.3.x and how remo [[elasticsearch-migration-guide-5.2-5.3.breaking-changes]] == Breaking Changes +During the parameter replacement in `@Query` annotated repository methods previous versions wrote the String `"null"` into the query that was sent to Elasticsearch when the actual parameter value was `null`. +As Elasticsearch does not store `null` values, this behaviour could lead to problems, for example whent the fields to be searched contains the string `"null"`. +In Version 5.3 a `null` value in a parameter will cause a `ConversionException` to be thrown. +If you are using `"null"` as the +`null_value` defined in a field mapping, then pass that string into the query instead of a Java `null`. [[elasticsearch-migration-guide-5.2-5.3.deprecations]] == Deprecations === Removals + The deprecated classes `org.springframework.data.elasticsearch.ELCQueries` - and `org.springframework.data.elasticsearch.client.elc.QueryBuilders` have been removed, use `org.springframework.data.elasticsearch.client.elc.Queries` instead. +and `org.springframework.data.elasticsearch.client.elc.QueryBuilders` have been removed, use `org.springframework.data.elasticsearch.client.elc.Queries` instead. diff --git a/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.3-5.4.adoc b/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.3-5.4.adoc new file mode 100644 index 0000000000..c5178ff75d --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.3-5.4.adoc @@ -0,0 +1,23 @@ +[[elasticsearch-migration-guide-5.3-5.4]] += Upgrading from 5.3.x to 5.4.x + +This section describes breaking changes from version 5.3.x to 5.4.x and how removed features can be replaced by new introduced features. + +[[elasticsearch-migration-guide-5.3-5.4.breaking-changes]] +== Breaking Changes + +[[elasticsearch-migration-guide-5.3-5.4.breaking-changes.knn-search]] +=== knn search +The `withKnnQuery` method in `NativeQueryBuilder` has been replaced with `withKnnSearches` to build a `NativeQuery` with knn search. + +`KnnQuery` and `KnnSearch` are two different classes in elasticsearch java client and are used for different queries, with different parameters supported: + +- `KnnSearch`: is https://www.elastic.co/guide/en/elasticsearch/reference/8.13/search-search.html#search-api-knn[the top level `knn` query] in the elasticsearch request; +- `KnnQuery`: is https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-knn-query.html[the `knn` query inside `query` clause]; + +If `KnnQuery` is still preferable, please be sure to construct it inside `query` clause manually, by means of `withQuery(co.elastic.clients.elasticsearch._types.query_dsl.Query query)` clause in `NativeQueryBuilder`. + +[[elasticsearch-migration-guide-5.3-5.4.deprecations]] +== Deprecations + +=== Removals diff --git a/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.4-5.5.adoc b/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.4-5.5.adoc new file mode 100644 index 0000000000..38b2b4af2b --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.4-5.5.adoc @@ -0,0 +1,30 @@ +[[elasticsearch-migration-guide-5.4-5.5]] += Upgrading from 5.4.x to 5.5.x + +This section describes breaking changes from version 5.4.x to 5.5.x and how removed features can be replaced by new introduced features. + +[[elasticsearch-migration-guide-5.4-5.5.breaking-changes]] +== Breaking Changes + +[[elasticsearch-migration-guide-5.4-5.5.deprecations]] +== Deprecations + +Some classes that probably are not used by a library user have been renamed, the classes with the old names are still there, but are deprecated: + +|=== +|old name|new name + +|ElasticsearchPartQuery|RepositoryPartQuery +|ElasticsearchStringQuery|RepositoryStringQuery +|ReactiveElasticsearchStringQuery|ReactiveRepositoryStringQuery +|=== + +=== Removals + +The following methods that had been deprecated since release 5.3 have been removed: +``` +DocumentOperations.delete(Query, Class) +DocumentOperations.delete(Query, Class, IndexCoordinates) +ReactiveDocumentOperations.delete(Query, Class) +ReactiveDocumentOperations.delete(Query, Class, IndexCoordinates) +``` diff --git a/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.5-6.0.adoc b/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.5-6.0.adoc new file mode 100644 index 0000000000..7667701a17 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.5-6.0.adoc @@ -0,0 +1,21 @@ +[[elasticsearch-migration-guide-5.5-6.0]] += Upgrading from 5.5.x to 6.0.x + +This section describes breaking changes from version 5.5.x to 6.0.x and how removed features can be replaced by new introduced features. + +[[elasticsearch-migration-guide-5.5-6.0.breaking-changes]] +== Breaking Changes + +[[elasticsearch-migration-guide-5.5-6.0.deprecations]] +== Deprecations + + +=== Removals + +The `org.springframework.data.elasticsearch.core.query.ScriptType` enum has been removed. To distinguish between an inline and a stored script set the appropriate values in the `org.springframework.data.elasticsearch.core.query.ScriptData` record. + +These methods have been removed because the Elasticsearch Client 9 does not support them anymore: +``` +org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchIndicesClient.unfreeze(UnfreezeRequest) +org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchIndicesClient.unfreeze(Function>) +``` diff --git a/src/main/java/org/springframework/data/elasticsearch/BulkFailureException.java b/src/main/java/org/springframework/data/elasticsearch/BulkFailureException.java index 9308caa924..6d40bca631 100644 --- a/src/main/java/org/springframework/data/elasticsearch/BulkFailureException.java +++ b/src/main/java/org/springframework/data/elasticsearch/BulkFailureException.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 the original author or authors. + * Copyright 2020-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/ElasticsearchErrorCause.java b/src/main/java/org/springframework/data/elasticsearch/ElasticsearchErrorCause.java index 130f218f8f..22dbbfd7c0 100644 --- a/src/main/java/org/springframework/data/elasticsearch/ElasticsearchErrorCause.java +++ b/src/main/java/org/springframework/data/elasticsearch/ElasticsearchErrorCause.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,10 @@ */ package org.springframework.data.elasticsearch; -import org.springframework.lang.Nullable; - import java.util.List; +import org.jspecify.annotations.Nullable; + /** * Object describing an Elasticsearch error * @@ -26,8 +26,7 @@ * @since 4.4 */ public class ElasticsearchErrorCause { - @Nullable - private final String type; + @Nullable private final String type; private final String reason; diff --git a/src/main/java/org/springframework/data/elasticsearch/NoSuchIndexException.java b/src/main/java/org/springframework/data/elasticsearch/NoSuchIndexException.java index a431801aa4..c1eab9bcf7 100644 --- a/src/main/java/org/springframework/data/elasticsearch/NoSuchIndexException.java +++ b/src/main/java/org/springframework/data/elasticsearch/NoSuchIndexException.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 the original author or authors. + * Copyright 2019-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/ResourceFailureException.java b/src/main/java/org/springframework/data/elasticsearch/ResourceFailureException.java index 8dac09e1dc..493d3b4b7b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/ResourceFailureException.java +++ b/src/main/java/org/springframework/data/elasticsearch/ResourceFailureException.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/ResourceNotFoundException.java b/src/main/java/org/springframework/data/elasticsearch/ResourceNotFoundException.java index 5ba76a6b29..5e97b4e00b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/ResourceNotFoundException.java +++ b/src/main/java/org/springframework/data/elasticsearch/ResourceNotFoundException.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/RestStatusException.java b/src/main/java/org/springframework/data/elasticsearch/RestStatusException.java index b743cc794f..c707686098 100644 --- a/src/main/java/org/springframework/data/elasticsearch/RestStatusException.java +++ b/src/main/java/org/springframework/data/elasticsearch/RestStatusException.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/UncategorizedElasticsearchException.java b/src/main/java/org/springframework/data/elasticsearch/UncategorizedElasticsearchException.java index c04de263ee..ffc71ef7ba 100644 --- a/src/main/java/org/springframework/data/elasticsearch/UncategorizedElasticsearchException.java +++ b/src/main/java/org/springframework/data/elasticsearch/UncategorizedElasticsearchException.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 the original author or authors. + * Copyright 2020-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,8 @@ */ package org.springframework.data.elasticsearch; +import org.jspecify.annotations.Nullable; import org.springframework.dao.UncategorizedDataAccessException; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch diff --git a/src/main/java/org/springframework/data/elasticsearch/VersionConflictException.java b/src/main/java/org/springframework/data/elasticsearch/VersionConflictException.java index ea02677529..b3f31d3550 100644 --- a/src/main/java/org/springframework/data/elasticsearch/VersionConflictException.java +++ b/src/main/java/org/springframework/data/elasticsearch/VersionConflictException.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Alias.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Alias.java new file mode 100644 index 0000000000..0f707e942e --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Alias.java @@ -0,0 +1,79 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Identifies an alias for the index. + * + * @author Youssef Aouichaoui + * @since 5.4 + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE }) +@Repeatable(Aliases.class) +public @interface Alias { + /** + * @return Index alias name. Alias for {@link #alias}. + */ + @AliasFor("alias") + String value() default ""; + + /** + * @return Index alias name. Alias for {@link #value}. + */ + @AliasFor("value") + String alias() default ""; + + /** + * @return Query used to limit documents the alias can access. + */ + Filter filter() default @Filter; + + /** + * @return Used to route indexing operations to a specific shard. + */ + String indexRouting() default ""; + + /** + * @return Used to route indexing and search operations to a specific shard. + */ + String routing() default ""; + + /** + * @return Used to route search operations to a specific shard. + */ + String searchRouting() default ""; + + /** + * @return Is the alias hidden? + */ + boolean isHidden() default false; + + /** + * @return Is it the 'write index' for the alias? + */ + boolean isWriteIndex() default false; +} diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Aliases.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Aliases.java new file mode 100644 index 0000000000..ea5d895294 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Aliases.java @@ -0,0 +1,36 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Container annotation that aggregates several {@link Alias} annotations. + * + * @author Youssef Aouichaoui + * @see Alias + * @since 5.4 + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE }) +public @interface Aliases { + Alias[] value(); +} diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/CompletionContext.java b/src/main/java/org/springframework/data/elasticsearch/annotations/CompletionContext.java index 3d93be01ec..da27c1245b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/CompletionContext.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/CompletionContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 the original author or authors. + * Copyright 2019-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/CompletionField.java b/src/main/java/org/springframework/data/elasticsearch/annotations/CompletionField.java index 04401340d8..94ca1ea724 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/CompletionField.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/CompletionField.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 the original author or authors. + * Copyright 2013-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/CountQuery.java b/src/main/java/org/springframework/data/elasticsearch/annotations/CountQuery.java index 0a0e80752b..80bb7c15f9 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/CountQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/CountQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/DateFormat.java b/src/main/java/org/springframework/data/elasticsearch/annotations/DateFormat.java index 4e48e567a1..9f3b7f9d78 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/DateFormat.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/DateFormat.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java index 391e303e06..1131b2cd59 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 the original author or authors. + * Copyright 2013-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -100,6 +100,13 @@ */ boolean storeVersionInSource() default true; + /** + * Aliases for the index. + * + * @since 5.4 + */ + Alias[] aliases() default {}; + /** * @since 4.3 */ diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Dynamic.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Dynamic.java index 23a82f384b..9868c6e3c6 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Dynamic.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Dynamic.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java index dd299a4930..97815477ae 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 the original author or authors. + * Copyright 2013-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,8 @@ * @author Brian Kimmig * @author Morgan Lutz * @author Sascha Woo + * @author Haibo Liu + * @author Andriy Redko */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @@ -128,6 +130,10 @@ boolean norms() default true; /** + * NOte that null_value setting are not supported in Elasticsearch for all types. For example setting a null_value on + * a field with type text will throw an exception in the server when the mapping is written to Elasticsearch. Alas, + * the Elasticsearch documentation does not specify on which types it is allowed on which it is not. + * * @since 4.0 */ String nullValue() default ""; @@ -195,6 +201,27 @@ */ int dims() default -1; + /** + * to be used in combination with {@link FieldType#Dense_Vector} + * + * @since 5.4 + */ + String elementType() default FieldElementType.DEFAULT; + + /** + * to be used in combination with {@link FieldType#Dense_Vector} + * + * @since 5.4 + */ + KnnSimilarity knnSimilarity() default KnnSimilarity.DEFAULT; + + /** + * to be used in combination with {@link FieldType#Dense_Vector} + * + * @since 5.4 + */ + KnnIndexOptions[] knnIndexOptions() default {}; + /** * Controls how Elasticsearch dynamically adds fields to the inner object within the document.
* To be used in combination with {@link FieldType#Object} or {@link FieldType#Nested} @@ -218,4 +245,11 @@ * @since 5.1 */ boolean storeEmptyValue() default true; + + /** + * overrides the field type in the mapping which otherwise will be taken from corresponding {@link FieldType} + * + * @since 5.4 + */ + String mappedTypeName() default ""; } diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/FieldElementType.java b/src/main/java/org/springframework/data/elasticsearch/annotations/FieldElementType.java new file mode 100644 index 0000000000..49271764ba --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/FieldElementType.java @@ -0,0 +1,26 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.annotations; + +/** + * @author Haibo Liu + * @since 5.4 + */ +public final class FieldElementType { + public final static String DEFAULT = ""; + public final static String FLOAT = "float"; + public final static String BYTE = "byte"; +} diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/FieldType.java b/src/main/java/org/springframework/data/elasticsearch/annotations/FieldType.java index f148efb638..f701948d6d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/FieldType.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/FieldType.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 the original author or authors. + * Copyright 2013-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Filter.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Filter.java new file mode 100644 index 0000000000..7f07df55d1 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Filter.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.annotations; + +import org.springframework.core.annotation.AliasFor; + +/** + * Query used to limit documents. + * + * @author Youssef Aouichaoui + * @since 5.4 + */ +public @interface Filter { + /** + * @return Query used to limit documents. Alias for {@link #query}. + */ + @AliasFor("query") + String value() default ""; + + /** + * @return Query used to limit documents. Alias for {@link #value}. + */ + @AliasFor("value") + String query() default ""; +} diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/GeoPointField.java b/src/main/java/org/springframework/data/elasticsearch/annotations/GeoPointField.java index 7feda38237..05695abc98 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/GeoPointField.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/GeoPointField.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 the original author or authors. + * Copyright 2013-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/GeoShapeField.java b/src/main/java/org/springframework/data/elasticsearch/annotations/GeoShapeField.java index 0a3be4d2d6..0121b07ee1 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/GeoShapeField.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/GeoShapeField.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 the original author or authors. + * Copyright 2017-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Highlight.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Highlight.java index 3b93f232d6..30312ab434 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Highlight.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Highlight.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 the original author or authors. + * Copyright 2020-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/HighlightField.java b/src/main/java/org/springframework/data/elasticsearch/annotations/HighlightField.java index 73063b3451..f6318be98a 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/HighlightField.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/HighlightField.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 the original author or authors. + * Copyright 2020-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/HighlightParameters.java b/src/main/java/org/springframework/data/elasticsearch/annotations/HighlightParameters.java index 530c813ca5..d4e8bbfd2b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/HighlightParameters.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/HighlightParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 the original author or authors. + * Copyright 2020-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/IndexOptions.java b/src/main/java/org/springframework/data/elasticsearch/annotations/IndexOptions.java index 89637309a9..2de226c7b1 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/IndexOptions.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/IndexOptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 the original author or authors. + * Copyright 2019-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/IndexPrefixes.java b/src/main/java/org/springframework/data/elasticsearch/annotations/IndexPrefixes.java index 76a481f735..01adc8fbb4 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/IndexPrefixes.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/IndexPrefixes.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 the original author or authors. + * Copyright 2019-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/IndexedIndexName.java b/src/main/java/org/springframework/data/elasticsearch/annotations/IndexedIndexName.java index fd8cc8146a..4d76b97492 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/IndexedIndexName.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/IndexedIndexName.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,6 @@ */ package org.springframework.data.elasticsearch.annotations; -import org.springframework.data.annotation.ReadOnlyProperty; -import org.springframework.data.annotation.Transient; - import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -25,10 +22,10 @@ import java.lang.annotation.Target; /** - * Annotation to mark a String property of an entity to be filled with the name of the index where the entity was - * stored after it is indexed into Elasticsearch. This can be used when the name of the index is dynamically created - * or when a document was indexed into a write alias. - * + * Annotation to mark a String property of an entity to be filled with the name of the index where the entity was stored + * after it is indexed into Elasticsearch. This can be used when the name of the index is dynamically created or when a + * document was indexed into a write alias. + *

* This can not be used to specify the index where an entity should be written to. * * @author Peter-Josef Meisch diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/InnerField.java b/src/main/java/org/springframework/data/elasticsearch/annotations/InnerField.java index ceb6054119..651bf5a825 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/InnerField.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/InnerField.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,8 @@ * @author Aleksei Arsenev * @author Brian Kimmig * @author Morgan Lutz + * @author Haibo Liu + * @author Andriy Redko */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) @@ -149,4 +151,32 @@ * @since 4.2 */ int dims() default -1; + + /** + * to be used in combination with {@link FieldType#Dense_Vector} + * + * @since 5.4 + */ + String elementType() default FieldElementType.DEFAULT; + + /** + * to be used in combination with {@link FieldType#Dense_Vector} + * + * @since 5.4 + */ + KnnSimilarity knnSimilarity() default KnnSimilarity.DEFAULT; + + /** + * to be used in combination with {@link FieldType#Dense_Vector} + * + * @since 5.4 + */ + KnnIndexOptions[] knnIndexOptions() default {}; + + /** + * overrides the field type in the mapping which otherwise will be taken from corresponding {@link FieldType} + * + * @since 5.4 + */ + String mappedTypeName() default ""; } diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/JoinTypeRelation.java b/src/main/java/org/springframework/data/elasticsearch/annotations/JoinTypeRelation.java index 4e544bd4c4..eb2e1e4623 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/JoinTypeRelation.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/JoinTypeRelation.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 the original author or authors. + * Copyright 2020-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/JoinTypeRelations.java b/src/main/java/org/springframework/data/elasticsearch/annotations/JoinTypeRelations.java index fb1be2b68c..2004200cf2 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/JoinTypeRelations.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/JoinTypeRelations.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 the original author or authors. + * Copyright 2020-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/KnnAlgorithmType.java b/src/main/java/org/springframework/data/elasticsearch/annotations/KnnAlgorithmType.java new file mode 100644 index 0000000000..6110e54be8 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/KnnAlgorithmType.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.annotations; + +/** + * @author Haibo Liu + * @since 5.4 + */ +public enum KnnAlgorithmType { + HNSW("hnsw"), + INT8_HNSW("int8_hnsw"), + FLAT("flat"), + INT8_FLAT("int8_flat"), + DEFAULT(""); + + private final String type; + + KnnAlgorithmType(String type) { + this.type = type; + } + + public String getType() { + return type; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/KnnIndexOptions.java b/src/main/java/org/springframework/data/elasticsearch/annotations/KnnIndexOptions.java new file mode 100644 index 0000000000..56d871d3b5 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/KnnIndexOptions.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.annotations; + +/** + * @author Haibo Liu + * @since 5.4 + */ +public @interface KnnIndexOptions { + + KnnAlgorithmType type() default KnnAlgorithmType.DEFAULT; + + /** + * Only applicable to {@link KnnAlgorithmType#HNSW} and {@link KnnAlgorithmType#INT8_HNSW} index types. + */ + int m() default -1; + + /** + * Only applicable to {@link KnnAlgorithmType#HNSW} and {@link KnnAlgorithmType#INT8_HNSW} index types. + */ + int efConstruction() default -1; + + /** + * Only applicable to {@link KnnAlgorithmType#INT8_HNSW} and {@link KnnAlgorithmType#INT8_FLAT} index types. + */ + float confidenceInterval() default -1F; +} diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/KnnSimilarity.java b/src/main/java/org/springframework/data/elasticsearch/annotations/KnnSimilarity.java new file mode 100644 index 0000000000..d03c42a6fd --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/KnnSimilarity.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.annotations; + +/** + * @author Haibo Liu + * @since 5.4 + */ +public enum KnnSimilarity { + L2_NORM("l2_norm"), + DOT_PRODUCT("dot_product"), + COSINE("cosine"), + MAX_INNER_PRODUCT("max_inner_product"), + DEFAULT(""); + + private final String similarity; + + KnnSimilarity(String similarity) { + this.similarity = similarity; + } + + public String getSimilarity() { + return similarity; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Mapping.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Mapping.java index 0fd36357df..c2d48c3884 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Mapping.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Mapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,6 +82,6 @@ MappingAlias[] aliases() default {}; enum Detection { - DEFAULT, TRUE, FALSE; + DEFAULT, TRUE, FALSE } } diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/MappingAlias.java b/src/main/java/org/springframework/data/elasticsearch/annotations/MappingAlias.java index f71e664ee2..791659e9d5 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/MappingAlias.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/MappingAlias.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/MultiField.java b/src/main/java/org/springframework/data/elasticsearch/annotations/MultiField.java index 1523cd88ba..9dff38c1f1 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/MultiField.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/MultiField.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/NullValueType.java b/src/main/java/org/springframework/data/elasticsearch/annotations/NullValueType.java index 8707def774..a131b12a8e 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/NullValueType.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/NullValueType.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 the original author or authors. + * Copyright 2020-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Query.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Query.java index f5746a3551..9f1b755c35 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Query.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Query.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 the original author or authors. + * Copyright 2013-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/SearchTemplateQuery.java b/src/main/java/org/springframework/data/elasticsearch/annotations/SearchTemplateQuery.java new file mode 100644 index 0000000000..f50675d979 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/SearchTemplateQuery.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.annotations; + +import org.springframework.data.annotation.QueryAnnotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark a repository method as a search template method. The annotation defines the search template id, + * the parameters for the search template are taken from the method's arguments. + * + * @author P.J. Meisch (pj.meisch@sothawo.com) + * @since 5.5 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Documented +@QueryAnnotation +public @interface SearchTemplateQuery { + /** + * The id of the search template. Must not be empt or null. + */ + String id(); +} diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Setting.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Setting.java index 51f31b3b25..926154f1f2 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Setting.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Setting.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Similarity.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Similarity.java index 143996ea54..46cafd91a2 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Similarity.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Similarity.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 the original author or authors. + * Copyright 2019-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/SourceFilters.java b/src/main/java/org/springframework/data/elasticsearch/annotations/SourceFilters.java index 73f434999a..055ecc616f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/SourceFilters.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/SourceFilters.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/TermVector.java b/src/main/java/org/springframework/data/elasticsearch/annotations/TermVector.java index 377ed9063a..25de2cbcad 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/TermVector.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/TermVector.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 the original author or authors. + * Copyright 2019-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/ValueConverter.java b/src/main/java/org/springframework/data/elasticsearch/annotations/ValueConverter.java index 5e07262799..eb848bfed2 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/ValueConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/ValueConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ * Defines the class implementing the {@link PropertyValueConverter} interface. If this is a normal class, it must * provide a default constructor with no arguments. If this is an enum and thus implementing a singleton by enum it * must only have one enum value. - * + * * @return the class to use for conversion */ Class value(); diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/WriteOnlyProperty.java b/src/main/java/org/springframework/data/elasticsearch/annotations/WriteOnlyProperty.java index 12ca0e1d22..7704450e26 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/WriteOnlyProperty.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/WriteOnlyProperty.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/WriteTypeHint.java b/src/main/java/org/springframework/data/elasticsearch/annotations/WriteTypeHint.java index d62a704b7c..86a844cc18 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/WriteTypeHint.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/WriteTypeHint.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/package-info.java b/src/main/java/org/springframework/data/elasticsearch/annotations/package-info.java index 60fe252678..4b8ccdf64e 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/package-info.java @@ -1,3 +1,2 @@ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.annotations; diff --git a/src/main/java/org/springframework/data/elasticsearch/aot/ElasticsearchAotPredicates.java b/src/main/java/org/springframework/data/elasticsearch/aot/ElasticsearchAotPredicates.java index e751a40237..c3921b8940 100644 --- a/src/main/java/org/springframework/data/elasticsearch/aot/ElasticsearchAotPredicates.java +++ b/src/main/java/org/springframework/data/elasticsearch/aot/ElasticsearchAotPredicates.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,11 +25,11 @@ */ public class ElasticsearchAotPredicates { - public static final Predicate IS_REACTIVE_LIBARARY_AVAILABLE = ( + public static final Predicate IS_REACTIVE_LIBRARY_AVAILABLE = ( lib) -> ReactiveWrappers.isAvailable(lib); public static boolean isReactorPresent() { - return IS_REACTIVE_LIBARARY_AVAILABLE.test(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR); + return IS_REACTIVE_LIBRARY_AVAILABLE.test(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR); } } diff --git a/src/main/java/org/springframework/data/elasticsearch/aot/SpringDataElasticsearchRuntimeHints.java b/src/main/java/org/springframework/data/elasticsearch/aot/SpringDataElasticsearchRuntimeHints.java index 0dfbb146fa..100b2ae449 100644 --- a/src/main/java/org/springframework/data/elasticsearch/aot/SpringDataElasticsearchRuntimeHints.java +++ b/src/main/java/org/springframework/data/elasticsearch/aot/SpringDataElasticsearchRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.util.Arrays; +import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -32,7 +33,6 @@ 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.lang.Nullable; /** * @author Peter-Josef Meisch diff --git a/src/main/java/org/springframework/data/elasticsearch/aot/package-info.java b/src/main/java/org/springframework/data/elasticsearch/aot/package-info.java index 292bf8a1a1..56697c1029 100644 --- a/src/main/java/org/springframework/data/elasticsearch/aot/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/aot/package-info.java @@ -1,3 +1,2 @@ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.aot; diff --git a/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java b/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java index ddc0710263..f092e2bf6b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 the original author or authors. + * Copyright 2018-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,8 +25,8 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.support.HttpHeaders; -import org.springframework.lang.Nullable; /** * Configuration interface exposing common client configuration properties for Elasticsearch clients. diff --git a/src/main/java/org/springframework/data/elasticsearch/client/ClientConfigurationBuilder.java b/src/main/java/org/springframework/data/elasticsearch/client/ClientConfigurationBuilder.java index 2eb55b7702..71af992127 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/ClientConfigurationBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/ClientConfigurationBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 the original author or authors. + * Copyright 2018-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,11 +25,11 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationBuilderWithRequiredEndpoint; import org.springframework.data.elasticsearch.client.ClientConfiguration.MaybeSecureClientConfigurationBuilder; import org.springframework.data.elasticsearch.client.ClientConfiguration.TerminalClientConfigurationBuilder; import org.springframework.data.elasticsearch.support.HttpHeaders; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/data/elasticsearch/client/DefaultClientConfiguration.java b/src/main/java/org/springframework/data/elasticsearch/client/DefaultClientConfiguration.java index e5f50ed0e4..ea097bbb59 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/DefaultClientConfiguration.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/DefaultClientConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 the original author or authors. + * Copyright 2018-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,9 +24,8 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; -import org.elasticsearch.client.RestClientBuilder.HttpClientConfigCallback; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.support.HttpHeaders; -import org.springframework.lang.Nullable; /** * Default {@link ClientConfiguration} implementation. diff --git a/src/main/java/org/springframework/data/elasticsearch/client/ElasticsearchHost.java b/src/main/java/org/springframework/data/elasticsearch/client/ElasticsearchHost.java index f422ecded2..014acb6328 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/ElasticsearchHost.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/ElasticsearchHost.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 the original author or authors. + * Copyright 2018-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/client/InetSocketAddressParser.java b/src/main/java/org/springframework/data/elasticsearch/client/InetSocketAddressParser.java index e5d9cd1f2d..33f71a49ed 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/InetSocketAddressParser.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/InetSocketAddressParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 the original author or authors. + * Copyright 2018-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/client/NoReachableHostException.java b/src/main/java/org/springframework/data/elasticsearch/client/NoReachableHostException.java index 2487768785..b8a560db63 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/NoReachableHostException.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/NoReachableHostException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 the original author or authors. + * Copyright 2018-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/client/UnsupportedBackendOperation.java b/src/main/java/org/springframework/data/elasticsearch/client/UnsupportedBackendOperation.java index 1268ff3262..0264b95c00 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/UnsupportedBackendOperation.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/UnsupportedBackendOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/client/UnsupportedClientOperationException.java b/src/main/java/org/springframework/data/elasticsearch/client/UnsupportedClientOperationException.java index 112a03c783..322646bc66 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/UnsupportedClientOperationException.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/UnsupportedClientOperationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/AbstractQueryProcessor.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/AbstractQueryProcessor.java new file mode 100644 index 0000000000..ff0e1bd3a0 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/AbstractQueryProcessor.java @@ -0,0 +1,70 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.client.elc; + +import java.util.function.Consumer; + +import org.jspecify.annotations.Nullable; +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; +import org.springframework.data.elasticsearch.core.query.Query; +import org.springframework.data.elasticsearch.core.query.StringQuery; + +/** + * An abstract class that serves as a base for query processors. It provides a common interface and basic functionality + * for query processing. + * + * @author Aouichaoui Youssef + * @since 5.3 + */ +public abstract class AbstractQueryProcessor { + + /** + * Convert a spring-data-elasticsearch {@literal query} to an Elasticsearch {@literal query}. + * + * @param query spring-data-elasticsearch {@literal query}. + * @param queryConverter correct mapped field names and the values to the converted values. + * @return an Elasticsearch {@literal query}. + */ + + static co.elastic.clients.elasticsearch._types.query_dsl.@Nullable Query getEsQuery(@Nullable Query query, + @Nullable Consumer queryConverter) { + if (query == null) { + return null; + } + + if (queryConverter != null) { + queryConverter.accept(query); + } + + co.elastic.clients.elasticsearch._types.query_dsl.Query esQuery = null; + + if (query instanceof CriteriaQuery criteriaQuery) { + esQuery = CriteriaQueryProcessor.createQuery(criteriaQuery.getCriteria()); + } else if (query instanceof StringQuery stringQuery) { + esQuery = Queries.wrapperQueryAsQuery(stringQuery.getSource()); + } else if (query instanceof NativeQuery nativeQuery) { + if (nativeQuery.getQuery() != null) { + esQuery = nativeQuery.getQuery(); + } else if (nativeQuery.getSpringDataQuery() != null) { + esQuery = getEsQuery(nativeQuery.getSpringDataQuery(), queryConverter); + } + } else { + throw new IllegalArgumentException("unhandled Query implementation " + query.getClass().getName()); + } + + return esQuery; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/Aggregation.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/Aggregation.java index ab46cad895..23e2b6ae47 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/Aggregation.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/Aggregation.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/AutoCloseableElasticsearchClient.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/AutoCloseableElasticsearchClient.java index d6489ba19c..3ce661e1fd 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/AutoCloseableElasticsearchClient.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/AutoCloseableElasticsearchClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,10 @@ 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 java.io.IOException; + import org.elasticsearch.client.RestClient; import org.springframework.util.Assert; @@ -37,12 +38,10 @@ public AutoCloseableElasticsearchClient(ElasticsearchTransport transport) { } @Override - public void close() throws Exception { - transport.close(); - } - - @Override - public ElasticsearchClusterClient cluster() { - return super.cluster(); + public void close() throws IOException { + // since Elasticsearch 8.16 the ElasticsearchClient implements (through ApiClient) the Closeable interface and + // handles closing of the underlying transport. We now just call the base class, but keep this as we + // have been implementing AutoCloseable since 4.4 and won't change that to a mere Closeable + super.close(); } } diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ChildTemplate.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ChildTemplate.java index 633c858d46..4d3ebf5bd7 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ChildTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ChildTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ClusterTemplate.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ClusterTemplate.java index 44f3f7a51b..fcba35fa7d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ClusterTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ClusterTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaFilterProcessor.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaFilterProcessor.java index d692743f90..702d8501b3 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaFilterProcessor.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaFilterProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ 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.elasticsearch.utils.geohash.Geohash; import org.springframework.data.geo.Box; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metrics; @@ -69,10 +70,17 @@ public static Optional createQuery(Criteria criteria) { for (Criteria chainedCriteria : criteria.getCriteriaChain()) { if (chainedCriteria.isOr()) { - BoolQuery.Builder boolQueryBuilder = QueryBuilders.bool(); - queriesForEntries(chainedCriteria).forEach(boolQueryBuilder::should); - filterQueries.add(new Query(boolQueryBuilder.build())); + Collection queriesForEntries = queriesForEntries(chainedCriteria); + + if (!queriesForEntries.isEmpty()) { + BoolQuery.Builder boolQueryBuilder = QueryBuilders.bool(); + queriesForEntries.forEach(boolQueryBuilder::should); + filterQueries.add(new Query(boolQueryBuilder.build())); + } } else if (chainedCriteria.isNegating()) { + + Assert.notNull(criteria.getField(), "criteria must have a field"); + Collection negatingFilters = buildNegatingFilter(criteria.getField().getName(), criteria.getFilterCriteriaEntries()); filterQueries.addAll(negatingFilters); @@ -116,6 +124,7 @@ private static Collection buildNegatingFilter(String fieldName, private static Collection queriesForEntries(Criteria criteria) { Assert.notNull(criteria.getField(), "criteria must have a field"); + String fieldName = criteria.getField().getName(); Assert.notNull(fieldName, "Unknown field"); @@ -177,7 +186,7 @@ private static ObjectBuilder withinQuery(String fieldName, Obj .distance(dist) // .distanceType(GeoDistanceType.Plane) // .location(location -> { - if (values[0]instanceof GeoPoint loc) { + if (values[0] instanceof GeoPoint loc) { location.latlon(latlon -> latlon.lat(loc.getLat()).lon(loc.getLon())); } else if (values[0] instanceof Point point) { GeoPoint loc = GeoPoint.fromPoint(point); @@ -245,7 +254,7 @@ private static void twoParameterBBox(GeoBoundingBoxQuery.Builder queryBuilder, O 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 topLeft) { + if (values[0] instanceof GeoPoint topLeft) { GeoPoint bottomRight = (GeoPoint) values[1]; queryBuilder.boundingBox(bb -> bb // .tlbr(tlbr -> tlbr // @@ -267,7 +276,10 @@ private static void twoParameterBBox(GeoBoundingBoxQuery.Builder queryBuilder, O .tlbr(tlbr -> tlbr // .topLeft(glb -> { if (isGeoHash) { - glb.geohash(gh -> gh.geohash(topLeft)); + // although the builder in 8.13.2 supports geohash, the server throws an error, so we convert to a + // lat,lon string here + glb.text(Geohash.toLatLon(topLeft)); + // glb.geohash(gh -> gh.geohash(topLeft)); } else { glb.text(topLeft); } @@ -275,7 +287,8 @@ private static void twoParameterBBox(GeoBoundingBoxQuery.Builder queryBuilder, O }) // .bottomRight(glb -> { if (isGeoHash) { - glb.geohash(gh -> gh.geohash(bottomRight)); + glb.text(Geohash.toLatLon(bottomRight)); + // glb.geohash(gh -> gh.geohash(bottomRight)); } else { glb.text(bottomRight); } diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryException.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryException.java index 2d43553d55..cb6cccf973 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryException.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryException.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java index 080920959e..1c9c9ef53a 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,22 +16,28 @@ package org.springframework.data.elasticsearch.client.elc; import static org.springframework.data.elasticsearch.client.elc.Queries.*; +import static org.springframework.data.elasticsearch.client.elc.TypeUtils.*; 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.elasticsearch.core.search.InnerHits; import co.elastic.clients.json.JsonData; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Objects; +import org.jspecify.annotations.Nullable; 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.data.elasticsearch.core.query.HasChildQuery; +import org.springframework.data.elasticsearch.core.query.HasParentQuery; +import org.springframework.data.elasticsearch.core.query.InnerHitsQuery; import org.springframework.util.Assert; /** @@ -42,7 +48,7 @@ * @author Ezequiel Antúnez Camacho * @since 4.4 */ -class CriteriaQueryProcessor { +class CriteriaQueryProcessor extends AbstractQueryProcessor { /** * creates a query from the criteria @@ -110,11 +116,18 @@ public static Query createQuery(Criteria criteria) { } } + var filterQuery = CriteriaFilterProcessor.createQuery(criteria); if (shouldQueries.isEmpty() && mustNotQueries.isEmpty() && mustQueries.isEmpty()) { - return null; + + if (filterQuery.isEmpty()) { + return null; + } + + // we need something to add the filter to + mustQueries.add(Query.of(qb -> qb.matchAll(m -> m))); } - Query query = new Query.Builder().bool(boolQueryBuilder -> { + return new Query.Builder().bool(boolQueryBuilder -> { if (!shouldQueries.isEmpty()) { boolQueryBuilder.should(shouldQueries); @@ -128,10 +141,10 @@ public static Query createQuery(Criteria criteria) { boolQueryBuilder.must(mustQueries); } + filterQuery.ifPresent(boolQueryBuilder::filter); + return boolQueryBuilder; }).build(); - - return query; } @Nullable @@ -233,51 +246,57 @@ private static Query.Builder queryFor(Criteria.CriteriaEntry entry, Field field, queryBuilder.queryString(queryStringQuery(fieldName, '*' + searchText, true, boost)); break; case EXPRESSION: - queryBuilder.queryString(queryStringQuery(fieldName, value.toString(), boost)); + queryBuilder.queryString(queryStringQuery(fieldName, Objects.requireNonNull(value).toString(), boost)); break; case LESS: - queryBuilder // - .range(rb -> rb // - .field(fieldName) // - .lt(JsonData.of(value)) // - .boost(boost)); // + queryBuilder + .range(rb -> rb + .untyped(ut -> ut + .field(fieldName) + .lt(JsonData.of(value)) + .boost(boost))); break; case LESS_EQUAL: - queryBuilder // - .range(rb -> rb // - .field(fieldName) // - .lte(JsonData.of(value)) // - .boost(boost)); // + queryBuilder + .range(rb -> rb + .untyped(ut -> ut + .field(fieldName) + .lte(JsonData.of(value)) + .boost(boost))); break; case GREATER: - queryBuilder // - .range(rb -> rb // - .field(fieldName) // - .gt(JsonData.of(value)) // - .boost(boost)); // + queryBuilder + .range(rb -> rb + .untyped(ut -> ut + .field(fieldName) + .gt(JsonData.of(value)) + .boost(boost))); break; case GREATER_EQUAL: - queryBuilder // - .range(rb -> rb // - .field(fieldName) // - .gte(JsonData.of(value)) // - .boost(boost)); // + queryBuilder + .range(rb -> rb + .untyped(ut -> ut + .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; - }); // + Assert.notNull(value, "value for a between condition must not be null"); + queryBuilder + .range(rb -> rb + .untyped(ut -> { + ut.field(fieldName); + if (ranges[0] != null) { + ut.gte(JsonData.of(ranges[0])); + } + + if (ranges[1] != null) { + ut.lte(JsonData.of(ranges[1])); + } + ut.boost(boost); // + return ut; + })); break; case FUZZY: @@ -288,10 +307,10 @@ private static Query.Builder queryFor(Criteria.CriteriaEntry entry, Field field, .boost(boost)); // break; case MATCHES: - queryBuilder.match(matchQuery(fieldName, value.toString(), Operator.Or, boost)); + queryBuilder.match(matchQuery(fieldName, Objects.requireNonNull(value).toString(), Operator.Or, boost)); break; case MATCHES_ALL: - queryBuilder.match(matchQuery(fieldName, value.toString(), Operator.And, boost)); + queryBuilder.match(matchQuery(fieldName, Objects.requireNonNull(value).toString(), Operator.And, boost)); break; case IN: @@ -340,9 +359,35 @@ private static Query.Builder queryFor(Criteria.CriteriaEntry entry, Field field, queryBuilder // .regexp(rb -> rb // .field(fieldName) // - .value(value.toString()) // + .value(Objects.requireNonNull(value).toString()) // .boost(boost)); // break; + case HAS_CHILD: + if (value instanceof HasChildQuery query) { + queryBuilder.hasChild(hcb -> hcb + .type(query.getType()) + .query(getEsQuery(query.getQuery(), null)) + .innerHits(getInnerHits(query.getInnerHitsQuery())) + .ignoreUnmapped(query.getIgnoreUnmapped()) + .minChildren(query.getMinChildren()) + .maxChildren(query.getMaxChildren()) + .scoreMode(scoreMode(query.getScoreMode()))); + } else { + throw new CriteriaQueryException("value for " + fieldName + " is not a has_child query"); + } + break; + case HAS_PARENT: + if (value instanceof HasParentQuery query) { + queryBuilder.hasParent(hpb -> hpb + .parentType(query.getParentType()) + .query(getEsQuery(query.getQuery(), null)) + .innerHits(getInnerHits(query.getInnerHitsQuery())) + .ignoreUnmapped(query.getIgnoreUnmapped()) + .score(query.getScore())); + } else { + throw new CriteriaQueryException("value for " + fieldName + " is not a has_parent query"); + } + break; default: throw new CriteriaQueryException("Could not build query for " + entry); } @@ -365,7 +410,7 @@ private static String orQueryString(Iterable iterable) { if (item != null) { - if (sb.length() > 0) { + if (!sb.isEmpty()) { sb.append(' '); } sb.append('"'); @@ -397,4 +442,19 @@ public static String escape(String s) { return sb.toString(); } + /** + * Convert a spring-data-elasticsearch {@literal inner_hits} to an Elasticsearch {@literal inner_hits} query. + * + * @param query spring-data-elasticsearch {@literal inner_hits}. + * @return an Elasticsearch {@literal inner_hits} query. + */ + @Nullable + private static InnerHits getInnerHits(@Nullable InnerHitsQuery query) { + if (query == null) { + return null; + } + + return InnerHits.of(iqb -> iqb.from(query.getFrom()).size(query.getSize()).name(query.getName())); + } + } diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/DocumentAdapters.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/DocumentAdapters.java index 1b2dfadbb5..53e8cefa7b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/DocumentAdapters.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/DocumentAdapters.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,16 +24,9 @@ import co.elastic.clients.elasticsearch.core.search.NestedIdentity; import co.elastic.clients.json.JsonData; import co.elastic.clients.json.JsonpMapper; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.MultiGetItem; import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.document.Explanation; @@ -41,200 +34,208 @@ 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; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + /** * Utility class to adapt different Elasticsearch responses to a * {@link org.springframework.data.elasticsearch.core.document.Document} * * @author Peter-Josef Meisch * @author Haibo Liu + * @author Mohamed El Harrougui * @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> highlightFields = hit.highlight(); - - Map innerHits = new LinkedHashMap<>(); - hit.innerHits().forEach((name, innerHitsResult) -> { - // noinspection ReturnOfNull - innerHits.put(name, SearchDocumentResponseBuilder.from(innerHitsResult.hits(), null, null, null, null, null, - searchDocument -> null, jsonpMapper)); - }); - - NestedMetaData nestedMetaData = from(hit.nested()); - - Explanation explanation = from(hit.explanation()); - - List matchedQueries = hit.matchedQueries(); - - Function, EntityAsMap> fromFields = fields -> { - StringBuilder sb = new StringBuilder("{"); - final boolean[] firstField = { true }; - hit.fields().forEach((key, jsonData) -> { - if (!firstField[0]) { - sb.append(','); - } - sb.append('"').append(key).append("\":") // - .append(jsonData.toJson(jsonpMapper).toString()); - firstField[0] = false; - }); - sb.append('}'); - return new EntityAsMap().fromJson(sb.toString()); - }; - - EntityAsMap hitFieldsAsMap = fromFields.apply(hit.fields()); - - Map> documentFields = new LinkedHashMap<>(); - hitFieldsAsMap.forEach((key, value) -> { - if (value instanceof List) { - // noinspection unchecked - documentFields.put(key, (List) value); - } else { - documentFields.put(key, Collections.singletonList(value)); - } - }); - - Document document; - Object source = hit.source(); - if (source == null) { - document = Document.from(hitFieldsAsMap); - } else { - if (source instanceof EntityAsMap entityAsMap) { - document = Document.from(entityAsMap); - } else if (source instanceof JsonData jsonData) { - document = Document.from(jsonData.to(EntityAsMap.class)); - } else { - - 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().stream().map(TypeUtils::toObject).toArray(), - documentFields, highlightFields, innerHits, nestedMetaData, explanation, matchedQueries, hit.routing()); - } - - public static SearchDocument from(CompletionSuggestOption completionSuggestOption) { - - Document document = completionSuggestOption.source() != null ? Document.from(completionSuggestOption.source()) - : Document.create(); - document.setIndex(completionSuggestOption.index()); - - if (completionSuggestOption.id() != null) { - document.setId(completionSuggestOption.id()); - } - - float score = completionSuggestOption.score() != null ? completionSuggestOption.score().floatValue() : Float.NaN; - return new SearchDocumentAdapter(document, score, new Object[] {}, Collections.emptyMap(), Collections.emptyMap(), - Collections.emptyMap(), null, null, null, completionSuggestOption.routing()); - } - - @Nullable - private static Explanation from(@Nullable co.elastic.clients.elasticsearch.core.explain.Explanation explanation) { - - if (explanation == null) { - return null; - } - List 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 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 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> from(MgetResponse 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()); - } + 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> highlightFields = hit.highlight(); + + Map innerHits = new LinkedHashMap<>(); + hit.innerHits().forEach((name, innerHitsResult) -> { + // noinspection ReturnOfNull + innerHits.put(name, SearchDocumentResponseBuilder.from(innerHitsResult.hits(), null, null, null, 0, null, null, + searchDocument -> null, jsonpMapper)); + }); + + NestedMetaData nestedMetaData = from(hit.nested()); + + Explanation explanation = from(hit.explanation()); + + Map matchedQueries = hit.matchedQueries(); + + Function, EntityAsMap> fromFields = fields -> { + StringBuilder sb = new StringBuilder("{"); + final boolean[] firstField = {true}; + hit.fields().forEach((key, jsonData) -> { + if (!firstField[0]) { + sb.append(','); + } + sb.append('"').append(key).append("\":") // + .append(jsonData.toJson(jsonpMapper).toString()); + firstField[0] = false; + }); + sb.append('}'); + return new EntityAsMap().fromJson(sb.toString()); + }; + + EntityAsMap hitFieldsAsMap = fromFields.apply(hit.fields()); + + Map> documentFields = new LinkedHashMap<>(); + hitFieldsAsMap.forEach((key, value) -> { + if (value instanceof List) { + // noinspection unchecked + documentFields.put(key, (List) value); + } else { + documentFields.put(key, Collections.singletonList(value)); + } + }); + + Document document; + Object source = hit.source(); + if (source == null) { + document = Document.from(hitFieldsAsMap); + } else { + if (source instanceof EntityAsMap entityAsMap) { + document = Document.from(entityAsMap); + } else if (source instanceof JsonData jsonData) { + document = Document.from(jsonData.to(EntityAsMap.class)); + } else { + + 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().stream().map(TypeUtils::toObject).toArray(), + documentFields, highlightFields, innerHits, nestedMetaData, explanation, matchedQueries, hit.routing()); + } + + public static SearchDocument from(CompletionSuggestOption completionSuggestOption) { + + Document document = completionSuggestOption.source() != null ? Document.from(completionSuggestOption.source()) + : Document.create(); + document.setIndex(completionSuggestOption.index()); + + if (completionSuggestOption.id() != null) { + document.setId(completionSuggestOption.id()); + } + + float score = completionSuggestOption.score() != null ? completionSuggestOption.score().floatValue() : Float.NaN; + return new SearchDocumentAdapter(document, score, new Object[]{}, Collections.emptyMap(), Collections.emptyMap(), + Collections.emptyMap(), null, null, null, completionSuggestOption.routing()); + } + + @Nullable + private static Explanation from(co.elastic.clients.elasticsearch.core.explain.@Nullable Explanation explanation) { + + if (explanation == null) { + return null; + } + List 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 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 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> from(MgetResponse 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()); + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchAggregation.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchAggregation.java index 4a4b442b11..828e81bf4b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchAggregation.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchAggregation.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchAggregations.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchAggregations.java index 10633a0ece..95e7788024 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchAggregations.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchAggregations.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,8 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.AggregationsContainer; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClientBeanDefinitionParser.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClientBeanDefinitionParser.java index 9bde4ada1a..dc1e0701d3 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClientBeanDefinitionParser.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClientBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClientFactoryBean.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClientFactoryBean.java index d7e2aa10dc..2ca7fcbb82 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClientFactoryBean.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClientFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.FactoryBeanNotInitializedException; import org.springframework.beans.factory.InitializingBean; import org.springframework.data.elasticsearch.client.ClientConfiguration; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClients.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClients.java index 85620fe20e..c4f6452cc0 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClients.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClients.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,9 +46,9 @@ import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestClientBuilder; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.client.ClientConfiguration; import org.springframework.data.elasticsearch.support.HttpHeaders; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -329,7 +329,7 @@ public static ElasticsearchTransport getElasticsearchTransport(RestClient restCl Assert.notNull(jsonpMapper, "jsonpMapper must not be null"); TransportOptions.Builder transportOptionsBuilder = transportOptions != null ? transportOptions.toBuilder() - : new RestClientOptions(RequestOptions.DEFAULT).toBuilder(); + : new RestClientOptions(RequestOptions.DEFAULT, false).toBuilder(); RestClientOptions.Builder restClientOptionsBuilder = getRestClientOptionsBuilder(transportOptions); diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchConfiguration.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchConfiguration.java index a0632d557f..93d26101ab 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchConfiguration.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -135,6 +135,6 @@ public JsonpMapper jsonpMapper() { * @return the options that should be added to every request. Must not be {@literal null} */ public TransportOptions transportOptions() { - return new RestClientOptions(RequestOptions.DEFAULT); + return new RestClientOptions(RequestOptions.DEFAULT, false); } } diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchExceptionTranslator.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchExceptionTranslator.java index 5cdd90d0b1..224e9c671f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchExceptionTranslator.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchExceptionTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java index e535f857f9..3e0d6235d9 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,8 @@ import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem; import co.elastic.clients.elasticsearch.core.msearch.MultiSearchResponseItem; import co.elastic.clients.elasticsearch.core.search.ResponseBody; +import co.elastic.clients.elasticsearch.sql.ElasticsearchSqlClient; +import co.elastic.clients.elasticsearch.sql.QueryResponse; import co.elastic.clients.json.JsonpMapper; import co.elastic.clients.transport.Version; @@ -38,6 +40,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.BulkFailureException; import org.springframework.data.elasticsearch.client.UnsupportedBackendOperation; import org.springframework.data.elasticsearch.core.AbstractElasticsearchTemplate; @@ -51,19 +54,12 @@ import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; -import org.springframework.data.elasticsearch.core.query.BaseQueryBuilder; -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.SearchTemplateQuery; -import org.springframework.data.elasticsearch.core.query.UpdateQuery; +import org.springframework.data.elasticsearch.core.query.*; 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.script.Script; -import org.springframework.lang.Nullable; +import org.springframework.data.elasticsearch.core.sql.SqlResponse; import org.springframework.util.Assert; /** @@ -81,6 +77,7 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate { private static final Log LOGGER = LogFactory.getLog(ElasticsearchTemplate.class); private final ElasticsearchClient client; + private final ElasticsearchSqlClient sqlClient; private final RequestConverter requestConverter; private final ResponseConverter responseConverter; private final JsonpMapper jsonpMapper; @@ -92,6 +89,7 @@ public ElasticsearchTemplate(ElasticsearchClient client) { Assert.notNull(client, "client must not be null"); this.client = client; + this.sqlClient = client.sql(); this.jsonpMapper = client._transport().jsonpMapper(); requestConverter = new RequestConverter(elasticsearchConverter, jsonpMapper); responseConverter = new ResponseConverter(jsonpMapper); @@ -104,6 +102,7 @@ public ElasticsearchTemplate(ElasticsearchClient client, ElasticsearchConverter Assert.notNull(client, "client must not be null"); this.client = client; + this.sqlClient = client.sql(); this.jsonpMapper = client._transport().jsonpMapper(); requestConverter = new RequestConverter(elasticsearchConverter, jsonpMapper); responseConverter = new ResponseConverter(jsonpMapper); @@ -178,8 +177,12 @@ public void bulkUpdate(List queries, BulkOptions bulkOptions, Index } @Override - public ByQueryResponse delete(Query query, Class clazz, IndexCoordinates index) { + public ByQueryResponse delete(DeleteQuery query, Class clazz) { + return delete(query, clazz, getIndexCoordinatesFor(clazz)); + } + @Override + public ByQueryResponse delete(DeleteQuery query, Class clazz, IndexCoordinates index) { Assert.notNull(query, "query must not be null"); DeleteByQueryRequest request = requestConverter.documentDeleteByQueryRequest(query, routingResolver.getRouting(), @@ -499,18 +502,19 @@ public List> multiSearch(List queries, List> multiSearch(List multiSearchQueryParameters, boolean isSearchTemplateQuery) { - return isSearchTemplateQuery ? - doMultiTemplateSearch(multiSearchQueryParameters.stream() - .map(p -> new MultiSearchTemplateQueryParameter((SearchTemplateQuery) p.query, p.clazz, p.index)) - .toList()) + return isSearchTemplateQuery ? doMultiTemplateSearch(multiSearchQueryParameters.stream() + .map(p -> new MultiSearchTemplateQueryParameter((SearchTemplateQuery) p.query, p.clazz, p.index)) + .toList()) : doMultiSearch(multiSearchQueryParameters); } - private List> doMultiTemplateSearch(List mSearchTemplateQueryParameters) { + private List> doMultiTemplateSearch( + List mSearchTemplateQueryParameters) { MsearchTemplateRequest request = requestConverter.searchMsearchTemplateRequest(mSearchTemplateQueryParameters, routingResolver.getRouting()); - MsearchTemplateResponse response = execute(client -> client.msearchTemplate(request, EntityAsMap.class)); + MsearchTemplateResponse response = execute( + client -> client.msearchTemplate(request, EntityAsMap.class)); List> responseItems = response.responses(); Assert.isTrue(mSearchTemplateQueryParameters.size() == responseItems.size(), @@ -645,6 +649,19 @@ public boolean deleteScript(String name) { DeleteScriptRequest request = requestConverter.scriptDelete(name); return execute(client -> client.deleteScript(request)).acknowledged(); } + + @Override + public SqlResponse search(SqlQuery query) { + Assert.notNull(query, "Query must not be null."); + + try { + QueryResponse response = sqlClient.query(requestConverter.sqlQueryRequest(query)); + + return responseConverter.sqlResponse(response); + } catch (IOException e) { + throw exceptionTranslator.translateException(e); + } + } // endregion // region client callback diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/EntityAsMap.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/EntityAsMap.java index b014f89ec3..e54d13ea74 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/EntityAsMap.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/EntityAsMap.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/HighlightQueryBuilder.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/HighlightQueryBuilder.java index 183d622baa..dfe850e4d8 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/HighlightQueryBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/HighlightQueryBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; 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; @@ -27,7 +28,6 @@ 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; /** @@ -43,7 +43,8 @@ class HighlightQueryBuilder { private final RequestConverter requestConverter; HighlightQueryBuilder( - MappingContext, ElasticsearchPersistentProperty> mappingContext, RequestConverter requestConverter) { + MappingContext, ElasticsearchPersistentProperty> mappingContext, + RequestConverter requestConverter) { this.mappingContext = mappingContext; this.requestConverter = requestConverter; } diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/IndicesTemplate.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/IndicesTemplate.java index ec6ea3914b..5a735a7240 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/IndicesTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/IndicesTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,13 +21,13 @@ import co.elastic.clients.transport.ElasticsearchTransport; import co.elastic.clients.transport.endpoints.BooleanResponse; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.elasticsearch.UncategorizedElasticsearchException; @@ -46,9 +46,10 @@ import org.springframework.data.elasticsearch.core.index.GetTemplateRequest; import org.springframework.data.elasticsearch.core.index.PutIndexTemplateRequest; import org.springframework.data.elasticsearch.core.index.PutTemplateRequest; +import org.springframework.data.elasticsearch.core.mapping.Alias; +import org.springframework.data.elasticsearch.core.mapping.CreateIndexSettings; 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; /** @@ -60,8 +61,6 @@ public class IndicesTemplate extends ChildTemplate implements IndexOperations { - private static final Log LOGGER = LogFactory.getLog(IndicesTemplate.class); - // we need a cluster client as well because ES has put some methods from the indices API into the cluster client // (component templates) private final ClusterTemplate clusterTemplate; @@ -85,7 +84,7 @@ public IndicesTemplate(ElasticsearchIndicesClient client, ClusterTemplate cluste } public IndicesTemplate(ElasticsearchIndicesClient client, ClusterTemplate clusterTemplate, - ElasticsearchConverter elasticsearchConverter, IndexCoordinates boundIndex) { + ElasticsearchConverter elasticsearchConverter, IndexCoordinates boundIndex) { super(client, elasticsearchConverter); Assert.notNull(clusterTemplate, "cluster must not be null"); @@ -137,11 +136,14 @@ public boolean createWithMapping() { protected boolean doCreate(IndexCoordinates indexCoordinates, Map 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); + Set aliases = (boundClass != null) ? getAliasesFor(boundClass) : new HashSet<>(); + CreateIndexSettings indexSettings = CreateIndexSettings.builder(indexCoordinates) + .withAliases(aliases) + .withSettings(settings) + .withMapping(mapping) + .build(); + + CreateIndexRequest createIndexRequest = requestConverter.indicesCreateRequest(indexSettings); CreateIndexResponse createIndexResponse = execute(client -> client.create(createIndexRequest)); return Boolean.TRUE.equals(createIndexResponse.acknowledged()); } @@ -236,8 +238,7 @@ public Map getMapping() { GetMappingRequest getMappingRequest = requestConverter.indicesGetMappingRequest(indexCoordinates); GetMappingResponse getMappingResponse = execute(client -> client.getMapping(getMappingRequest)); - Document mappingResponse = responseConverter.indicesGetMapping(getMappingResponse, indexCoordinates); - return mappingResponse; + return responseConverter.indicesGetMapping(getMappingResponse, indexCoordinates); } @Override @@ -449,5 +450,14 @@ public IndexCoordinates getIndexCoordinates() { public IndexCoordinates getIndexCoordinatesFor(Class clazz) { return getRequiredPersistentEntity(clazz).getIndexCoordinates(); } + + /** + * Get the {@link Alias} of the provided class. + * + * @param clazz provided class that can be used to extract aliases. + */ + public Set getAliasesFor(Class clazz) { + return getRequiredPersistentEntity(clazz).getAliases(); + } // endregion } diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/JsonUtils.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/JsonUtils.java index 64697766e7..5a927774f9 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/JsonUtils.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/JsonUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,11 @@ import jakarta.json.stream.JsonGenerator; import java.io.ByteArrayOutputStream; -import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * @author Peter-Josef Meisch @@ -44,17 +43,13 @@ public static String toJson(Object object, JsonpMapper mapper) { mapper.serialize(object, generator); generator.close(); String json = "{}"; - try { - json = baos.toString("UTF-8"); - } catch (UnsupportedEncodingException e) { - LOGGER.warn("could not read json", e); - } - + json = baos.toString(StandardCharsets.UTF_8); return json; } @Nullable - public static String queryToJson(@Nullable co.elastic.clients.elasticsearch._types.query_dsl.Query query, JsonpMapper mapper) { + public static String queryToJson(co.elastic.clients.elasticsearch._types.query_dsl.@Nullable Query query, + JsonpMapper mapper) { if (query == null) { return null; diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/NativeQuery.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/NativeQuery.java index 880cb7ae08..d8d2d21aec 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/NativeQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/NativeQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ */ package org.springframework.data.elasticsearch.client.elc; -import co.elastic.clients.elasticsearch._types.KnnQuery; +import co.elastic.clients.elasticsearch._types.KnnSearch; import co.elastic.clients.elasticsearch._types.SortOptions; import co.elastic.clients.elasticsearch._types.aggregations.Aggregation; import co.elastic.clients.elasticsearch._types.query_dsl.Query; @@ -28,9 +28,8 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.query.BaseQuery; -import org.springframework.data.elasticsearch.core.query.ScriptedField; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -39,12 +38,13 @@ * * @author Peter-Josef Meisch * @author Sascha Woo + * @author Haibo Liu * @since 4.4 */ public class NativeQuery extends BaseQuery { @Nullable private final Query query; - @Nullable private org.springframework.data.elasticsearch.core.query.Query springDataQuery; + private org.springframework.data.elasticsearch.core.query.@Nullable Query springDataQuery; @Nullable private Query filter; // note: the new client does not have pipeline aggs, these are just set up as normal aggs private final Map aggregations = new LinkedHashMap<>(); @@ -53,7 +53,7 @@ public class NativeQuery extends BaseQuery { private List sortOptions = Collections.emptyList(); private Map searchExtensions = Collections.emptyMap(); - @Nullable private KnnQuery knnQuery; + @Nullable private List knnSearches = Collections.emptyList(); public NativeQuery(NativeQueryBuilder builder) { super(builder); @@ -70,7 +70,7 @@ public NativeQuery(NativeQueryBuilder builder) { "Cannot add an NativeQuery in a NativeQuery"); } this.springDataQuery = builder.getSpringDataQuery(); - this.knnQuery = builder.getKnnQuery(); + this.knnSearches = builder.getKnnSearches(); } public NativeQuery(@Nullable Query query) { @@ -117,20 +117,19 @@ public Map getSearchExtensions() { * @see NativeQueryBuilder#withQuery(org.springframework.data.elasticsearch.core.query.Query). * @since 5.1 */ - public void setSpringDataQuery(@Nullable org.springframework.data.elasticsearch.core.query.Query springDataQuery) { + public void setSpringDataQuery(org.springframework.data.elasticsearch.core.query.@Nullable Query springDataQuery) { this.springDataQuery = springDataQuery; } /** - * @since 5.1 + * @since 5.3.1 */ @Nullable - public KnnQuery getKnnQuery() { - return knnQuery; + public List getKnnSearches() { + return knnSearches; } - @Nullable - public org.springframework.data.elasticsearch.core.query.Query getSpringDataQuery() { + public org.springframework.data.elasticsearch.core.query.@Nullable Query getSpringDataQuery() { return springDataQuery; } } diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/NativeQueryBuilder.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/NativeQueryBuilder.java index e2eeb49fa4..e8a1e748a0 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/NativeQueryBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/NativeQueryBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.data.elasticsearch.client.elc; import co.elastic.clients.elasticsearch._types.KnnQuery; +import co.elastic.clients.elasticsearch._types.KnnSearch; import co.elastic.clients.elasticsearch._types.SortOptions; import co.elastic.clients.elasticsearch._types.aggregations.Aggregation; import co.elastic.clients.elasticsearch._types.query_dsl.Query; @@ -26,18 +27,20 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.query.BaseQueryBuilder; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** * @author Peter-Josef Meisch * @author Sascha Woo + * @author Haibo Liu * @since 4.4 */ public class NativeQueryBuilder extends BaseQueryBuilder { @@ -47,11 +50,12 @@ public class NativeQueryBuilder extends BaseQueryBuilder aggregations = new LinkedHashMap<>(); @Nullable private Suggester suggester; @Nullable private FieldCollapse fieldCollapse; - private List sortOptions = new ArrayList<>(); - private Map searchExtensions = new LinkedHashMap<>(); + private final List sortOptions = new ArrayList<>(); + private final Map searchExtensions = new LinkedHashMap<>(); - @Nullable private org.springframework.data.elasticsearch.core.query.Query springDataQuery; + private org.springframework.data.elasticsearch.core.query.@Nullable Query springDataQuery; @Nullable private KnnQuery knnQuery; + @Nullable private List knnSearches = Collections.emptyList(); public NativeQueryBuilder() {} @@ -92,8 +96,15 @@ public KnnQuery getKnnQuery() { return knnQuery; } + /** + * @since 5.3.1 + */ @Nullable - public org.springframework.data.elasticsearch.core.query.Query getSpringDataQuery() { + public List getKnnSearches() { + return knnSearches; + } + + public org.springframework.data.elasticsearch.core.query.@Nullable Query getSpringDataQuery() { return springDataQuery; } @@ -202,13 +213,30 @@ public NativeQueryBuilder withQuery(org.springframework.data.elasticsearch.core. } /** - * @since 5.1 + * @since 5.4 */ - public NativeQueryBuilder withKnnQuery(KnnQuery knnQuery) { - this.knnQuery = knnQuery; + public NativeQueryBuilder withKnnSearches(List knnSearches) { + this.knnSearches = knnSearches; return this; } + /** + * @since 5.4 + */ + public NativeQueryBuilder withKnnSearches(Function> fn) { + + Assert.notNull(fn, "fn must not be null"); + + return withKnnSearches(fn.apply(new KnnSearch.Builder()).build()); + } + + /** + * @since 5.4 + */ + public NativeQueryBuilder withKnnSearches(KnnSearch knnSearch) { + return withKnnSearches(List.of(knnSearch)); + } + public NativeQuery build() { Assert.isTrue(query == null || springDataQuery == null, "Cannot have both a native query and a Spring Data query"); return new NativeQuery(this); diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/Queries.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/Queries.java index a0062bb9df..7259f0ca41 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/Queries.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/Queries.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,9 +34,9 @@ import java.util.List; import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.geo.GeoPoint; import org.springframework.data.elasticsearch.core.query.BaseQueryBuilder; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveChildTemplate.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveChildTemplate.java index 60e02c1ba7..8cdd00cb05 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveChildTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveChildTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveClusterTemplate.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveClusterTemplate.java index b3b0acda05..3207fd5117 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveClusterTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveClusterTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchClient.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchClient.java index e998faf4b5..7241fa7b89 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchClient.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,15 +27,17 @@ import co.elastic.clients.util.ObjectBuilder; import reactor.core.publisher.Mono; +import java.io.IOException; import java.util.function.Function; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** * Reactive version of {@link co.elastic.clients.elasticsearch.ElasticsearchClient}. * * @author Peter-Josef Meisch + * @author maryantocinn * @since 4.4 */ public class ReactiveElasticsearchClient extends ApiClient @@ -55,8 +57,11 @@ public ReactiveElasticsearchClient withTransportOptions(@Nullable TransportOptio } @Override - public void close() throws Exception { - transport.close(); + public void close() throws IOException { + // since Elasticsearch 8.16 the ElasticsearchClient implements (through ApiClient) the Closeable interface and + // handles closing of the underlying transport. We now just call the base class, but keep this as we + // have been implementing AutoCloseable since 4.4 and won't change that to a mere Closeable + super.close(); } // region child clients @@ -69,6 +74,10 @@ public ReactiveElasticsearchIndicesClient indices() { return new ReactiveElasticsearchIndicesClient(transport, transportOptions); } + public ReactiveElasticsearchSqlClient sql() { + return new ReactiveElasticsearchSqlClient(transport, transportOptions); + } + // endregion // region info @@ -122,7 +131,8 @@ public Mono> get(GetRequest request, Class tClass) { // java.lang.Class) // noinspection unchecked JsonEndpoint, ErrorResponse> endpoint = (JsonEndpoint, ErrorResponse>) GetRequest._ENDPOINT; - endpoint = new EndpointWithResponseMapperAttr<>(endpoint, "co.elastic.clients:Deserializer:_global.get.TDocument", + endpoint = new EndpointWithResponseMapperAttr<>(endpoint, + "co.elastic.clients:Deserializer:_global.get.Response.TDocument", getDeserializer(tClass)); return Mono.fromFuture(transport.performRequestAsync(request, endpoint, transportOptions)); @@ -141,7 +151,7 @@ public Mono> update(UpdateRequest request, Class< // noinspection unchecked JsonEndpoint, UpdateResponse, ErrorResponse> endpoint = new EndpointWithResponseMapperAttr( - UpdateRequest._ENDPOINT, "co.elastic.clients:Deserializer:_global.update.TDocument", + UpdateRequest._ENDPOINT, "co.elastic.clients:Deserializer:_global.update.Response.TDocument", this.getDeserializer(clazz)); return Mono.fromFuture(transport.performRequestAsync(request, endpoint, this.transportOptions)); } @@ -167,7 +177,8 @@ public Mono> mget(MgetRequest request, Class clazz) { // noinspection unchecked JsonEndpoint, ErrorResponse> endpoint = (JsonEndpoint, ErrorResponse>) MgetRequest._ENDPOINT; - endpoint = new EndpointWithResponseMapperAttr<>(endpoint, "co.elastic.clients:Deserializer:_global.mget.TDocument", + endpoint = new EndpointWithResponseMapperAttr<>(endpoint, + "co.elastic.clients:Deserializer:_global.mget.Response.TDocument", this.getDeserializer(clazz)); return Mono.fromFuture(transport.performRequestAsync(request, endpoint, transportOptions)); @@ -223,6 +234,26 @@ public Mono deleteByQuery( return deleteByQuery(fn.apply(new DeleteByQueryRequest.Builder()).build()); } + /** + * @since 5.4 + */ + public Mono count(CountRequest request) { + + Assert.notNull(request, "request must not be null"); + + return Mono.fromFuture(transport.performRequestAsync(request, CountRequest._ENDPOINT, transportOptions)); + } + + /** + * @since 5.4 + */ + public Mono count(Function> fn) { + + Assert.notNull(fn, "fn must not be null"); + + return count(fn.apply(new CountRequest.Builder()).build()); + } + // endregion // region search @@ -278,7 +309,7 @@ public Mono> scroll(ScrollRequest request, Class tDocum // noinspection unchecked JsonEndpoint, ErrorResponse> endpoint = (JsonEndpoint, ErrorResponse>) ScrollRequest._ENDPOINT; endpoint = new EndpointWithResponseMapperAttr<>(endpoint, - "co.elastic.clients:Deserializer:_global.scroll.TDocument", getDeserializer(tDocumentClass)); + "co.elastic.clients:Deserializer:_global.scroll.Response.TDocument", getDeserializer(tDocumentClass)); return Mono.fromFuture(transport.performRequestAsync(request, endpoint, transportOptions)); } diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchClusterClient.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchClusterClient.java index bbe1177a9f..b90f0da967 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchClusterClient.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchClusterClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,15 @@ package org.springframework.data.elasticsearch.client.elc; import co.elastic.clients.ApiClient; -import co.elastic.clients.elasticsearch.cluster.*; +import co.elastic.clients.elasticsearch.cluster.DeleteComponentTemplateRequest; +import co.elastic.clients.elasticsearch.cluster.DeleteComponentTemplateResponse; +import co.elastic.clients.elasticsearch.cluster.ExistsComponentTemplateRequest; +import co.elastic.clients.elasticsearch.cluster.GetComponentTemplateRequest; +import co.elastic.clients.elasticsearch.cluster.GetComponentTemplateResponse; +import co.elastic.clients.elasticsearch.cluster.HealthRequest; +import co.elastic.clients.elasticsearch.cluster.HealthResponse; +import co.elastic.clients.elasticsearch.cluster.PutComponentTemplateRequest; +import co.elastic.clients.elasticsearch.cluster.PutComponentTemplateResponse; import co.elastic.clients.transport.ElasticsearchTransport; import co.elastic.clients.transport.TransportOptions; import co.elastic.clients.transport.endpoints.BooleanResponse; @@ -25,7 +33,7 @@ import java.util.function.Function; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Reactive version of the {@link co.elastic.clients.elasticsearch.cluster.ElasticsearchClusterClient} diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchConfiguration.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchConfiguration.java index 6117f28305..2506b59c01 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchConfiguration.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -125,6 +125,6 @@ public JsonpMapper jsonpMapper() { * @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(); + return new RestClientOptions(RequestOptions.DEFAULT, false).toBuilder().build(); } } diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchIndicesClient.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchIndicesClient.java index f274f7c66b..3ebde9776b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchIndicesClient.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchIndicesClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ import java.util.function.Function; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Reactive version of the {@link co.elastic.clients.elasticsearch.indices.ElasticsearchIndicesClient} @@ -539,14 +539,6 @@ public Mono stats() { return stats(builder -> builder); } - public Mono unfreeze(UnfreezeRequest request) { - return Mono.fromFuture(transport.performRequestAsync(request, UnfreezeRequest._ENDPOINT, transportOptions)); - } - - public Mono unfreeze(Function> fn) { - return unfreeze(fn.apply(new UnfreezeRequest.Builder()).build()); - } - public Mono updateAliases(UpdateAliasesRequest request) { return Mono.fromFuture(transport.performRequestAsync(request, UpdateAliasesRequest._ENDPOINT, transportOptions)); } diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchSqlClient.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchSqlClient.java new file mode 100644 index 0000000000..c14bb48657 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchSqlClient.java @@ -0,0 +1,72 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.client.elc; + +import co.elastic.clients.ApiClient; +import co.elastic.clients.elasticsearch._types.ElasticsearchException; +import co.elastic.clients.elasticsearch.sql.QueryRequest; +import co.elastic.clients.elasticsearch.sql.QueryResponse; +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.io.IOException; +import java.util.function.Function; + +import org.jetbrains.annotations.Nullable; + +/** + * Reactive version of {@link co.elastic.clients.elasticsearch.sql.ElasticsearchSqlClient}. + * + * @author Aouichaoui Youssef + * @since 5.4 + */ +public class ReactiveElasticsearchSqlClient extends ApiClient { + public ReactiveElasticsearchSqlClient(ElasticsearchTransport transport, @Nullable TransportOptions transportOptions) { + super(transport, transportOptions); + } + + @Override + public ReactiveElasticsearchSqlClient withTransportOptions(@Nullable TransportOptions transportOptions) { + return new ReactiveElasticsearchSqlClient(transport, transportOptions); + } + + /** + * Executes a SQL request + * + * @param fn a function that initializes a builder to create the {@link QueryRequest}. + */ + public final Mono query(Function> fn) + throws IOException, ElasticsearchException { + return query(fn.apply(new QueryRequest.Builder()).build()); + } + + /** + * Executes a SQL request. + */ + public Mono query(QueryRequest query) { + return Mono.fromFuture(transport.performRequestAsync(query, QueryRequest._ENDPOINT, transportOptions)); + } + + /** + * Executes a SQL request. + */ + public Mono query() { + return Mono.fromFuture( + transport.performRequestAsync(new QueryRequest.Builder().build(), QueryRequest._ENDPOINT, transportOptions)); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchTemplate.java index e9e7fb9812..a98e41ab90 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.data.domain.Sort; import org.springframework.data.elasticsearch.BulkFailureException; @@ -57,18 +58,12 @@ 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.BaseQuery; -import org.springframework.data.elasticsearch.core.query.BaseQueryBuilder; -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.SearchTemplateQuery; -import org.springframework.data.elasticsearch.core.query.UpdateQuery; +import org.springframework.data.elasticsearch.core.query.*; 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.script.Script; -import org.springframework.lang.Nullable; +import org.springframework.data.elasticsearch.core.sql.SqlResponse; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -84,575 +79,604 @@ */ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearchTemplate { - private static final Log LOGGER = LogFactory.getLog(ReactiveElasticsearchTemplate.class); - - private final ReactiveElasticsearchClient client; - private final RequestConverter requestConverter; - private final ResponseConverter responseConverter; - private final JsonpMapper jsonpMapper; - private final ElasticsearchExceptionTranslator exceptionTranslator; - - public 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 Mono> doIndex(T entity, IndexCoordinates index) { - - IndexRequest indexRequest = requestConverter.documentIndexRequest(getIndexQuery(entity), index, - getRefreshPolicy()); - return Mono.just(entity) // - .zipWith(// - Mono.from(execute(client -> client.index(indexRequest))) // - .map(indexResponse -> new IndexResponseMetaData(indexResponse.id(), // - indexResponse.index(), // - indexResponse.seqNo(), // - indexResponse.primaryTerm(), // - indexResponse.version() // - ))); - } - - @Override - public Flux saveAll(Mono> entitiesPublisher, IndexCoordinates index) { - - Assert.notNull(entitiesPublisher, "entitiesPublisher must not be null!"); - - return entitiesPublisher // - .flatMapMany(entities -> Flux.fromIterable(entities) // - .concatMap(entity -> maybeCallbackBeforeConvert(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(); - var updatedEntity = entityOperations.updateIndexedObject( - savedEntity, new IndexedObjectInformation( // - response.id(), // - response.index(), // - response.seqNo(), // - response.primaryTerm(), // - response.version()), - converter, - routingResolver); - return maybeCallbackAfterSave(updatedEntity, index); - }); - }); - } - - @Override - protected Mono doExists(String id, IndexCoordinates index) { - - Assert.notNull(id, "id must not be null"); - Assert.notNull(index, "index must not be null"); - - ExistsRequest existsRequest = requestConverter.documentExistsRequest(id, routingResolver.getRouting(), index); - - return Mono.from(execute( - ((ClientCallback>) client -> client.exists(existsRequest)))) - .map(BooleanResponse::value) // - .onErrorReturn(NoSuchIndexException.class, false); - } + private static final Log LOGGER = LogFactory.getLog(ReactiveElasticsearchTemplate.class); + + private final ReactiveElasticsearchClient client; + private final ReactiveElasticsearchSqlClient sqlClient; + private final RequestConverter requestConverter; + private final ResponseConverter responseConverter; + private final JsonpMapper jsonpMapper; + private final ElasticsearchExceptionTranslator exceptionTranslator; + + public ReactiveElasticsearchTemplate(ReactiveElasticsearchClient client, ElasticsearchConverter converter) { + super(converter); + + Assert.notNull(client, "client must not be null"); + + this.client = client; + this.sqlClient = client.sql(); + this.jsonpMapper = client._transport().jsonpMapper(); + requestConverter = new RequestConverter(converter, jsonpMapper); + responseConverter = new ResponseConverter(jsonpMapper); + exceptionTranslator = new ElasticsearchExceptionTranslator(jsonpMapper); + } + + // region Document operations + @Override + protected Mono> doIndex(T entity, IndexCoordinates index) { + + IndexRequest indexRequest = requestConverter.documentIndexRequest(getIndexQuery(entity), index, + getRefreshPolicy()); + return Mono.just(entity) // + .zipWith(// + Mono.from(execute(client -> client.index(indexRequest))) // + .map(indexResponse -> new IndexResponseMetaData(indexResponse.id(), // + indexResponse.index(), // + indexResponse.seqNo(), // + indexResponse.primaryTerm(), // + indexResponse.version() // + ))); + } + + @Override + public Flux saveAll(Mono> entitiesPublisher, IndexCoordinates index) { + + Assert.notNull(entitiesPublisher, "entitiesPublisher must not be null!"); + + return entitiesPublisher // + .flatMapMany(entities -> Flux.fromIterable(entities) // + .concatMap(entity -> maybeCallbackBeforeConvert(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(); + var updatedEntity = entityOperations.updateIndexedObject( + savedEntity, new IndexedObjectInformation( // + response.id(), // + response.index(), // + response.seqNo(), // + response.primaryTerm(), // + response.version()), + converter, + routingResolver); + return maybeCallbackAfterSave(updatedEntity, index); + }); + }); + } + + @Override + protected Mono doExists(String id, IndexCoordinates index) { + + Assert.notNull(id, "id must not be null"); + Assert.notNull(index, "index must not be null"); + + ExistsRequest existsRequest = requestConverter.documentExistsRequest(id, routingResolver.getRouting(), index); + + return Mono.from(execute( + ((ClientCallback>) client -> client.exists(existsRequest)))) + .map(BooleanResponse::value) // + .onErrorReturn(NoSuchIndexException.class, false); + } + + @Override + public Mono delete(DeleteQuery query, Class entityType, IndexCoordinates index) { + Assert.notNull(query, "query must not be null"); + + DeleteByQueryRequest request = requestConverter.documentDeleteByQueryRequest(query, routingResolver.getRouting(), + entityType, index, getRefreshPolicy()); + return Mono.from(execute(client -> client.deleteByQuery(request))).map(responseConverter::byQueryResponse); + } + + @Override + public Mono get(String id, Class 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); + + Mono> getResponse = Mono + .from(execute(client -> client.get(getRequest, EntityAsMap.class))); + + ReadDocumentCallback callback = new ReadDocumentCallback<>(converter, entityType, index); + return getResponse.flatMap(response -> callback.toEntity(DocumentAdapters.from(response))); + } + + @Override + public Mono 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( // + client -> client.reindex(reindexRequestES))).map(responseConverter::reindexResponse); + } + + @Override + public Mono 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( // + client -> client.reindex(reindexRequestES))) + .flatMap(response -> (response.task() == null) + ? Mono.error( + new UnsupportedBackendOperation("ElasticsearchClient did not return a task id on submit request")) + : Mono.just(response.task())); + } + + @Override + public Mono update(UpdateQuery updateQuery, IndexCoordinates index) { + + Assert.notNull(updateQuery, "UpdateQuery must not be null"); + Assert.notNull(index, "Index must not be null"); + + UpdateRequest request = requestConverter.documentUpdateRequest(updateQuery, index, getRefreshPolicy(), + routingResolver.getRouting()); + + return Mono.from(execute(client -> client.update(request, Document.class))).flatMap(response -> { + UpdateResponse.Result result = result(response.result()); + return result == null ? Mono.empty() : Mono.just(UpdateResponse.of(result)); + }); + } + + @Override + public Mono updateByQuery(UpdateQuery updateQuery, IndexCoordinates index) { + throw new UnsupportedOperationException("not implemented"); + } - @Override - public Mono delete(Query query, Class entityType, IndexCoordinates index) { + @Override + public Mono bulkUpdate(List queries, BulkOptions bulkOptions, IndexCoordinates index) { - Assert.notNull(query, "query must not be null"); - - DeleteByQueryRequest request = requestConverter.documentDeleteByQueryRequest(query, routingResolver.getRouting(), - entityType, index, getRefreshPolicy()); - return Mono.from(execute(client -> client.deleteByQuery(request))).map(responseConverter::byQueryResponse); - } - - @Override - public Mono get(String id, Class 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); - - Mono> getResponse = Mono - .from(execute(client -> client.get(getRequest, EntityAsMap.class))); - - ReadDocumentCallback callback = new ReadDocumentCallback<>(converter, entityType, index); - return getResponse.flatMap(response -> callback.toEntity(DocumentAdapters.from(response))); - } - - @Override - public Mono 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( // - client -> client.reindex(reindexRequestES))).map(responseConverter::reindexResponse); - } - - @Override - public Mono 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( // - client -> client.reindex(reindexRequestES))) - .flatMap(response -> (response.task() == null) - ? Mono.error( - new UnsupportedBackendOperation("ElasticsearchClient did not return a task id on submit request")) - : Mono.just(response.task())); - } + 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"); - @Override - public Mono update(UpdateQuery updateQuery, IndexCoordinates index) { - - Assert.notNull(updateQuery, "UpdateQuery must not be null"); - Assert.notNull(index, "Index must not be null"); - - UpdateRequest request = requestConverter.documentUpdateRequest(updateQuery, index, getRefreshPolicy(), - routingResolver.getRouting()); - - return Mono.from(execute(client -> client.update(request, Document.class))).flatMap(response -> { - UpdateResponse.Result result = result(response.result()); - return result == null ? Mono.empty() : Mono.just(UpdateResponse.of(result)); - }); - } - - @Override - public Mono updateByQuery(UpdateQuery updateQuery, IndexCoordinates index) { - throw new UnsupportedOperationException("not implemented"); - } + return doBulkOperation(queries, bulkOptions, index).then(); + } - @Override - public Mono bulkUpdate(List queries, BulkOptions bulkOptions, IndexCoordinates index) { + private Flux doBulkOperation(List 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"); + 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())); - return doBulkOperation(queries, bulkOptions, index).then(); - } + } - private Flux doBulkOperation(List queries, BulkOptions bulkOptions, IndexCoordinates index) { + private Mono checkForBulkOperationFailure(BulkResponse bulkResponse) { - 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())); + if (bulkResponse.errors()) { + Map failedDocuments = new HashMap<>(); - } + for (BulkResponseItem item : bulkResponse.items()) { - private Mono checkForBulkOperationFailure(BulkResponse bulkResponse) { + if (item.error() != null) { + failedDocuments.put(item.id(), new BulkFailureException.FailureDetails(item.status(), 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); + } + } - if (bulkResponse.errors()) { - Map failedDocuments = new HashMap<>(); + @Override + protected Mono doDeleteById(String id, @Nullable String routing, IndexCoordinates index) { - for (BulkResponseItem item : bulkResponse.items()) { + Assert.notNull(id, "id must not be null"); + Assert.notNull(index, "index must not be null"); - if (item.error() != null) { - failedDocuments.put(item.id(), new BulkFailureException.FailureDetails(item.status(), 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); - } - } + return Mono.defer(() -> { + DeleteRequest deleteRequest = requestConverter.documentDeleteRequest(id, routing, index, getRefreshPolicy()); + return doDelete(deleteRequest); + }); + } - @Override - protected Mono 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 doDelete(DeleteRequest request) { - - return Mono.from(execute(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()); - } - - @Override - public Flux> multiGet(Query query, Class 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); - - ReadDocumentCallback callback = new ReadDocumentCallback<>(converter, clazz, index); - - Publisher> response = execute(client -> client.mget(request, EntityAsMap.class)); - - return Mono.from(response)// - .flatMapMany(it -> Flux.fromIterable(DocumentAdapters.from(it))) // - .flatMap(multiGetItem -> { - if (multiGetItem.isFailed()) { - return Mono.just(MultiGetItem.of(null, multiGetItem.getFailure())); - } else { - return callback.toEntity(multiGetItem.getItem()) // - .map(t -> MultiGetItem.of(t, multiGetItem.getFailure())); - } - }); - } - - // endregion - - @Override - protected ReactiveElasticsearchTemplate doCopy() { - return new ReactiveElasticsearchTemplate(client, converter); - } - - // region search operations - - @Override - protected Flux doFind(Query query, Class 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"); - - if (query instanceof SearchTemplateQuery searchTemplateQuery) { - return Flux.defer(() -> doSearch(searchTemplateQuery, clazz, index)); - } else { - return Flux.defer(() -> { - boolean queryIsUnbounded = !(query.getPageable().isPaged() || query.isLimiting()); - return queryIsUnbounded ? doFindUnbounded(query, clazz, index) : doFindBounded(query, clazz, index); - }); - } - } - - private Flux doFindUnbounded(Query query, Class clazz, IndexCoordinates index) { - - if (query instanceof BaseQuery baseQuery) { - var pitKeepAlive = Duration.ofMinutes(5); - // setup functions for Flux.usingWhen() - Mono resourceSupplier = openPointInTime(index, pitKeepAlive, true) - .map(pit -> new PitSearchAfter(baseQuery, pit)); - - Function> asyncComplete = this::cleanupPit; - - BiFunction> asyncError = (psa, ex) -> { - if (LOGGER.isErrorEnabled()) { - LOGGER.error("Error during pit/search_after", ex); - } - return cleanupPit(psa); - }; - - Function> asyncCancel = psa -> { - if (LOGGER.isWarnEnabled()) { - LOGGER.warn("pit/search_after was cancelled"); - } - return cleanupPit(psa); - }; - - Function>> resourceClosure = psa -> { - - baseQuery.setPointInTime(new Query.PointInTime(psa.getPit(), pitKeepAlive)); - baseQuery.addSort(Sort.by("_shard_doc")); - SearchRequest firstSearchRequest = requestConverter.searchRequest(baseQuery, routingResolver.getRouting(), - clazz, index, false, true); - - return Mono.from(execute(client -> client.search(firstSearchRequest, EntityAsMap.class))) - .expand(entityAsMapSearchResponse -> { - - var hits = entityAsMapSearchResponse.hits().hits(); - if (CollectionUtils.isEmpty(hits)) { - return Mono.empty(); - } - - List sortOptions = hits.get(hits.size() - 1).sort().stream().map(TypeUtils::toObject) - .collect(Collectors.toList()); - baseQuery.setSearchAfter(sortOptions); - SearchRequest followSearchRequest = requestConverter.searchRequest(baseQuery, - routingResolver.getRouting(), clazz, index, false, true); - return Mono.from(execute(client -> client.search(followSearchRequest, EntityAsMap.class))); - }); - - }; - - Flux> searchResponses = Flux.usingWhen(resourceSupplier, resourceClosure, asyncComplete, - asyncError, asyncCancel); - return searchResponses.flatMapIterable(entityAsMapSearchResponse -> entityAsMapSearchResponse.hits().hits()) - .map(entityAsMapHit -> DocumentAdapters.from(entityAsMapHit, jsonpMapper)); - } else { - return Flux.error(new IllegalArgumentException("Query must be derived from BaseQuery")); - } - } - - private Publisher cleanupPit(PitSearchAfter psa) { - var baseQuery = psa.getBaseQuery(); - baseQuery.setPointInTime(null); - baseQuery.setSearchAfter(null); - baseQuery.setSort(psa.getSort()); - var pit = psa.getPit(); - return StringUtils.hasText(pit) ? closePointInTime(pit) : Mono.empty(); - } - - static private class PitSearchAfter { - private final BaseQuery baseQuery; - @Nullable - private final Sort sort; - private final String pit; - - PitSearchAfter(BaseQuery baseQuery, String pit) { - this.baseQuery = baseQuery; - this.sort = baseQuery.getSort(); - this.pit = pit; - } - - public BaseQuery getBaseQuery() { - return baseQuery; - } - - @Nullable - public Sort getSort() { - return sort; - } - - public String getPit() { - return pit; - } - } - - @Override - protected Mono doCount(Query query, Class entityType, IndexCoordinates index) { - - Assert.notNull(query, "query must not be null"); - Assert.notNull(index, "index must not be null"); + private Mono doDelete(DeleteRequest request) { - SearchRequest searchRequest = requestConverter.searchRequest(query, routingResolver.getRouting(), entityType, index, - true); + return Mono.from(execute(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()); + } + + @Override + public Flux> multiGet(Query query, Class 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); + + ReadDocumentCallback callback = new ReadDocumentCallback<>(converter, clazz, index); + + Publisher> response = execute(client -> client.mget(request, EntityAsMap.class)); + + return Mono.from(response)// + .flatMapMany(it -> Flux.fromIterable(DocumentAdapters.from(it))) // + .flatMap(multiGetItem -> { + if (multiGetItem.isFailed()) { + return Mono.just(MultiGetItem.of(null, multiGetItem.getFailure())); + } else { + return callback.toEntity(multiGetItem.getItem()) // + .map(t -> MultiGetItem.of(t, multiGetItem.getFailure())); + } + }); + } + + // endregion + + @Override + protected ReactiveElasticsearchTemplate doCopy() { + return new ReactiveElasticsearchTemplate(client, converter); + } + + // region search operations + + @Override + protected Flux doFind(Query query, Class 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"); + + if (query instanceof SearchTemplateQuery searchTemplateQuery) { + return Flux.defer(() -> doSearch(searchTemplateQuery, clazz, index)); + } else { + return Flux.defer(() -> { + boolean queryIsUnbounded = !(query.getPageable().isPaged() || query.isLimiting()); + return queryIsUnbounded ? doFindUnbounded(query, clazz, index) : doFindBounded(query, clazz, index); + }); + } + } + + private Flux doFindUnbounded(Query query, Class clazz, IndexCoordinates index) { + + if (query instanceof BaseQuery baseQuery) { + var pitKeepAlive = Duration.ofMinutes(5); + // setup functions for Flux.usingWhen() + Mono resourceSupplier = openPointInTime(index, pitKeepAlive, true) + .map(pit -> new PitSearchAfter(baseQuery, pit)); + + Function> asyncComplete = this::cleanupPit; + + BiFunction> asyncError = (psa, ex) -> { + if (LOGGER.isErrorEnabled()) { + LOGGER.error("Error during pit/search_after", ex); + } + return cleanupPit(psa); + }; + + Function> asyncCancel = psa -> { + if (LOGGER.isWarnEnabled()) { + LOGGER.warn("pit/search_after was cancelled"); + } + return cleanupPit(psa); + }; + + Function>> resourceClosure = psa -> { + + baseQuery.setPointInTime(new Query.PointInTime(psa.getPit(), pitKeepAlive)); + + // only add _shard_doc if there is not a field_collapse and a sort with the same name + boolean addShardDoc = true; + + if (query instanceof NativeQuery nativeQuery && nativeQuery.getFieldCollapse() != null) { + var field = nativeQuery.getFieldCollapse().field(); + + if (nativeQuery.getSortOptions().stream() + .anyMatch(sortOptions -> sortOptions.isField() && sortOptions.field().field().equals(field))) { + addShardDoc = false; + } + + if (query.getSort() != null + && query.getSort().stream().anyMatch(order -> order.getProperty().equals(field))) { + addShardDoc = false; + } + } + + if (addShardDoc) { + baseQuery.addSort(Sort.by("_shard_doc")); + } + + SearchRequest firstSearchRequest = requestConverter.searchRequest(baseQuery, routingResolver.getRouting(), + clazz, index, false, true); + + return Mono.from(execute(client -> client.search(firstSearchRequest, EntityAsMap.class))) + .expand(entityAsMapSearchResponse -> { + + var hits = entityAsMapSearchResponse.hits().hits(); + if (CollectionUtils.isEmpty(hits)) { + return Mono.empty(); + } + + List sortOptions = hits.get(hits.size() - 1).sort().stream().map(TypeUtils::toObject) + .collect(Collectors.toList()); + baseQuery.setSearchAfter(sortOptions); + SearchRequest followSearchRequest = requestConverter.searchRequest(baseQuery, + routingResolver.getRouting(), clazz, index, false, true); + return Mono.from(execute(client -> client.search(followSearchRequest, EntityAsMap.class))); + }); + + }; + + Flux> searchResponses = Flux.usingWhen(resourceSupplier, resourceClosure, asyncComplete, + asyncError, asyncCancel); + return searchResponses.flatMapIterable(entityAsMapSearchResponse -> entityAsMapSearchResponse.hits().hits()) + .map(entityAsMapHit -> DocumentAdapters.from(entityAsMapHit, jsonpMapper)); + } else { + return Flux.error(new IllegalArgumentException("Query must be derived from BaseQuery")); + } + } + + private Publisher cleanupPit(PitSearchAfter psa) { + var baseQuery = psa.getBaseQuery(); + baseQuery.setPointInTime(null); + baseQuery.setSearchAfter(null); + baseQuery.setSort(psa.getSort()); + var pit = psa.getPit(); + return StringUtils.hasText(pit) ? closePointInTime(pit) : Mono.empty(); + } + + static private class PitSearchAfter { + private final BaseQuery baseQuery; + @Nullable private final Sort sort; + private final String pit; + + PitSearchAfter(BaseQuery baseQuery, String pit) { + this.baseQuery = baseQuery; + this.sort = baseQuery.getSort(); + this.pit = pit; + } - return Mono.from(execute(client -> client.search(searchRequest, EntityAsMap.class))) - .map(searchResponse -> searchResponse.hits().total() != null ? searchResponse.hits().total().value() : 0L); - } - - private Flux doFindBounded(Query query, Class clazz, IndexCoordinates index) { + public BaseQuery getBaseQuery() { + return baseQuery; + } + + @Nullable + public Sort getSort() { + return sort; + } - SearchRequest searchRequest = requestConverter.searchRequest(query, routingResolver.getRouting(), clazz, index, - false, false); - - return Mono.from(execute(client -> client.search(searchRequest, EntityAsMap.class))) // - .flatMapIterable(entityAsMapSearchResponse -> entityAsMapSearchResponse.hits().hits()) // - .map(entityAsMapHit -> DocumentAdapters.from(entityAsMapHit, jsonpMapper)); - } - - private Flux doSearch(SearchTemplateQuery query, Class clazz, IndexCoordinates index) { - - var request = requestConverter.searchTemplate(query, routingResolver.getRouting(), index); - - return Mono.from(execute(client -> client.searchTemplate(request, EntityAsMap.class))) // - .flatMapIterable(entityAsMapSearchResponse -> entityAsMapSearchResponse.hits().hits()) // - .map(entityAsMapHit -> DocumentAdapters.from(entityAsMapHit, jsonpMapper)); - } - - @Override - protected Mono 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, routingResolver.getRouting(), clazz, index, - false); - - // noinspection unchecked - SearchDocumentCallback callback = new ReadSearchDocumentCallback<>((Class) clazz, index); - SearchDocumentResponse.EntityCreator entityCreator = searchDocument -> callback.toEntity(searchDocument) - .toFuture(); - - return Mono.from(execute(client -> client.search(searchRequest, EntityAsMap.class))) - .map(searchResponse -> SearchDocumentResponseBuilder.from(searchResponse, entityCreator, jsonpMapper)); - } - - @Override - public Flux> aggregate(Query query, Class entityType, IndexCoordinates index) { - - return doFindForResponse(query, entityType, index).flatMapMany(searchDocumentResponse -> { - ElasticsearchAggregations aggregations = (ElasticsearchAggregations) searchDocumentResponse.getAggregations(); - return aggregations == null ? Flux.empty() : Flux.fromIterable(aggregations.aggregations()); - }); - } - - @Override - public Mono openPointInTime(IndexCoordinates index, Duration keepAlive, Boolean ignoreUnavailable) { - - Assert.notNull(index, "index must not be null"); - Assert.notNull(keepAlive, "keepAlive must not be null"); - Assert.notNull(ignoreUnavailable, "ignoreUnavailable must not be null"); - - var request = requestConverter.searchOpenPointInTimeRequest(index, keepAlive, ignoreUnavailable); - return Mono.from(execute(client -> client.openPointInTime(request))).map(OpenPointInTimeResponse::id); - } - - @Override - public Mono closePointInTime(String pit) { - - Assert.notNull(pit, "pit must not be null"); - - ClosePointInTimeRequest request = requestConverter.searchClosePointInTime(pit); - return Mono.from(execute(client -> client.closePointInTime(request))).map(ClosePointInTimeResponse::succeeded); - } - - // endregion - - // region script operations - @Override - public Mono putScript(Script script) { - - Assert.notNull(script, "script must not be null"); - - var request = requestConverter.scriptPut(script); - return Mono.from(execute(client -> client.putScript(request))).map(PutScriptResponse::acknowledged); - } - - @Override - public Mono