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 021cf949cc..e075a74d86 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,3 +1,3 @@ -#Wed Oct 04 16:58:13 PDT 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.5/apache-maven-3.9.5-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 685270466c..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 ci/verify.sh' - sh "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 ci/verify.sh' - sh "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" + } } } } @@ -90,28 +92,26 @@ pipeline { label 'data' } 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=spring-builds+jenkins -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=https://repo.spring.io ' + - "-Dartifactory.username=${ARTIFACTORY_USR} " + - "-Dartifactory.password=${ARTIFACTORY_PSW} " + - "-Dartifactory.staging-repository=libs-snapshot-local " + - "-Dartifactory.build-name=spring-data-elasticsearch " + - "-Dartifactory.build-number=${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 d106098ef8..0242089d82 100644 --- a/README.adoc +++ b/README.adoc @@ -1,6 +1,4 @@ -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-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Gradle Enterprise", link="https://ge.spring.io/scans?search.rootProjectNames=Spring%20Data%20Elasticsearch"] += 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 9f86e51ebf..ca174330ee 100755 --- a/ci/clean.sh +++ b/ci/clean.sh @@ -2,11 +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=spring-builds+jenkins -Duser.home=/tmp/jenkins-home" \ - ./mvnw -s settings.xml clean -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-elasticsearch +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 -Ddevelocity.storage.directory=/tmp/jenkins-home/.develocity-root diff --git a/ci/pipeline.properties b/ci/pipeline.properties index d65c9db587..cde4a8e881 100644 --- a/ci/pipeline.properties +++ b/ci/pipeline.properties @@ -1,22 +1,19 @@ # Java versions -java.main.tag=17.0.8_7-jdk-focal -java.next.tag=21_35-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,6 +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 -develocity.cache.credentials=gradle_enterprise_cache_user +artifactory.url=https://repo.spring.io +artifactory.repository.snapshot=libs-snapshot-local 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 82734212ba..46afc80280 100755 --- a/ci/verify.sh +++ b/ci/verify.sh @@ -3,14 +3,8 @@ set -euo pipefail mkdir -p /tmp/jenkins-home/.m2/spring-data-elasticsearch -chown -R 1001:1001 . +export JENKINS_USER=${JENKINS_USER_NAME} -export DEVELOCITY_CACHE_USERNAME=${DEVELOCITY_CACHE_USR} -export DEVELOCITY_CACHE_PASSWORD=${DEVELOCITY_CACHE_PSW} - -# 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=spring-builds+jenkins -Duser.home=/tmp/jenkins-home" \ +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 \ No newline at end of file + -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 c13bab9a2d..4fcfd20c49 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.11.1 + 9.0.2 - 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 @@ -324,6 +324,13 @@ test + + com.tngtech.archunit + archunit-junit5 + ${archunit} + test + + @@ -436,25 +443,6 @@ - - jdk13+ - - - [13,) - - - - - org.apache.maven.plugins - maven-surefire-plugin - - -XX:+AllowRedefinitionToAddDeleteMethods - - - - - - antora-process-resources @@ -472,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 d7cfa9826c..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 ==== @@ -150,7 +150,7 @@ ClientConfiguration clientConfiguration = ClientConfiguration.builder() return headers; }) .withClientConfigurer( <.> - ElasticsearchClientConfigurationCallback.from(clientBuilder -> { + ElasticsearchHttpClientConfigurationCallback.from(clientBuilder -> { // ... return clientBuilder; })) @@ -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: @@ -192,6 +192,7 @@ This callback provides a `org.elasticsearch.client.RestClientBuilder` that can b [source,java] ---- ClientConfiguration.builder() + .connectedTo("localhost:9200", "localhost:9291") .withClientConfigurer(ElasticsearchClients.ElasticsearchRestClientConfigurationCallback.from(restClientBuilder -> { // configure the Elasticsearch RestClient return restClientBuilder; @@ -210,6 +211,7 @@ used by the `RestClient`. [source,java] ---- ClientConfiguration.builder() + .connectedTo("localhost:9200", "localhost:9291") .withClientConfigurer(ElasticsearchClients.ElasticsearchHttpClientConfigurationCallback.from(httpAsyncClientBuilder -> { // configure the HttpAsyncClient return httpAsyncClientBuilder; diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc index 021161650c..d4a9c565d0 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc @@ -1,6 +1,40 @@ [[new-features]] = What's new +[[new-features.6-0-0]] +== New in Spring Data Elasticsearch 6.0 + +* Upgarde to Spring 7 +* Switch to jspecify nullability annotations +* Upgrade to Elasticsearch 9.0.2 + + +[[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.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-repositories.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repositories.adoc index 8a1f7fe664..1d08868c10 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repositories.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repositories.adoc @@ -12,10 +12,10 @@ class Book { @Id private String id; - @Field(type = FieldType.text) + @Field(type = FieldType.Text) private String name; - @Field(type = FieldType.text) + @Field(type = FieldType.Text) private String summary; @Field(type = FieldType.Integer) 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 2b725a820f..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] ---- { @@ -361,3 +370,223 @@ would make an https://www.elastic.co/guide/en/elasticsearch/reference/current/qu } ---- ==== + +[[elasticsearch.query-methods.at-query.spel]] +=== Using SpEL Expressions + +.Declare query on the method using the `@Query` annotation with SpEL expression. +==== +https://docs.spring.io/spring-framework/reference/core/expressions.html[SpEL expression] is also supported when defining query in `@Query`. + +[source,java] +---- +interface BookRepository extends ElasticsearchRepository { + @Query(""" + { + "bool":{ + "must":[ + { + "term":{ + "name": "#{#name}" + } + } + ] + } + } + """) + Page findByName(String name, Pageable pageable); +} +---- + +If for example the function is called with the parameter _John_, it would produce the following query body: + +[source,json] +---- +{ + "bool":{ + "must":[ + { + "term":{ + "name": "John" + } + } + ] + } +} +---- +==== + +.accessing parameter property. +==== +Supposing that we have the following class as query parameter type: + +[source,java] +---- +public record QueryParameter(String value) { +} +---- + +It's easy to access the parameter by `#` symbol, then reference the property `value` with a simple `.`: + +[source,java] +---- +interface BookRepository extends ElasticsearchRepository { + @Query(""" + { + "bool":{ + "must":[ + { + "term":{ + "name": "#{#parameter.value}" + } + } + ] + } + } + """) + Page findByName(QueryParameter parameter, Pageable pageable); +} +---- + +We can pass `new QueryParameter("John")` as the parameter now, and it will produce the same query string as above. +==== + +.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: + +[source,java] +---- +interface BookRepository extends ElasticsearchRepository { + @Query(""" + { + "bool":{ + "must":[ + { + "term":{ + "name": "#{@queryParameter.value}" + } + } + ] + } + } + """) + Page findByName(Pageable pageable); +} +---- +==== + +.SpEL and `Collection` param. +==== +`Collection` parameter is also supported and is as easy to use as normal `String`, such as the following `terms` query: + +[source,java] +---- +interface BookRepository extends ElasticsearchRepository { + @Query(""" + { + "bool":{ + "must":[ + { + "terms":{ + "name": #{#names} + } + } + ] + } + } + """) + Page findByName(Collection names, Pageable pageable); +} +---- + +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] +---- +{ + "bool":{ + "must":[ + { + "terms":{ + "name": ["name1", "name2"] + } + } + ] + } +} +---- +==== + +.access property in the `Collection` param. +==== +https://docs.spring.io/spring-framework/reference/core/expressions/language-ref/collection-projection.html[SpEL Collection Projection] is convenient to use when values in the `Collection` parameter is not plain `String`: + +[source,java] +---- +interface BookRepository extends ElasticsearchRepository { + @Query(""" + { + "bool":{ + "must":[ + { + "terms":{ + "name": #{#parameters.![value]} + } + } + ] + } + } + """) + 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. +==== + +.alter parameter name by using `@Param` +==== +When accessing the parameter by SpEL, it's also useful to alter the parameter name to another one by `@Param` annotation in Sping Data: + +[source,java] +---- +interface BookRepository extends ElasticsearchRepository { + @Query(""" + { + "bool":{ + "must":[ + { + "terms":{ + "name": #{#another.![value]} + } + } + ] + } + } + """) + Page findByName(@Param("another") Collection parameters, Pageable pageable); +} +---- + +==== + +[[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 19d824b00e..861e2ff3bb 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/versions.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/versions.adoc @@ -1,24 +1,27 @@ [[preface.versions]] = Versions -The following table shows the Elasticsearch versions that are used by Spring Data release trains and version of Spring Data Elasticsearch included in that, as well as the Spring Boot versions referring to that particular Spring Data release train. -The Elasticsearch version given shows with which client libraries Spring Data Elasticsearch was built and tested. +The following table shows the Elasticsearch and Spring versions that are used by Spring Data release trains and the version of Spring Data Elasticsearch included in that. -[cols="^,^,^,^,^",options="header"] +[cols="^,^,^,^",options="header"] |=== -| Spring Data Release Train | Spring Data Elasticsearch | Elasticsearch | Spring Framework | Spring Boot -| 2023.1 (Vaughan) | 5.2.x | 8.11.1 | 6.1.x | 3.2.x -| 2023.0 (Ullmann) | 5.1.x | 8.7.1 | 6.0.x | 3.1.x -| 2022.0 (Turing) | 5.0.xfootnote:oom[Out of maintenance] | 8.5.3 | 6.0.x | 3.0.x -| 2021.2 (Raj) | 4.4.xfootnote:oom[] | 7.17.3 | 5.3.x | 2.7.x -| 2021.1 (Q) | 4.3.xfootnote:oom[] | 7.15.2 | 5.3.x | 2.6.x -| 2021.0 (Pascal) | 4.2.xfootnote:oom[] | 7.12.0 | 5.3.x | 2.5.x -| 2020.0 (Ockham) | 4.1.xfootnote:oom[] | 7.9.3 | 5.3.2 | 2.4.x -| Neumann | 4.0.xfootnote:oom[] | 7.6.2 | 5.2.12 |2.3.x -| Moore | 3.2.xfootnote:oom[] |6.8.12 | 5.2.12| 2.2.x -| Lovelace | 3.1.xfootnote:oom[] | 6.2.2 | 5.1.19 |2.1.x -| Kay | 3.0.xfootnote:oom[] | 5.5.0 | 5.0.13 | 2.0.x -| Ingalls | 2.1.xfootnote:oom[] | 2.4.0 | 4.3.25 | 1.5.x +| Spring Data Release Train | Spring Data Elasticsearch | Elasticsearch | Spring Framework +| 2025.1 (in development) | 6.0.x | 9.0.2 | 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 +| 2020.0 (Ockham) | 4.1.xfootnote:oom[] | 7.9.3 | 5.3.2 +| Neumann | 4.0.xfootnote:oom[] | 7.6.2 | 5.2.12 +| Moore | 3.2.xfootnote:oom[] |6.8.12 | 5.2.12 +| Lovelace | 3.1.xfootnote:oom[] | 6.2.2 | 5.1.19 +| Kay | 3.0.xfootnote:oom[] | 5.5.0 | 5.0.13 +| Ingalls | 2.1.xfootnote:oom[] | 2.4.0 | 4.3.25 |=== Support for upcoming versions of Elasticsearch is being tracked and general compatibility should be given assuming the usage of the xref:elasticsearch/template.adoc[ElasticsearchOperations interface]. diff --git a/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-4.4-5.0.adoc b/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-4.4-5.0.adoc index eb627f141a..e11955fefa 100644 --- a/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-4.4-5.0.adoc +++ b/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-4.4-5.0.adoc @@ -49,7 +49,7 @@ Also the reactive implementation that was provided up to now has been moved here If you are using `ElasticsearchRestTemplate` directly and not the `ElasticsearchOperations` interface you'll need to adjust your imports as well. When working with the `NativeSearchQuery` class, you'll need to switch to the `NativeQuery` class, which can take a -`Query` instance comign from the new Elasticsearch client libraries. +`Query` instance coming from the new Elasticsearch client libraries. You'll find plenty of examples in the test code. [[elasticsearch-migration-guide-4.4-5.0.breaking-changes-records]] 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 new file mode 100644 index 0000000000..808578cb59 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.2-5.3.adoc @@ -0,0 +1,21 @@ +[[elasticsearch-migration-guide-5.2-5.3]] += Upgrading from 5.2.x to 5.3.x + +This section describes breaking changes from version 5.2.x to 5.3.x and how removed features can be replaced by new introduced features. + +[[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. 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 8d5a10a2ac..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-2023 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 38f52df0c8..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-2023 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 b7a1bbd6e0..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-2023 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 0b9c0bf3a5..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-2023 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 03170a0708..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-2023 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 8849e21d83..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-2023 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 c86be98f0a..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-2023 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 d9c2e83dff..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 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 4fd6faa383..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-2023 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 95e5060f93..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-2023 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 463452ca1d..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-2023 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 1119374dcd..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-2023 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. @@ -39,41 +39,173 @@ public enum DateFormat { basic_t_time("'T'HHmmss.SSSXXX"), // basic_t_time_no_millis("'T'HHmmssXXX"), // basic_week_date("YYYY'W'wwe"), // week-based-year! + /** + * @since 5.3 + */ + strict_basic_week_date("YYYY'W'wwe"), // week-based-year! basic_week_date_time("YYYY'W'wwe'T'HHmmss.SSSX"), // here Elasticsearch uses a different zone format + /** + * @since 5.3 + */ + strict_basic_week_date_time("YYYY'W'wwe'T'HHmmss.SSSX"), // here Elasticsearch uses a different zone format basic_week_date_time_no_millis("YYYY'W'wwe'T'HHmmssX"), // + /** + * @since 5.3 + */ + strict_basic_week_date_time_no_millis("YYYY'W'wwe'T'HHmmssX"), // date("uuuu-MM-dd"), // + /** + * @since 5.3 + */ + strict_date("uuuu-MM-dd"), // date_hour("uuuu-MM-dd'T'HH"), // + /** + * @since 5.3 + */ + strict_date_hour("uuuu-MM-dd'T'HH"), // date_hour_minute("uuuu-MM-dd'T'HH:mm"), // + /** + * @since 5.3 + */ + strict_date_hour_minute("uuuu-MM-dd'T'HH:mm"), // date_hour_minute_second("uuuu-MM-dd'T'HH:mm:ss"), // + /** + * @since 5.3 + */ + strict_date_hour_minute_second("uuuu-MM-dd'T'HH:mm:ss"), // date_hour_minute_second_fraction("uuuu-MM-dd'T'HH:mm:ss.SSS"), // + /** + * @since 5.3 + */ + strict_date_hour_minute_second_fraction("uuuu-MM-dd'T'HH:mm:ss.SSS"), // date_hour_minute_second_millis("uuuu-MM-dd'T'HH:mm:ss.SSS"), // + /** + * @since 5.3 + */ + strict_date_hour_minute_second_millis("uuuu-MM-dd'T'HH:mm:ss.SSS"), // date_optional_time("uuuu-MM-dd['T'HH:mm:ss.SSSXXX]"), // + /** + * @since 5.3 + */ + strict_date_optional_time("uuuu-MM-dd['T'HH:mm:ss.SSSXXX]"), // strict_date_optional_time_nanos("uuuu-MM-dd['T'HH:mm:ss.SSSSSSXXX]"), // date_time("uuuu-MM-dd'T'HH:mm:ss.SSSXXX"), // + /** + * @since 5.3 + */ + strict_date_time("uuuu-MM-dd'T'HH:mm:ss.SSSXXX"), // date_time_no_millis("uuuu-MM-dd'T'HH:mm:ssVV"), // here Elasticsearch uses the zone-id in its implementation + /** + * @since 5.3 + */ + strict_date_time_no_millis("uuuu-MM-dd'T'HH:mm:ssVV"), // here Elasticsearch uses the zone-id in its implementation epoch_millis("epoch_millis"), // epoch_second("epoch_second"), // hour("HH"), // + /** + * @since 5.3 + */ + strict_hour("HH"), // hour_minute("HH:mm"), // + /** + * @since 5.3 + */ + strict_hour_minute("HH:mm"), // hour_minute_second("HH:mm:ss"), // + /** + * @since 5.3 + */ + strict_hour_minute_second("HH:mm:ss"), // hour_minute_second_fraction("HH:mm:ss.SSS"), // + /** + * @since 5.3 + */ + strict_hour_minute_second_fraction("HH:mm:ss.SSS"), // hour_minute_second_millis("HH:mm:ss.SSS"), // + /** + * @since 5.3 + */ + strict_hour_minute_second_millis("HH:mm:ss.SSS"), // ordinal_date("uuuu-DDD"), // + /** + * @since 5.3 + */ + strict_ordinal_date("uuuu-DDD"), // ordinal_date_time("uuuu-DDD'T'HH:mm:ss.SSSXXX"), // + /** + * @since 5.3 + */ + strict_ordinal_date_time("uuuu-DDD'T'HH:mm:ss.SSSXXX"), // ordinal_date_time_no_millis("uuuu-DDD'T'HH:mm:ssXXX"), // + /** + * @since 5.3 + */ + strict_ordinal_date_time_no_millis("uuuu-DDD'T'HH:mm:ssXXX"), // time("HH:mm:ss.SSSXXX"), // + /** + * @since 5.3 + */ + strict_time("HH:mm:ss.SSSXXX"), // time_no_millis("HH:mm:ssXXX"), // + /** + * @since 5.3 + */ + strict_time_no_millis("HH:mm:ssXXX"), // t_time("'T'HH:mm:ss.SSSXXX"), // + /** + * @since 5.3 + */ + strict_t_time("'T'HH:mm:ss.SSSXXX"), // t_time_no_millis("'T'HH:mm:ssXXX"), // + /** + * @since 5.3 + */ + strict_t_time_no_millis("'T'HH:mm:ssXXX"), // week_date("YYYY-'W'ww-e"), // + /** + * @since 5.3 + */ + strict_week_date("YYYY-'W'ww-e"), // week_date_time("YYYY-'W'ww-e'T'HH:mm:ss.SSSXXX"), // + /** + * @since 5.3 + */ + strict_week_date_time("YYYY-'W'ww-e'T'HH:mm:ss.SSSXXX"), // week_date_time_no_millis("YYYY-'W'ww-e'T'HH:mm:ssXXX"), // + /** + * @since 5.3 + */ + strict_week_date_time_no_millis("YYYY-'W'ww-e'T'HH:mm:ssXXX"), // weekyear(""), // no TemporalAccessor available for these 3 + /** + * @since 5.3 + */ + strict_weekyear(""), // no TemporalAccessor available for these 3 weekyear_week(""), // + /** + * @since 5.3 + */ + strict_weekyear_week(""), // weekyear_week_day(""), // + /** + * @since 5.3 + */ + strict_strict_weekyear_week_day(""), // year("uuuu"), // + /** + * @since 5.3 + */ + strict_year("uuuu"), // year_month("uuuu-MM"), // - year_month_day("uuuu-MM-dd"); // + /** + * @since 5.3 + */ + strict_year_month("uuuu-MM"), // + year_month_day("uuuu-MM-dd"), // + /** + * @since 5.3 + */ + strict_year_month_day("uuuu-MM-dd"); // private final String pattern; 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 fb24e05483..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-2023 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 fae31617a9..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-2023 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 0459857d31..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-2023 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 d6cb398ba5..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-2023 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 e17b85ff01..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-2023 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 865bf694da..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-2023 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 0c59f2d149..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-2023 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 4dbf7e92ba..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-2023 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 55cb5ab579..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-2023 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. @@ -21,6 +21,7 @@ /** * @author Peter-Josef Meisch + * @author Haibo Liu * @since 4.0 */ @Documented @@ -59,6 +60,8 @@ int numberOfFragments() default -1; + Query highlightQuery() default @Query; + String order() default ""; int phraseLimit() default -1; 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 b396786abb..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-2023 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 9d8333b0f8..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-2023 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 1a15f506c9..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 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 3d1bd46e62..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-2023 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 f814588112..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-2023 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 6127b94849..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-2023 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 2cdaa29efd..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-2023 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. @@ -74,7 +74,14 @@ */ String runtimeFieldsPath() default ""; + /** + * field alias definitions to be written to the index mapping + * + * @since 5.3 + */ + 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 new file mode 100644 index 0000000000..791659e9d5 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/MappingAlias.java @@ -0,0 +1,45 @@ +/* + * 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.Documented; +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; + +/** + * Defines a field alias in the index mapping. + * + * @author Peter-Josef Meisch + * @since 5.3 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Documented +@Inherited +public @interface MappingAlias { + /** + * the name of the alias. + */ + String name(); + + /** + * the path of the alias. + */ + String path(); +} 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 70f58dca9b..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-2023 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 d3df4bdd94..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-2023 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 0d97af4e6f..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-2023 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 db4288565d..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-2023 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 dd1ce02a68..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-2023 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 2b488205d2..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-2023 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 e37a0133ff..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-2023 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 2f8485d848..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-2023 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 cfc36f4561..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-2023 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 23fbcfd036..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-2023 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 4d301a5838..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 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 4a1b344a86..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 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 f7d469f4f0..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-2023 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 f9521d12c2..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-2023 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 f41292fa8d..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-2023 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 a6406dfad2..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-2023 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 a8dc5e13f8..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-2023 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 4a28d664d0..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-2023 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 252f5524ec..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-2023 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 bd5a539c51..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-2023 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 7e66d9c05c..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-2023 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 d559d4f7c7..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-2023 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 d5c1bcf4c1..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-2023 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 0454b4a452..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-2023 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 9b52c375e0..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-2023 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; @@ -50,6 +51,7 @@ * filter. * * @author Peter-Josef Meisch + * @author Junghoon Ban * @since 4.4 */ class CriteriaFilterProcessor { @@ -68,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); @@ -115,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"); @@ -169,17 +179,17 @@ private static ObjectBuilder withinQuery(String fieldName, Obj Assert.isTrue(values[1] instanceof String || values[1] instanceof Distance, "Second element of a geo distance filter must be a text or a Distance"); - String dist = (values[1] instanceof Distance) ? extractDistanceString((Distance) values[1]) : (String) values[1]; + String dist = (values[1] instanceof Distance distance) ? extractDistanceString(distance) : (String) values[1]; return QueryBuilders.geoDistance() // .field(fieldName) // .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) { - GeoPoint loc = GeoPoint.fromPoint((Point) values[0]); + } else if (values[0] instanceof Point point) { + GeoPoint loc = GeoPoint.fromPoint(point); location.latlon(latlon -> latlon.lat(loc.getLat()).lon(loc.getLon())); } else { String loc = (String) values[0]; @@ -220,8 +230,8 @@ private static void oneParameterBBox(GeoBoundingBoxQuery.Builder queryBuilder, O "single-element of boundedBy filter must be type of GeoBox or Box"); GeoBox geoBBox; - if (value instanceof Box) { - geoBBox = GeoBox.fromBox((Box) value); + if (value instanceof Box box) { + geoBBox = GeoBox.fromBox(box); } else { geoBBox = (GeoBox) value; } @@ -244,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 // @@ -266,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); } @@ -274,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 cdb4e98e4c..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-2023 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 5e01e2f81a..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-2023 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 @@ -174,6 +187,12 @@ private static Query queryForEntries(Criteria criteria) { .scoreMode(ChildScoreMode.Avg)); } + if (criteria.isNegating() && criteria.isOr()) { + final Query query = queryBuilder.build(); + queryBuilder = new Query.Builder(); + queryBuilder.bool(mnqb -> mnqb.mustNot(query)); + } + return queryBuilder.build(); } @@ -227,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: @@ -282,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: @@ -334,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); } @@ -359,7 +410,7 @@ private static String orQueryString(Iterable iterable) { if (item != null) { - if (sb.length() > 0) { + if (!sb.isEmpty()) { sb.append(' '); } sb.append('"'); @@ -391,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 78ae3b0d52..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-2023 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,199 +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, - 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::toString).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 e98f790467..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-2023 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 a21d04b1c3..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-2023 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 b60b9605ca..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-2023 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 65868e5360..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-2023 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 b38e40b523..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-2023 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 d48d08a472..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-2023 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. @@ -31,6 +31,10 @@ import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; import org.springframework.util.Assert; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + /** * Base class for a @{@link org.springframework.context.annotation.Configuration} class to set up the Elasticsearch * connection using the Elasticsearch Client. This class exposes different parts of the setup as Spring beans. Deriving @@ -118,13 +122,19 @@ public ElasticsearchOperations elasticsearchOperations(ElasticsearchConverter el */ @Bean public JsonpMapper jsonpMapper() { - return new JacksonJsonpMapper(); + // we need to create our own objectMapper that keeps null values in order to provide the storeNullValue + // functionality. The one Elasticsearch would provide removes the nulls. We remove unwanted nulls before they get + // into this mapper, so we can safely keep them here. + var objectMapper = (new ObjectMapper()) + .configure(SerializationFeature.INDENT_OUTPUT, false) + .setSerializationInclusion(JsonInclude.Include.ALWAYS); + return new JacksonJsonpMapper(objectMapper); } /** * @return the options that should be added to every request. Must not be {@literal null} */ public TransportOptions transportOptions() { - return new RestClientOptions(RequestOptions.DEFAULT); + 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 2851190d93..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-2023 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 @@ * appropriate: any other exception may have resulted from user code, and should not be translated. * * @author Peter-Josef Meisch + * @author Junghoon Ban * @since 4.4 */ public class ElasticsearchExceptionTranslator implements PersistenceExceptionTranslator { @@ -59,7 +60,7 @@ public ElasticsearchExceptionTranslator(JsonpMapper jsonpMapper) { */ public RuntimeException translateException(Throwable throwable) { - RuntimeException runtimeException = throwable instanceof RuntimeException ? (RuntimeException) throwable + RuntimeException runtimeException = throwable instanceof RuntimeException ex ? ex : new RuntimeException(throwable.getMessage(), throwable); RuntimeException potentiallyTranslatedException = translateExceptionIfPossible(runtimeException); 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 89f346e7c5..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-2023 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,12 +23,15 @@ 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; import java.io.IOException; import java.time.Duration; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -37,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; @@ -50,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; /** @@ -72,6 +69,7 @@ * @author Peter-Josef Meisch * @author Hamid Rahimi * @author Illia Ulianov + * @author Haibo Liu * @since 4.4 */ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate { @@ -79,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; @@ -90,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); @@ -102,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); @@ -176,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(), @@ -437,13 +442,10 @@ public List> multiSearch(List queries, Class< Assert.notNull(queries, "queries must not be null"); Assert.notNull(clazz, "clazz must not be null"); - List multiSearchQueryParameters = new ArrayList<>(queries.size()); - for (Query query : queries) { - multiSearchQueryParameters.add(new MultiSearchQueryParameter(query, clazz, getIndexCoordinatesFor(clazz))); - } - + int size = queries.size(); // noinspection unchecked - return doMultiSearch(multiSearchQueryParameters).stream().map(searchHits -> (SearchHits) searchHits) + return multiSearch(queries, Collections.nCopies(size, clazz), Collections.nCopies(size, index)) + .stream().map(searchHits -> (SearchHits) searchHits) .collect(Collectors.toList()); } @@ -454,14 +456,7 @@ public List> multiSearch(List queries, List multiSearchQueryParameters = new ArrayList<>(queries.size()); - Iterator> it = classes.iterator(); - for (Query query : queries) { - Class clazz = it.next(); - multiSearchQueryParameters.add(new MultiSearchQueryParameter(query, clazz, getIndexCoordinatesFor(clazz))); - } - - return doMultiSearch(multiSearchQueryParameters); + return multiSearch(queries, classes, classes.stream().map(this::getIndexCoordinatesFor).toList()); } @Override @@ -473,14 +468,7 @@ public List> multiSearch(List queries, List multiSearchQueryParameters = new ArrayList<>(queries.size()); - Iterator> it = classes.iterator(); - for (Query query : queries) { - Class clazz = it.next(); - multiSearchQueryParameters.add(new MultiSearchQueryParameter(query, clazz, index)); - } - - return doMultiSearch(multiSearchQueryParameters); + return multiSearch(queries, classes, Collections.nCopies(queries.size(), index)); } @Override @@ -497,16 +485,50 @@ public List> multiSearch(List queries, List> it = classes.iterator(); Iterator indexesIt = indexes.iterator(); + Assert.isTrue(!queries.isEmpty(), "queries should have at least 1 query"); + boolean isSearchTemplateQuery = queries.get(0) instanceof SearchTemplateQuery; + for (Query query : queries) { + Assert.isTrue((query instanceof SearchTemplateQuery) == isSearchTemplateQuery, + "SearchTemplateQuery can't be mixed with other types of query in multiple search"); + Class clazz = it.next(); IndexCoordinates index = indexesIt.next(); multiSearchQueryParameters.add(new MultiSearchQueryParameter(query, clazz, index)); } - return doMultiSearch(multiSearchQueryParameters); + return multiSearch(multiSearchQueryParameters, isSearchTemplateQuery); + } + + private List> multiSearch(List multiSearchQueryParameters, + boolean isSearchTemplateQuery) { + return isSearchTemplateQuery ? doMultiTemplateSearch(multiSearchQueryParameters.stream() + .map(p -> new MultiSearchTemplateQueryParameter((SearchTemplateQuery) p.query, p.clazz, p.index)) + .toList()) + : doMultiSearch(multiSearchQueryParameters); + } + + private List> doMultiTemplateSearch( + List mSearchTemplateQueryParameters) { + MsearchTemplateRequest request = requestConverter.searchMsearchTemplateRequest(mSearchTemplateQueryParameters, + routingResolver.getRouting()); + + MsearchTemplateResponse response = execute( + client -> client.msearchTemplate(request, EntityAsMap.class)); + List> responseItems = response.responses(); + + Assert.isTrue(mSearchTemplateQueryParameters.size() == responseItems.size(), + "number of response items does not match number of requests"); + + int size = mSearchTemplateQueryParameters.size(); + List> classes = mSearchTemplateQueryParameters + .stream().map(MultiSearchTemplateQueryParameter::clazz).collect(Collectors.toList()); + List indices = mSearchTemplateQueryParameters + .stream().map(MultiSearchTemplateQueryParameter::index).collect(Collectors.toList()); + + return getSearchHitsFromMsearchResponse(size, classes, indices, responseItems); } - @SuppressWarnings({ "unchecked", "rawtypes" }) private List> doMultiSearch(List multiSearchQueryParameters) { MsearchRequest request = requestConverter.searchMsearchRequest(multiSearchQueryParameters, @@ -518,22 +540,37 @@ private List> doMultiSearch(List multiS Assert.isTrue(multiSearchQueryParameters.size() == responseItems.size(), "number of response items does not match number of requests"); - List> searchHitsList = new ArrayList<>(multiSearchQueryParameters.size()); + int size = multiSearchQueryParameters.size(); + List> classes = multiSearchQueryParameters + .stream().map(MultiSearchQueryParameter::clazz).collect(Collectors.toList()); + List indices = multiSearchQueryParameters + .stream().map(MultiSearchQueryParameter::index).collect(Collectors.toList()); + + return getSearchHitsFromMsearchResponse(size, classes, indices, responseItems); + } - Iterator queryIterator = multiSearchQueryParameters.iterator(); + /** + * {@link MsearchResponse} and {@link MsearchTemplateResponse} share the same {@link MultiSearchResponseItem} + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + private List> getSearchHitsFromMsearchResponse(int size, List> classes, + List indices, List> responseItems) { + List> searchHitsList = new ArrayList<>(size); + Iterator> clazzIter = classes.iterator(); + Iterator indexIter = indices.iterator(); Iterator> responseIterator = responseItems.iterator(); - while (queryIterator.hasNext()) { - MultiSearchQueryParameter queryParameter = queryIterator.next(); + while (clazzIter.hasNext() && indexIter.hasNext()) { MultiSearchResponseItem responseItem = responseIterator.next(); if (responseItem.isResult()) { - Class clazz = queryParameter.clazz; + Class clazz = clazzIter.next(); + IndexCoordinates index = indexIter.next(); ReadDocumentCallback documentCallback = new ReadDocumentCallback<>(elasticsearchConverter, clazz, - queryParameter.index); + index); SearchDocumentResponseCallback> callback = new ReadSearchDocumentResponseCallback<>(clazz, - queryParameter.index); + index); SearchHits searchHits = callback.doWith( SearchDocumentResponseBuilder.from(responseItem.result(), getEntityCreator(documentCallback), jsonpMapper)); @@ -541,8 +578,8 @@ private List> doMultiSearch(List multiS searchHitsList.add(searchHits); } else { if (LOGGER.isWarnEnabled()) { - LOGGER - .warn(String.format("multisearch responsecontains failure: {}", responseItem.failure().error().reason())); + LOGGER.warn(String.format("multisearch response contains failure: %s", + responseItem.failure().error().reason())); } } } @@ -556,6 +593,12 @@ private List> doMultiSearch(List multiS record MultiSearchQueryParameter(Query query, Class clazz, IndexCoordinates index) { } + /** + * value class combining the information needed for a single query in a template multisearch request. + */ + record MultiSearchTemplateQueryParameter(SearchTemplateQuery query, Class clazz, IndexCoordinates index) { + } + @Override public String openPointInTime(IndexCoordinates index, Duration keepAlive, Boolean ignoreUnavailable) { @@ -606,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 c0b53585cf..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-2023 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 fa7ccc9ec0..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-2023 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; /** @@ -35,14 +35,18 @@ * {@link co.elastic.clients.elasticsearch.core.search.Highlight}. * * @author Peter-Josef Meisch + * @author Haibo Liu * @since 4.4 */ class HighlightQueryBuilder { private final MappingContext, ElasticsearchPersistentProperty> mappingContext; + private final RequestConverter requestConverter; HighlightQueryBuilder( - MappingContext, ElasticsearchPersistentProperty> mappingContext) { + MappingContext, ElasticsearchPersistentProperty> mappingContext, + RequestConverter requestConverter) { this.mappingContext = mappingContext; + this.requestConverter = requestConverter; } public co.elastic.clients.elasticsearch.core.search.Highlight getHighlight(Highlight highlight, @@ -52,7 +56,7 @@ public co.elastic.clients.elasticsearch.core.search.Highlight getHighlight(Highl // in the old implementation we could use one addParameters method, but in the new Elasticsearch client // the builder for highlight and highlightfield share no code - addParameters(highlight.getParameters(), highlightBuilder); + addParameters(highlight.getParameters(), highlightBuilder, type); for (HighlightField highlightField : highlight.getFields()) { String mappedName = mapFieldName(highlightField.getName(), type); @@ -69,7 +73,7 @@ public co.elastic.clients.elasticsearch.core.search.Highlight getHighlight(Highl * the builder for highlight and highlight fields don't share code, so we have these two methods here that basically are almost copies */ private void addParameters(HighlightParameters parameters, - co.elastic.clients.elasticsearch.core.search.Highlight.Builder builder) { + co.elastic.clients.elasticsearch.core.search.Highlight.Builder builder, @Nullable Class type) { if (StringUtils.hasLength(parameters.getBoundaryChars())) { builder.boundaryChars(parameters.getBoundaryChars()); @@ -103,6 +107,10 @@ private void addParameters(HighlightParameters parameters, builder.numberOfFragments(parameters.getNumberOfFragments()); } + if (parameters.getHighlightQuery() != null) { + builder.highlightQuery(requestConverter.getQuery(parameters.getHighlightQuery(), type)); + } + if (StringUtils.hasLength(parameters.getOrder())) { builder.order(highlighterOrder(parameters.getOrder())); } @@ -174,6 +182,10 @@ private void addParameters(HighlightFieldParameters parameters, builder.numberOfFragments(parameters.getNumberOfFragments()); } + if (parameters.getHighlightQuery() != null) { + builder.highlightQuery(requestConverter.getQuery(parameters.getHighlightQuery(), type)); + } + if (StringUtils.hasLength(parameters.getOrder())) { builder.order(highlighterOrder(parameters.getOrder())); } 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 47c6f29fa4..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-2023 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 5c9781b5f5..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-2023 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 93d3e2f442..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-2023 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 3a831e4bd4..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-2023 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 c7ea1e3728..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-2023 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/QueryBuilders.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/QueryBuilders.java deleted file mode 100644 index 27c83c4923..0000000000 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/QueryBuilders.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright 2022-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.elasticsearch.client.elc; - -import co.elastic.clients.elasticsearch._types.FieldValue; -import co.elastic.clients.elasticsearch._types.LatLonGeoLocation; -import co.elastic.clients.elasticsearch._types.query_dsl.IdsQuery; -import co.elastic.clients.elasticsearch._types.query_dsl.MatchAllQuery; -import co.elastic.clients.elasticsearch._types.query_dsl.MatchQuery; -import co.elastic.clients.elasticsearch._types.query_dsl.Operator; -import co.elastic.clients.elasticsearch._types.query_dsl.Query; -import co.elastic.clients.elasticsearch._types.query_dsl.QueryStringQuery; -import co.elastic.clients.elasticsearch._types.query_dsl.TermQuery; -import co.elastic.clients.elasticsearch._types.query_dsl.WildcardQuery; -import co.elastic.clients.elasticsearch._types.query_dsl.WrapperQuery; -import co.elastic.clients.util.ObjectBuilder; - -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.List; -import java.util.function.Function; - -import org.springframework.data.elasticsearch.core.geo.GeoPoint; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - -/** - * Utility class simplifying the creation of some more complex queries and type. - * - * @author Peter-Josef Meisch - * @since 4.4 - * @deprecated since 5.1, use {@link Queries} instead. - */ -@Deprecated(forRemoval = true) -public final class QueryBuilders { - - private QueryBuilders() {} - - public static IdsQuery idsQuery(List ids) { - - Assert.notNull(ids, "ids must not be null"); - - return IdsQuery.of(i -> i.values(ids)); - } - - public static Query idsQueryAsQuery(List ids) { - - Assert.notNull(ids, "ids must not be null"); - - Function> builder = b -> b.ids(idsQuery(ids)); - - return builder.apply(new Query.Builder()).build(); - } - - public static MatchQuery matchQuery(String fieldName, String query, @Nullable Operator operator, - @Nullable Float boost) { - - Assert.notNull(fieldName, "fieldName must not be null"); - Assert.notNull(query, "query must not be null"); - - return MatchQuery.of(mb -> mb.field(fieldName).query(FieldValue.of(query)).operator(operator).boost(boost)); - } - - public static Query matchQueryAsQuery(String fieldName, String query, @Nullable Operator operator, - @Nullable Float boost) { - - Function> builder = b -> b.match(matchQuery(fieldName, query, operator, boost)); - - return builder.apply(new Query.Builder()).build(); - } - - public static MatchAllQuery matchAllQuery() { - - return MatchAllQuery.of(b -> b); - } - - public static Query matchAllQueryAsQuery() { - - Function> builder = b -> b.matchAll(matchAllQuery()); - - return builder.apply(new Query.Builder()).build(); - } - - public static QueryStringQuery queryStringQuery(String fieldName, String query, @Nullable Float boost) { - return queryStringQuery(fieldName, query, null, null, boost); - } - - public static QueryStringQuery queryStringQuery(String fieldName, String query, Operator defaultOperator, - @Nullable Float boost) { - return queryStringQuery(fieldName, query, null, defaultOperator, boost); - } - - public static QueryStringQuery queryStringQuery(String fieldName, String query, @Nullable Boolean analyzeWildcard, - @Nullable Float boost) { - return queryStringQuery(fieldName, query, analyzeWildcard, null, boost); - } - - public static QueryStringQuery queryStringQuery(String fieldName, String query, @Nullable Boolean analyzeWildcard, - @Nullable Operator defaultOperator, @Nullable Float boost) { - - Assert.notNull(fieldName, "fieldName must not be null"); - Assert.notNull(query, "query must not be null"); - - return QueryStringQuery.of(qs -> qs.fields(fieldName).query(query).analyzeWildcard(analyzeWildcard) - .defaultOperator(defaultOperator).boost(boost)); - } - - public static TermQuery termQuery(String fieldName, String value) { - - Assert.notNull(fieldName, "fieldName must not be null"); - Assert.notNull(value, "value must not be null"); - - return TermQuery.of(t -> t.field(fieldName).value(FieldValue.of(value))); - } - - public static Query termQueryAsQuery(String fieldName, String value) { - - Function> builder = q -> q.term(termQuery(fieldName, value)); - return builder.apply(new Query.Builder()).build(); - } - - public static WildcardQuery wildcardQuery(String field, String value) { - - Assert.notNull(field, "field must not be null"); - Assert.notNull(value, "value must not be null"); - - return WildcardQuery.of(w -> w.field(field).wildcard(value)); - } - - public static Query wildcardQueryAsQuery(String field, String value) { - Function> builder = q -> q.wildcard(wildcardQuery(field, value)); - return builder.apply(new Query.Builder()).build(); - } - - public static Query wrapperQueryAsQuery(String query) { - - Function> builder = q -> q.wrapper(wrapperQuery(query)); - - return builder.apply(new Query.Builder()).build(); - } - - public static WrapperQuery wrapperQuery(String query) { - - Assert.notNull(query, "query must not be null"); - - String encodedValue = Base64.getEncoder().encodeToString(query.getBytes(StandardCharsets.UTF_8)); - - return WrapperQuery.of(wq -> wq.query(encodedValue)); - } - - public static LatLonGeoLocation latLon(GeoPoint geoPoint) { - - Assert.notNull(geoPoint, "geoPoint must not be null"); - - return latLon(geoPoint.getLat(), geoPoint.getLon()); - } - - public static LatLonGeoLocation latLon(double lat, double lon) { - return LatLonGeoLocation.of(_0 -> _0.lat(lat).lon(lon)); - } -} 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 3f25f7e19b..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-2023 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 697aa09099..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-2023 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 5f65f25493..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-2023 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 0e5f4b7d25..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-2023 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 c3ebf9909b..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-2023 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 c5b106a119..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-2023 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 06b177b51a..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-2023 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; @@ -79,6 +74,7 @@ * * @author Peter-Josef Meisch * @author Illia Ulianov + * @author Junghoon Ban * @since 4.4 */ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearchTemplate { @@ -86,6 +82,7 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch 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; @@ -97,6 +94,7 @@ public ReactiveElasticsearchTemplate(ReactiveElasticsearchClient client, Elastic 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); @@ -170,8 +168,7 @@ protected Mono doExists(String id, IndexCoordinates index) { } @Override - public Mono delete(Query query, Class entityType, IndexCoordinates index) { - + public Mono delete(DeleteQuery query, Class entityType, IndexCoordinates index) { Assert.notNull(query, "query must not be null"); DeleteByQueryRequest request = requestConverter.documentDeleteByQueryRequest(query, routingResolver.getRouting(), @@ -384,7 +381,28 @@ private Flux doFindUnbounded(Query query, Class clazz, IndexC Function>> resourceClosure = psa -> { baseQuery.setPointInTime(new Query.PointInTime(psa.getPit(), pitKeepAlive)); - baseQuery.addSort(Sort.by("_shard_doc")); + + // 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); @@ -614,13 +632,21 @@ public BaseQueryBuilder queryBuilderWithIds(List ids) { return NativeQuery.builder().withIds(ids); } + @Override + public Mono search(SqlQuery query) { + Assert.notNull(query, "Query must not be null."); + + co.elastic.clients.elasticsearch.sql.QueryRequest request = requestConverter.sqlQueryRequest(query); + return sqlClient.query(request).onErrorMap(this::translateException).map(responseConverter::sqlResponse); + } + /** * Callback interface to be used with {@link #execute(ReactiveElasticsearchTemplate.ClientCallback<>)} for operating * directly on {@link ReactiveElasticsearchClient}. * * @param */ - interface ClientCallback> { + public interface ClientCallback> { T doWithClient(ReactiveElasticsearchClient client); } @@ -645,7 +671,7 @@ public Publisher execute(ReactiveElasticsearchTemplate.ClientCallback createWithMapping() { private Mono doCreate(IndexCoordinates indexCoordinates, Map settings, @Nullable Document mapping) { - - 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); Mono createIndexResponse = Mono.from(execute(client -> client.create(createIndexRequest))); return createIndexResponse.map(CreateIndexResponse::acknowledged); } @@ -435,6 +444,15 @@ private IndexCoordinates getIndexCoordinatesFor(Class clazz) { return elasticsearchConverter.getMappingContext().getRequiredPersistentEntity(clazz).getIndexCoordinates(); } + /** + * Get the {@link Alias} of the provided class. + * + * @param clazz provided class that can be used to extract aliases. + */ + private Set getAliasesFor(Class clazz) { + return elasticsearchConverter.getMappingContext().getRequiredPersistentEntity(clazz).getAliases(); + } + private Class checkForBoundClass() { if (boundClass == null) { throw new InvalidDataAccessApiUsageException("IndexOperations are not bound"); diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java index bb9d0dcec3..e1a6c09275 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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. @@ -20,7 +20,6 @@ import co.elastic.clients.elasticsearch._types.Conflicts; import co.elastic.clients.elasticsearch._types.ExpandWildcard; -import co.elastic.clients.elasticsearch._types.InlineScript; import co.elastic.clients.elasticsearch._types.NestedSortValue; import co.elastic.clients.elasticsearch._types.OpType; import co.elastic.clients.elasticsearch._types.SortOptions; @@ -43,17 +42,21 @@ import co.elastic.clients.elasticsearch.core.bulk.IndexOperation; import co.elastic.clients.elasticsearch.core.bulk.UpdateOperation; import co.elastic.clients.elasticsearch.core.mget.MultiGetOperation; -import co.elastic.clients.elasticsearch.core.msearch.MultisearchBody; +import co.elastic.clients.elasticsearch.core.msearch.MultisearchHeader; import co.elastic.clients.elasticsearch.core.search.Highlight; import co.elastic.clients.elasticsearch.core.search.Rescore; +import co.elastic.clients.elasticsearch.core.search.SearchRequestBody; import co.elastic.clients.elasticsearch.core.search.SourceConfig; import co.elastic.clients.elasticsearch.indices.*; import co.elastic.clients.elasticsearch.indices.ExistsIndexTemplateRequest; import co.elastic.clients.elasticsearch.indices.ExistsRequest; import co.elastic.clients.elasticsearch.indices.update_aliases.Action; +import co.elastic.clients.elasticsearch.sql.query.SqlFormat; import co.elastic.clients.json.JsonData; import co.elastic.clients.json.JsonpDeserializer; import co.elastic.clients.json.JsonpMapper; +import co.elastic.clients.util.NamedValue; +import co.elastic.clients.util.ObjectBuilder; import jakarta.json.stream.JsonParser; import java.io.ByteArrayInputStream; @@ -66,12 +69,16 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Sort; import org.springframework.data.elasticsearch.core.RefreshPolicy; @@ -85,6 +92,8 @@ 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.ElasticsearchPersistentProperty; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; @@ -93,7 +102,6 @@ import org.springframework.data.elasticsearch.core.reindex.Remote; import org.springframework.data.elasticsearch.core.script.Script; import org.springframework.data.elasticsearch.support.DefaultStringObjectMap; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -105,10 +113,10 @@ * @author Sascha Woo * @author cdalxndr * @author scoobyzhang + * @author Haibo Liu * @since 4.4 */ -@SuppressWarnings("ClassCanBeRecord") -class RequestConverter { +class RequestConverter extends AbstractQueryProcessor { private static final Log LOGGER = LogFactory.getLog(RequestConverter.class); @@ -153,7 +161,6 @@ public co.elastic.clients.elasticsearch.cluster.PutComponentTemplateRequest clus aliasActions.getActions().forEach(aliasAction -> { if (aliasAction instanceof AliasAction.Add add) { var parameters = add.getParameters(); - // noinspection DuplicatedCode String[] parametersAliases = parameters.getAliases(); if (parametersAliases != null) { for (String aliasName : parametersAliases) { @@ -167,9 +174,9 @@ public co.elastic.clients.elasticsearch.cluster.PutComponentTemplateRequest clus })); } - private Alias.Builder buildAlias(AliasActionParameters parameters, Alias.Builder aliasBuilder) { + private co.elastic.clients.elasticsearch.indices.Alias.Builder buildAlias(AliasActionParameters parameters, + co.elastic.clients.elasticsearch.indices.Alias.Builder aliasBuilder) { - // noinspection DuplicatedCode if (parameters.getRouting() != null) { aliasBuilder.routing(parameters.getRouting()); } @@ -232,17 +239,25 @@ public ExistsRequest indicesExistsRequest(IndexCoordinates indexCoordinates) { return new ExistsRequest.Builder().index(Arrays.asList(indexCoordinates.getIndexNames())).build(); } - public CreateIndexRequest indicesCreateRequest(IndexCoordinates indexCoordinates, Map settings, - @Nullable Document mapping) { - - Assert.notNull(indexCoordinates, "indexCoordinates must not be null"); - Assert.notNull(settings, "settings must not be null"); + public CreateIndexRequest indicesCreateRequest(CreateIndexSettings indexSettings) { + Map aliases = new HashMap<>(); + for (Alias alias : indexSettings.getAliases()) { + co.elastic.clients.elasticsearch.indices.Alias esAlias = co.elastic.clients.elasticsearch.indices.Alias + .of(ab -> ab.filter(getQuery(alias.getFilter(), null)) + .routing(alias.getRouting()) + .indexRouting(alias.getIndexRouting()) + .searchRouting(alias.getSearchRouting()) + .isHidden(alias.getHidden()) + .isWriteIndex(alias.getWriteIndex())); + aliases.put(alias.getAlias(), esAlias); + } // note: the new client does not support the index.storeType anymore return new CreateIndexRequest.Builder() // - .index(indexCoordinates.getIndexName()) // - .settings(indexSettings(settings)) // - .mappings(typeMapping(mapping)) // + .index(indexSettings.getIndexCoordinates().getIndexName()) // + .aliases(aliases) + .settings(indexSettings(indexSettings.getSettings())) // + .mappings(typeMapping(indexSettings.getMapping())) // .build(); } @@ -396,11 +411,8 @@ public co.elastic.clients.elasticsearch.indices.PutTemplateRequest indicesPutTem .order(putTemplateRequest.getOrder()); if (putTemplateRequest.getSettings() != null) { - Function, String> keyMapper = Map.Entry::getKey; - Function, JsonData> valueMapper = entry -> JsonData.of(entry.getValue(), jsonpMapper); - Map settings = putTemplateRequest.getSettings().entrySet().stream() - .collect(Collectors.toMap(keyMapper, valueMapper)); - builder.settings(settings); + Map settings = getTemplateParams(putTemplateRequest.getSettings().entrySet()); + builder.settings(sb -> sb.otherSettings(settings)); } if (putTemplateRequest.getMappings() != null) { @@ -415,7 +427,6 @@ public co.elastic.clients.elasticsearch.indices.PutTemplateRequest indicesPutTem if (aliasActions != null) { aliasActions.getActions().forEach(aliasAction -> { AliasActionParameters parameters = aliasAction.getParameters(); - // noinspection DuplicatedCode String[] parametersAliases = parameters.getAliases(); if (parametersAliases != null) { @@ -449,7 +460,6 @@ public co.elastic.clients.elasticsearch.indices.PutIndexTemplateRequest indicesP aliasActions.getActions().forEach(aliasAction -> { if (aliasAction instanceof AliasAction.Add add) { var parameters = add.getParameters(); - // noinspection DuplicatedCode String[] parametersAliases = parameters.getAliases(); if (parametersAliases != null) { for (String aliasName : parametersAliases) { @@ -523,6 +533,27 @@ public co.elastic.clients.elasticsearch.indices.GetTemplateRequest indicesGetTem .of(gtr -> gtr.name(getTemplateRequest.getTemplateName()).flatSettings(true)); } + public co.elastic.clients.elasticsearch.sql.QueryRequest sqlQueryRequest(SqlQuery query) { + Assert.notNull(query, "Query must not be null."); + + return co.elastic.clients.elasticsearch.sql.QueryRequest.of(sqb -> sqb + .query(query.getQuery()) + .catalog(query.getCatalog()) + .columnar(query.getColumnar()) + .cursor(query.getCursor()) + .fetchSize(query.getFetchSize()) + .fieldMultiValueLeniency(query.getFieldMultiValueLeniency()) + .indexUsingFrozen(query.getIndexIncludeFrozen()) + .keepAlive(time(query.getKeepAlive())) + .keepOnCompletion(query.getKeepOnCompletion()) + .pageTimeout(time(query.getPageTimeout())) + .requestTimeout(time(query.getRequestTimeout())) + .waitForCompletionTimeout(time(query.getWaitForCompletionTimeout())) + .filter(getQuery(query.getFilter(), null)) + .timeZone(Objects.toString(query.getTimeZone(), null)) + .format(SqlFormat.Json)); + } + // endregion // region documents @@ -545,13 +576,12 @@ public IndexRequest documentIndexRequest(IndexQuery query, IndexCoordinates i Object queryObject = query.getObject(); if (queryObject != null) { - String id = StringUtils.hasText(query.getId()) ? query.getId() : getPersistentEntityId(queryObject); - builder // - .id(id) // + builder + .id(StringUtils.hasText(query.getId()) ? query.getId() : getPersistentEntityId(queryObject)) .document(elasticsearchConverter.mapObject(queryObject)); } else if (query.getSource() != null) { - builder // - .id(query.getId()) // + builder + .id(query.getId()) .document(new DefaultStringObjectMap<>().fromJson(query.getSource())); } else { throw new InvalidDataAccessApiUsageException( @@ -597,12 +627,13 @@ private IndexOperation bulkIndexOperation(IndexQuery query, IndexCoordinates Object queryObject = query.getObject(); if (queryObject != null) { - String id = StringUtils.hasText(query.getId()) ? query.getId() : getPersistentEntityId(queryObject); - builder // - .id(id) // + builder + .id(StringUtils.hasText(query.getId()) ? query.getId() : getPersistentEntityId(queryObject)) .document(elasticsearchConverter.mapObject(queryObject)); } else if (query.getSource() != null) { - builder.document(new DefaultStringObjectMap<>().fromJson(query.getSource())); + builder + .id(query.getId()) + .document(new DefaultStringObjectMap<>().fromJson(query.getSource())); } else { throw new InvalidDataAccessApiUsageException( "object or source is null, failed to index the document [id: " + query.getId() + ']'); @@ -638,12 +669,13 @@ private CreateOperation bulkCreateOperation(IndexQuery query, IndexCoordinate Object queryObject = query.getObject(); if (queryObject != null) { - String id = StringUtils.hasText(query.getId()) ? query.getId() : getPersistentEntityId(queryObject); - builder // - .id(id) // + builder + .id(StringUtils.hasText(query.getId()) ? query.getId() : getPersistentEntityId(queryObject)) .document(elasticsearchConverter.mapObject(queryObject)); } else if (query.getSource() != null) { - builder.document(new DefaultStringObjectMap<>().fromJson(query.getSource())); + builder + .id(query.getId()) + .document(new DefaultStringObjectMap<>().fromJson(query.getSource())); } else { throw new InvalidDataAccessApiUsageException( "object or source is null, failed to index the document [id: " + query.getId() + ']'); @@ -706,8 +738,7 @@ private CreateOperation bulkCreateOperation(IndexQuery query, IndexCoordinate return uob.build(); } - @Nullable - private co.elastic.clients.elasticsearch._types.Script getScript(@Nullable ScriptData scriptData) { + private co.elastic.clients.elasticsearch._types.@Nullable Script getScript(@Nullable ScriptData scriptData) { if (scriptData == null) { return null; @@ -719,16 +750,11 @@ private co.elastic.clients.elasticsearch._types.Script getScript(@Nullable Scrip scriptData.params().forEach((key, value) -> params.put(key, JsonData.of(value, jsonpMapper))); } return co.elastic.clients.elasticsearch._types.Script.of(sb -> { - if (scriptData.type() == ScriptType.INLINE) { - sb.inline(is -> is // - .lang(scriptData.language()) // - .source(scriptData.script()) // - .params(params)); // - } else if (scriptData.type() == ScriptType.STORED) { - sb.stored(ss -> ss // - .id(scriptData.script()) // - .params(params) // - ); + sb.lang(scriptData.language()) + .params(params) + .id(scriptData.scriptName()); + if (scriptData.script() != null){ + sb.source(s -> s.scriptString(scriptData.script())); } return sb; }); @@ -900,7 +926,13 @@ public co.elastic.clients.elasticsearch.core.ReindexRequest reindex(ReindexReque ReindexRequest.Script script = reindexRequest.getScript(); if (script != null) { - builder.script(s -> s.inline(InlineScript.of(i -> i.lang(script.getLang()).source(script.getSource())))); + builder.script(sb -> { + if (script.getSource() != null){ + sb.source(s -> s.scriptString(script.getSource())); + } + sb.lang(script.getLang()); + return sb; + }); } builder.timeout(time(reindexRequest.getTimeout())) // @@ -966,6 +998,81 @@ public DeleteByQueryRequest documentDeleteByQueryRequest(Query query, @Nullable }); } + public DeleteByQueryRequest documentDeleteByQueryRequest(DeleteQuery query, @Nullable String routing, Class clazz, + IndexCoordinates index, @Nullable RefreshPolicy refreshPolicy) { + Assert.notNull(query, "query must not be null"); + Assert.notNull(index, "index must not be null"); + + return DeleteByQueryRequest.of(dqb -> { + dqb.index(Arrays.asList(index.getIndexNames())) // + .query(getQuery(query.getQuery(), clazz))// + .refresh(deleteByQueryRefresh(refreshPolicy)) + .requestsPerSecond(query.getRequestsPerSecond()) + .maxDocs(query.getMaxDocs()) + .scroll(time(query.getScroll())) + .scrollSize(query.getScrollSize()); + + if (query.getRouting() != null) { + dqb.routing(query.getRouting()); + } else if (StringUtils.hasText(routing)) { + dqb.routing(routing); + } + + if (query.getQ() != null) { + dqb.q(query.getQ()) + .analyzer(query.getAnalyzer()) + .analyzeWildcard(query.getAnalyzeWildcard()) + .defaultOperator(operator(query.getDefaultOperator())) + .df(query.getDf()) + .lenient(query.getLenient()); + } + + if (query.getExpandWildcards() != null && !query.getExpandWildcards().isEmpty()) { + dqb.expandWildcards(expandWildcards(query.getExpandWildcards())); + } + if (query.getStats() != null && !query.getStats().isEmpty()) { + dqb.stats(query.getStats()); + } + if (query.getSlices() != null) { + dqb.slices(sb -> sb.value(query.getSlices())); + } + if (query.getSort() != null) { + ElasticsearchPersistentEntity persistentEntity = getPersistentEntity(clazz); + List sortOptions = getSortOptions(query.getSort(), persistentEntity); + + if (!sortOptions.isEmpty()) { + dqb.sort( + sortOptions.stream() + .map(sortOption -> { + String order = "asc"; + var sortField = sortOption.field(); + if (sortField.order() != null) { + order = sortField.order().jsonValue(); + } + + return sortField.field() + ':' + order; + }) + .collect(Collectors.toList())); + } + } + if (query.getRefresh() != null) { + dqb.refresh(query.getRefresh()); + } + dqb.allowNoIndices(query.getAllowNoIndices()) + .conflicts(conflicts(query.getConflicts())) + .ignoreUnavailable(query.getIgnoreUnavailable()) + .preference(query.getPreference()) + .requestCache(query.getRequestCache()) + .searchType(searchType(query.getSearchType())) + .searchTimeout(time(query.getSearchTimeout())) + .terminateAfter(query.getTerminateAfter()) + .timeout(time(query.getTimeout())) + .version(query.getVersion()); + + return dqb; + }); + } + public UpdateRequest documentUpdateRequest(UpdateQuery query, IndexCoordinates index, @Nullable RefreshPolicy refreshPolicy, @Nullable String routing) { @@ -981,21 +1088,14 @@ public DeleteByQueryRequest documentDeleteByQueryRequest(Query query, @Nullable } uqb.script(sb -> { - if (query.getScriptType() == ScriptType.INLINE) { - sb.inline(is -> is // - .lang(query.getLang()) // - .source(query.getScript()) // - .params(params)); // - } else if (query.getScriptType() == ScriptType.STORED) { - sb.stored(ss -> ss // - .id(query.getScript()) // - .params(params) // - ); + sb.lang(query.getLang()).params(params); + if (query.getScript() != null){ + sb.source(s -> s.scriptString(query.getScript())); } - return sb; - } + sb.id(query.getId()); - ); + return sb; + }); } uqb // @@ -1140,11 +1240,40 @@ public SearchRequest searchRequest(Query query, @Nullable String routing, @N builder.routing(routing); } - addFilter(query, builder); + addPostFilter(query, builder); return builder.build(); } + public MsearchTemplateRequest searchMsearchTemplateRequest( + List multiSearchTemplateQueryParameters, + @Nullable String routing) { + + // basically the same stuff as in template search + return MsearchTemplateRequest.of(mtrb -> { + multiSearchTemplateQueryParameters.forEach(param -> { + var query = param.query(); + mtrb.searchTemplates(stb -> stb + .header(msearchHeaderBuilder(query, param.index(), routing)) + .body(bb -> { + bb.explain(query.getExplain()) // + .id(query.getId()); // + if (query.getSource() != null){ + bb.source(s -> s.scriptString(query.getSource())); + } + + if (!CollectionUtils.isEmpty(query.getParams())) { + Map params = getTemplateParams(query.getParams().entrySet()); + bb.params(params); + } + + return bb; + })); + }); + return mtrb; + }); + } + public MsearchRequest searchMsearchRequest( List multiSearchQueryParameters, @Nullable String routing) { @@ -1156,28 +1285,7 @@ public MsearchRequest searchMsearchRequest( var query = param.query(); mrb.searches(sb -> sb // - .header(h -> { - var searchType = (query instanceof NativeQuery nativeQuery && nativeQuery.getKnnQuery() != null) ? null - : searchType(query.getSearchType()); - - h // - .index(Arrays.asList(param.index().getIndexNames())) // - .searchType(searchType) // - .requestCache(query.getRequestCache()) // - ; - - if (StringUtils.hasText(query.getRoute())) { - h.routing(query.getRoute()); - } else if (StringUtils.hasText(routing)) { - h.routing(routing); - } - - if (query.getPreference() != null) { - h.preference(query.getPreference()); - } - - return h; - }) // + .header(msearchHeaderBuilder(query, param.index(), routing)) // .body(bb -> { bb // .query(getQuery(query, param.clazz()))// @@ -1188,11 +1296,15 @@ public MsearchRequest searchMsearchRequest( .timeout(timeStringMs(query.getTimeout())) // ; - if (query.getPageable().isPaged()) { - bb // - .from((int) query.getPageable().getOffset()) // - .size(query.getPageable().getPageSize()); - } + var offset = query.getPageable().isPaged() ? query.getPageable().getOffset() : 0; + var pageSize = query.getPageable().isPaged() ? query.getPageable().getPageSize() + : INDEX_MAX_RESULT_WINDOW; + // if we have both a page size and a max results, we take the min, this is necessary for + // searchForStream to work correctly (#3098) as there the page size defines what is + // returned in a single request, and the max result determines the total number of + // documents returned + var size = query.isLimiting() ? Math.min(pageSize, query.getMaxResults()) : pageSize; + bb.from((int) offset).size(size); if (!isEmpty(query.getFields())) { bb.fields(fb -> { @@ -1205,10 +1317,6 @@ public MsearchRequest searchMsearchRequest( bb.storedFields(query.getStoredFields()); } - if (query.isLimiting()) { - bb.size(query.getMaxResults()); - } - if (query.getMinScore() > 0) { bb.minScore((double) query.getMinScore()); } @@ -1242,17 +1350,18 @@ public MsearchRequest searchMsearchRequest( String script = runtimeField.getScript(); if (script != null) { - rfb - .script(s -> s - .inline(is -> { - is.source(script); - - if (runtimeField.getParams() != null) { - is.params(TypeUtils.paramsMap(runtimeField.getParams())); - } - return is; - })); + rfb.script(s -> { + if (script != null) { + s.source(so -> so.scriptString(script)); + } + + if (runtimeField.getParams() != null) { + s.params(TypeUtils.paramsMap(runtimeField.getParams())); + } + return s; + }); } + return rfb; }); runtimeMappings.put(runtimeField.getName(), esRuntimeField); @@ -1261,9 +1370,14 @@ public MsearchRequest searchMsearchRequest( } if (!isEmpty(query.getIndicesBoost())) { - bb.indicesBoost(query.getIndicesBoost().stream() - .map(indexBoost -> Map.of(indexBoost.getIndexName(), (double) indexBoost.getBoost())) - .collect(Collectors.toList())); + Stream> namedValueStream = query.getIndicesBoost().stream() + .map(indexBoost -> { + var namedValue = new NamedValue(indexBoost.getIndexName(), + Float.valueOf(indexBoost.getBoost()).doubleValue()); + return namedValue; + }); + List> namedValueList = namedValueStream.collect(Collectors.toList()); + bb.indicesBoost(namedValueList); } query.getScriptedFields().forEach(scriptedField -> bb.scriptFields(scriptedField.getFieldName(), @@ -1283,6 +1397,35 @@ public MsearchRequest searchMsearchRequest( }); } + /** + * {@link MsearchRequest} and {@link MsearchTemplateRequest} share the same {@link MultisearchHeader} + */ + private Function> msearchHeaderBuilder(Query query, + IndexCoordinates index, @Nullable String routing) { + return h -> { + var searchType = (query instanceof NativeQuery nativeQuery && !isEmpty(nativeQuery.getKnnSearches())) ? null + : searchType(query.getSearchType()); + + h // + .index(Arrays.asList(index.getIndexNames())) // + .searchType(searchType) // + .requestCache(query.getRequestCache()) // + ; + + if (StringUtils.hasText(query.getRoute())) { + h.routing(query.getRoute()); + } else if (StringUtils.hasText(routing)) { + h.routing(routing); + } + + if (query.getPreference() != null) { + h.preference(query.getPreference()); + } + + return h; + }; + } + private void prepareSearchRequest(Query query, @Nullable String routing, @Nullable Class clazz, IndexCoordinates indexCoordinates, SearchRequest.Builder builder, boolean forCount, boolean forBatchedSearch) { @@ -1292,7 +1435,7 @@ private void prepareSearchRequest(Query query, @Nullable String routing, @Nu ElasticsearchPersistentEntity persistentEntity = getPersistentEntity(clazz); - var searchType = (query instanceof NativeQuery nativeQuery && nativeQuery.getKnnQuery() != null) ? null + var searchType = (query instanceof NativeQuery nativeQuery && !isEmpty(nativeQuery.getKnnSearches())) ? null : searchType(query.getSearchType()); builder // @@ -1333,13 +1476,14 @@ private void prepareSearchRequest(Query query, @Nullable String routing, @Nu builder.seqNoPrimaryTerm(true); } - if (query.getPageable().isPaged()) { - builder // - .from((int) query.getPageable().getOffset()) // - .size(query.getPageable().getPageSize()); - } else { - builder.from(0).size(INDEX_MAX_RESULT_WINDOW); - } + var offset = query.getPageable().isPaged() ? query.getPageable().getOffset() : 0; + var pageSize = query.getPageable().isPaged() ? query.getPageable().getPageSize() : INDEX_MAX_RESULT_WINDOW; + // if we have both a page size and a max results, we take the min, this is necessary for + // searchForStream to work correctly (#3098) as there the page size defines what is + // returned in a single request, and the max result determines the total number of + // documents returned + var size = query.isLimiting() ? Math.min(pageSize, query.getMaxResults()) : pageSize; + builder.from((int) offset).size(size); if (!isEmpty(query.getFields())) { var fieldAndFormats = query.getFields().stream().map(field -> FieldAndFormat.of(b -> b.field(field))).toList(); @@ -1354,10 +1498,6 @@ private void prepareSearchRequest(Query query, @Nullable String routing, @Nu addIndicesOptions(builder, query.getIndicesOptions()); } - if (query.isLimiting()) { - builder.size(query.getMaxResults()); - } - if (query.getMinScore() > 0) { builder.minScore((double) query.getMinScore()); } @@ -1370,8 +1510,8 @@ private void prepareSearchRequest(Query query, @Nullable String routing, @Nu if (query instanceof NativeQuery nativeQuery) { prepareNativeSearch(nativeQuery, builder); } - // query.getSort() must be checked after prepareNativeSearch as this already might hav a sort set that must have - // higher priority + // query.getSort() must be checked after prepareNativeSearch as this already might have a sort set + // that must have higher priority if (query.getSort() != null) { List sortOptions = getSortOptions(query.getSort(), persistentEntity); @@ -1393,7 +1533,15 @@ private void prepareSearchRequest(Query query, @Nullable String routing, @Nu } if (!isEmpty(query.getSearchAfter())) { - builder.searchAfter(query.getSearchAfter().stream().map(TypeUtils::toFieldValue).toList()); + var fieldValues = query.getSearchAfter().stream().map(TypeUtils::toFieldValue).toList(); + + // when there is a field collapse on a native query, and we have a search_after, then the search_after + // must only have one entry + if (query instanceof NativeQuery nativeQuery && nativeQuery.getFieldCollapse() != null) { + builder.searchAfter(fieldValues.get(0)); + } else { + builder.searchAfter(fieldValues); + } } query.getRescorerQueries().forEach(rescorerQuery -> builder.rescore(getRescore(rescorerQuery))); @@ -1406,16 +1554,16 @@ private void prepareSearchRequest(Query query, @Nullable String routing, @Nu rfb.type(RuntimeFieldType._DESERIALIZER.parse(runtimeField.getType())); String script = runtimeField.getScript(); if (script != null) { - rfb - .script(s -> s - .inline(is -> { - is.source(script); + rfb.script(s -> { + if (script != null) { + s.source(so -> so.scriptString(script)); + } - if (runtimeField.getParams() != null) { - is.params(TypeUtils.paramsMap(runtimeField.getParams())); - } - return is; - })); + if (runtimeField.getParams() != null) { + s.params(TypeUtils.paramsMap(runtimeField.getParams())); + } + return s; + }); } return rfb; @@ -1437,9 +1585,14 @@ private void prepareSearchRequest(Query query, @Nullable String routing, @Nu } if (!isEmpty(query.getIndicesBoost())) { - builder.indicesBoost(query.getIndicesBoost().stream() - .map(indexBoost -> Map.of(indexBoost.getIndexName(), (double) indexBoost.getBoost())) - .collect(Collectors.toList())); + Stream> namedValueStream = query.getIndicesBoost().stream() + .map(indexBoost -> { + var namedValue = new NamedValue(indexBoost.getIndexName(), + Float.valueOf(indexBoost.getBoost()).doubleValue()); + return namedValue; + }); + List> namedValueList = namedValueStream.collect(Collectors.toList()); + builder.indicesBoost(namedValueList); } if (!isEmpty(query.getDocValueFields())) { @@ -1494,17 +1647,17 @@ private Rescore getRescore(RescorerQuery rescorerQuery) { private void addHighlight(Query query, SearchRequest.Builder builder) { Highlight highlight = query.getHighlightQuery() - .map(highlightQuery -> new HighlightQueryBuilder(elasticsearchConverter.getMappingContext()) + .map(highlightQuery -> new HighlightQueryBuilder(elasticsearchConverter.getMappingContext(), this) .getHighlight(highlightQuery.getHighlight(), highlightQuery.getType())) .orElse(null); builder.highlight(highlight); } - private void addHighlight(Query query, MultisearchBody.Builder builder) { + private void addHighlight(Query query, SearchRequestBody.Builder builder) { Highlight highlight = query.getHighlightQuery() - .map(highlightQuery -> new HighlightQueryBuilder(elasticsearchConverter.getMappingContext()) + .map(highlightQuery -> new HighlightQueryBuilder(elasticsearchConverter.getMappingContext(), this) .getHighlight(highlightQuery.getHighlight(), highlightQuery.getType())) .orElse(null); @@ -1581,7 +1734,7 @@ private SortOptions getSortOptions(Sort.Order order, @Nullable ElasticsearchPers } @Nullable - private NestedSortValue getNestedSort(@Nullable Order.Nested nested, + private NestedSortValue getNestedSort(Order.@Nullable Nested nested, @Nullable ElasticsearchPersistentEntity persistentEntity) { return (nested == null || persistentEntity == null) ? null : NestedSortValue.of(b -> b // @@ -1611,8 +1764,8 @@ private void prepareNativeSearch(NativeQuery query, SearchRequest.Builder builde .sort(query.getSortOptions()) // ; - if (query.getKnnQuery() != null) { - builder.knn(query.getKnnQuery()); + if (!isEmpty(query.getKnnSearches())) { + builder.knn(query.getKnnSearches()); } if (!isEmpty(query.getAggregations())) { @@ -1625,15 +1778,15 @@ private void prepareNativeSearch(NativeQuery query, SearchRequest.Builder builde } @SuppressWarnings("DuplicatedCode") - private void prepareNativeSearch(NativeQuery query, MultisearchBody.Builder builder) { + private void prepareNativeSearch(NativeQuery query, SearchRequestBody.Builder builder) { builder // .suggest(query.getSuggester()) // .collapse(query.getFieldCollapse()) // .sort(query.getSortOptions()); - if (query.getKnnQuery() != null) { - builder.knn(query.getKnnQuery()); + if (!isEmpty(query.getKnnSearches())) { + builder.knn(query.getKnnSearches()); } if (!isEmpty(query.getAggregations())) { @@ -1645,47 +1798,22 @@ private void prepareNativeSearch(NativeQuery query, MultisearchBody.Builder buil } } - @Nullable - private co.elastic.clients.elasticsearch._types.query_dsl.Query getQuery(@Nullable Query query, + co.elastic.clients.elasticsearch._types.query_dsl.@Nullable Query getQuery(@Nullable Query query, @Nullable Class clazz) { + return getEsQuery(query, (q) -> elasticsearchConverter.updateQuery(q, clazz)); + } - if (query == null) { - return null; - } - - elasticsearchConverter.updateQuery(query, clazz); - - co.elastic.clients.elasticsearch._types.query_dsl.Query esQuery = null; + private void addPostFilter(Query query, SearchRequest.Builder builder) { - if (query instanceof CriteriaQuery) { - esQuery = CriteriaQueryProcessor.createQuery(((CriteriaQuery) query).getCriteria()); - } else if (query instanceof StringQuery) { - esQuery = Queries.wrapperQueryAsQuery(((StringQuery) query).getSource()); - } else if (query instanceof NativeQuery nativeQuery) { + // we only need to handle NativeQuery here. filter from a CriteriaQuery are added into the query and not as post + // filter anymore, StringQuery do not have post filters + if (query instanceof NativeQuery nativeQuery) { - if (nativeQuery.getQuery() != null) { - esQuery = nativeQuery.getQuery(); + if (nativeQuery.getFilter() != null) { + builder.postFilter(nativeQuery.getFilter()); } else if (nativeQuery.getSpringDataQuery() != null) { - esQuery = getQuery(nativeQuery.getSpringDataQuery(), clazz); + addPostFilter(nativeQuery.getSpringDataQuery(), builder); } - } else { - throw new IllegalArgumentException("unhandled Query implementation " + query.getClass().getName()); - } - - return esQuery; - } - - private void addFilter(Query query, SearchRequest.Builder builder) { - - if (query instanceof CriteriaQuery) { - CriteriaFilterProcessor.createQuery(((CriteriaQuery) query).getCriteria()).ifPresent(builder::postFilter); - } else // noinspection StatementWithEmptyBody - if (query instanceof StringQuery) { - // no filter for StringQuery - } else if (query instanceof NativeQuery) { - builder.postFilter(((NativeQuery) query).getFilter()); - } else { - throw new IllegalArgumentException("unhandled Query implementation " + query.getClass().getName()); } } @@ -1769,9 +1897,11 @@ public SearchTemplateRequest searchTemplate(SearchTemplateQuery query, @Nullable .id(query.getId()) // .index(Arrays.asList(index.getIndexNames())) // .preference(query.getPreference()) // - .searchType(searchType(query.getSearchType())).source(query.getSource()) // - ; + .searchType(searchType(query.getSearchType())); // + if (query.getSource() != null) { + builder.source(so -> so.scriptString(query.getSource())); + } if (query.getRoute() != null) { builder.routing(query.getRoute()); } else if (StringUtils.hasText(routing)) { @@ -1788,10 +1918,7 @@ public SearchTemplateRequest searchTemplate(SearchTemplateQuery query, @Nullable } if (!CollectionUtils.isEmpty(query.getParams())) { - Function, String> keyMapper = Map.Entry::getKey; - Function, JsonData> valueMapper = entry -> JsonData.of(entry.getValue(), jsonpMapper); - Map params = query.getParams().entrySet().stream() - .collect(Collectors.toMap(keyMapper, valueMapper)); + Map params = getTemplateParams(query.getParams().entrySet()); builder.params(params); } @@ -1799,6 +1926,14 @@ public SearchTemplateRequest searchTemplate(SearchTemplateQuery query, @Nullable }); } + @NotNull + private Map getTemplateParams(Set> query) { + Function, String> keyMapper = Map.Entry::getKey; + Function, JsonData> valueMapper = entry -> JsonData.of(entry.getValue(), jsonpMapper); + return query.stream() + .collect(Collectors.toMap(keyMapper, valueMapper)); + } + // endregion public PutScriptRequest scriptPut(Script script) { @@ -1808,8 +1943,8 @@ public PutScriptRequest scriptPut(Script script) { return PutScriptRequest.of(b -> b // .id(script.id()) // .script(sb -> sb // - .lang(script.language()) // - .source(script.source()))); + .lang(script.language()) // + .source(s -> s.scriptString(script.source())))); } public GetScriptRequest scriptGet(String name) { @@ -1899,9 +2034,12 @@ private VersionType retrieveVersionTypeFromPersistentEntity(@Nullable Class c private SourceConfig getSourceConfig(Query query) { if (query.getSourceFilter() != null) { - return SourceConfig.of(s -> s // - .filter(sfb -> { - SourceFilter sourceFilter = query.getSourceFilter(); + return SourceConfig.of(s -> { + SourceFilter sourceFilter = query.getSourceFilter(); + if (sourceFilter.fetchSource() != null) { + s.fetch(sourceFilter.fetchSource()); + } else { + s.filter(sfb -> { String[] includes = sourceFilter.getIncludes(); String[] excludes = sourceFilter.getExcludes(); @@ -1914,7 +2052,10 @@ private SourceConfig getSourceConfig(Query query) { } return sfb; - })); + }); + } + return s; + }); } else { return null; } diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ResponseConverter.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ResponseConverter.java index 9083fc8ae2..771b1104a9 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ResponseConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ResponseConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,9 +15,8 @@ */ package org.springframework.data.elasticsearch.client.elc; -import static org.springframework.data.elasticsearch.client.elc.JsonUtils.toJson; -import static org.springframework.data.elasticsearch.client.elc.TypeUtils.removePrefixFromJson; -import static org.springframework.data.elasticsearch.client.elc.TypeUtils.typeMapping; +import static org.springframework.data.elasticsearch.client.elc.JsonUtils.*; +import static org.springframework.data.elasticsearch.client.elc.TypeUtils.*; import co.elastic.clients.elasticsearch._types.BulkIndexByScrollFailure; import co.elastic.clients.elasticsearch._types.ErrorCause; @@ -34,27 +33,39 @@ import co.elastic.clients.elasticsearch.indices.*; import co.elastic.clients.elasticsearch.indices.get_index_template.IndexTemplateItem; import co.elastic.clients.elasticsearch.indices.get_mapping.IndexMappingRecord; +import co.elastic.clients.elasticsearch.sql.QueryResponse; +import co.elastic.clients.json.JsonData; import co.elastic.clients.json.JsonpMapper; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.ElasticsearchErrorCause; import org.springframework.data.elasticsearch.core.IndexInformation; import org.springframework.data.elasticsearch.core.MultiGetItem; import org.springframework.data.elasticsearch.core.cluster.ClusterHealth; import org.springframework.data.elasticsearch.core.document.Document; -import org.springframework.data.elasticsearch.core.index.*; +import org.springframework.data.elasticsearch.core.index.AliasData; +import org.springframework.data.elasticsearch.core.index.Settings; +import org.springframework.data.elasticsearch.core.index.TemplateData; +import org.springframework.data.elasticsearch.core.index.TemplateResponse; +import org.springframework.data.elasticsearch.core.index.TemplateResponseData; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.query.ByQueryResponse; import org.springframework.data.elasticsearch.core.query.StringQuery; import org.springframework.data.elasticsearch.core.reindex.ReindexResponse; import org.springframework.data.elasticsearch.core.script.Script; +import org.springframework.data.elasticsearch.core.sql.SqlResponse; import org.springframework.data.elasticsearch.support.DefaultStringObjectMap; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -81,7 +92,7 @@ public ClusterHealth clusterHealth(HealthResponse healthResponse) { return ClusterHealth.builder() // .withActivePrimaryShards(healthResponse.activePrimaryShards()) // .withActiveShards(healthResponse.activeShards()) // - .withActiveShardsPercent(Double.parseDouble(healthResponse.activeShardsPercentAsNumber()))// + .withActiveShardsPercent(healthResponse.activeShardsPercentAsNumber())// .withClusterName(healthResponse.clusterName()) // .withDelayedUnassignedShards(healthResponse.delayedUnassignedShards()) // .withInitializingShards(healthResponse.initializingShards()) // @@ -121,8 +132,7 @@ private TemplateResponse clusterGetComponentTemplate( .build(); } - private TemplateResponseData clusterGetComponentTemplateData( - ComponentTemplateSummary componentTemplateSummary) { + private TemplateResponseData clusterGetComponentTemplateData(ComponentTemplateSummary componentTemplateSummary) { var mapping = typeMapping(componentTemplateSummary.mappings()); var settings = new Settings(); @@ -181,9 +191,9 @@ public Document indicesGetMapping(GetMappingResponse getMappingResponse, IndexCo Assert.notNull(getMappingResponse, "getMappingResponse must not be null"); Assert.notNull(indexCoordinates, "indexCoordinates must not be null"); - Map mappings = getMappingResponse.result(); + Map mappings = getMappingResponse.mappings(); - if (mappings == null || mappings.size() == 0) { + if (mappings == null || mappings.isEmpty()) { return Document.create(); } @@ -209,7 +219,7 @@ public List indicesGetIndexInformations(GetIndexResponse getIn List indexInformationList = new ArrayList<>(); - getIndexResponse.result().forEach((indexName, indexState) -> { + getIndexResponse.indices().forEach((indexName, indexState) -> { Settings settings = indexState.settings() != null ? Settings.parse(toJson(indexState.settings(), jsonpMapper)) : new Settings(); Document mappings = indexState.mappings() != null ? Document.parse(toJson(indexState.mappings(), jsonpMapper)) @@ -229,7 +239,7 @@ public Map> indicesGetAliasData(GetAliasResponse getAlias Assert.notNull(getAliasResponse, "getAliasResponse must not be null"); Map> aliasDataMap = new HashMap<>(); - getAliasResponse.result().forEach((indexName, alias) -> { + getAliasResponse.aliases().forEach((indexName, alias) -> { Set aliasDataSet = new HashSet<>(); alias.aliases() .forEach((aliasName, aliasDefinition) -> aliasDataSet.add(indicesGetAliasData(aliasName, aliasDefinition))); @@ -326,7 +336,7 @@ private TemplateResponse indexGetComponentTemplate(IndexTemplateItem indexTempla } private TemplateResponseData indexGetComponentTemplateData(IndexTemplateSummary indexTemplateSummary, - List composedOf) { + List composedOf) { var mapping = typeMapping(indexTemplateSummary.mappings()); Function indexSettingsToSettings = indexSettings -> { @@ -390,7 +400,6 @@ public ReindexResponse reindexResponse(co.elastic.clients.elasticsearch.core.Rei private ReindexResponse.Failure reindexResponseFailureOf(BulkIndexByScrollFailure failure) { return ReindexResponse.Failure.builder() // .withIndex(failure.index()) // - .withType(failure.type()) // .withId(failure.id()) // .withStatus(failure.status())// .withErrorCause(toErrorCause(failure.cause())) // @@ -401,14 +410,12 @@ private ReindexResponse.Failure reindexResponseFailureOf(BulkIndexByScrollFailur private ByQueryResponse.Failure byQueryResponseFailureOf(BulkIndexByScrollFailure failure) { return ByQueryResponse.Failure.builder() // .withIndex(failure.index()) // - .withType(failure.type()) // .withId(failure.id()) // .withStatus(failure.status())// .withErrorCause(toErrorCause(failure.cause())).build(); } - @Nullable - public static MultiGetItem.Failure getFailure(MultiGetResponseItem itemResponse) { + public static MultiGetItem.@Nullable Failure getFailure(MultiGetResponseItem itemResponse) { MultiGetError responseFailure = itemResponse.isFailure() ? itemResponse.failure() : null; @@ -490,6 +497,10 @@ public ByQueryResponse byQueryResponse(UpdateByQueryResponse response) { builder.withDeleted(response.deleted()); } + if(response.updated() != null) { + builder.withUpdated(response.updated()); + } + if (response.batches() != null) { builder.withBatches(Math.toIntExact(response.batches())); } @@ -524,11 +535,34 @@ public Script scriptResponse(GetScriptResponse response) { ? Script.builder() // .withId(response.id()) // .withLanguage(response.script().lang()) // - .withSource(response.script().source()).build() // + .withSource(response.script().source().scriptString()).build() // : null; } // endregion + // region sql + public SqlResponse sqlResponse(QueryResponse response) { + SqlResponse.Builder builder = SqlResponse.builder(); + builder.withRunning(Boolean.TRUE.equals(response.isRunning())) + .withPartial(Boolean.TRUE.equals(response.isPartial())).withCursor(response.cursor()); + + final List columns = response.columns().stream() + .map(column -> new SqlResponse.Column(column.name(), column.type())).toList(); + builder.withColumns(columns); + + for (List rowValues : response.rows()) { + SqlResponse.Row.Builder rowBuilder = SqlResponse.Row.builder(); + for (int idx = 0; idx < rowValues.size(); idx++) { + rowBuilder.withValue(columns.get(idx), rowValues.get(idx).toJson()); + } + + builder.withRow(rowBuilder.build()); + } + + return builder.build(); + } + // end region + // region helper functions private long timeToLong(Time time) { @@ -541,7 +575,7 @@ private long timeToLong(Time time) { } @Nullable - private static ElasticsearchErrorCause toErrorCause(@Nullable ErrorCause errorCause) { + static ElasticsearchErrorCause toErrorCause(@Nullable ErrorCause errorCause) { if (errorCause != null) { return new ElasticsearchErrorCause( // diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/SearchDocumentResponseBuilder.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/SearchDocumentResponseBuilder.java index 00293b407f..94a6ab1e54 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/SearchDocumentResponseBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/SearchDocumentResponseBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,6 +15,8 @@ */ package org.springframework.data.elasticsearch.client.elc; +import co.elastic.clients.elasticsearch._types.ShardFailure; +import co.elastic.clients.elasticsearch._types.ShardStatistics; import co.elastic.clients.elasticsearch._types.aggregations.Aggregate; import co.elastic.clients.elasticsearch.core.SearchResponse; import co.elastic.clients.elasticsearch.core.SearchTemplateResponse; @@ -27,6 +29,7 @@ import co.elastic.clients.elasticsearch.core.search.TotalHits; import co.elastic.clients.json.JsonpMapper; +import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -36,6 +39,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; +import org.springframework.data.elasticsearch.core.SearchShardStatistics; import org.springframework.data.elasticsearch.core.TotalHitsRelation; import org.springframework.data.elasticsearch.core.document.SearchDocument; import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse; @@ -44,7 +49,6 @@ import org.springframework.data.elasticsearch.core.suggest.response.Suggest; import org.springframework.data.elasticsearch.core.suggest.response.TermSuggestion; import org.springframework.data.elasticsearch.support.ScoreDoc; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -52,6 +56,8 @@ * Factory class to create {@link SearchDocumentResponse} instances. * * @author Peter-Josef Meisch + * @author Haibo Liu + * @author Mohamed El Harrougui * @since 4.4 */ class SearchDocumentResponseBuilder { @@ -78,8 +84,11 @@ public static SearchDocumentResponse from(ResponseBody response Map aggregations = responseBody.aggregations(); Map>> suggest = responseBody.suggest(); var pointInTimeId = responseBody.pitId(); + var shards = responseBody.shards(); + var executionDurationInMillis = responseBody.took(); - return from(hitsMetadata, scrollId, pointInTimeId, aggregations, suggest, entityCreator, jsonpMapper); + return from(hitsMetadata, shards, scrollId, pointInTimeId, executionDurationInMillis, aggregations, suggest, + entityCreator, jsonpMapper); } /** @@ -98,13 +107,16 @@ public static SearchDocumentResponse from(SearchTemplateResponse SearchDocumentResponse from(SearchTemplateResponse SearchDocumentResponse from(HitsMetadata hitsMetadata, @Nullable String scrollId, - @Nullable String pointInTimeId, @Nullable Map aggregations, + public static SearchDocumentResponse from(HitsMetadata hitsMetadata, @Nullable ShardStatistics shards, + @Nullable String scrollId, @Nullable String pointInTimeId, long executionDurationInMillis, + @Nullable Map aggregations, Map>> suggestES, SearchDocumentResponse.EntityCreator entityCreator, JsonpMapper jsonpMapper) { @@ -145,6 +158,8 @@ public static SearchDocumentResponse from(HitsMetadata hitsMetadata, @Nul float maxScore = hitsMetadata.maxScore() != null ? hitsMetadata.maxScore().floatValue() : Float.NaN; + Duration executionDuration = Duration.ofMillis(executionDurationInMillis); + List searchDocuments = new ArrayList<>(); for (Hit hit : hitsMetadata.hits()) { searchDocuments.add(DocumentAdapters.from(hit, jsonpMapper)); @@ -155,8 +170,19 @@ public static SearchDocumentResponse from(HitsMetadata hitsMetadata, @Nul Suggest suggest = suggestFrom(suggestES, entityCreator); - return new SearchDocumentResponse(totalHits, totalHitsRelation, maxScore, scrollId, pointInTimeId, searchDocuments, - aggregationsContainer, suggest); + SearchShardStatistics shardStatistics = shards != null ? shardsFrom(shards) : null; + + return new SearchDocumentResponse(totalHits, totalHitsRelation, maxScore, executionDuration, scrollId, + pointInTimeId, searchDocuments, + aggregationsContainer, suggest, shardStatistics); + } + + private static SearchShardStatistics shardsFrom(ShardStatistics shards) { + List failures = shards.failures(); + List searchFailures = failures.stream().map(f -> SearchShardStatistics.Failure + .of(f.index(), f.node(), f.status(), f.shard(), null, ResponseConverter.toErrorCause(f.reason()))).toList(); + return SearchShardStatistics.of(shards.failed(), shards.successful(), shards.total(), shards.skipped(), + searchFailures); } @Nullable @@ -218,9 +244,8 @@ private static PhraseSuggestion getPhraseSuggestion(String name, List options = new ArrayList<>(); - phraseSuggestOptions.forEach(optionES -> options - .add(new PhraseSuggestion.Entry.Option(optionES.text(), optionES.highlighted(), optionES.score(), - optionES.collateMatch()))); + phraseSuggestOptions.forEach(optionES -> options.add(new PhraseSuggestion.Entry.Option(optionES.text(), + optionES.highlighted(), optionES.score(), optionES.collateMatch()))); entries.add(new PhraseSuggestion.Entry(phraseSuggest.text(), phraseSuggest.offset(), phraseSuggest.length(), options, null)); }); diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/TypeUtils.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/TypeUtils.java index 534c4590dd..db38baf135 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/TypeUtils.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/TypeUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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. @@ -18,6 +18,8 @@ import co.elastic.clients.elasticsearch._types.*; import co.elastic.clients.elasticsearch._types.mapping.FieldType; import co.elastic.clients.elasticsearch._types.mapping.TypeMapping; +import co.elastic.clients.elasticsearch._types.query_dsl.ChildScoreMode; +import co.elastic.clients.elasticsearch._types.query_dsl.Operator; import co.elastic.clients.elasticsearch.core.search.BoundaryScanner; import co.elastic.clients.elasticsearch.core.search.HighlighterEncoder; import co.elastic.clients.elasticsearch.core.search.HighlighterFragmenter; @@ -36,18 +38,21 @@ import java.util.Map; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort; import org.springframework.data.elasticsearch.core.RefreshPolicy; import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.query.GeoDistanceOrder; +import org.springframework.data.elasticsearch.core.query.HasChildQuery; import org.springframework.data.elasticsearch.core.query.IndexQuery; import org.springframework.data.elasticsearch.core.query.IndicesOptions; import org.springframework.data.elasticsearch.core.query.Order; import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.RescorerQuery; import org.springframework.data.elasticsearch.core.query.UpdateResponse; +import org.springframework.data.elasticsearch.core.query.types.ConflictsType; +import org.springframework.data.elasticsearch.core.query.types.OperatorType; import org.springframework.data.elasticsearch.core.reindex.ReindexRequest; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -215,7 +220,7 @@ static GeoDistanceType geoDistanceType(GeoDistanceOrder.DistanceType distanceTyp } @Nullable - static SortOrder sortOrder(@Nullable Sort.Direction direction) { + static SortOrder sortOrder(Sort.@Nullable Direction direction) { if (direction == null) { return null; @@ -296,7 +301,7 @@ static HighlighterTagsSchema highlighterTagsSchema(@Nullable String value) { } @Nullable - static OpType opType(@Nullable IndexQuery.OpType opType) { + static OpType opType(IndexQuery.@Nullable OpType opType) { if (opType != null) { return switch (opType) { @@ -320,8 +325,7 @@ static Refresh refresh(@Nullable RefreshPolicy refreshPolicy) { }; } - @Nullable - static UpdateResponse.Result result(@Nullable Result result) { + static UpdateResponse.@Nullable Result result(@Nullable Result result) { if (result == null) { return null; @@ -338,7 +342,7 @@ static UpdateResponse.Result result(@Nullable Result result) { } @Nullable - static ScoreMode scoreMode(@Nullable RescorerQuery.ScoreMode scoreMode) { + static ScoreMode scoreMode(RescorerQuery.@Nullable ScoreMode scoreMode) { if (scoreMode == null) { return null; @@ -356,7 +360,7 @@ static ScoreMode scoreMode(@Nullable RescorerQuery.ScoreMode scoreMode) { } @Nullable - static SearchType searchType(@Nullable Query.SearchType searchType) { + static SearchType searchType(Query.@Nullable SearchType searchType) { if (searchType == null) { return null; @@ -413,7 +417,7 @@ static String timeStringMs(@Nullable Duration duration) { @Nullable static VersionType versionType( - @Nullable org.springframework.data.elasticsearch.annotations.Document.VersionType versionType) { + org.springframework.data.elasticsearch.annotations.Document.@Nullable VersionType versionType) { if (versionType != null) { return switch (versionType) { @@ -500,4 +504,48 @@ static Map paramsMap(Map params) { }); return mappedParams; } + + /** + * Convert a spring-data-elasticsearch operator to an Elasticsearch operator. + * + * @param operator spring-data-elasticsearch operator. + * @return an Elasticsearch Operator. + * @since 5.3 + */ + @Nullable + static Operator operator(@Nullable OperatorType operator) { + return operator != null ? Operator.valueOf(operator.name()) : null; + } + + /** + * Convert a spring-data-elasticsearch {@literal conflicts} to an Elasticsearch {@literal conflicts}. + * + * @param conflicts spring-data-elasticsearch {@literal conflicts}. + * @return an Elasticsearch {@literal conflicts}. + * @since 5.3 + */ + @Nullable + static Conflicts conflicts(@Nullable ConflictsType conflicts) { + return conflicts != null ? Conflicts.valueOf(conflicts.name()) : null; + } + + /** + * Convert a spring-data-elasticsearch {@literal scoreMode} to an Elasticsearch {@literal scoreMode}. + * + * @param scoreMode spring-data-elasticsearch {@literal scoreMode}. + * @return an Elasticsearch {@literal scoreMode}. + */ + static ChildScoreMode scoreMode(HasChildQuery.@Nullable ScoreMode scoreMode) { + if (scoreMode == null) { + return ChildScoreMode.None; + } + + return switch (scoreMode) { + case Avg -> ChildScoreMode.Avg; + case Max -> ChildScoreMode.Max; + case Min -> ChildScoreMode.Min; + case Sum -> ChildScoreMode.Sum; + default -> ChildScoreMode.None; + }; + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/aot/ElasticsearchClientRuntimeHints.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/aot/ElasticsearchClientRuntimeHints.java index 8076ce98b8..2c177a13ac 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/aot/ElasticsearchClientRuntimeHints.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/aot/ElasticsearchClientRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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,11 +19,11 @@ import co.elastic.clients.elasticsearch._types.mapping.TypeMapping; import co.elastic.clients.elasticsearch.indices.IndexSettings; import co.elastic.clients.elasticsearch.indices.PutMappingRequest; -import org.springframework.aot.hint.MemberCategory; + +import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; -import org.springframework.lang.Nullable; /** * runtime hints for the Elasticsearch client libraries, as these do not provide any of their own. diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/aot/package-info.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/aot/package-info.java index 7d6be368bf..2718c30fb3 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/aot/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/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.client.elc.aot; diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/package-info.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/package-info.java index ab8646f0a9..011b5f7fb3 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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. @@ -18,6 +18,5 @@ * This package contains classes that use the new Elasticsearch client library (co.elastic.clients:elasticsearch-java) * to access Elasticsearch. */ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.client.elc; diff --git a/src/main/java/org/springframework/data/elasticsearch/client/package-info.java b/src/main/java/org/springframework/data/elasticsearch/client/package-info.java index 3594797d63..1aa3d179af 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/package-info.java @@ -1,3 +1,2 @@ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.client; diff --git a/src/main/java/org/springframework/data/elasticsearch/client/util/ScrollState.java b/src/main/java/org/springframework/data/elasticsearch/client/util/ScrollState.java index c4ed1f942a..a20805ad20 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/util/ScrollState.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/util/ScrollState.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -19,7 +19,7 @@ import java.util.List; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.StringUtils; /** diff --git a/src/main/java/org/springframework/data/elasticsearch/client/util/package-info.java b/src/main/java/org/springframework/data/elasticsearch/client/util/package-info.java index 79701328cb..3e0be80923 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/util/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/util/package-info.java @@ -1,3 +1,2 @@ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.client.util; diff --git a/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingBeanDefinitionParser.java b/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingBeanDefinitionParser.java index 838f1b7a6d..527ae4ad13 100644 --- a/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingBeanDefinitionParser.java +++ b/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -17,6 +17,7 @@ import static org.springframework.data.config.ParsingUtils.*; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -28,7 +29,6 @@ import org.springframework.data.elasticsearch.core.event.AuditingEntityCallback; import org.springframework.data.elasticsearch.core.event.ReactiveAuditingEntityCallback; import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.w3c.dom.Element; diff --git a/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingRegistrar.java b/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingRegistrar.java index dc0ca6d63d..c77af25f1d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingRegistrar.java +++ b/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/config/ElasticsearchConfigurationSupport.java b/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchConfigurationSupport.java index 18b92e5784..725382eaa3 100644 --- a/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchConfigurationSupport.java +++ b/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchConfigurationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 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. @@ -20,6 +20,7 @@ import java.util.HashSet; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; @@ -34,7 +35,6 @@ import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; import org.springframework.data.mapping.model.FieldNamingStrategy; import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; diff --git a/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchNamespaceHandler.java b/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchNamespaceHandler.java index 1dbc8fe87d..f7304fb563 100644 --- a/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchNamespaceHandler.java +++ b/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchNamespaceHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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/config/EnableElasticsearchAuditing.java b/src/main/java/org/springframework/data/elasticsearch/config/EnableElasticsearchAuditing.java index 6bad2445b3..7a416e548d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/config/EnableElasticsearchAuditing.java +++ b/src/main/java/org/springframework/data/elasticsearch/config/EnableElasticsearchAuditing.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/config/EnableReactiveElasticsearchAuditing.java b/src/main/java/org/springframework/data/elasticsearch/config/EnableReactiveElasticsearchAuditing.java index cae34e4eb5..804dd09e32 100644 --- a/src/main/java/org/springframework/data/elasticsearch/config/EnableReactiveElasticsearchAuditing.java +++ b/src/main/java/org/springframework/data/elasticsearch/config/EnableReactiveElasticsearchAuditing.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/config/PersistentEntitiesFactoryBean.java b/src/main/java/org/springframework/data/elasticsearch/config/PersistentEntitiesFactoryBean.java index 7b24d5b819..6bc684111b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/config/PersistentEntitiesFactoryBean.java +++ b/src/main/java/org/springframework/data/elasticsearch/config/PersistentEntitiesFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/config/ReactiveElasticsearchAuditingRegistrar.java b/src/main/java/org/springframework/data/elasticsearch/config/ReactiveElasticsearchAuditingRegistrar.java index 7abb4d33e4..a25b6ba1aa 100644 --- a/src/main/java/org/springframework/data/elasticsearch/config/ReactiveElasticsearchAuditingRegistrar.java +++ b/src/main/java/org/springframework/data/elasticsearch/config/ReactiveElasticsearchAuditingRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/config/package-info.java b/src/main/java/org/springframework/data/elasticsearch/config/package-info.java index 6461b9824f..351c2495d4 100644 --- a/src/main/java/org/springframework/data/elasticsearch/config/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/config/package-info.java @@ -1,3 +1,2 @@ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.config; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchTemplate.java index 1eab3bdd4e..6a0870375f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -24,6 +24,7 @@ import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -57,8 +58,6 @@ import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.util.Streamable; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -299,11 +298,6 @@ public String delete(String id, Class entityType) { return this.delete(id, getIndexCoordinatesFor(entityType)); } - @Override - public ByQueryResponse delete(Query query, Class clazz) { - return delete(query, clazz, getIndexCoordinatesFor(clazz)); - } - @Override public String delete(Object entity) { return delete(entity, getIndexCoordinatesFor(entity.getClass())); @@ -779,8 +773,7 @@ public T doWith(@Nullable Document document) { } protected interface SearchDocumentResponseCallback { - @NonNull - T doWith(@NonNull SearchDocumentResponse response); + T doWith(SearchDocumentResponse response); } protected class ReadSearchDocumentResponseCallback implements SearchDocumentResponseCallback> { @@ -795,7 +788,6 @@ public ReadSearchDocumentResponseCallback(Class type, IndexCoordinates index) this.type = type; } - @NonNull @Override public SearchHits doWith(SearchDocumentResponse response) { List entities = response.getSearchDocuments().stream().map(delegate::doWith).collect(Collectors.toList()); @@ -816,7 +808,6 @@ public ReadSearchScrollDocumentResponseCallback(Class type, IndexCoordinates this.type = type; } - @NonNull @Override public SearchScrollHits doWith(SearchDocumentResponse response) { List entities = response.getSearchDocuments().stream().map(delegate::doWith).collect(Collectors.toList()); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/AbstractReactiveElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/AbstractReactiveElasticsearchTemplate.java index 476148f929..a3b313e538 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/AbstractReactiveElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/AbstractReactiveElasticsearchTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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. @@ -26,6 +26,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.springframework.beans.BeansException; @@ -46,6 +47,7 @@ import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; import org.springframework.data.elasticsearch.core.query.ByQueryResponse; +import org.springframework.data.elasticsearch.core.query.DeleteQuery; import org.springframework.data.elasticsearch.core.query.IndexQuery; import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm; @@ -55,8 +57,6 @@ import org.springframework.data.elasticsearch.core.suggest.response.Suggest; import org.springframework.data.elasticsearch.support.VersionInfo; import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -233,6 +233,7 @@ public Flux save(Flux entities, IndexCoordinates index, int bulkSize) .subscribe(new Subscriber<>() { @Nullable private Subscription subscription = null; private final AtomicBoolean upstreamComplete = new AtomicBoolean(false); + private final AtomicBoolean onNextHasBeenCalled = new AtomicBoolean(false); @Override public void onSubscribe(Subscription subscription) { @@ -242,6 +243,7 @@ public void onSubscribe(Subscription subscription) { @Override public void onNext(List entityList) { + onNextHasBeenCalled.set(true); saveAll(entityList, index) .map(sink::tryEmitNext) .doOnComplete(() -> { @@ -267,6 +269,10 @@ public void onError(Throwable throwable) { @Override public void onComplete() { upstreamComplete.set(true); + if (!onNextHasBeenCalled.get()) { + // this happens when an empty flux is saved + sink.tryEmitComplete(); + } } }); return sink.asFlux(); @@ -409,7 +415,7 @@ public Mono delete(String id, IndexCoordinates index) { abstract protected Mono doDeleteById(String id, @Nullable String routing, IndexCoordinates index); @Override - public Mono delete(Query query, Class entityType) { + public Mono delete(DeleteQuery query, Class entityType) { return delete(query, entityType, getIndexCoordinatesFor(entityType)); } // endregion @@ -575,7 +581,6 @@ protected interface DocumentCallback { * @param document the document to convert * @return a Mono of the entity */ - @NonNull Mono toEntity(@Nullable Document document); } @@ -593,7 +598,6 @@ public ReadDocumentCallback(EntityReader reader, Class t this.index = index; } - @NonNull public Mono toEntity(@Nullable Document document) { if (document == null) { return Mono.empty(); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ActiveShardCount.java b/src/main/java/org/springframework/data/elasticsearch/core/ActiveShardCount.java index 359c69b436..8b1a9c47df 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ActiveShardCount.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ActiveShardCount.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/core/AggregationContainer.java b/src/main/java/org/springframework/data/elasticsearch/core/AggregationContainer.java index fc33d5e8e4..2cf49b3f6f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/AggregationContainer.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/AggregationContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/core/AggregationsContainer.java b/src/main/java/org/springframework/data/elasticsearch/core/AggregationsContainer.java index 7e2ad40c8b..d201d9ed81 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/AggregationsContainer.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/AggregationsContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/core/DocumentOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/DocumentOperations.java index a51bc911b4..b0ab53bc69 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/DocumentOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/DocumentOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -18,16 +18,17 @@ import java.util.Collection; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.query.BulkOptions; import org.springframework.data.elasticsearch.core.query.ByQueryResponse; +import org.springframework.data.elasticsearch.core.query.DeleteQuery; import org.springframework.data.elasticsearch.core.query.IndexQuery; import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.UpdateQuery; import org.springframework.data.elasticsearch.core.query.UpdateResponse; import org.springframework.data.elasticsearch.core.reindex.ReindexRequest; import org.springframework.data.elasticsearch.core.reindex.ReindexResponse; -import org.springframework.lang.Nullable; /** * The operations for the @@ -275,23 +276,24 @@ default void bulkUpdate(List queries, IndexCoordinates index) { * Delete all records matching the query. * * @param query query defining the objects - * @param clazz The entity class, must be annotated with + * @param clazz The entity class must be annotated with * {@link org.springframework.data.elasticsearch.annotations.Document} * @return response with detailed information - * @since 4.1 + * @since 5.3 */ - ByQueryResponse delete(Query query, Class clazz); + ByQueryResponse delete(DeleteQuery query, Class clazz); /** * Delete all records matching the query. * * @param query query defining the objects - * @param clazz The entity class, must be annotated with + * @param clazz The entity class must be annotated with * {@link org.springframework.data.elasticsearch.annotations.Document} * @param index the index from which to delete * @return response with detailed information + * @since 5.3 */ - ByQueryResponse delete(Query query, Class clazz, IndexCoordinates index); + ByQueryResponse delete(DeleteQuery query, Class clazz, IndexCoordinates index); /** * Partially update a document by the given entity. diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java index be35f840dc..954b123c72 100755 --- a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -15,12 +15,13 @@ */ package org.springframework.data.elasticsearch.core; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.cluster.ClusterOperations; import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.routing.RoutingResolver; import org.springframework.data.elasticsearch.core.script.ScriptOperations; -import org.springframework.lang.Nullable; +import org.springframework.data.elasticsearch.core.sql.SqlOperations; /** * ElasticsearchOperations. Since 4.0 this interface only contains common helper functions, the other methods have been @@ -35,7 +36,7 @@ * @author Dmitriy Yakovlev * @author Peter-Josef Meisch */ -public interface ElasticsearchOperations extends DocumentOperations, SearchOperations, ScriptOperations { +public interface ElasticsearchOperations extends DocumentOperations, SearchOperations, ScriptOperations, SqlOperations { /** * get an {@link IndexOperations} that is bound to the given class diff --git a/src/main/java/org/springframework/data/elasticsearch/core/EntityOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/EntityOperations.java index 68c14b08d7..f1e3b64d48 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/EntityOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/EntityOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 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. @@ -17,6 +17,7 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.ConversionService; import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; import org.springframework.data.elasticsearch.core.join.JoinField; @@ -29,7 +30,6 @@ import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -123,7 +123,7 @@ public T updateIndexedObject(T entity, // Only deal with text because ES generated Ids are strings! if (indexedObjectInformation.id() != null && idProperty != null - // isReadable from the base class is false in case of records + // isReadable from the base class is false in case of records && (idProperty.isReadable() || idProperty.getOwner().getType().isRecord()) && idProperty.getType().isAssignableFrom(String.class)) { propertyAccessor.setProperty(idProperty, indexedObjectInformation.id()); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/IndexInformation.java b/src/main/java/org/springframework/data/elasticsearch/core/IndexInformation.java index d27703f15a..cff0543154 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/IndexInformation.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/IndexInformation.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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. @@ -18,10 +18,10 @@ import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.index.AliasData; import org.springframework.data.elasticsearch.core.index.Settings; -import org.springframework.lang.Nullable; /** * Immutable object that holds information(name, settings, mappings, aliases) about an Index diff --git a/src/main/java/org/springframework/data/elasticsearch/core/IndexOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/IndexOperations.java index 4caa3c68bb..66bf1cf0b8 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/IndexOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/IndexOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -19,10 +19,10 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.index.*; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; -import org.springframework.lang.Nullable; /** * The operations for the diff --git a/src/main/java/org/springframework/data/elasticsearch/core/IndexOperationsAdapter.java b/src/main/java/org/springframework/data/elasticsearch/core/IndexOperationsAdapter.java index ad406884ee..a62ba05339 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/IndexOperationsAdapter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/IndexOperationsAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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. @@ -22,10 +22,10 @@ import java.util.Objects; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.index.*; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/data/elasticsearch/core/IndexedObjectInformation.java b/src/main/java/org/springframework/data/elasticsearch/core/IndexedObjectInformation.java index 1c7d95f4dd..91b259a7db 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/IndexedObjectInformation.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/IndexedObjectInformation.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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,7 +15,7 @@ */ package org.springframework.data.elasticsearch.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Value class capturing information about a newly indexed document in Elasticsearch. diff --git a/src/main/java/org/springframework/data/elasticsearch/core/MultiGetItem.java b/src/main/java/org/springframework/data/elasticsearch/core/MultiGetItem.java index b06ace378b..a28f83fd4b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/MultiGetItem.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/MultiGetItem.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,8 +15,8 @@ */ package org.springframework.data.elasticsearch.core; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.ElasticsearchErrorCause; -import org.springframework.lang.Nullable; /** * Response object for items returned from multiget requests, encapsulating the returned data and potential error diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveDocumentOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveDocumentOperations.java index 65aec898db..dba06262c3 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveDocumentOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveDocumentOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -25,6 +25,7 @@ import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.query.BulkOptions; import org.springframework.data.elasticsearch.core.query.ByQueryResponse; +import org.springframework.data.elasticsearch.core.query.DeleteQuery; import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.UpdateQuery; import org.springframework.data.elasticsearch.core.query.UpdateResponse; @@ -331,8 +332,9 @@ default Mono bulkUpdate(List queries, IndexCoordinates index) * @param query must not be {@literal null}. * @param entityType must not be {@literal null}. * @return a {@link Mono} emitting the number of the removed documents. + * @since 5.3 */ - Mono delete(Query query, Class entityType); + Mono delete(DeleteQuery query, Class entityType); /** * Delete the documents matching the given {@link Query} extracting index from entity metadata. @@ -341,8 +343,9 @@ default Mono bulkUpdate(List queries, IndexCoordinates index) * @param entityType must not be {@literal null}. * @param index the target index, must not be {@literal null} * @return a {@link Mono} emitting the number of the removed documents. + * @since 5.3 */ - Mono delete(Query query, Class entityType, IndexCoordinates index); + Mono delete(DeleteQuery query, Class entityType, IndexCoordinates index); /** * Partial update of the document. diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchOperations.java index b5add5ef50..91554fa0d0 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 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. @@ -15,13 +15,14 @@ */ package org.springframework.data.elasticsearch.core; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.cluster.ReactiveClusterOperations; import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.routing.RoutingResolver; import org.springframework.data.elasticsearch.core.script.ReactiveScriptOperations; -import org.springframework.lang.Nullable; +import org.springframework.data.elasticsearch.core.sql.ReactiveSqlOperations; /** * Interface that specifies a basic set of Elasticsearch operations executed in a reactive way. @@ -31,7 +32,7 @@ * @since 3.2 */ public interface ReactiveElasticsearchOperations - extends ReactiveDocumentOperations, ReactiveSearchOperations, ReactiveScriptOperations { + extends ReactiveDocumentOperations, ReactiveSearchOperations, ReactiveScriptOperations, ReactiveSqlOperations { /** * Get the {@link ElasticsearchConverter} used. diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveIndexOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveIndexOperations.java index ad5d219514..1c9a28c71c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveIndexOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveIndexOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/core/ReactiveResourceUtil.java b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveResourceUtil.java index 66cf014f1e..ddbc0d61cc 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveResourceUtil.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveResourceUtil.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/core/ReactiveSearchHitSupport.java b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveSearchHitSupport.java index 60af99c9cf..95a431e653 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveSearchHitSupport.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveSearchHitSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/core/ReactiveSearchHits.java b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveSearchHits.java index 94d536296a..2d84a54572 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveSearchHits.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveSearchHits.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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. @@ -17,14 +17,17 @@ import reactor.core.publisher.Flux; +import java.time.Duration; + +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.suggest.response.Suggest; -import org.springframework.lang.Nullable; /** * Encapsulates a Flux of {@link SearchHit}s with additional information from the search. * * @param the result data class. * @author Peter-Josef Meisch + * @author Mohamed El Harrougui * @since 4.4 */ public interface ReactiveSearchHits { @@ -37,6 +40,11 @@ public interface ReactiveSearchHits { float getMaxScore(); + /** + * @return the execution duration it took to complete the request + */ + Duration getExecutionDuration(); + /** * @return the {@link SearchHit}s from the search result. */ diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveSearchHitsImpl.java b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveSearchHitsImpl.java index c84a83c57c..604bd59591 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveSearchHitsImpl.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveSearchHitsImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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. @@ -17,11 +17,14 @@ import reactor.core.publisher.Flux; +import java.time.Duration; + +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.suggest.response.Suggest; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch + * @author Mohamed El Harrougui * @since 4.4 */ public class ReactiveSearchHitsImpl implements ReactiveSearchHits { @@ -58,6 +61,11 @@ public float getMaxScore() { return delegate.getMaxScore(); } + @Override + public Duration getExecutionDuration() { + return delegate.getExecutionDuration(); + } + @Override public boolean hasSearchHits() { return delegate.hasSearchHits(); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveSearchOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveSearchOperations.java index 7519460edf..b620f4c58c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveSearchOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveSearchOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/core/RefreshPolicy.java b/src/main/java/org/springframework/data/elasticsearch/core/RefreshPolicy.java index 5d6a059250..44e237720b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/RefreshPolicy.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/RefreshPolicy.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -22,5 +22,5 @@ * @since 4.2 */ public enum RefreshPolicy { - NONE, IMMEDIATE, WAIT_UNTIL; + NONE, IMMEDIATE, WAIT_UNTIL } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ResourceUtil.java b/src/main/java/org/springframework/data/elasticsearch/core/ResourceUtil.java index dd2c5f4d55..93530c5339 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ResourceUtil.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ResourceUtil.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/core/SearchHit.java b/src/main/java/org/springframework/data/elasticsearch/core/SearchHit.java index caf4d63b71..6a6f21920c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/SearchHit.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/SearchHit.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -23,9 +23,9 @@ import java.util.Map; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.document.Explanation; import org.springframework.data.elasticsearch.core.document.NestedMetaData; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -48,12 +48,12 @@ public class SearchHit { @Nullable private final NestedMetaData nestedMetaData; @Nullable private final String routing; @Nullable private final Explanation explanation; - private final List matchedQueries = new ArrayList<>(); + private final Map matchedQueries = new LinkedHashMap<>(); public SearchHit(@Nullable String index, @Nullable String id, @Nullable String routing, float score, @Nullable Object[] sortValues, @Nullable Map> highlightFields, @Nullable Map> innerHits, @Nullable NestedMetaData nestedMetaData, - @Nullable Explanation explanation, @Nullable List matchedQueries, T content) { + @Nullable Explanation explanation, @Nullable Map matchedQueries, T content) { this.index = index; this.id = id; this.routing = routing; @@ -73,7 +73,7 @@ public SearchHit(@Nullable String index, @Nullable String id, @Nullable String r this.content = content; if (matchedQueries != null) { - this.matchedQueries.addAll(matchedQueries); + this.matchedQueries.putAll(matchedQueries); } } @@ -194,7 +194,7 @@ public Explanation getExplanation() { * @return the matched queries for this SearchHit. */ @Nullable - public List getMatchedQueries() { + public Map getMatchedQueries() { return matchedQueries; } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/SearchHitMapping.java b/src/main/java/org/springframework/data/elasticsearch/core/SearchHitMapping.java index 357ec3ed13..bba964246b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/SearchHitMapping.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/SearchHitMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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,6 +15,7 @@ */ package org.springframework.data.elasticsearch.core; +import java.time.Duration; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedList; @@ -22,6 +23,7 @@ import java.util.Map; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.UncategorizedElasticsearchException; import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; import org.springframework.data.elasticsearch.core.document.Document; @@ -33,7 +35,6 @@ import org.springframework.data.elasticsearch.core.suggest.response.CompletionSuggestion; import org.springframework.data.elasticsearch.core.suggest.response.Suggest; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -46,6 +47,8 @@ * @author Matt Gilene * @author Sascha Woo * @author Jakob Hoeper + * @author Haibo Liu + * @author Mohamed El Harrougui * @since 4.0 */ public class SearchHitMapping { @@ -84,7 +87,9 @@ private SearchHitsImpl mapHitsFromResponse(SearchDocumentResponse searchDocum "Count of documents must match the count of entities"); long totalHits = searchDocumentResponse.getTotalHits(); + SearchShardStatistics shardStatistics = searchDocumentResponse.getSearchShardStatistics(); float maxScore = searchDocumentResponse.getMaxScore(); + Duration executionDuration = searchDocumentResponse.getExecutionDuration(); String scrollId = searchDocumentResponse.getScrollId(); String pointInTimeId = searchDocumentResponse.getPointInTimeId(); @@ -102,8 +107,8 @@ private SearchHitsImpl mapHitsFromResponse(SearchDocumentResponse searchDocum Suggest suggest = searchDocumentResponse.getSuggest(); mapHitsInCompletionSuggestion(suggest); - return new SearchHitsImpl<>(totalHits, totalHitsRelation, maxScore, scrollId, pointInTimeId, searchHits, - aggregations, suggest); + return new SearchHitsImpl<>(totalHits, totalHitsRelation, maxScore, executionDuration, scrollId, pointInTimeId, + searchHits, aggregations, suggest, shardStatistics); } @SuppressWarnings("unchecked") @@ -165,7 +170,7 @@ private Map> mapInnerHits(SearchDocument searchDocument) { Map> innerHits = new LinkedHashMap<>(); Map documentInnerHits = searchDocument.getInnerHits(); - if (documentInnerHits != null && documentInnerHits.size() > 0) { + if (documentInnerHits != null && !documentInnerHits.isEmpty()) { SearchHitMapping searchDocumentSearchHitMapping = SearchHitMapping .mappingFor(SearchDocument.class, converter); @@ -233,14 +238,16 @@ private SearchHits mapInnerDocuments(SearchHits searchHits, C scrollId = searchHitsImpl.getScrollId(); } - return new SearchHitsImpl<>(searchHits.getTotalHits(), // - searchHits.getTotalHitsRelation(), // - searchHits.getMaxScore(), // - scrollId, // - searchHits.getPointInTimeId(), // - convertedSearchHits, // - searchHits.getAggregations(), // - searchHits.getSuggest()); + return new SearchHitsImpl<>(searchHits.getTotalHits(), + searchHits.getTotalHitsRelation(), + searchHits.getMaxScore(), + searchHits.getExecutionDuration(), + scrollId, + searchHits.getPointInTimeId(), + convertedSearchHits, + searchHits.getAggregations(), + searchHits.getSuggest(), + searchHits.getSearchShardStatistics()); } } catch (Exception e) { throw new UncategorizedElasticsearchException("Unable to convert inner hits.", e); @@ -284,8 +291,8 @@ private ElasticsearchPersistentEntityWithNestedMetaData getPersistentEntity( } private static class ElasticsearchPersistentEntityWithNestedMetaData { - @Nullable private ElasticsearchPersistentEntity entity; - private NestedMetaData nestedMetaData; + @Nullable private final ElasticsearchPersistentEntity entity; + private final NestedMetaData nestedMetaData; public ElasticsearchPersistentEntityWithNestedMetaData(@Nullable ElasticsearchPersistentEntity entity, NestedMetaData nestedMetaData) { diff --git a/src/main/java/org/springframework/data/elasticsearch/core/SearchHitSupport.java b/src/main/java/org/springframework/data/elasticsearch/core/SearchHitSupport.java index b6fa455c7f..d9ed18aeb6 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/SearchHitSupport.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/SearchHitSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -21,11 +21,11 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; -import org.springframework.data.repository.util.ReactiveWrappers; import org.springframework.data.util.CloseableIterator; -import org.springframework.lang.Nullable; +import org.springframework.data.util.ReactiveWrappers; /** * Utility class with helper methods for working with {@link SearchHit}. diff --git a/src/main/java/org/springframework/data/elasticsearch/core/SearchHits.java b/src/main/java/org/springframework/data/elasticsearch/core/SearchHits.java index 9356808798..47276a493e 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/SearchHits.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/SearchHits.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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,18 +15,21 @@ */ package org.springframework.data.elasticsearch.core; +import java.time.Duration; import java.util.Iterator; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.suggest.response.Suggest; import org.springframework.data.util.Streamable; -import org.springframework.lang.Nullable; /** * Encapsulates a list of {@link SearchHit}s with additional information from the search. * * @param the result data class. * @author Sascha Woo + * @author Haibo Liu + * @author Mohamed El Harrougui * @since 4.0 */ public interface SearchHits extends Streamable> { @@ -42,6 +45,11 @@ public interface SearchHits extends Streamable> { */ float getMaxScore(); + /** + * @return the execution duration it took to complete the request + */ + Duration getExecutionDuration(); + /** * @param index position in List. * @return the {@link SearchHit} at position {index} @@ -108,4 +116,10 @@ default Iterator> iterator() { */ @Nullable String getPointInTimeId(); + + /** + * @return shard statistics for the search hit. + */ + @Nullable + SearchShardStatistics getSearchShardStatistics(); } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/SearchHitsImpl.java b/src/main/java/org/springframework/data/elasticsearch/core/SearchHitsImpl.java index e80f1e3c76..50a11a174d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/SearchHitsImpl.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/SearchHitsImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -15,12 +15,13 @@ */ package org.springframework.data.elasticsearch.core; +import java.time.Duration; import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.suggest.response.Suggest; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -29,6 +30,8 @@ * @param the result data class. * @author Peter-Josef Meisch * @author Sascha Woo + * @author Haibo Liu + * @author Mohamed El Harrougui * @since 4.0 */ public class SearchHitsImpl implements SearchScrollHits { @@ -36,36 +39,42 @@ public class SearchHitsImpl implements SearchScrollHits { private final long totalHits; private final TotalHitsRelation totalHitsRelation; private final float maxScore; + private final Duration executionDuration; @Nullable private final String scrollId; private final List> searchHits; private final Lazy>> unmodifiableSearchHits; @Nullable private final AggregationsContainer aggregations; @Nullable private final Suggest suggest; - @Nullable private String pointInTimeId; + @Nullable private final String pointInTimeId; + @Nullable private final SearchShardStatistics searchShardStatistics; /** * @param totalHits the number of total hits for the search * @param totalHitsRelation the relation {@see TotalHitsRelation}, must not be {@literal null} * @param maxScore the maximum score + * @param executionDuration the execution duration it took to complete the request * @param scrollId the scroll id if available * @param searchHits must not be {@literal null} * @param aggregations the aggregations if available */ - public SearchHitsImpl(long totalHits, TotalHitsRelation totalHitsRelation, float maxScore, @Nullable String scrollId, - @Nullable String pointInTimeId, List> searchHits, - @Nullable AggregationsContainer aggregations, @Nullable Suggest suggest) { + public SearchHitsImpl(long totalHits, TotalHitsRelation totalHitsRelation, float maxScore, Duration executionDuration, + @Nullable String scrollId, @Nullable String pointInTimeId, List> searchHits, + @Nullable AggregationsContainer aggregations, @Nullable Suggest suggest, + @Nullable SearchShardStatistics searchShardStatistics) { Assert.notNull(searchHits, "searchHits must not be null"); this.totalHits = totalHits; this.totalHitsRelation = totalHitsRelation; this.maxScore = maxScore; + this.executionDuration = executionDuration; this.scrollId = scrollId; this.pointInTimeId = pointInTimeId; this.searchHits = searchHits; this.aggregations = aggregations; this.suggest = suggest; this.unmodifiableSearchHits = Lazy.of(() -> Collections.unmodifiableList(searchHits)); + this.searchShardStatistics = searchShardStatistics; } // region getter @@ -84,6 +93,11 @@ public float getMaxScore() { return maxScore; } + @Override + public Duration getExecutionDuration() { + return executionDuration; + } + @Override @Nullable public String getScrollId() { @@ -118,16 +132,23 @@ public String getPointInTimeId() { return pointInTimeId; } + @Override + public SearchShardStatistics getSearchShardStatistics() { + return searchShardStatistics; + } + @Override public String toString() { return "SearchHits{" + // "totalHits=" + totalHits + // ", totalHitsRelation=" + totalHitsRelation + // ", maxScore=" + maxScore + // + ", executionDuration=" + executionDuration + // ", scrollId='" + scrollId + '\'' + // ", pointInTimeId='" + pointInTimeId + '\'' + // ", searchHits={" + searchHits.size() + " elements}" + // ", aggregations=" + aggregations + // + ", shardStatistics=" + searchShardStatistics + // '}'; } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/SearchHitsIterator.java b/src/main/java/org/springframework/data/elasticsearch/core/SearchHitsIterator.java index 3f92a2157e..a24f547ad4 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/SearchHitsIterator.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/SearchHitsIterator.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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,14 +15,17 @@ */ package org.springframework.data.elasticsearch.core; +import java.time.Duration; + +import org.jspecify.annotations.Nullable; import org.springframework.data.util.CloseableIterator; -import org.springframework.lang.Nullable; /** * A {@link SearchHitsIterator} encapsulates {@link SearchHit} results that can be wrapped in a Java 8 * {@link java.util.stream.Stream}. * * @author Sascha Woo + * @author Mohamed El Harrougui * @param * @since 4.0 */ @@ -39,6 +42,11 @@ public interface SearchHitsIterator extends CloseableIterator> { */ float getMaxScore(); + /** + * @return the execution duration it took to complete the request + */ + Duration getExecutionDuration(); + /** * @return the number of total hits. */ diff --git a/src/main/java/org/springframework/data/elasticsearch/core/SearchOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/SearchOperations.java index 54d25a173b..d738f1fb5d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/SearchOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/SearchOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -18,11 +18,11 @@ import java.time.Duration; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.query.BaseQueryBuilder; import org.springframework.data.elasticsearch.core.query.MoreLikeThisQuery; import org.springframework.data.elasticsearch.core.query.Query; -import org.springframework.lang.Nullable; /** * The operations for the @@ -233,7 +233,8 @@ List> multiSearch(List queries, List> cl Query idsQuery(List ids); /** - * Creates a {@link BaseQueryBuilder} that has the given ids setto the parameter value. No other properties of the bulder are set. + * Creates a {@link BaseQueryBuilder} that has the given ids setto the parameter value. No other properties of the + * bulder are set. * * @param ids the list of ids must not be {@literal null} * @return query returning the documents with the given ids diff --git a/src/main/java/org/springframework/data/elasticsearch/core/SearchPage.java b/src/main/java/org/springframework/data/elasticsearch/core/SearchPage.java index 76f54ddad4..54089e2a40 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/SearchPage.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/SearchPage.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/core/SearchScrollHits.java b/src/main/java/org/springframework/data/elasticsearch/core/SearchScrollHits.java index fefdddd4d0..291472bb09 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/SearchScrollHits.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/SearchScrollHits.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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,13 +15,13 @@ */ package org.springframework.data.elasticsearch.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * This interface is used to expose the current {@code scrollId} from the underlying scroll context. *

* Internal use only. - * + * * @author Sascha Woo * @author Peter-Josef Meisch * @param diff --git a/src/main/java/org/springframework/data/elasticsearch/core/SearchShardStatistics.java b/src/main/java/org/springframework/data/elasticsearch/core/SearchShardStatistics.java new file mode 100644 index 0000000000..c6ddef4282 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/SearchShardStatistics.java @@ -0,0 +1,130 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.core; + +import java.util.List; + +import org.jspecify.annotations.Nullable; +import org.springframework.data.elasticsearch.ElasticsearchErrorCause; + +/** + * @author Haibo Liu + * @since 5.3 + */ +public class SearchShardStatistics { + private final Number failed; + + private final Number successful; + + private final Number total; + + @Nullable private final Number skipped; + + private final List failures; + + private SearchShardStatistics(Number failed, Number successful, Number total, @Nullable Number skipped, + List failures) { + this.failed = failed; + this.successful = successful; + this.total = total; + this.skipped = skipped; + this.failures = failures; + } + + public static SearchShardStatistics of(Number failed, Number successful, Number total, @Nullable Number skipped, + List failures) { + return new SearchShardStatistics(failed, successful, total, skipped, failures); + } + + public Number getFailed() { + return failed; + } + + public Number getSuccessful() { + return successful; + } + + public Number getTotal() { + return total; + } + + @Nullable + public Number getSkipped() { + return skipped; + } + + public boolean isFailed() { + return failed.intValue() > 0; + } + + public List getFailures() { + return failures; + } + + public static class Failure { + @Nullable private final String index; + @Nullable private final String node; + @Nullable private final String status; + private final int shard; + @Nullable private final Exception exception; + @Nullable private final ElasticsearchErrorCause elasticsearchErrorCause; + + private Failure(@Nullable String index, @Nullable String node, @Nullable String status, int shard, + @Nullable Exception exception, @Nullable ElasticsearchErrorCause elasticsearchErrorCause) { + this.index = index; + this.node = node; + this.status = status; + this.shard = shard; + this.exception = exception; + this.elasticsearchErrorCause = elasticsearchErrorCause; + } + + public static SearchShardStatistics.Failure of(@Nullable String index, @Nullable String node, + @Nullable String status, int shard, @Nullable Exception exception, + @Nullable ElasticsearchErrorCause elasticsearchErrorCause) { + return new SearchShardStatistics.Failure(index, node, status, shard, exception, elasticsearchErrorCause); + } + + @Nullable + public String getIndex() { + return index; + } + + @Nullable + public String getNode() { + return node; + } + + @Nullable + public String getStatus() { + return status; + } + + @Nullable + public Exception getException() { + return exception; + } + + public int getShard() { + return shard; + } + + @Nullable + public ElasticsearchErrorCause getElasticsearchErrorCause() { + return elasticsearchErrorCause; + } + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/StreamQueries.java b/src/main/java/org/springframework/data/elasticsearch/core/StreamQueries.java index d414099f0f..d96e643e55 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/StreamQueries.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/StreamQueries.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -15,6 +15,7 @@ */ package org.springframework.data.elasticsearch.core; +import java.time.Duration; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; @@ -22,8 +23,8 @@ import java.util.function.Consumer; import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.client.util.ScrollState; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -31,6 +32,7 @@ * * @author Mark Paluch * @author Sascha Woo + * @author Mohamed El Harrougui * @since 3.2 */ abstract class StreamQueries { @@ -56,15 +58,16 @@ static SearchHitsIterator streamResults(int maxCount, SearchScrollHits AggregationsContainer aggregations = searchHits.getAggregations(); float maxScore = searchHits.getMaxScore(); + Duration executionDuration = searchHits.getExecutionDuration(); long totalHits = searchHits.getTotalHits(); TotalHitsRelation totalHitsRelation = searchHits.getTotalHitsRelation(); return new SearchHitsIterator<>() { - private volatile AtomicInteger currentCount = new AtomicInteger(); + private final AtomicInteger currentCount = new AtomicInteger(); private volatile Iterator> currentScrollHits = searchHits.iterator(); private volatile boolean continueScroll = currentScrollHits.hasNext(); - private volatile ScrollState scrollState = new ScrollState(searchHits.getScrollId()); + private final ScrollState scrollState = new ScrollState(searchHits.getScrollId()); private volatile boolean isClosed = false; @Override @@ -86,6 +89,11 @@ public float getMaxScore() { return maxScore; } + @Override + public Duration getExecutionDuration() { + return executionDuration; + } + @Override public long getTotalHits() { return totalHits; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/TotalHitsRelation.java b/src/main/java/org/springframework/data/elasticsearch/core/TotalHitsRelation.java index b3fb9cff20..33e2449b02 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/TotalHitsRelation.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/TotalHitsRelation.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/core/cluster/ClusterHealth.java b/src/main/java/org/springframework/data/elasticsearch/core/cluster/ClusterHealth.java index e30e55248e..70ab96571f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/cluster/ClusterHealth.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/cluster/ClusterHealth.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/core/cluster/ClusterOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/cluster/ClusterOperations.java index ff7ba80550..c35c7c449b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/cluster/ClusterOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/cluster/ClusterOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/core/cluster/ReactiveClusterOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/cluster/ReactiveClusterOperations.java index 03108f6297..ca0feddb11 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/cluster/ReactiveClusterOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/cluster/ReactiveClusterOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/core/cluster/package-info.java b/src/main/java/org/springframework/data/elasticsearch/core/cluster/package-info.java index 6d49233c5e..8532409aa0 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/cluster/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/cluster/package-info.java @@ -1,6 +1,5 @@ /** * Interfaces and classes related to Elasticsearch cluster information and management. */ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.core.cluster; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/AbstractPropertyValueConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/AbstractPropertyValueConverter.java index 3bc05849d4..153a6feb16 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/AbstractPropertyValueConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/AbstractPropertyValueConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/core/convert/AbstractRangePropertyValueConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/AbstractRangePropertyValueConverter.java index f4995409ce..87606763bd 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/AbstractRangePropertyValueConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/AbstractRangePropertyValueConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/core/convert/ConversionException.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/ConversionException.java index 13f640c402..c5ad77bdde 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/ConversionException.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/ConversionException.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/core/convert/DateFormatter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/DateFormatter.java index 7f742e1c3a..f208349613 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/DateFormatter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/DateFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,14 +19,14 @@ /** * Interface to convert from and to {@link TemporalAccessor}s. - * + * * @author Peter-Josef Meisch * @since 4.2 */ public interface DateFormatter { /** * Formats a {@link TemporalAccessor} into a String. - * + * * @param accessor must not be {@literal null} * @return the formatted String */ @@ -34,7 +34,7 @@ public interface DateFormatter { /** * Parses a String into a {@link TemporalAccessor}. - * + * * @param input the String to parse, must not be {@literal null} * @param type the class of T * @param the {@link TemporalAccessor} implementation diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/DatePropertyValueConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/DatePropertyValueConverter.java index bc35e8cae7..2696633d14 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/DatePropertyValueConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/DatePropertyValueConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/core/convert/DateRangePropertyValueConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/DateRangePropertyValueConverter.java index 9c93f5259a..2c87322878 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/DateRangePropertyValueConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/DateRangePropertyValueConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/core/convert/DefaultElasticsearchTypeMapper.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/DefaultElasticsearchTypeMapper.java index 35365b8d28..1303d5af70 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/DefaultElasticsearchTypeMapper.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/DefaultElasticsearchTypeMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -19,6 +19,7 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.data.convert.DefaultTypeMapper; import org.springframework.data.convert.SimpleTypeInformationMapper; import org.springframework.data.convert.TypeAliasAccessor; @@ -27,7 +28,6 @@ import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; /** * Elasticsearch specific {@link org.springframework.data.convert.TypeMapper} implementation. diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchConverter.java index 9aefc4e6f0..a078bde2be 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -15,6 +15,7 @@ */ package org.springframework.data.elasticsearch.core.convert; +import org.jspecify.annotations.Nullable; import org.springframework.data.convert.EntityConverter; import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; @@ -23,7 +24,6 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -110,6 +110,6 @@ default Document mapObject(@Nullable Object source) { * @return a String wihere the property names are replaced with field names * @since 5.2 */ - public String updateFieldNames(String propertyPath, ElasticsearchPersistentEntity persistentEntity); + String updateFieldNames(String propertyPath, ElasticsearchPersistentEntity persistentEntity); // endregion } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchCustomConversions.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchCustomConversions.java index dbf197f69f..a573523b26 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchCustomConversions.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchCustomConversions.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 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. @@ -129,7 +129,7 @@ public Double convert(BigDecimal source) { @WritingConverter enum ByteArrayToBase64Converter implements Converter { - INSTANCE,; + INSTANCE; @Override public String convert(byte[] source) { diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverter.java index d926832d35..9c5800b093 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/core/convert/ElasticsearchTypeMapper.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchTypeMapper.java index 3de9df6254..fa4f6e4e56 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchTypeMapper.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchTypeMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -17,11 +17,11 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.data.convert.TypeMapper; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.lang.Nullable; /** * Elasticsearch specific {@link TypeMapper} definition. diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/GeoConverters.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/GeoConverters.java index e51202c9b6..b6ad5b4ede 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/GeoConverters.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/GeoConverters.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -344,7 +344,7 @@ public GeoJsonPolygon convert(Map source) { String type = GeoConverters.getGeoJsonType(source); Assert.isTrue(type.equalsIgnoreCase(GeoJsonPolygon.TYPE), "does not contain a type 'Polygon'"); List lines = geoJsonLineStringsFromMap(source); - Assert.isTrue(lines.size() > 0, "no linestrings defined in polygon"); + Assert.isTrue(!lines.isEmpty(), "no linestrings defined in polygon"); GeoJsonPolygon geoJsonPolygon = GeoJsonPolygon.of(lines.get(0)); for (int i = 1; i < lines.size(); i++) { geoJsonPolygon = geoJsonPolygon.withInnerRing(lines.get(i)); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingConversionException.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingConversionException.java new file mode 100644 index 0000000000..e823105ad7 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingConversionException.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.core.convert; + +import org.jspecify.annotations.Nullable; + +/** + * @since 5.3 + * @author Peter-Josef Meisch + */ +public class MappingConversionException extends RuntimeException { + private final String documentId; + + public MappingConversionException(@Nullable String documentId, Throwable cause) { + super(cause); + this.documentId = documentId != null ? documentId : "\"null\""; + } + + public String getDocumentId() { + return documentId; + } + + @Override + public String getMessage() { + return "Conversion exception when converting document id " + documentId; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java index 01b49cc934..3b757e18db 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -24,6 +24,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; @@ -34,7 +35,11 @@ import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.core.env.Environment; +import org.springframework.core.env.EnvironmentCapable; +import org.springframework.core.env.StandardEnvironment; import org.springframework.data.convert.CustomConversions; +import org.springframework.data.domain.Sort; import org.springframework.data.elasticsearch.annotations.FieldType; import org.springframework.data.elasticsearch.annotations.ScriptedField; import org.springframework.data.elasticsearch.core.document.Document; @@ -47,6 +52,7 @@ import org.springframework.data.elasticsearch.core.query.CriteriaQuery; import org.springframework.data.elasticsearch.core.query.FetchSourceFilter; import org.springframework.data.elasticsearch.core.query.Field; +import org.springframework.data.elasticsearch.core.query.Order; import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm; import org.springframework.data.elasticsearch.core.query.SourceFilter; @@ -58,8 +64,8 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.*; import org.springframework.data.util.TypeInformation; +import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.format.datetime.DateFormatterRegistrar; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -80,10 +86,12 @@ * @author Marc Vanbrabant * @author Anton Naydenov * @author vdisk + * @author Junghoon Ban + * @author llosimura * @since 3.2 */ public class MappingElasticsearchConverter - implements ElasticsearchConverter, ApplicationContextAware, InitializingBean { + implements ElasticsearchConverter, ApplicationContextAware, InitializingBean, EnvironmentCapable { private static final String INCOMPATIBLE_TYPES = "Cannot convert %1$s of type %2$s into an instance of %3$s! Implement a custom Converter<%2$s, %3$s> and register it with the CustomConversions."; private static final String INVALID_TYPE_TO_READ = "Expected to read Document %s into type %s but didn't find a PersistentEntity for the latter!"; @@ -93,7 +101,14 @@ public class MappingElasticsearchConverter private final MappingContext, ElasticsearchPersistentProperty> mappingContext; private final GenericConversionService conversionService; private CustomConversions conversions = new ElasticsearchCustomConversions(Collections.emptyList()); + + protected @Nullable Environment environment; + private final SpELContext spELContext = new SpELContext(new MapAccessor()); + private final SpelExpressionParser expressionParser = new SpelExpressionParser(); + private final CachingValueExpressionEvaluatorFactory expressionEvaluatorFactory = new CachingValueExpressionEvaluatorFactory( + expressionParser, this, spELContext); + private final EntityInstantiators instantiators = new EntityInstantiators(); private final ElasticsearchTypeMapper typeMapper; @@ -121,6 +136,14 @@ public void setApplicationContext(ApplicationContext applicationContext) throws } } + @Override + public Environment getEnvironment() { + if (environment == null) { + environment = new StandardEnvironment(); + } + return environment; + } + @Override public MappingContext, ElasticsearchPersistentProperty> getMappingContext() { return mappingContext; @@ -159,7 +182,8 @@ public ElasticsearchTypeMapper getTypeMapper() { @Override public R read(Class type, Document source) { - Reader reader = new Reader(mappingContext, conversionService, conversions, typeMapper, spELContext, instantiators); + Reader reader = new Reader(mappingContext, conversionService, conversions, typeMapper, expressionEvaluatorFactory, + instantiators); return reader.read(type, source); } @@ -199,29 +223,29 @@ private Base( */ private static class Reader extends Base { - private final SpELContext spELContext; private final EntityInstantiators instantiators; + private final CachingValueExpressionEvaluatorFactory expressionEvaluatorFactory; public Reader( MappingContext, ElasticsearchPersistentProperty> mappingContext, GenericConversionService conversionService, CustomConversions conversions, ElasticsearchTypeMapper typeMapper, - SpELContext spELContext, EntityInstantiators instantiators) { + CachingValueExpressionEvaluatorFactory expressionEvaluatorFactory, EntityInstantiators instantiators) { super(mappingContext, conversionService, conversions, typeMapper); - this.spELContext = spELContext; + this.expressionEvaluatorFactory = expressionEvaluatorFactory; this.instantiators = instantiators; } - @SuppressWarnings("unchecked") /** * Reads the given source into the given type. * - * @param type they type to convert the given source to. + * @param type the type to convert the given source to. * @param source the source to create an object of the given type from. * @return the object that was read */ R read(Class type, Document source) { + // noinspection unchecked TypeInformation typeInformation = TypeInformation.of((Class) ClassUtils.getUserClass(type)); R r = read(typeInformation, source); @@ -313,8 +337,7 @@ private R readMap(TypeInformation type, Map source) { private R readEntity(ElasticsearchPersistentEntity entity, Map source) { ElasticsearchPersistentEntity targetEntity = computeClosestEntity(entity, source); - - SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(source, spELContext); + ValueExpressionEvaluator evaluator = expressionEvaluatorFactory.create(source); MapValueAccessor accessor = new MapValueAccessor(source); InstanceCreatorMetadata creatorMetadata = entity.getInstanceCreatorMetadata(); @@ -332,50 +355,56 @@ private R readEntity(ElasticsearchPersistentEntity entity, Map propertyAccessor = new ConvertingPropertyAccessor<>( - targetEntity.getPropertyAccessor(result), conversionService); - // Only deal with String because ES generated Ids are strings ! - if (idProperty != null && idProperty.isReadable() && idProperty.getType().isAssignableFrom(String.class)) { - propertyAccessor.setProperty(idProperty, document.getId()); + try { + R result = readProperties(targetEntity, instance, valueProvider); + + if (document != null) { + if (document.hasId()) { + ElasticsearchPersistentProperty idProperty = targetEntity.getIdProperty(); + PersistentPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor<>( + targetEntity.getPropertyAccessor(result), conversionService); + // Only deal with String because ES generated Ids are strings ! + if (idProperty != null && idProperty.isReadable() && idProperty.getType().isAssignableFrom(String.class)) { + propertyAccessor.setProperty(idProperty, document.getId()); + } } - } - if (document.hasVersion()) { - long version = document.getVersion(); - ElasticsearchPersistentProperty versionProperty = targetEntity.getVersionProperty(); - // Only deal with Long because ES versions are longs ! - if (versionProperty != null && versionProperty.getType().isAssignableFrom(Long.class)) { - // check that a version was actually returned in the response, -1 would indicate that - // a search didn't request the version ids in the response, which would be an issue - Assert.isTrue(version != -1, "Version in response is -1"); - targetEntity.getPropertyAccessor(result).setProperty(versionProperty, version); + if (document.hasVersion()) { + long version = document.getVersion(); + ElasticsearchPersistentProperty versionProperty = targetEntity.getVersionProperty(); + // Only deal with Long because ES versions are longs ! + if (versionProperty != null && versionProperty.getType().isAssignableFrom(Long.class)) { + // check that a version was actually returned in the response, -1 would indicate that + // a search didn't request the version ids in the response, which would be an issue + Assert.isTrue(version != -1, "Version in response is -1"); + targetEntity.getPropertyAccessor(result).setProperty(versionProperty, version); + } } - } - if (targetEntity.hasSeqNoPrimaryTermProperty() && document.hasSeqNo() && document.hasPrimaryTerm()) { - if (isAssignedSeqNo(document.getSeqNo()) && isAssignedPrimaryTerm(document.getPrimaryTerm())) { - SeqNoPrimaryTerm seqNoPrimaryTerm = new SeqNoPrimaryTerm(document.getSeqNo(), document.getPrimaryTerm()); - ElasticsearchPersistentProperty property = targetEntity.getRequiredSeqNoPrimaryTermProperty(); - targetEntity.getPropertyAccessor(result).setProperty(property, seqNoPrimaryTerm); + if (targetEntity.hasSeqNoPrimaryTermProperty() && document.hasSeqNo() && document.hasPrimaryTerm()) { + if (isAssignedSeqNo(document.getSeqNo()) && isAssignedPrimaryTerm(document.getPrimaryTerm())) { + SeqNoPrimaryTerm seqNoPrimaryTerm = new SeqNoPrimaryTerm(document.getSeqNo(), document.getPrimaryTerm()); + ElasticsearchPersistentProperty property = targetEntity.getRequiredSeqNoPrimaryTermProperty(); + targetEntity.getPropertyAccessor(result).setProperty(property, seqNoPrimaryTerm); + } } } - } - if (source instanceof SearchDocument searchDocument) { - populateScriptFields(targetEntity, result, searchDocument); + if (source instanceof SearchDocument searchDocument) { + populateScriptedFields(targetEntity, result, searchDocument); + } + return result; + } catch (ConversionException e) { + String documentId = (document != null && document.hasId()) ? document.getId() : null; + throw new MappingConversionException(documentId, e); } - - return result; } private ParameterValueProvider getParameterProvider( - ElasticsearchPersistentEntity entity, MapValueAccessor source, SpELExpressionEvaluator evaluator) { + ElasticsearchPersistentEntity entity, MapValueAccessor source, ValueExpressionEvaluator evaluator) { ElasticsearchPropertyValueProvider provider = new ElasticsearchPropertyValueProvider(source, evaluator); @@ -384,7 +413,7 @@ private ParameterValueProvider getParameterProv PersistentEntityParameterValueProvider parameterProvider = new PersistentEntityParameterValueProvider<>( entity, provider, null); - return new ConverterAwareSpELExpressionParameterValueProvider(evaluator, conversionService, parameterProvider); + return new ConverterAwareValueExpressionParameterValueProvider(evaluator, conversionService, parameterProvider); } private boolean isAssignedSeqNo(long seqNo) { @@ -466,7 +495,7 @@ private T readValue(Object value, TypeInformation type) { TypeInformation collectionComponentType = getCollectionComponentType(type); if (collectionComponentType != null) { Object o = read(collectionComponentType, (Map) value); - return getCollectionWithSingleElement(type, collectionComponentType, o); + return (o != null) ? getCollectionWithSingleElement(type, collectionComponentType, o) : null; } return (T) read(type, (Map) value); } else { @@ -475,7 +504,7 @@ private T readValue(Object value, TypeInformation type) { if (collectionComponentType != null && collectionComponentType.isAssignableFrom(TypeInformation.of(value.getClass()))) { Object o = getPotentiallyConvertedSimpleRead(value, collectionComponentType); - return getCollectionWithSingleElement(type, collectionComponentType, o); + return (o != null) ? getCollectionWithSingleElement(type, collectionComponentType, o) : null; } return (T) getPotentiallyConvertedSimpleRead(value, rawType); @@ -493,7 +522,7 @@ private static T getCollectionWithSingleElement(TypeInformation collectio /** * @param type the type to check - * @return true if type is a collectoin, null otherwise, + * @return the collection type if type is a collection, null otherwise, */ @Nullable TypeInformation getCollectionComponentType(TypeInformation type) { @@ -503,17 +532,15 @@ TypeInformation getCollectionComponentType(TypeInformation type) { private Object propertyConverterRead(ElasticsearchPersistentProperty property, Object source) { PropertyValueConverter propertyValueConverter = Objects.requireNonNull(property.getPropertyValueConverter()); - if (source instanceof String[]) { + if (source instanceof String[] strings) { // convert to a List - source = Arrays.asList((String[]) source); + source = Arrays.asList(strings); } - if (source instanceof List) { - source = ((List) source).stream().map(it -> convertOnRead(propertyValueConverter, it)) - .collect(Collectors.toList()); - } else if (source instanceof Set) { - source = ((Set) source).stream().map(it -> convertOnRead(propertyValueConverter, it)) - .collect(Collectors.toSet()); + if (source instanceof List list) { + source = list.stream().map(it -> convertOnRead(propertyValueConverter, it)).collect(Collectors.toList()); + } else if (source instanceof Set set) { + source = set.stream().map(it -> convertOnRead(propertyValueConverter, it)).collect(Collectors.toSet()); } else { source = convertOnRead(propertyValueConverter, source); } @@ -611,9 +638,10 @@ private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullab * but will be removed from spring-data-commons, so we do it here */ @Nullable - private Object convertFromCollectionToObject(Object value, @Nullable Class target) { + private Object convertFromCollectionToObject(Object value, Class target) { if (value.getClass().isArray()) { + // noinspection ArraysAsListWithZeroOrOneArgument value = Arrays.asList(value); } @@ -624,17 +652,32 @@ private Object convertFromCollectionToObject(Object value, @Nullable Class ta return conversionService.convert(value, target); } - private void populateScriptFields(ElasticsearchPersistentEntity entity, T result, + /** + * Checks if any of the properties of the entity is annotated with + * + * @{@link ScriptedField}. If so, the value of this property is set from the returned fields in the document. + * @param entity the entity to defining the persistent property + * @param result the rsult to populate + * @param searchDocument the search result caontaining the fields + * @param the result type + */ + private void populateScriptedFields(ElasticsearchPersistentEntity entity, T result, SearchDocument searchDocument) { Map> fields = searchDocument.getFields(); entity.doWithProperties((SimplePropertyHandler) property -> { - if (property.isAnnotationPresent(ScriptedField.class) && fields.containsKey(property.getName())) { + if (property.isAnnotationPresent(ScriptedField.class)) { ScriptedField scriptedField = property.findAnnotation(ScriptedField.class); // noinspection ConstantConditions String name = scriptedField.name().isEmpty() ? property.getName() : scriptedField.name(); - Object value = searchDocument.getFieldValue(name); - - entity.getPropertyAccessor(result).setProperty(property, value); + if (fields.containsKey(name)) { + if (property.isCollectionLike()) { + List values = searchDocument.getFieldValues(name); + entity.getPropertyAccessor(result).setProperty(property, values); + } else { + Object value = searchDocument.getFieldValue(name); + entity.getPropertyAccessor(result).setProperty(property, value); + } + } } }); } @@ -663,9 +706,9 @@ private ElasticsearchPersistentEntity computeClosestEntity(ElasticsearchPersi class ElasticsearchPropertyValueProvider implements PropertyValueProvider { final MapValueAccessor accessor; - final SpELExpressionEvaluator evaluator; + final ValueExpressionEvaluator evaluator; - ElasticsearchPropertyValueProvider(MapValueAccessor accessor, SpELExpressionEvaluator evaluator) { + ElasticsearchPropertyValueProvider(MapValueAccessor accessor, ValueExpressionEvaluator evaluator) { this.accessor = accessor; this.evaluator = evaluator; } @@ -685,33 +728,29 @@ public T getPropertyValue(ElasticsearchPersistentProperty property) { } /** - * Extension of {@link SpELExpressionParameterValueProvider} to recursively trigger value conversion on the raw + * Extension of {@link ValueExpressionParameterValueProvider} to recursively trigger value conversion on the raw * resolved SpEL value. * * @author Mark Paluch */ - private class ConverterAwareSpELExpressionParameterValueProvider - extends SpELExpressionParameterValueProvider { + private class ConverterAwareValueExpressionParameterValueProvider + extends ValueExpressionParameterValueProvider { /** - * Creates a new {@link ConverterAwareSpELExpressionParameterValueProvider}. + * Creates a new {@link ConverterAwareValueExpressionParameterValueProvider}. * * @param evaluator must not be {@literal null}. * @param conversionService must not be {@literal null}. * @param delegate must not be {@literal null}. */ - public ConverterAwareSpELExpressionParameterValueProvider(SpELExpressionEvaluator evaluator, + public ConverterAwareValueExpressionParameterValueProvider(ValueExpressionEvaluator evaluator, ConversionService conversionService, ParameterValueProvider delegate) { super(evaluator, conversionService, delegate); } - /* - * (non-Javadoc) - * @see org.springframework.data.mapping.model.SpELExpressionParameterValueProvider#potentiallyConvertSpelValue(java.lang.Object, org.springframework.data.mapping.PreferredConstructor.Parameter) - */ @Override - protected T potentiallyConvertSpelValue(Object object, + protected T potentiallyConvertExpressionValue(Object object, Parameter parameter) { return readValue(object, parameter.getType()); } @@ -988,12 +1027,8 @@ private void writeProperties(ElasticsearchPersistentEntity entity, Persistent private static boolean hasEmptyValue(Object value) { - if (value instanceof String s && s.isEmpty() || value instanceof Collection c && c.isEmpty() - || value instanceof Map m && m.isEmpty()) { - return true; - } - - return false; + return value instanceof String s && s.isEmpty() || value instanceof Collection c && c.isEmpty() + || value instanceof Map m && m.isEmpty(); } @SuppressWarnings("unchecked") @@ -1212,7 +1247,7 @@ public void updateQuery(Query query, @Nullable Class domainClass) { return; } - updatePropertiesInFieldsAndSourceFilter(query, domainClass); + updatePropertiesInFieldsSortAndSourceFilter(query, domainClass); if (query instanceof CriteriaQuery criteriaQuery) { updatePropertiesInCriteriaQuery(criteriaQuery, domainClass); @@ -1223,7 +1258,14 @@ public void updateQuery(Query query, @Nullable Class domainClass) { } } - private void updatePropertiesInFieldsAndSourceFilter(Query query, Class domainClass) { + /** + * replaces the names of fields in the query, the sort or soucre filters with the field names used in Elasticsearch + * when they are defined on the ElasticsearchProperties + * + * @param query the query to process + * @param domainClass the domain class (persistent entity) + */ + private void updatePropertiesInFieldsSortAndSourceFilter(Query query, Class domainClass) { ElasticsearchPersistentEntity persistentEntity = mappingContext.getPersistentEntity(domainClass); @@ -1231,12 +1273,12 @@ private void updatePropertiesInFieldsAndSourceFilter(Query query, Class domai List fields = query.getFields(); if (!fields.isEmpty()) { - query.setFields(updateFieldNames(fields, persistentEntity)); + query.setFields(propertyToFieldNames(fields, persistentEntity)); } List storedFields = query.getStoredFields(); if (!CollectionUtils.isEmpty(storedFields)) { - query.setStoredFields(updateFieldNames(storedFields, persistentEntity)); + query.setStoredFields(propertyToFieldNames(storedFields, persistentEntity)); } SourceFilter sourceFilter = query.getSourceFilter(); @@ -1247,37 +1289,60 @@ private void updatePropertiesInFieldsAndSourceFilter(Query query, Class domai String[] excludes = null; if (sourceFilter.getIncludes() != null) { - includes = updateFieldNames(Arrays.asList(sourceFilter.getIncludes()), persistentEntity) + includes = propertyToFieldNames(Arrays.asList(sourceFilter.getIncludes()), persistentEntity) .toArray(new String[] {}); } if (sourceFilter.getExcludes() != null) { - excludes = updateFieldNames(Arrays.asList(sourceFilter.getExcludes()), persistentEntity) + excludes = propertyToFieldNames(Arrays.asList(sourceFilter.getExcludes()), persistentEntity) .toArray(new String[] {}); } - query.addSourceFilter(new FetchSourceFilter(includes, excludes)); + query.addSourceFilter(new FetchSourceFilter(sourceFilter.fetchSource(), includes, excludes)); + } + + if (query.getSort() != null) { + var sort = query.getSort(); + // stream the orders and map them to a new order with the changed names, + // then replace the existing sort with a new sort containing the new orders. + var newOrders = sort.stream().map(order -> { + var fieldNames = updateFieldNames(order.getProperty(), persistentEntity); + + if (order instanceof Order springDataElasticsearchOrder) { + return springDataElasticsearchOrder.withProperty(fieldNames); + } else { + return new Sort.Order(order.getDirection(), + fieldNames, + order.isIgnoreCase(), + order.getNullHandling()); + } + }).toList(); + + if (query instanceof BaseQuery baseQuery) { + baseQuery.setSort(Sort.by(newOrders)); + } } } } /** - * relaces the fieldName with the property name of a property of the persistentEntity with the corresponding - * fieldname. If no such property exists, the original fieldName is kept. + * replaces property name of a property of the persistentEntity with the corresponding fieldname. If no such property + * exists, the original fieldName is kept. * - * @param fieldNames list of fieldnames + * @param propertyNames list of fieldnames * @param persistentEntity the persistent entity to check * @return an updated list of field names */ - private List updateFieldNames(List fieldNames, ElasticsearchPersistentEntity persistentEntity) { - return fieldNames.stream().map(fieldName -> updateFieldName(persistentEntity, fieldName)) + private List propertyToFieldNames(List propertyNames, + ElasticsearchPersistentEntity persistentEntity) { + return propertyNames.stream().map(propertyName -> propertyToFieldName(persistentEntity, propertyName)) .collect(Collectors.toList()); } @NotNull - private String updateFieldName(ElasticsearchPersistentEntity persistentEntity, String fieldName) { - ElasticsearchPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(fieldName); - return persistentProperty != null ? persistentProperty.getFieldName() : fieldName; + private String propertyToFieldName(ElasticsearchPersistentEntity persistentEntity, String propertyName) { + ElasticsearchPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(propertyName); + return persistentProperty != null ? persistentProperty.getFieldName() : propertyName; } private void updatePropertiesInCriteriaQuery(CriteriaQuery criteriaQuery, Class domainClass) { @@ -1307,53 +1372,21 @@ private void updatePropertiesInCriteria(Criteria criteria, ElasticsearchPersiste return; } - String[] fieldNames = field.getName().split("\\."); - - ElasticsearchPersistentEntity currentEntity = persistentEntity; - ElasticsearchPersistentProperty persistentProperty = null; - int propertyCount = 0; - boolean isNested = false; - - for (int i = 0; i < fieldNames.length; i++) { - persistentProperty = currentEntity.getPersistentProperty(fieldNames[i]); - - if (persistentProperty != null) { - propertyCount++; - fieldNames[i] = persistentProperty.getFieldName(); - - org.springframework.data.elasticsearch.annotations.Field fieldAnnotation = persistentProperty - .findAnnotation(org.springframework.data.elasticsearch.annotations.Field.class); - - if (fieldAnnotation != null && fieldAnnotation.type() == FieldType.Nested) { - isNested = true; - } - - try { - currentEntity = mappingContext.getPersistentEntity(persistentProperty.getActualType()); - } catch (Exception e) { - // using system types like UUIDs will lead to java.lang.reflect.InaccessibleObjectException in JDK 16 - // so if we cannot get an entity here, bail out. - currentEntity = null; - } - } - - if (currentEntity == null) { - break; - } - } + var propertyNamesUpdate = updatePropertyNames(persistentEntity, field.getName()); + var fieldNames = propertyNamesUpdate.names(); field.setName(String.join(".", fieldNames)); - if (propertyCount > 1 && isNested) { + if (propertyNamesUpdate.propertyCount() > 1 && propertyNamesUpdate.nestedProperty()) { List propertyNames = Arrays.asList(fieldNames); - field.setPath(String.join(".", propertyNames.subList(0, propertyCount - 1))); + field.setPath(String.join(".", propertyNames.subList(0, propertyNamesUpdate.propertyCount - 1))); } - if (persistentProperty != null) { + if (propertyNamesUpdate.persistentProperty != null) { - if (persistentProperty.hasPropertyValueConverter()) { + if (propertyNamesUpdate.persistentProperty.hasPropertyValueConverter()) { PropertyValueConverter propertyValueConverter = Objects - .requireNonNull(persistentProperty.getPropertyValueConverter()); + .requireNonNull(propertyNamesUpdate.persistentProperty.getPropertyValueConverter()); criteria.getQueryCriteriaEntries().forEach(criteriaEntry -> { if (criteriaEntry.getKey().hasValue()) { @@ -1372,7 +1405,7 @@ private void updatePropertiesInCriteria(Criteria criteria, ElasticsearchPersiste }); } - org.springframework.data.elasticsearch.annotations.Field fieldAnnotation = persistentProperty + org.springframework.data.elasticsearch.annotations.Field fieldAnnotation = propertyNamesUpdate.persistentProperty .findAnnotation(org.springframework.data.elasticsearch.annotations.Field.class); if (fieldAnnotation != null) { @@ -1381,34 +1414,74 @@ private void updatePropertiesInCriteria(Criteria criteria, ElasticsearchPersiste } } + static record PropertyNamesUpdate( + String[] names, + Boolean nestedProperty, + Integer propertyCount, + ElasticsearchPersistentProperty persistentProperty) { + } + @Override public String updateFieldNames(String propertyPath, ElasticsearchPersistentEntity persistentEntity) { Assert.notNull(propertyPath, "propertyPath must not be null"); Assert.notNull(persistentEntity, "persistentEntity must not be null"); - var properties = propertyPath.split("\\.", 2); + var propertyNamesUpdate = updatePropertyNames(persistentEntity, propertyPath); + return String.join(".", propertyNamesUpdate.names()); + } - if (properties.length > 0) { - var propertyName = properties[0]; - var fieldName = updateFieldName(persistentEntity, propertyName); + /** + * Parse a propertyPath and replace the path values with the field names from a persistentEntity. path entries not + * found in the entity are kept as they are. + * + * @return the eventually modified names, a flag if a nested entity was encountered the number of processed + * propertiesand the last processed PersistentProperty. + */ + PropertyNamesUpdate updatePropertyNames(ElasticsearchPersistentEntity persistentEntity, String propertyPath) { - if (properties.length > 1) { - var persistentProperty = persistentEntity.getPersistentProperty(propertyName); - return (persistentProperty != null) - ? fieldName + "." + updateFieldNames(properties[1], mappingContext.getPersistentEntity(persistentProperty)) - : fieldName; - } else { - return fieldName; + String[] propertyNames = propertyPath.split("\\."); + String[] fieldNames = Arrays.copyOf(propertyNames, propertyNames.length); + + ElasticsearchPersistentEntity currentEntity = persistentEntity; + ElasticsearchPersistentProperty persistentProperty = null; + + int propertyCount = 0; + boolean isNested = false; + + for (int i = 0; i < propertyNames.length; i++) { + persistentProperty = currentEntity.getPersistentProperty(propertyNames[i]); + + if (persistentProperty != null) { + propertyCount++; + fieldNames[i] = persistentProperty.getFieldName(); + + org.springframework.data.elasticsearch.annotations.Field fieldAnnotation = persistentProperty + .findAnnotation(org.springframework.data.elasticsearch.annotations.Field.class); + + if (fieldAnnotation != null && fieldAnnotation.type() == FieldType.Nested) { + isNested = true; + } + + try { + currentEntity = mappingContext.getPersistentEntity(persistentProperty.getActualType()); + } catch (Exception e) { + // using system types like UUIDs will lead to java.lang.reflect.InaccessibleObjectException in JDK 16 + // so if we cannot get an entity here, bail out. + currentEntity = null; + } + } + + if (currentEntity == null) { + break; } - } else { - return propertyPath; } + return new PropertyNamesUpdate(fieldNames, isNested, propertyCount, persistentProperty); } - // endregion + @SuppressWarnings("ClassCanBeRecord") static class MapValueAccessor { final Map target; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/NumberRangePropertyValueConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/NumberRangePropertyValueConverter.java index 3e896258ab..6a47f37f23 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/NumberRangePropertyValueConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/NumberRangePropertyValueConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/core/convert/TemporalPropertyValueConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/TemporalPropertyValueConverter.java index 5bcc88d84a..efc2ab53bb 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/TemporalPropertyValueConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/TemporalPropertyValueConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/core/convert/TemporalRangePropertyValueConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/TemporalRangePropertyValueConverter.java index ce9ae6d2e8..f41107922d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/TemporalRangePropertyValueConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/TemporalRangePropertyValueConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/core/convert/package-info.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/package-info.java index 5ce581e585..88db6c0aa1 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/package-info.java @@ -1,3 +1,2 @@ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.core.convert; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/document/Document.java b/src/main/java/org/springframework/data/elasticsearch/core/document/Document.java index 8ea130ac68..db17704d07 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/document/Document.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/document/Document.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -18,24 +18,20 @@ import java.io.IOException; import java.util.LinkedHashMap; import java.util.Map; -import java.util.function.BooleanSupplier; import java.util.function.Function; -import java.util.function.IntSupplier; -import java.util.function.LongSupplier; -import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.convert.ConversionException; import org.springframework.data.elasticsearch.support.StringObjectMap; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** - * A representation of a Elasticsearch document as extended {@link StringObjectMap Map}. All iterators preserve original - * insertion order. + * A representation of an Elasticsearch document as extended {@link StringObjectMap Map}. All iterators preserve + * original insertion order. *

* Document does not allow {@code null} keys. It allows {@literal null} values. *

- * Implementing classes can bei either mutable or immutable. In case a subclass is immutable, its methods may throw + * Implementing classes can be either mutable or immutable. In case a subclass is immutable, its methods may throw * {@link UnsupportedOperationException} when calling modifying methods. * * @author Mark Paluch @@ -60,7 +56,7 @@ static Document create() { * @param map source map containing key-value pairs and sub-documents. must not be {@literal null}. * @return a new {@link Document}. */ - static Document from(Map map) { + static Document from(Map map) { Assert.notNull(map, "Map must not be null"); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/document/Explanation.java b/src/main/java/org/springframework/data/elasticsearch/core/document/Explanation.java index c8ca1cfea6..5e62be4c95 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/document/Explanation.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/document/Explanation.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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. @@ -18,7 +18,7 @@ import java.util.List; import java.util.Objects; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/data/elasticsearch/core/document/MapDocument.java b/src/main/java/org/springframework/data/elasticsearch/core/document/MapDocument.java index 2041a37871..3b435e39b2 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/document/MapDocument.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/document/MapDocument.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -21,10 +21,9 @@ import java.util.Set; import java.util.function.BiConsumer; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.support.DefaultStringObjectMap; -import org.springframework.data.elasticsearch.support.StringObjectMap; import org.springframework.data.mapping.MappingException; -import org.springframework.lang.Nullable; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/document/NestedMetaData.java b/src/main/java/org/springframework/data/elasticsearch/core/document/NestedMetaData.java index e835f14aba..727e9a6a41 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/document/NestedMetaData.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/document/NestedMetaData.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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,7 +15,7 @@ */ package org.springframework.data.elasticsearch.core.document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * meta data returned for nested inner hits. diff --git a/src/main/java/org/springframework/data/elasticsearch/core/document/SearchDocument.java b/src/main/java/org/springframework/data/elasticsearch/core/document/SearchDocument.java index 0dc459e91d..b51c028e77 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/document/SearchDocument.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/document/SearchDocument.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -18,7 +18,7 @@ import java.util.List; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Extension to {@link Document} exposing search response related data. @@ -57,6 +57,20 @@ default V getFieldValue(final String name) { return (V) values.get(0); } + /** + * @param name the field name + * @param the type of elements + * @return the values of the given field. + */ + @Nullable + default List getFieldValues(final String name) { + List values = getFields().get(name); + if (values == null) { + return null; + } + return (List) values; + } + /** * @return the sort values for the search hit */ @@ -111,5 +125,5 @@ default String getRouting() { * @return the matched queries for the SearchHit. */ @Nullable - List getMatchedQueries(); + Map getMatchedQueries(); } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/document/SearchDocumentAdapter.java b/src/main/java/org/springframework/data/elasticsearch/core/document/SearchDocumentAdapter.java index faf06e95e3..4a1bf2f35c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/document/SearchDocumentAdapter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/document/SearchDocumentAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,7 +22,7 @@ import java.util.Set; import java.util.function.BiConsumer; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@link SearchDocument} implementation using a {@link Document} delegate. @@ -41,12 +41,12 @@ public class SearchDocumentAdapter implements SearchDocument { private final Map innerHits = new HashMap<>(); @Nullable private final NestedMetaData nestedMetaData; @Nullable private final Explanation explanation; - @Nullable private final List matchedQueries; + @Nullable private final Map matchedQueries; @Nullable private final String routing; public SearchDocumentAdapter(Document delegate, float score, Object[] sortValues, Map> fields, Map> highlightFields, Map innerHits, - @Nullable NestedMetaData nestedMetaData, @Nullable Explanation explanation, @Nullable List matchedQueries, + @Nullable NestedMetaData nestedMetaData, @Nullable Explanation explanation, @Nullable Map matchedQueries, @Nullable String routing) { this.delegate = delegate; @@ -249,7 +249,7 @@ public Explanation getExplanation() { @Override @Nullable - public List getMatchedQueries() { + public Map getMatchedQueries() { return matchedQueries; } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/document/SearchDocumentResponse.java b/src/main/java/org/springframework/data/elasticsearch/core/document/SearchDocumentResponse.java index adac6231e0..a4256f3300 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/document/SearchDocumentResponse.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/document/SearchDocumentResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -15,18 +15,22 @@ */ package org.springframework.data.elasticsearch.core.document; +import java.time.Duration; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.AggregationsContainer; +import org.springframework.data.elasticsearch.core.SearchShardStatistics; import org.springframework.data.elasticsearch.core.suggest.response.Suggest; -import org.springframework.lang.Nullable; /** * This represents the complete search response from Elasticsearch, including the returned documents. * * @author Peter-Josef Meisch + * @author Haibo Liu + * @author Mohamed El Harrougui * @since 4.0 */ public class SearchDocumentResponse { @@ -34,24 +38,29 @@ public class SearchDocumentResponse { private final long totalHits; private final String totalHitsRelation; private final float maxScore; + private final Duration executionDuration; @Nullable private final String scrollId; private final List searchDocuments; @Nullable private final AggregationsContainer aggregations; @Nullable private final Suggest suggest; @Nullable String pointInTimeId; + @Nullable private final SearchShardStatistics searchShardStatistics; - public SearchDocumentResponse(long totalHits, String totalHitsRelation, float maxScore, @Nullable String scrollId, - @Nullable String pointInTimeId, List searchDocuments, - @Nullable AggregationsContainer aggregationsContainer, @Nullable Suggest suggest) { + public SearchDocumentResponse(long totalHits, String totalHitsRelation, float maxScore, Duration executionDuration, + @Nullable String scrollId, @Nullable String pointInTimeId, List searchDocuments, + @Nullable AggregationsContainer aggregationsContainer, @Nullable Suggest suggest, + @Nullable SearchShardStatistics searchShardStatistics) { this.totalHits = totalHits; this.totalHitsRelation = totalHitsRelation; this.maxScore = maxScore; + this.executionDuration = executionDuration; this.scrollId = scrollId; this.pointInTimeId = pointInTimeId; this.searchDocuments = searchDocuments; this.aggregations = aggregationsContainer; this.suggest = suggest; + this.searchShardStatistics = searchShardStatistics; } public long getTotalHits() { @@ -66,6 +75,10 @@ public float getMaxScore() { return maxScore; } + public Duration getExecutionDuration() { + return executionDuration; + } + @Nullable public String getScrollId() { return scrollId; @@ -93,6 +106,11 @@ public String getPointInTimeId() { return pointInTimeId; } + @Nullable + public SearchShardStatistics getSearchShardStatistics() { + return searchShardStatistics; + } + /** * A function to convert a {@link SearchDocument} async into an entity. Asynchronous so that it can be used from the * imperative and the reactive code. diff --git a/src/main/java/org/springframework/data/elasticsearch/core/document/package-info.java b/src/main/java/org/springframework/data/elasticsearch/core/document/package-info.java index 03bee729ba..514f80ad17 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/document/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/document/package-info.java @@ -1,6 +1,5 @@ /** * Classes related to the Document structure of Elasticsearch documents and search responses. */ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.core.document; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/event/AfterConvertCallback.java b/src/main/java/org/springframework/data/elasticsearch/core/event/AfterConvertCallback.java index 06c009e9ce..01c185d6cf 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/event/AfterConvertCallback.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/event/AfterConvertCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/core/event/AfterLoadCallback.java b/src/main/java/org/springframework/data/elasticsearch/core/event/AfterLoadCallback.java index 7827a30128..5d898f3556 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/event/AfterLoadCallback.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/event/AfterLoadCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/core/event/AfterSaveCallback.java b/src/main/java/org/springframework/data/elasticsearch/core/event/AfterSaveCallback.java index 542cee3e91..37b41fc2a2 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/event/AfterSaveCallback.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/event/AfterSaveCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/core/event/AuditingEntityCallback.java b/src/main/java/org/springframework/data/elasticsearch/core/event/AuditingEntityCallback.java index 1dc73c2eb4..26caf0f32f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/event/AuditingEntityCallback.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/event/AuditingEntityCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/core/event/BeforeConvertCallback.java b/src/main/java/org/springframework/data/elasticsearch/core/event/BeforeConvertCallback.java index 6f0f3d19b1..52f0ca3d94 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/event/BeforeConvertCallback.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/event/BeforeConvertCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -31,7 +31,7 @@ public interface BeforeConvertCallback extends EntityCallback { /** * Callback method that will be invoked before an entity is persisted. Can return the same or a different instance of * the domain entity class. - * + * * @param entity the entity being converted * @param index must not be {@literal null}. * @return the entity to be converted diff --git a/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveAfterConvertCallback.java b/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveAfterConvertCallback.java index 57382c33d4..9a4273d717 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveAfterConvertCallback.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveAfterConvertCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/core/event/ReactiveAfterLoadCallback.java b/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveAfterLoadCallback.java index fe716d9362..1e73244dff 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveAfterLoadCallback.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveAfterLoadCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/core/event/ReactiveAfterSaveCallback.java b/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveAfterSaveCallback.java index cfd6fde952..5a9c6dfe59 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveAfterSaveCallback.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveAfterSaveCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/core/event/ReactiveAuditingEntityCallback.java b/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveAuditingEntityCallback.java index 3bdec2eac9..4a471193fc 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveAuditingEntityCallback.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveAuditingEntityCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/core/event/ReactiveBeforeConvertCallback.java b/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveBeforeConvertCallback.java index 1f9928fe3b..48df60a780 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveBeforeConvertCallback.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveBeforeConvertCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -32,7 +32,7 @@ public interface ReactiveBeforeConvertCallback extends EntityCallback { /** * Callback method that will be invoked before an entity is persisted. Can return the same or a different instance of * the domain entity class. - * + * * @param entity the entity being converted * @param index must not be {@literal null}. * @return the entity to be converted diff --git a/src/main/java/org/springframework/data/elasticsearch/core/event/package-info.java b/src/main/java/org/springframework/data/elasticsearch/core/event/package-info.java index a48d01eb0a..f596552bf1 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/event/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/event/package-info.java @@ -1,6 +1,5 @@ /** * classes and interfaces related to Spring Data Elasticsearch events and callbacks. */ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.core.event; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoBox.java b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoBox.java index 030c8f3d11..b1a570ff6e 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoBox.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoBox.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -24,8 +24,8 @@ */ public class GeoBox { - private GeoPoint topLeft; - private GeoPoint bottomRight; + private final GeoPoint topLeft; + private final GeoPoint bottomRight; public GeoBox(GeoPoint topLeft, GeoPoint bottomRight) { this.topLeft = topLeft; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJson.java b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJson.java index a400ff710f..02494d190c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJson.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJson.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2023 the original author or authors. + * Copyright 2015-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,7 +21,7 @@ /** * Interface definition for structures defined in GeoJSON - * format. copied from Spring Data Mongodb + * format. copied from Spring Data Mongodb * * @author Christoph Strobl * @since 1.7 diff --git a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonGeometryCollection.java b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonGeometryCollection.java index 256498a27f..ef67ec2c95 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonGeometryCollection.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonGeometryCollection.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2023 the original author or authors. + * Copyright 2015-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/core/geo/GeoJsonLineString.java b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonLineString.java index abb942d46e..bea76baf60 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonLineString.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonLineString.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -69,7 +69,7 @@ public static GeoJsonLineString of(Point first, Point second, Point... others) { Assert.notNull(second, "Second point must not be null!"); Assert.notNull(others, "Additional points must not be null!"); - List points = new ArrayList<>(); + List points = new ArrayList<>(2 + others.length); points.add(first); points.add(second); points.addAll(Arrays.asList(others)); @@ -103,7 +103,7 @@ public static GeoJsonLineString of(GeoPoint first, GeoPoint second, GeoPoint... Assert.notNull(second, "Second point must not be null!"); Assert.notNull(others, "Additional points must not be null!"); - List points = new ArrayList<>(); + List points = new ArrayList<>(2 + others.length); points.add(GeoPoint.toPoint(first)); points.add(GeoPoint.toPoint(second)); points.addAll(Arrays.stream(others).map(GeoPoint::toPoint).collect(Collectors.toList())); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiLineString.java b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiLineString.java index d6f255bf0d..dec613fa90 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiLineString.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiLineString.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2023 the original author or authors. + * Copyright 2015-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,7 +27,7 @@ /** * {@link GeoJsonMultiLineString} is defined as list of {@link GeoJsonLineString}s.
* Copied from Spring Data Mongodb - * + * * @author Christoph Strobl * @author Peter-Josef Meisch * @since 4.1 diff --git a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiPoint.java b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiPoint.java index 56f5334886..410149b1c0 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiPoint.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiPoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2023 the original author or authors. + * Copyright 2015-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. @@ -69,7 +69,7 @@ public static GeoJsonMultiPoint of(Point first, Point second, Point... others) { Assert.notNull(second, "Second point must not be null!"); Assert.notNull(others, "Additional points must not be null!"); - List points = new ArrayList<>(); + List points = new ArrayList<>(2 + others.length); points.add(first); points.add(second); points.addAll(Arrays.asList(others)); @@ -103,7 +103,7 @@ public static GeoJsonMultiPoint of(GeoPoint first, GeoPoint second, GeoPoint... Assert.notNull(second, "Second point must not be null!"); Assert.notNull(others, "Additional points must not be null!"); - List points = new ArrayList<>(); + List points = new ArrayList<>(2 + others.length); points.add(GeoPoint.toPoint(first)); points.add(GeoPoint.toPoint(second)); points.addAll(Arrays.stream(others).map(GeoPoint::toPoint).collect(Collectors.toList())); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiPolygon.java b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiPolygon.java index 3158a27973..bcd46bc6f1 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiPolygon.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiPolygon.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2023 the original author or authors. + * Copyright 2015-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. @@ -33,7 +33,7 @@ public class GeoJsonMultiPolygon implements GeoJson> { public static final String TYPE = "MultiPolygon"; - private List coordinates = new ArrayList<>(); + private final List coordinates = new ArrayList<>(); private GeoJsonMultiPolygon(List polygons) { this.coordinates.addAll(polygons); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonPoint.java b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonPoint.java index 2b855273f8..a7bca7e6e2 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonPoint.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonPoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2023 the original author or authors. + * Copyright 2015-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/core/geo/GeoJsonPolygon.java b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonPolygon.java index 7b7a7d190e..2b8a6d879b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonPolygon.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoJsonPolygon.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2023 the original author or authors. + * Copyright 2015-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. @@ -189,7 +189,13 @@ public List getCoordinates() { @SafeVarargs private static List asList(T first, T second, T third, T fourth, T... others) { - ArrayList result = new ArrayList<>(3 + others.length); + Assert.notNull(first, "First element must not be null!"); + Assert.notNull(second, "Second element must not be null!"); + Assert.notNull(third, "Third element must not be null!"); + Assert.notNull(fourth, "Fourth element must not be null!"); + Assert.notNull(others, "Additional elements must not be null!"); + + ArrayList result = new ArrayList<>(4 + others.length); result.add(first); result.add(second); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoPoint.java b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoPoint.java index 96609bbf05..ade1a5532d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoPoint.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoPoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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/core/geo/package-info.java b/src/main/java/org/springframework/data/elasticsearch/core/geo/package-info.java index bb92b29296..b3dc72af02 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/geo/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/geo/package-info.java @@ -1,3 +1,2 @@ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.core.geo; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/AliasAction.java b/src/main/java/org/springframework/data/elasticsearch/core/index/AliasAction.java index b373acefd1..5297b19823 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/AliasAction.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/AliasAction.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/core/index/AliasActionParameters.java b/src/main/java/org/springframework/data/elasticsearch/core/index/AliasActionParameters.java index b8af9906a9..254b1b8ebc 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/AliasActionParameters.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/AliasActionParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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.core.index; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.query.Query; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/AliasActions.java b/src/main/java/org/springframework/data/elasticsearch/core/index/AliasActions.java index 2a18b46555..cc18d6abad 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/AliasActions.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/AliasActions.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -20,7 +20,7 @@ import java.util.Collections; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Class to define to actions to execute in alias management functions. diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/AliasData.java b/src/main/java/org/springframework/data/elasticsearch/core/index/AliasData.java index 4a43686e28..0625c68949 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/AliasData.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/AliasData.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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.core.index; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.query.Query; -import org.springframework.lang.Nullable; /** * value object to describe alias information. diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/ComponentTemplateRequestData.java b/src/main/java/org/springframework/data/elasticsearch/core/index/ComponentTemplateRequestData.java index ca0ab6d411..169847080d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/ComponentTemplateRequestData.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/ComponentTemplateRequestData.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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. @@ -17,8 +17,8 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.document.Document; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/DeleteComponentTemplateRequest.java b/src/main/java/org/springframework/data/elasticsearch/core/index/DeleteComponentTemplateRequest.java index a1d4b94a98..81e90ef635 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/DeleteComponentTemplateRequest.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/DeleteComponentTemplateRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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/core/index/DeleteIndexTemplateRequest.java b/src/main/java/org/springframework/data/elasticsearch/core/index/DeleteIndexTemplateRequest.java index 866e60c975..60d8f35cbf 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/DeleteIndexTemplateRequest.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/DeleteIndexTemplateRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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/core/index/DeleteTemplateRequest.java b/src/main/java/org/springframework/data/elasticsearch/core/index/DeleteTemplateRequest.java index 9341148153..8d11652e55 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/DeleteTemplateRequest.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/DeleteTemplateRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/core/index/ExistsComponentTemplateRequest.java b/src/main/java/org/springframework/data/elasticsearch/core/index/ExistsComponentTemplateRequest.java index fee29bf7f9..13e42bd353 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/ExistsComponentTemplateRequest.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/ExistsComponentTemplateRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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/core/index/ExistsIndexTemplateRequest.java b/src/main/java/org/springframework/data/elasticsearch/core/index/ExistsIndexTemplateRequest.java index f20dbfa1e1..34da32b46c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/ExistsIndexTemplateRequest.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/ExistsIndexTemplateRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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/core/index/ExistsTemplateRequest.java b/src/main/java/org/springframework/data/elasticsearch/core/index/ExistsTemplateRequest.java index 7bd99e21af..d9119add86 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/ExistsTemplateRequest.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/ExistsTemplateRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/core/index/GeoShapeMappingParameters.java b/src/main/java/org/springframework/data/elasticsearch/core/index/GeoShapeMappingParameters.java index 04136442d2..0abcebc834 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/GeoShapeMappingParameters.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/GeoShapeMappingParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -17,8 +17,8 @@ import java.io.IOException; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.annotations.GeoShapeField; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.fasterxml.jackson.databind.node.ObjectNode; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/GetComponentTemplateRequest.java b/src/main/java/org/springframework/data/elasticsearch/core/index/GetComponentTemplateRequest.java index a737c38ffe..9dbdce161b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/GetComponentTemplateRequest.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/GetComponentTemplateRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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/core/index/GetIndexTemplateRequest.java b/src/main/java/org/springframework/data/elasticsearch/core/index/GetIndexTemplateRequest.java index 08fb51bd63..ebdd1b5a6e 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/GetIndexTemplateRequest.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/GetIndexTemplateRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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/core/index/GetTemplateRequest.java b/src/main/java/org/springframework/data/elasticsearch/core/index/GetTemplateRequest.java index 59d343be93..3df81d560d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/GetTemplateRequest.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/GetTemplateRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/core/index/MappingBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java index fb50007938..a01c9526fe 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 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,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.ClassPathResource; import org.springframework.data.annotation.Transient; import org.springframework.data.elasticsearch.annotations.*; @@ -42,7 +43,6 @@ import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; @@ -69,6 +69,7 @@ * @author Peter-Josef Meisch * @author Xiao Yu * @author Subhobrata Dey + * @author Andriy Redko */ public class MappingBuilder { @@ -129,14 +130,13 @@ public String buildPropertyMapping(Class clazz) throws MappingException { } protected String buildPropertyMapping(ElasticsearchPersistentEntity entity, - @Nullable org.springframework.data.elasticsearch.core.document.Document runtimeFields) { + org.springframework.data.elasticsearch.core.document.@Nullable Document runtimeFields) { InternalBuilder internalBuilder = new InternalBuilder(); return internalBuilder.buildPropertyMapping(entity, runtimeFields); } - @Nullable - private org.springframework.data.elasticsearch.core.document.Document getRuntimeFields( + private org.springframework.data.elasticsearch.core.document.@Nullable Document getRuntimeFields( @Nullable ElasticsearchPersistentEntity entity) { if (entity != null) { @@ -160,7 +160,7 @@ private class InternalBuilder { private String nestedPropertyPrefix = ""; protected String buildPropertyMapping(ElasticsearchPersistentEntity entity, - @Nullable org.springframework.data.elasticsearch.core.document.Document runtimeFields) { + org.springframework.data.elasticsearch.core.document.@Nullable Document runtimeFields) { try { @@ -175,7 +175,9 @@ protected String buildPropertyMapping(ElasticsearchPersistentEntity entity, .findAnnotation(org.springframework.data.elasticsearch.annotations.Document.class); var dynamicMapping = docAnnotation != null ? docAnnotation.dynamic() : null; - mapEntity(objectNode, entity, true, "", false, FieldType.Auto, null, dynamicMapping, runtimeFields); + final FieldType fieldType = FieldType.Auto; + mapEntity(objectNode, entity, true, "", false, fieldType, fieldType.getMappedName(), null, dynamicMapping, + runtimeFields); if (!excludeFromSource.isEmpty()) { ObjectNode sourceNode = objectNode.putObject(SOURCE); @@ -211,11 +213,13 @@ private void writeTypeHintMapping(ObjectNode propertiesNode) throws IOException private void mapEntity(ObjectNode objectNode, @Nullable ElasticsearchPersistentEntity entity, boolean isRootObject, String nestedObjectFieldName, boolean nestedOrObjectField, FieldType fieldType, + String fieldTypeMappedName, @Nullable Field parentFieldAnnotation, @Nullable Dynamic dynamicMapping, @Nullable Document runtimeFields) throws IOException { - if (entity != null && entity.isAnnotationPresent(Mapping.class)) { - Mapping mappingAnnotation = entity.getRequiredAnnotation(Mapping.class); + var mappingAnnotation = entity != null ? entity.findAnnotation(Mapping.class) : null; + + if (mappingAnnotation != null) { if (!mappingAnnotation.enabled()) { objectNode.put(MAPPING_ENABLED, false); @@ -243,7 +247,7 @@ private void mapEntity(ObjectNode objectNode, @Nullable ElasticsearchPersistentE boolean writeNestedProperties = !isRootObject && (isAnyPropertyAnnotatedWithField(entity) || nestedOrObjectField); if (writeNestedProperties) { - String type = nestedOrObjectField ? fieldType.getMappedName() : FieldType.Object.getMappedName(); + String type = nestedOrObjectField ? fieldTypeMappedName : FieldType.Object.getMappedName(); ObjectNode nestedObjectNode = objectMapper.createObjectNode(); nestedObjectNode.put(FIELD_PARAM_TYPE, type); @@ -289,6 +293,16 @@ private void mapEntity(ObjectNode objectNode, @Nullable ElasticsearchPersistentE LOGGER.warn(String.format("error mapping property with name %s", property.getName()), e); } }); + + } + + // write the alias entries after the properties + if (mappingAnnotation != null) { + for (MappingAlias mappingAlias : mappingAnnotation.aliases()) { + var aliasNode = propertiesNode.putObject(mappingAlias.name()); + aliasNode.put(FIELD_PARAM_TYPE, FIELD_PARAM_TYPE_ALIAS); + aliasNode.put(FIELD_PARAM_PATH, mappingAlias.path()); + } } } @@ -334,8 +348,10 @@ private void buildPropertyMapping(ObjectNode propertiesNode, boolean isRootObjec : nestedPropertyPrefix + '.' + property.getFieldName(); Field fieldAnnotation = property.findAnnotation(Field.class); + MultiField multiFieldAnnotation = property.findAnnotation(MultiField.class); - if (fieldAnnotation != null && fieldAnnotation.excludeFromSource()) { + if ((fieldAnnotation != null && fieldAnnotation.excludeFromSource()) || + multiFieldAnnotation != null && multiFieldAnnotation.mainField().excludeFromSource()) { excludeFromSource.add(nestedPropertyPath); } @@ -359,15 +375,13 @@ private void buildPropertyMapping(ObjectNode propertiesNode, boolean isRootObjec nestedPropertyPrefix = nestedPropertyPath; mapEntity(propertiesNode, persistentEntity, false, property.getFieldName(), true, fieldAnnotation.type(), - fieldAnnotation, dynamicMapping, null); + getMappedTypeName(fieldAnnotation), fieldAnnotation, dynamicMapping, null); nestedPropertyPrefix = currentNestedPropertyPrefix; return; } } - MultiField multiField = property.findAnnotation(MultiField.class); - if (isCompletionProperty) { CompletionField completionField = property.findAnnotation(CompletionField.class); applyCompletionFieldMapping(propertiesNode, property, completionField); @@ -375,8 +389,8 @@ private void buildPropertyMapping(ObjectNode propertiesNode, boolean isRootObjec if (isRootObject && fieldAnnotation != null && property.isIdProperty()) { applyDefaultIdFieldMapping(propertiesNode, property); - } else if (multiField != null) { - addMultiFieldMapping(propertiesNode, property, multiField, isNestedOrObjectProperty, dynamicMapping); + } else if (multiFieldAnnotation != null) { + addMultiFieldMapping(propertiesNode, property, multiFieldAnnotation, isNestedOrObjectProperty, dynamicMapping); } else if (fieldAnnotation != null) { addSingleFieldMapping(propertiesNode, property, fieldAnnotation, isNestedOrObjectProperty, dynamicMapping); } @@ -432,7 +446,7 @@ private void applyCompletionFieldMapping(ObjectNode propertyNode, ElasticsearchP contextNode.put(FIELD_CONTEXT_NAME, context.name()); contextNode.put(FIELD_CONTEXT_TYPE, context.type().getMappedName()); - if (context.precision().length() > 0) { + if (!context.precision().isEmpty()) { contextNode.put(FIELD_CONTEXT_PRECISION, context.precision()); } @@ -462,7 +476,7 @@ private void applyDisabledPropertyMapping(ObjectNode propertiesNode, Elasticsear } propertiesNode.set(property.getFieldName(), objectMapper.createObjectNode() // - .put(FIELD_PARAM_TYPE, field.type().getMappedName()) // + .put(FIELD_PARAM_TYPE, getMappedTypeName(field)) // .put(MAPPING_ENABLED, false) // ); @@ -471,6 +485,16 @@ private void applyDisabledPropertyMapping(ObjectNode propertiesNode, Elasticsear } } + /** + * Return the mapping type name to be used for the {@link Field} + * + * @param field field to return the mapping type name for + * @return the mapping type name + */ + private String getMappedTypeName(Field field) { + return StringUtils.hasText(field.mappedTypeName()) ? field.mappedTypeName() : field.type().getMappedName(); + } + /** * Add mapping for @Field annotation * diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/MappingParameters.java b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingParameters.java index b202eb7227..b4eab12aff 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/MappingParameters.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -23,16 +23,8 @@ import java.util.List; import java.util.stream.Collectors; -import org.springframework.data.elasticsearch.annotations.DateFormat; -import org.springframework.data.elasticsearch.annotations.Field; -import org.springframework.data.elasticsearch.annotations.FieldType; -import org.springframework.data.elasticsearch.annotations.IndexOptions; -import org.springframework.data.elasticsearch.annotations.IndexPrefixes; -import org.springframework.data.elasticsearch.annotations.InnerField; -import org.springframework.data.elasticsearch.annotations.NullValueType; -import org.springframework.data.elasticsearch.annotations.Similarity; -import org.springframework.data.elasticsearch.annotations.TermVector; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.data.elasticsearch.annotations.*; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -49,6 +41,7 @@ * @author Brian Kimmig * @author Morgan Lutz * @author Sascha Woo + * @author Haibo Liu * @since 4.0 */ public final class MappingParameters { @@ -78,12 +71,18 @@ public final class MappingParameters { static final String FIELD_PARAM_ORIENTATION = "orientation"; static final String FIELD_PARAM_POSITIVE_SCORE_IMPACT = "positive_score_impact"; static final String FIELD_PARAM_DIMS = "dims"; + static final String FIELD_PARAM_ELEMENT_TYPE = "element_type"; + static final String FIELD_PARAM_M = "m"; + static final String FIELD_PARAM_EF_CONSTRUCTION = "ef_construction"; + static final String FIELD_PARAM_CONFIDENCE_INTERVAL = "confidence_interval"; static final String FIELD_PARAM_SCALING_FACTOR = "scaling_factor"; static final String FIELD_PARAM_SEARCH_ANALYZER = "search_analyzer"; static final String FIELD_PARAM_STORE = "store"; static final String FIELD_PARAM_SIMILARITY = "similarity"; static final String FIELD_PARAM_TERM_VECTOR = "term_vector"; static final String FIELD_PARAM_TYPE = "type"; + static final String FIELD_PARAM_PATH = "path"; + static final String FIELD_PARAM_TYPE_ALIAS = "alias"; private final String analyzer; private final boolean coerce; @@ -108,12 +107,16 @@ public final class MappingParameters { private final Integer positionIncrementGap; private final boolean positiveScoreImpact; private final Integer dims; + private final String elementType; + private final KnnSimilarity knnSimilarity; + @Nullable private final KnnIndexOptions knnIndexOptions; private final String searchAnalyzer; private final double scalingFactor; private final String similarity; private final boolean store; private final TermVector termVector; private final FieldType type; + private final String mappedTypeName; /** * extracts the mapping parameters from the relevant annotations. @@ -139,6 +142,7 @@ private MappingParameters(Field field) { store = field.store(); fielddata = field.fielddata(); type = field.type(); + mappedTypeName = StringUtils.hasText(field.mappedTypeName()) ? field.mappedTypeName() : type.getMappedName(); dateFormats = field.format(); dateFormatPatterns = field.pattern(); analyzer = field.analyzer(); @@ -169,9 +173,12 @@ private MappingParameters(Field field) { positiveScoreImpact = field.positiveScoreImpact(); dims = field.dims(); if (type == FieldType.Dense_Vector) { - Assert.isTrue(dims >= 1 && dims <= 2048, - "Invalid required parameter! Dense_Vector value \"dims\" must be between 1 and 2048."); + Assert.isTrue(dims >= 1 && dims <= 4096, + "Invalid required parameter! Dense_Vector value \"dims\" must be between 1 and 4096."); } + elementType = field.elementType(); + knnSimilarity = field.knnSimilarity(); + knnIndexOptions = field.knnIndexOptions().length > 0 ? field.knnIndexOptions()[0] : null; Assert.isTrue(field.enabled() || type == FieldType.Object, "enabled false is only allowed for field type object"); enabled = field.enabled(); eagerGlobalOrdinals = field.eagerGlobalOrdinals(); @@ -182,6 +189,7 @@ private MappingParameters(InnerField field) { store = field.store(); fielddata = field.fielddata(); type = field.type(); + mappedTypeName = StringUtils.hasText(field.mappedTypeName()) ? field.mappedTypeName() : type.getMappedName(); dateFormats = field.format(); dateFormatPatterns = field.pattern(); analyzer = field.analyzer(); @@ -212,9 +220,12 @@ private MappingParameters(InnerField field) { positiveScoreImpact = field.positiveScoreImpact(); dims = field.dims(); if (type == FieldType.Dense_Vector) { - Assert.isTrue(dims >= 1 && dims <= 2048, - "Invalid required parameter! Dense_Vector value \"dims\" must be between 1 and 2048."); + Assert.isTrue(dims >= 1 && dims <= 4096, + "Invalid required parameter! Dense_Vector value \"dims\" must be between 1 and 4096."); } + elementType = field.elementType(); + knnSimilarity = field.knnSimilarity(); + knnIndexOptions = field.knnIndexOptions().length > 0 ? field.knnIndexOptions()[0] : null; enabled = true; eagerGlobalOrdinals = field.eagerGlobalOrdinals(); } @@ -237,7 +248,7 @@ public void writeTypeAndParametersTo(ObjectNode objectNode) throws IOException { } if (type != FieldType.Auto) { - objectNode.put(FIELD_PARAM_TYPE, type.getMappedName()); + objectNode.put(FIELD_PARAM_TYPE, mappedTypeName); if (type == FieldType.Date || type == FieldType.Date_Nanos || type == FieldType.Date_Range) { List formats = new ArrayList<>(); @@ -354,6 +365,48 @@ public void writeTypeAndParametersTo(ObjectNode objectNode) throws IOException { if (type == FieldType.Dense_Vector) { objectNode.put(FIELD_PARAM_DIMS, dims); + + if (!FieldElementType.DEFAULT.equals(elementType)) { + objectNode.put(FIELD_PARAM_ELEMENT_TYPE, elementType); + } + + if (knnSimilarity != KnnSimilarity.DEFAULT) { + objectNode.put(FIELD_PARAM_SIMILARITY, knnSimilarity.getSimilarity()); + } + + if (knnSimilarity != KnnSimilarity.DEFAULT) { + Assert.isTrue(index, "knn similarity can only be specified when 'index' is true."); + objectNode.put(FIELD_PARAM_SIMILARITY, knnSimilarity.getSimilarity()); + } + + if (knnIndexOptions != null) { + Assert.isTrue(index, "knn index options can only be specified when 'index' is true."); + ObjectNode indexOptionsNode = objectNode.putObject(FIELD_PARAM_INDEX_OPTIONS); + KnnAlgorithmType algoType = knnIndexOptions.type(); + if (algoType != KnnAlgorithmType.DEFAULT) { + if (algoType == KnnAlgorithmType.INT8_HNSW || algoType == KnnAlgorithmType.INT8_FLAT) { + Assert.isTrue(!FieldElementType.BYTE.equals(elementType), + "'element_type' can only be float when using vector quantization."); + } + indexOptionsNode.put(FIELD_PARAM_TYPE, algoType.getType()); + } + if (knnIndexOptions.m() >= 0) { + Assert.isTrue(algoType == KnnAlgorithmType.HNSW || algoType == KnnAlgorithmType.INT8_HNSW, + "knn 'm' parameter can only be applicable to hnsw and int8_hnsw index types."); + indexOptionsNode.put(FIELD_PARAM_M, knnIndexOptions.m()); + } + if (knnIndexOptions.efConstruction() >= 0) { + Assert.isTrue(algoType == KnnAlgorithmType.HNSW || algoType == KnnAlgorithmType.INT8_HNSW, + "knn 'ef_construction' can only be applicable to hnsw and int8_hnsw index types."); + indexOptionsNode.put(FIELD_PARAM_EF_CONSTRUCTION, knnIndexOptions.efConstruction()); + } + if (knnIndexOptions.confidenceInterval() >= 0) { + Assert.isTrue(algoType == KnnAlgorithmType.INT8_HNSW + || algoType == KnnAlgorithmType.INT8_FLAT, + "knn 'confidence_interval' can only be applicable to int8_hnsw and int8_flat index types."); + indexOptionsNode.put(FIELD_PARAM_CONFIDENCE_INTERVAL, knnIndexOptions.confidenceInterval()); + } + } } if (!enabled) { diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/PutComponentTemplateRequest.java b/src/main/java/org/springframework/data/elasticsearch/core/index/PutComponentTemplateRequest.java index 8fdbf409c3..9463964ce0 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/PutComponentTemplateRequest.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/PutComponentTemplateRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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. @@ -17,7 +17,7 @@ import java.time.Duration; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** @@ -26,6 +26,7 @@ */ public record PutComponentTemplateRequest(String name, @Nullable Long version, @Nullable Boolean create, @Nullable Duration masterTimeout, ComponentTemplateRequestData template) { + public PutComponentTemplateRequest { Assert.notNull(name, "name must not be null"); Assert.notNull(template, "template must not be null"); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/PutIndexTemplateRequest.java b/src/main/java/org/springframework/data/elasticsearch/core/index/PutIndexTemplateRequest.java index 0783c050db..0ef348f0e1 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/PutIndexTemplateRequest.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/PutIndexTemplateRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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. @@ -17,8 +17,8 @@ import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.document.Document; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/PutTemplateRequest.java b/src/main/java/org/springframework/data/elasticsearch/core/index/PutTemplateRequest.java index fa4e0d455e..20befc1e09 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/PutTemplateRequest.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/PutTemplateRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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,12 +15,12 @@ */ package org.springframework.data.elasticsearch.core.index; +import java.util.Map; + +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.document.Document; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import java.util.Map; - /** * Request to create an index template. This is to create legacy templates (@see * https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates-v1.html) diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/ReactiveMappingBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/index/ReactiveMappingBuilder.java index 47f1aa35c1..87fc7a5c3f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/ReactiveMappingBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/ReactiveMappingBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,13 +19,13 @@ import reactor.core.publisher.Mono; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.annotations.Mapping; import org.springframework.data.elasticsearch.core.ReactiveResourceUtil; import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.mapping.MappingException; -import org.springframework.lang.Nullable; /** * Subclass of {@link MappingBuilder} with specialized methods To inhibit blocking calls diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/Settings.java b/src/main/java/org/springframework/data/elasticsearch/core/index/Settings.java index dfe73c4933..4989146a97 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/Settings.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/Settings.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/core/index/TemplateData.java b/src/main/java/org/springframework/data/elasticsearch/core/index/TemplateData.java index eb45bf608e..642d8dbbf6 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/TemplateData.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/TemplateData.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -17,8 +17,8 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.document.Document; -import org.springframework.lang.Nullable; /** * Data returned for template information retrieval. diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/TemplateResponse.java b/src/main/java/org/springframework/data/elasticsearch/core/index/TemplateResponse.java index 0ed58e4f76..9a38118042 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/TemplateResponse.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/TemplateResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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,7 +15,7 @@ */ package org.springframework.data.elasticsearch.core.index; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** @@ -23,6 +23,7 @@ * @since 5.1 */ public record TemplateResponse(String name, @Nullable Long version, @Nullable TemplateResponseData templateData) { + public TemplateResponse { Assert.notNull(name, "name must not be null"); } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/TemplateResponseData.java b/src/main/java/org/springframework/data/elasticsearch/core/index/TemplateResponseData.java index 925a6f59c0..ff1fc475cb 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/TemplateResponseData.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/TemplateResponseData.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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. @@ -18,8 +18,8 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.document.Document; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/package-info.java b/src/main/java/org/springframework/data/elasticsearch/core/index/package-info.java index 464fa781c6..8317bc819f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/package-info.java @@ -1,6 +1,5 @@ /** * Classes related to Elasticsearch index management. */ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.core.index; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/join/JoinField.java b/src/main/java/org/springframework/data/elasticsearch/core/join/JoinField.java index 08faa94a3b..0cc4a44dcb 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/join/JoinField.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/join/JoinField.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -17,8 +17,8 @@ import java.util.Objects; -import org.springframework.data.annotation.PersistenceConstructor; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.data.annotation.PersistenceCreator; /** * @author Subhobrata Dey @@ -39,7 +39,7 @@ public JoinField(String name) { this(name, null); } - @PersistenceConstructor + @PersistenceCreator public JoinField(String name, @Nullable ID parent) { this.name = name; this.parent = parent; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/Alias.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/Alias.java new file mode 100644 index 0000000000..7dc4d01251 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/Alias.java @@ -0,0 +1,218 @@ +/* + * 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.core.mapping; + +import java.util.Objects; + +import org.jspecify.annotations.Nullable; +import org.springframework.data.elasticsearch.core.query.Query; +import org.springframework.util.Assert; + +/** + * Immutable Value object encapsulating index alias(es). + * + * @author Youssef Aouichaoui + * @since 5.4 + */ +public class Alias { + /** + * Alias name for the index. + */ + private final String alias; + + /** + * Query used to limit documents the alias can access. + */ + @Nullable private final Query filter; + + /** + * Used to route indexing operations to a specific shard. + */ + @Nullable private final String indexRouting; + + /** + * Used to route search operations to a specific shard. + */ + @Nullable private final String searchRouting; + + /** + * Used to route indexing and search operations to a specific shard. + */ + @Nullable private final String routing; + + /** + * The alias is hidden? By default, this is set to {@code false}. + */ + @Nullable private final Boolean isHidden; + + /** + * The index is the 'write index' for the alias? By default, this is set to {@code false}. + */ + @Nullable private final Boolean isWriteIndex; + + private Alias(Builder builder) { + this.alias = builder.alias; + + this.filter = builder.filter; + + this.indexRouting = builder.indexRouting; + this.searchRouting = builder.searchRouting; + this.routing = builder.routing; + + this.isHidden = builder.isHidden; + this.isWriteIndex = builder.isWriteIndex; + } + + public String getAlias() { + return alias; + } + + @Nullable + public Query getFilter() { + return filter; + } + + @Nullable + public String getIndexRouting() { + return indexRouting; + } + + @Nullable + public String getSearchRouting() { + return searchRouting; + } + + @Nullable + public String getRouting() { + return routing; + } + + @Nullable + public Boolean getHidden() { + return isHidden; + } + + @Nullable + public Boolean getWriteIndex() { + return isWriteIndex; + } + + public static Builder builder(String alias) { + return new Builder(alias); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof Alias that)) + return false; + + return Objects.equals(alias, that.alias) && Objects.equals(filter, that.filter) + && Objects.equals(indexRouting, that.indexRouting) + && Objects.equals(searchRouting, that.searchRouting) + && Objects.equals(routing, that.routing) + && Objects.equals(isHidden, that.isHidden) + && Objects.equals(isWriteIndex, that.isWriteIndex); + } + + @Override + public int hashCode() { + return Objects.hash(alias, filter, indexRouting, searchRouting, routing, isHidden, isWriteIndex); + } + + public static class Builder { + private final String alias; + + @Nullable private Query filter; + + @Nullable private String indexRouting; + @Nullable private String searchRouting; + @Nullable private String routing; + + @Nullable private Boolean isHidden; + @Nullable private Boolean isWriteIndex; + + public Builder(String alias) { + Assert.notNull(alias, "alias must not be null"); + this.alias = alias; + } + + /** + * Query used to limit documents the alias can access. + */ + public Builder withFilter(@Nullable Query filter) { + this.filter = filter; + + return this; + } + + /** + * Used to route indexing operations to a specific shard. + */ + public Builder withIndexRouting(@Nullable String indexRouting) { + if (indexRouting != null && !indexRouting.trim().isEmpty()) { + this.indexRouting = indexRouting; + } + + return this; + } + + /** + * Used to route search operations to a specific shard. + */ + public Builder withSearchRouting(@Nullable String searchRouting) { + if (searchRouting != null && !searchRouting.trim().isEmpty()) { + this.searchRouting = searchRouting; + } + + return this; + } + + /** + * Used to route indexing and search operations to a specific shard. + */ + public Builder withRouting(@Nullable String routing) { + if (routing != null && !routing.trim().isEmpty()) { + this.routing = routing; + } + + return this; + } + + /** + * The alias is hidden? By default, this is set to {@code false}. + */ + public Builder withHidden(@Nullable Boolean hidden) { + isHidden = hidden; + + return this; + } + + /** + * The index is the 'write index' for the alias? By default, this is set to {@code false}. + */ + public Builder withWriteIndex(@Nullable Boolean writeIndex) { + isWriteIndex = writeIndex; + + return this; + } + + public Alias build() { + return new Alias(this); + } + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/CreateIndexSettings.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/CreateIndexSettings.java new file mode 100644 index 0000000000..cf5a24c636 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/CreateIndexSettings.java @@ -0,0 +1,114 @@ +/* + * 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.core.mapping; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.jspecify.annotations.Nullable; +import org.springframework.data.elasticsearch.core.document.Document; +import org.springframework.util.Assert; + +/** + * Encapsulating index mapping fields, settings, and index alias(es). + * + * @author Youssef Aouichaoui + * @since 5.3 + */ +public class CreateIndexSettings { + private final IndexCoordinates indexCoordinates; + private final Set aliases; + + @Nullable private final Map settings; + + @Nullable private final Document mapping; + + private CreateIndexSettings(Builder builder) { + this.indexCoordinates = builder.indexCoordinates; + this.aliases = builder.aliases; + + this.settings = builder.settings; + this.mapping = builder.mapping; + } + + public static Builder builder(IndexCoordinates indexCoordinates) { + return new Builder(indexCoordinates); + } + + public IndexCoordinates getIndexCoordinates() { + return indexCoordinates; + } + + public Alias[] getAliases() { + return aliases.toArray(Alias[]::new); + } + + @Nullable + public Map getSettings() { + return settings; + } + + @Nullable + public Document getMapping() { + return mapping; + } + + public static class Builder { + private final IndexCoordinates indexCoordinates; + private final Set aliases = new HashSet<>(); + + @Nullable private Map settings; + + @Nullable private Document mapping; + + public Builder(IndexCoordinates indexCoordinates) { + Assert.notNull(indexCoordinates, "indexCoordinates must not be null"); + this.indexCoordinates = indexCoordinates; + } + + public Builder withAlias(Alias alias) { + Assert.notNull(alias, "alias must not be null"); + this.aliases.add(alias); + + return this; + } + + public Builder withAliases(Set aliases) { + Assert.notNull(aliases, "aliases must not be null"); + this.aliases.addAll(aliases); + + return this; + } + + public Builder withSettings(Map settings) { + Assert.notNull(settings, "settings must not be null"); + this.settings = settings; + + return this; + } + + public Builder withMapping(@Nullable Document mapping) { + this.mapping = mapping; + + return this; + } + + public CreateIndexSettings build() { + return new CreateIndexSettings(this); + } + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentEntity.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentEntity.java index 15583fcd68..1cd45332ba 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentEntity.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -15,6 +15,9 @@ */ package org.springframework.data.elasticsearch.core.mapping; +import java.util.Set; + +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Dynamic; import org.springframework.data.elasticsearch.annotations.Field; @@ -23,7 +26,6 @@ import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.model.FieldNamingStrategy; -import org.springframework.lang.Nullable; /** * ElasticsearchPersistentEntity @@ -42,6 +44,14 @@ public interface ElasticsearchPersistentEntity extends PersistentEntity getAliases(); + short getShards(); short getReplicas(); @@ -60,13 +70,12 @@ public interface ElasticsearchPersistentEntity extends PersistentEntity extends BasicPersistentEntit private @Nullable ElasticsearchPersistentProperty seqNoPrimaryTermProperty; private @Nullable ElasticsearchPersistentProperty joinFieldProperty; private @Nullable ElasticsearchPersistentProperty indexedIndexNameProperty; - private @Nullable Document.VersionType versionType; - private boolean createIndexAndMapping; - private boolean alwaysWriteMapping; + private Document.@Nullable VersionType versionType; + private final boolean createIndexAndMapping; + private final boolean alwaysWriteMapping; private final Dynamic dynamic; private final Map fieldNamePropertyCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap routingExpressions = new ConcurrentHashMap<>(); private @Nullable String routing; private final ContextConfiguration contextConfiguration; + private final Set aliases = new HashSet<>(); private final ConcurrentHashMap indexNameExpressions = new ConcurrentHashMap<>(); private final Lazy indexNameEvaluationContext = Lazy.of(this::getIndexNameEvaluationContext); @@ -112,6 +117,7 @@ public SimpleElasticsearchPersistentEntity(TypeInformation typeInformation, this.dynamic = document.dynamic(); this.storeIdInSource = document.storeIdInSource(); this.storeVersionInSource = document.storeVersionInSource(); + buildAliases(); } else { this.dynamic = Dynamic.INHERIT; this.storeIdInSource = true; @@ -138,6 +144,11 @@ public IndexCoordinates getIndexCoordinates() { return resolve(IndexCoordinates.of(getIndexName())); } + @Override + public Set getAliases() { + return aliases; + } + @Nullable @Override public String getIndexStoreType() { @@ -165,9 +176,8 @@ public String getRefreshInterval() { return settingsParameter.get().refreshIntervall; } - @Nullable @Override - public Document.VersionType getVersionType() { + public Document.@Nullable VersionType getVersionType() { return versionType; } @@ -247,12 +257,12 @@ public void addPersistentProperty(ElasticsearchPersistentProperty property) { if (property.isIndexedIndexNameProperty()) { if (!property.getActualType().isAssignableFrom(String.class)) { - throw new MappingException(String.format("@IndexedIndexName annotation must be put on String property")); + throw new MappingException("@IndexedIndexName annotation must be put on String property"); } if (indexedIndexNameProperty != null) { throw new MappingException( - String.format("@IndexedIndexName annotation can only be put on one property in an entity")); + "@IndexedIndexName annotation can only be put on one property in an entity"); } this.indexedIndexNameProperty = property; @@ -543,9 +553,9 @@ private static class SettingsParameter { @Nullable String refreshIntervall; @Nullable String indexStoreType; @Nullable private String[] sortFields; - @Nullable private Setting.SortOrder[] sortOrders; - @Nullable private Setting.SortMode[] sortModes; - @Nullable private Setting.SortMissing[] sortMissingValues; + private Setting.@Nullable SortOrder[] sortOrders; + private Setting.@Nullable SortMode[] sortModes; + private Setting.@Nullable SortMissing[] sortMissingValues; Settings toSettings() { @@ -615,4 +625,35 @@ public boolean getWriteTypeHints() { public Dynamic dynamic() { return dynamic; } + + /** + * Building once the aliases for the current document. + */ + private void buildAliases() { + // Clear the existing aliases. + aliases.clear(); + + if (document != null) { + for (org.springframework.data.elasticsearch.annotations.Alias alias : document.aliases()) { + if (alias.value().isEmpty()) { + continue; + } + + Query query = null; + if (!alias.filter().value().isEmpty()) { + query = new StringQuery(alias.filter().value()); + } + + aliases.add( + Alias.builder(alias.value()) + .withFilter(query) + .withIndexRouting(alias.indexRouting()) + .withSearchRouting(alias.searchRouting()) + .withRouting(alias.routing()) + .withHidden(alias.isHidden()) + .withWriteIndex(alias.isWriteIndex()) + .build()); + } + } + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java index c73b6110cd..85a1040dc9 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -24,6 +24,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.domain.Range; @@ -58,7 +59,6 @@ import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -98,6 +98,7 @@ public SimpleElasticsearchPersistentProperty(Property property, this.isSeqNoPrimaryTerm = SeqNoPrimaryTerm.class.isAssignableFrom(getRawType()); boolean isField = isAnnotationPresent(Field.class); + boolean isMultiField = isAnnotationPresent(MultiField.class); if (isVersionProperty() && !getType().equals(Long.class)) { throw new MappingException(String.format("Version property %s must be of type Long!", property.getName())); @@ -109,8 +110,10 @@ public SimpleElasticsearchPersistentProperty(Property property, initPropertyValueConverter(); - storeNullValue = isField && getRequiredAnnotation(Field.class).storeNullValue(); - storeEmptyValue = isField ? getRequiredAnnotation(Field.class).storeEmptyValue() : true; + storeNullValue = isField ? getRequiredAnnotation(Field.class).storeNullValue() + : isMultiField && getRequiredAnnotation(MultiField.class).mainField().storeNullValue(); + storeEmptyValue = isField ? getRequiredAnnotation(Field.class).storeEmptyValue() + : !isMultiField || getRequiredAnnotation(MultiField.class).mainField().storeEmptyValue(); } @Override diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/package-info.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/package-info.java index 3c44ffb2cc..d9e5763668 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/package-info.java @@ -1,3 +1,2 @@ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.core.mapping; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/package-info.java b/src/main/java/org/springframework/data/elasticsearch/core/package-info.java index 9c29d17f65..bc7d9b994e 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/package-info.java @@ -1,3 +1,2 @@ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.core; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQuery.java index 9f9462370d..1cb0190b09 100755 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -27,9 +27,9 @@ import java.util.Optional; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQueryBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQueryBuilder.java index 18f17acf1e..540cfc6fa2 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQueryBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/BaseQueryBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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. @@ -23,9 +23,9 @@ import java.util.EnumSet; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -45,7 +45,7 @@ public abstract class BaseQueryBuilder ids = new ArrayList<>(); @Nullable private String route; - @Nullable private Query.SearchType searchType = Query.SearchType.QUERY_THEN_FETCH; + private Query.@Nullable SearchType searchType = Query.SearchType.QUERY_THEN_FETCH; @Nullable private IndicesOptions indicesOptions; private boolean trackScores; @Nullable private String preference; @@ -64,7 +64,7 @@ public abstract class BaseQueryBuilder idsWithRouting = new ArrayList<>(); private final List runtimeFields = new ArrayList<>(); - @Nullable private Query.PointInTime pointInTime; + private Query.@Nullable PointInTime pointInTime; @Nullable private Boolean allowNoIndices; private EnumSet expandWildcards = EnumSet.noneOf(IndicesOptions.WildcardStates.class); @@ -139,8 +139,7 @@ public List getIndicesBoost() { return indicesBoost; } - @Nullable - public Query.SearchType getSearchType() { + public Query.@Nullable SearchType getSearchType() { return searchType; } @@ -193,8 +192,8 @@ public List getRescorerQueries() { /** * @since 5.0 */ - @Nullable - public Query.PointInTime getPointInTime() { + + public Query.@Nullable PointInTime getPointInTime() { return pointInTime; } @@ -342,7 +341,7 @@ public SELF withIndicesBoost(IndexBoost... indicesBoost) { return self(); } - public SELF withSearchType(@Nullable Query.SearchType searchType) { + public SELF withSearchType(Query.@Nullable SearchType searchType) { this.searchType = searchType; return self(); } @@ -426,7 +425,7 @@ public SELF withRescorerQuery(RescorerQuery rescorerQuery) { /** * @since 5.0 */ - public SELF withPointInTime(@Nullable Query.PointInTime pointInTime) { + public SELF withPointInTime(Query.@Nullable PointInTime pointInTime) { this.pointInTime = pointInTime; return self(); } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/BulkOptions.java b/src/main/java/org/springframework/data/elasticsearch/core/query/BulkOptions.java index c696705a10..4e28b9d698 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/BulkOptions.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/BulkOptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -18,10 +18,10 @@ import java.time.Duration; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.ActiveShardCount; import org.springframework.data.elasticsearch.core.RefreshPolicy; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; -import org.springframework.lang.Nullable; /** * Options that may be passed to an diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/ByQueryResponse.java b/src/main/java/org/springframework/data/elasticsearch/core/query/ByQueryResponse.java index ed4368f0ff..334132ab9a 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/ByQueryResponse.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/ByQueryResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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. @@ -18,8 +18,8 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.ElasticsearchErrorCause; -import org.springframework.lang.Nullable; /** * Response of an update by query operation. @@ -167,7 +167,6 @@ public static ByQueryResponseBuilder builder() { public static class Failure { @Nullable private final String index; - @Nullable private final String type; @Nullable private final String id; @Nullable private final Exception cause; @Nullable private final Integer status; @@ -176,11 +175,10 @@ public static class Failure { @Nullable private final Boolean aborted; @Nullable private final ElasticsearchErrorCause elasticsearchErrorCause; - private Failure(@Nullable String index, @Nullable String type, @Nullable String id, @Nullable Exception cause, + private Failure(@Nullable String index, @Nullable String id, @Nullable Exception cause, @Nullable Integer status, @Nullable Long seqNo, @Nullable Long term, @Nullable Boolean aborted, @Nullable ElasticsearchErrorCause elasticsearchErrorCause) { this.index = index; - this.type = type; this.id = id; this.cause = cause; this.status = status; @@ -195,11 +193,6 @@ public String getIndex() { return index; } - @Nullable - public String getType() { - return type; - } - @Nullable public String getId() { return id; @@ -230,11 +223,17 @@ public Boolean getAborted() { return aborted; } + @Nullable + public ElasticsearchErrorCause getElasticsearchErrorCause() { + return elasticsearchErrorCause; + } + /** * Create a new {@link FailureBuilder} to build {@link Failure} * * @return a new {@link FailureBuilder} to build {@link Failure} */ + public static FailureBuilder builder() { return new FailureBuilder(); } @@ -244,7 +243,6 @@ public static FailureBuilder builder() { */ public static final class FailureBuilder { @Nullable private String index; - @Nullable private String type; @Nullable private String id; @Nullable private Exception cause; @Nullable private Integer status; @@ -260,11 +258,6 @@ public FailureBuilder withIndex(String index) { return this; } - public FailureBuilder withType(String type) { - this.type = type; - return this; - } - public FailureBuilder withId(String id) { this.id = id; return this; @@ -301,7 +294,7 @@ public FailureBuilder withErrorCause(ElasticsearchErrorCause elasticsearchErrorC } public Failure build() { - return new Failure(index, type, id, cause, status, seqNo, term, aborted, elasticsearchErrorCause); + return new Failure(index, id, cause, status, seqNo, term, aborted, elasticsearchErrorCause); } } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java b/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java index 8fe8922d3e..00a55116cc 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -22,7 +22,9 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.elasticsearch.core.geo.GeoBox; import org.springframework.data.elasticsearch.core.geo.GeoJson; @@ -30,7 +32,6 @@ import org.springframework.data.geo.Box; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -316,6 +317,9 @@ public Criteria or(Criteria criteria) { Criteria orCriteria = new OrCriteria(this.criteriaChain, criteria.getField()); orCriteria.queryCriteriaEntries.addAll(criteria.queryCriteriaEntries); orCriteria.filterCriteriaEntries.addAll(criteria.filterCriteriaEntries); + orCriteria.subCriteria.addAll(criteria.subCriteria); + orCriteria.boost = criteria.boost; + orCriteria.negating = criteria.isNegating(); return orCriteria; } @@ -670,8 +674,8 @@ public Criteria boundedBy(Box boundingBox) { */ public Criteria boundedBy(String topLeftGeohash, String bottomRightGeohash) { - Assert.isTrue(!StringUtils.isEmpty(topLeftGeohash), "topLeftGeohash must not be empty"); - Assert.isTrue(!StringUtils.isEmpty(bottomRightGeohash), "bottomRightGeohash must not be empty"); + Assert.isTrue(StringUtils.hasLength(topLeftGeohash), "topLeftGeohash must not be empty"); + Assert.isTrue(StringUtils.hasLength(bottomRightGeohash), "bottomRightGeohash must not be empty"); filterCriteriaEntries .add(new CriteriaEntry(OperationKey.BBOX, new Object[] { topLeftGeohash, bottomRightGeohash })); @@ -754,7 +758,7 @@ public Criteria within(Point location, Distance distance) { */ public Criteria within(String geoLocation, String distance) { - Assert.isTrue(!StringUtils.isEmpty(geoLocation), "geoLocation value must not be null"); + Assert.isTrue(StringUtils.hasLength(geoLocation), "geoLocation value must not be null"); filterCriteriaEntries.add(new CriteriaEntry(OperationKey.WITHIN, new Object[] { geoLocation, distance })); return this; @@ -813,6 +817,32 @@ public Criteria contains(GeoJson geoShape) { filterCriteriaEntries.add(new CriteriaEntry(OperationKey.GEO_CONTAINS, geoShape)); return this; } + + /** + * Adds a new filter CriteriaEntry for HAS_CHILD. + * + * @param query the has_child query. + * @return the current Criteria. + */ + public Criteria hasChild(HasChildQuery query) { + Assert.notNull(query, "has_child query must not be null."); + + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.HAS_CHILD, query)); + return this; + } + + /** + * Adds a new filter CriteriaEntry for HAS_PARENT. + * + * @param query the has_parent query. + * @return the current Criteria. + */ + public Criteria hasParent(HasParentQuery query) { + Assert.notNull(query, "has_parent query must not be null."); + + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.HAS_PARENT, query)); + return this; + } // endregion // region helper functions @@ -830,6 +860,7 @@ private List toCollection(Object... values) { // endregion + // region equals/hashcode @Override public boolean equals(Object o) { if (this == o) @@ -845,6 +876,8 @@ public boolean equals(Object o) { return false; if (!Objects.equals(field, criteria.field)) return false; + if (!criteriaChain.filter(this).equals(criteria.criteriaChain.filter(criteria))) + return false; if (!queryCriteriaEntries.equals(criteria.queryCriteriaEntries)) return false; if (!filterCriteriaEntries.equals(criteria.filterCriteriaEntries)) @@ -857,11 +890,16 @@ public int hashCode() { int result = field != null ? field.hashCode() : 0; result = 31 * result + (boost != +0.0f ? Float.floatToIntBits(boost) : 0); result = 31 * result + (negating ? 1 : 0); + // the criteriaChain contains "this" object, so we need to filter it out + // to avoid a stackoverflow here, because the hashcode implementation + // uses the element's hashcodes + result = 31 * result + criteriaChain.filter(this).hashCode(); result = 31 * result + queryCriteriaEntries.hashCode(); result = 31 * result + filterCriteriaEntries.hashCode(); result = 31 * result + subCriteria.hashCode(); return result; } + // endregion @Override public String toString() { @@ -909,7 +947,17 @@ public Operator getOperator() { * * @since 4.1 */ - public static class CriteriaChain extends LinkedList {} + public static class CriteriaChain extends LinkedList { + /** + * return a copy of this list with the given element filtered out. + * + * @param criteria the element to filter + * @return the filtered list + */ + List filter(Criteria criteria) { + return this.stream().filter(c -> c != criteria).collect(Collectors.toList()); + } + } /** * Operator to join the entries of the criteria chain @@ -974,7 +1022,11 @@ public enum OperationKey { // /** * @since 5.1 */ - REGEXP; + REGEXP, + /** + * @since 5.3 + */ + HAS_CHILD, HAS_PARENT; /** * @return true if this key does not have an associated value diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/CriteriaQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/CriteriaQuery.java index 3b09c70758..195d29e0b5 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/CriteriaQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/CriteriaQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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/core/query/CriteriaQueryBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/query/CriteriaQueryBuilder.java index bc820ff719..b2fc5139b8 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/CriteriaQueryBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/CriteriaQueryBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/core/query/DeleteQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/DeleteQuery.java new file mode 100644 index 0000000000..40f75e60b0 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/DeleteQuery.java @@ -0,0 +1,676 @@ +/* + * 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.core.query; + +import java.time.Duration; +import java.util.EnumSet; +import java.util.List; + +import org.jspecify.annotations.Nullable; +import org.springframework.data.domain.Sort; +import org.springframework.data.elasticsearch.core.query.Query.SearchType; +import org.springframework.data.elasticsearch.core.query.types.ConflictsType; +import org.springframework.data.elasticsearch.core.query.types.OperatorType; +import org.springframework.util.Assert; + +/** + * Defines a delete request. + * + * @author Aouichaoui Youssef + * @see docs + * @since 5.3 + */ +public class DeleteQuery { + // For Lucene query + /** + * Query in the Lucene query string syntax. + */ + @Nullable private final String q; + + /** + * If true, wildcard and prefix queries are analyzed. Defaults to false. This parameter can only be used when the + * lucene query {@code q} parameter is specified. + */ + @Nullable private final Boolean analyzeWildcard; + + /** + * Analyzer to use for the query string. This parameter can only be used when the lucene query {@code q} parameter is + * specified. + */ + @Nullable private final String analyzer; + + /** + * The default operator for a query string query: {@literal AND} or {@literal OR}. Defaults to {@literal OR}. This + * parameter can only be used when the lucene query {@code q} parameter is specified. + */ + @Nullable private final OperatorType defaultOperator; + + /** + * Field to be used as the default when no field prefix is specified in the query string. This parameter can only be + * used when the lucene query {@code q} parameter is specified. + *

+ * e.g: {@code {"query":{"prefix":{"user.name":{"value":"es"}}}} } + */ + @Nullable private final String df; + + /** + * If a query contains errors related to the format of the data being entered, they will be disregarded unless + * specified otherwise. By default, this feature is turned off. + */ + @Nullable private final Boolean lenient; + + // For ES query + + /** + * An error will occur if the condition is {@code false} and any of the following are true: a wildcard expression, an + * index alias, or the {@literal _all value} only targets missing or closed indices. By default, this is set to + * {@code true}. + */ + @Nullable private final Boolean allowNoIndices; + + /** + * Define the types of conflicts that occur when a query encounters version conflicts: abort or proceed. Defaults to + * abort. + */ + @Nullable private final ConflictsType conflicts; + + /** + * Type of index that wildcard patterns can match. Defaults to {@literal open}. + */ + @Nullable private final EnumSet expandWildcards; + + /** + * An error occurs if it is directed at an index that is missing or closed when it is {@code false}. By default, this + * is set to {@code false}. + */ + @Nullable private final Boolean ignoreUnavailable; + + /** + * Maximum number of documents to process. Defaults to all documents. + */ + @Nullable private final Long maxDocs; + + /** + * Specifies the node or shard the operation should be performed on. + */ + @Nullable private final String preference; + + /** + * Use the request cache when it is {@code true}. By default, use the index-level setting. + */ + @Nullable private final Boolean requestCache; + + /** + * Refreshes all shards involved in the deleting by query after the request completes when it is {@code true}. By + * default, this is set to {@code false}. + */ + @Nullable private final Boolean refresh; + + /** + * Limited this request to a certain number of sub-requests per second. By default, this is set to {@code -1} (no + * throttle). + */ + @Nullable private final Float requestsPerSecond; + + /** + * Custom value used to route operations to a specific shard. + */ + @Nullable private final String routing; + + /** + * Period to retain the search context for scrolling. + */ + @Nullable private final Duration scroll; + + /** + * Size of the scroll request that powers the operation. By default, this is set to {@code 1000}. + */ + @Nullable private final Long scrollSize; + + /** + * The type of the search operation. + */ + @Nullable private final SearchType searchType; + + /** + * Explicit timeout for each search request. By default, this is set to no timeout. + */ + @Nullable private final Duration searchTimeout; + + /** + * The number of slices this task should be divided into. By default, this is set to {@code 1} meaning the task isn’t + * sliced into subtasks. + */ + @Nullable private final Integer slices; + + /** + * Sort search results in a specific order. + */ + @Nullable private final Sort sort; + + /** + * Specific {@code tag} of the request for logging and statistical purposes. + */ + @Nullable private final List stats; + + /** + * The Maximum number of documents that can be collected for each shard. If a query exceeds this limit, Elasticsearch + * will stop the query. + */ + @Nullable private final Long terminateAfter; + + /** + * Period each deletion request waits for active shards. By default, this is set to {@code 1m} (one minute). + */ + @Nullable private final Duration timeout; + + /** + * Returns the document version as part of a hit. + */ + @Nullable private final Boolean version; + + // Body + /** + * Query that specifies the documents to delete. + */ + private final Query query; + + public static Builder builder(Query query) { + return new Builder(query); + } + + private DeleteQuery(Builder builder) { + this.q = builder.luceneQuery; + this.analyzeWildcard = builder.analyzeWildcard; + this.analyzer = builder.analyzer; + this.defaultOperator = builder.defaultOperator; + this.df = builder.defaultField; + this.lenient = builder.lenient; + + this.allowNoIndices = builder.allowNoIndices; + this.conflicts = builder.conflicts; + this.expandWildcards = builder.expandWildcards; + this.ignoreUnavailable = builder.ignoreUnavailable; + this.maxDocs = builder.maxDocs; + this.preference = builder.preference; + this.requestCache = builder.requestCache; + this.refresh = builder.refresh; + this.requestsPerSecond = builder.requestsPerSecond; + this.routing = builder.routing; + this.scroll = builder.scrollTime; + this.scrollSize = builder.scrollSize; + this.searchType = builder.searchType; + this.searchTimeout = builder.searchTimeout; + this.slices = builder.slices; + this.sort = builder.sort; + this.stats = builder.stats; + this.terminateAfter = builder.terminateAfter; + this.timeout = builder.timeout; + this.version = builder.version; + + this.query = builder.query; + } + + @Nullable + public String getQ() { + return q; + } + + @Nullable + public Boolean getAnalyzeWildcard() { + return analyzeWildcard; + } + + @Nullable + public String getAnalyzer() { + return analyzer; + } + + @Nullable + public OperatorType getDefaultOperator() { + return defaultOperator; + } + + @Nullable + public String getDf() { + return df; + } + + @Nullable + public Boolean getLenient() { + return lenient; + } + + @Nullable + public Boolean getAllowNoIndices() { + return allowNoIndices; + } + + @Nullable + public ConflictsType getConflicts() { + return conflicts; + } + + @Nullable + public EnumSet getExpandWildcards() { + return expandWildcards; + } + + @Nullable + public Boolean getIgnoreUnavailable() { + return ignoreUnavailable; + } + + @Nullable + public Long getMaxDocs() { + return maxDocs; + } + + @Nullable + public String getPreference() { + return preference; + } + + @Nullable + public Boolean getRequestCache() { + return requestCache; + } + + @Nullable + public Boolean getRefresh() { + return refresh; + } + + @Nullable + public Float getRequestsPerSecond() { + return requestsPerSecond; + } + + @Nullable + public String getRouting() { + return routing; + } + + @Nullable + public Duration getScroll() { + return scroll; + } + + @Nullable + public Long getScrollSize() { + return scrollSize; + } + + @Nullable + public SearchType getSearchType() { + return searchType; + } + + @Nullable + public Duration getSearchTimeout() { + return searchTimeout; + } + + @Nullable + public Integer getSlices() { + return slices; + } + + @Nullable + public Sort getSort() { + return sort; + } + + @Nullable + public List getStats() { + return stats; + } + + @Nullable + public Long getTerminateAfter() { + return terminateAfter; + } + + @Nullable + public Duration getTimeout() { + return timeout; + } + + @Nullable + public Boolean getVersion() { + return version; + } + + @Nullable + public Query getQuery() { + return query; + } + + public static final class Builder { + // For Lucene query + @Nullable private String luceneQuery; + @Nullable private Boolean analyzeWildcard; + @Nullable private String analyzer; + @Nullable private OperatorType defaultOperator; + @Nullable private String defaultField; + @Nullable private Boolean lenient; + + // For ES query + @Nullable private Boolean allowNoIndices; + @Nullable private ConflictsType conflicts; + @Nullable private EnumSet expandWildcards; + @Nullable private Boolean ignoreUnavailable; + @Nullable private Long maxDocs; + @Nullable private String preference; + @Nullable private Boolean requestCache; + @Nullable private Boolean refresh; + @Nullable private Float requestsPerSecond; + @Nullable private String routing; + @Nullable private Duration scrollTime; + @Nullable private Long scrollSize; + @Nullable private SearchType searchType; + @Nullable private Duration searchTimeout; + @Nullable private Integer slices; + @Nullable private Sort sort; + @Nullable private List stats; + @Nullable private Long terminateAfter; + @Nullable private Duration timeout; + @Nullable private Boolean version; + + // Body + private final Query query; + + private Builder(Query query) { + Assert.notNull(query, "query must not be null"); + + this.query = query; + } + + /** + * Query in the Lucene query string syntax. + */ + public Builder withLuceneQuery(@Nullable String luceneQuery) { + this.luceneQuery = luceneQuery; + + return this; + } + + /** + * If true, wildcard and prefix queries are analyzed. Defaults to false. This parameter can only be used when the + * lucene query {@code q} parameter is specified. + */ + public Builder withAnalyzeWildcard(@Nullable Boolean analyzeWildcard) { + this.analyzeWildcard = analyzeWildcard; + + return this; + } + + /** + * Analyzer to use for the query string. This parameter can only be used when the lucene query {@code q} parameter + * is specified. + */ + public Builder withAnalyzer(@Nullable String analyzer) { + this.analyzer = analyzer; + + return this; + } + + /** + * The default operator for a query string query: {@literal AND} or {@literal OR}. Defaults to {@literal OR}. This + * parameter can only be used when the lucene query {@code q} parameter is specified. + */ + public Builder withDefaultOperator(@Nullable OperatorType defaultOperator) { + this.defaultOperator = defaultOperator; + + return this; + } + + /** + * Field to be used as the default when no field prefix is specified in the query string. This parameter can only be + * used when the lucene query {@code q} parameter is specified. + *

+ * e.g: {@code {"query":{"prefix":{"user.name":{"value":"es"}}}} } + */ + public Builder withDefaultField(@Nullable String defaultField) { + this.defaultField = defaultField; + + return this; + } + + /** + * If a query contains errors related to the format of the data being entered, they will be disregarded unless + * specified otherwise. By default, this feature is turned off. + */ + public Builder withLenient(@Nullable Boolean lenient) { + this.lenient = lenient; + + return this; + } + + /** + * An error will occur if the condition is {@code false} and any of the following are true: a wildcard expression, + * an index alias, or the {@literal _all value} only targets missing or closed indices. By default, this is set to + * {@code true}. + */ + public Builder withAllowNoIndices(@Nullable Boolean allowNoIndices) { + this.allowNoIndices = allowNoIndices; + + return this; + } + + /** + * Define the types of conflicts that occur when a query encounters version conflicts: abort or proceed. Defaults to + * abort. + */ + public Builder withConflicts(@Nullable ConflictsType conflicts) { + this.conflicts = conflicts; + + return this; + } + + /** + * Type of index that wildcard patterns can match. Defaults to {@literal open}. + */ + public Builder setExpandWildcards(@Nullable EnumSet expandWildcards) { + this.expandWildcards = expandWildcards; + + return this; + } + + /** + * An error occurs if it is directed at an index that is missing or closed when it is {@code false}. By default, + * this is set to {@code false}. + */ + public Builder withIgnoreUnavailable(@Nullable Boolean ignoreUnavailable) { + this.ignoreUnavailable = ignoreUnavailable; + + return this; + } + + /** + * Maximum number of documents to process. Defaults to all documents. + */ + public Builder withMaxDocs(@Nullable Long maxDocs) { + this.maxDocs = maxDocs; + + return this; + } + + /** + * Specifies the node or shard the operation should be performed on. + */ + public Builder withPreference(@Nullable String preference) { + this.preference = preference; + + return this; + } + + /** + * Use the request cache when it is {@code true}. By default, use the index-level setting. + */ + public Builder withRequestCache(@Nullable Boolean requestCache) { + this.requestCache = requestCache; + + return this; + } + + /** + * Refreshes all shards involved in the deleting by query after the request completes when it is {@code true}. By + * default, this is set to {@code false}. + */ + public Builder withRefresh(@Nullable Boolean refresh) { + this.refresh = refresh; + + return this; + } + + /** + * Limited this request to a certain number of sub-requests per second. By default, this is set to {@code -1} (no + * throttle). + */ + public Builder withRequestsPerSecond(@Nullable Float requestsPerSecond) { + this.requestsPerSecond = requestsPerSecond; + + return this; + } + + /** + * Custom value used to route operations to a specific shard. + */ + public Builder withRouting(@Nullable String routing) { + this.routing = routing; + + return this; + } + + /** + * Period to retain the search context for scrolling. + */ + public Builder withScrollTime(@Nullable Duration scrollTime) { + this.scrollTime = scrollTime; + + return this; + } + + /** + * Size of the scroll request that powers the operation. By default, this is set to {@code 1000}. + */ + public Builder withScrollSize(@Nullable Long scrollSize) { + this.scrollSize = scrollSize; + + return this; + } + + /** + * The type of the search operation. + */ + public Builder withSearchType(@Nullable SearchType searchType) { + this.searchType = searchType; + + return this; + } + + /** + * Explicit timeout for each search request. By default, this is set to no timeout. + */ + public Builder withSearchTimeout(@Nullable Duration searchTimeout) { + this.searchTimeout = searchTimeout; + + return this; + } + + /** + * The number of slices this task should be divided into. By default, this is set to {@code 1} meaning the task + * isn’t sliced into subtasks. + */ + public Builder withSlices(@Nullable Integer slices) { + this.slices = slices; + + return this; + } + + /** + * Sort search results in a specific order. + */ + public Builder withSort(@Nullable Sort sort) { + this.sort = sort; + + return this; + } + + /** + * Specific {@code tag} of the request for logging and statistical purposes. + */ + public Builder withStats(@Nullable List stats) { + this.stats = stats; + + return this; + } + + /** + * The Maximum number of documents that can be collected for each shard. If a query exceeds this limit, + * Elasticsearch will stop the query. + */ + public Builder withTerminateAfter(@Nullable Long terminateAfter) { + this.terminateAfter = terminateAfter; + + return this; + } + + /** + * Period each deletion request waits for active shards. By default, this is set to {@code 1m} (one minute). + */ + public Builder withTimeout(@Nullable Duration timeout) { + this.timeout = timeout; + + return this; + } + + /** + * Returns the document version as part of a hit. + */ + public Builder withVersion(@Nullable Boolean version) { + this.version = version; + + return this; + } + + public DeleteQuery build() { + if (luceneQuery == null) { + if (defaultField != null) { + throw new IllegalArgumentException("When defining the df parameter, you must include the Lucene query."); + } + if (analyzer != null) { + throw new IllegalArgumentException( + "When defining the analyzer parameter, you must include the Lucene query."); + } + if (analyzeWildcard != null) { + throw new IllegalArgumentException( + "When defining the analyzeWildcard parameter, you must include the Lucene query."); + } + if (defaultOperator != null) { + throw new IllegalArgumentException( + "When defining the defaultOperator parameter, you must include the Lucene query."); + } + if (lenient != null) { + throw new IllegalArgumentException("When defining the lenient parameter, you must include the Lucene query."); + } + } + + return new DeleteQuery(this); + } + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/DocValueField.java b/src/main/java/org/springframework/data/elasticsearch/core/query/DocValueField.java index 71ad09e9e7..d35af8b280 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/DocValueField.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/DocValueField.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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,7 +15,7 @@ */ package org.springframework.data.elasticsearch.core.query; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/FetchSourceFilter.java b/src/main/java/org/springframework/data/elasticsearch/core/query/FetchSourceFilter.java index 0182c7459e..e6ffa17ed6 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/FetchSourceFilter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/FetchSourceFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-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. @@ -17,7 +17,7 @@ import java.util.function.Function; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** @@ -28,14 +28,16 @@ */ public class FetchSourceFilter implements SourceFilter { + @Nullable private final Boolean fetchSource; @Nullable private final String[] includes; @Nullable private final String[] excludes; /** * @since 5.2 */ - public static SourceFilter of(@Nullable final String[] includes, @Nullable final String[] excludes) { - return new FetchSourceFilter(includes, excludes); + public static SourceFilter of(@Nullable Boolean fetchSource, @Nullable final String[] includes, + @Nullable final String[] excludes) { + return new FetchSourceFilter(fetchSource, includes, excludes); } /** @@ -48,11 +50,18 @@ public static SourceFilter of(Functiondocs + * @since 5.3 + */ +public class HasChildQuery { + /** + * Name of the child relationship mapped for the join field. + */ + private final String type; + + /** + * Query that specifies the documents to run on child documents of the {@link #type} field. + */ + private final Query query; + + /** + * Indicates whether to ignore an unmapped {@link #type} and not return any documents instead of an error. Default, + * this is set to {@code false}. + */ + @Nullable private final Boolean ignoreUnmapped; + + /** + * The Maximum number of child documents that match the {@link #query} allowed for a returned parent document. If the + * parent document exceeds this limit, it is excluded from the search results. + */ + @Nullable private final Integer maxChildren; + + /** + * Minimum number of child documents that match the query required to match the {@link #query} for a returned parent + * document. If the parent document does not meet this limit, it is excluded from the search results. + */ + @Nullable private final Integer minChildren; + + /** + * Indicates how scores for matching child documents affect the root parent document’s relevance score. + */ + @Nullable private final ScoreMode scoreMode; + + /** + * Obtaining nested objects and documents that have a parent-child relationship. + */ + @Nullable private final InnerHitsQuery innerHitsQuery; + + public static Builder builder(String type) { + return new Builder(type); + } + + private HasChildQuery(Builder builder) { + this.type = builder.type; + this.query = builder.query; + this.innerHitsQuery = builder.innerHitsQuery; + + this.ignoreUnmapped = builder.ignoreUnmapped; + + this.maxChildren = builder.maxChildren; + this.minChildren = builder.minChildren; + + this.scoreMode = builder.scoreMode; + } + + public String getType() { + return type; + } + + public Query getQuery() { + return query; + } + + @Nullable + public Boolean getIgnoreUnmapped() { + return ignoreUnmapped; + } + + @Nullable + public Integer getMaxChildren() { + return maxChildren; + } + + @Nullable + public Integer getMinChildren() { + return minChildren; + } + + @Nullable + public ScoreMode getScoreMode() { + return scoreMode; + } + + @Nullable + public InnerHitsQuery getInnerHitsQuery() { + return innerHitsQuery; + } + + public enum ScoreMode { + Default, Avg, Max, Min, Sum + } + + public static final class Builder { + private final String type; + private Query query; + + @Nullable private Boolean ignoreUnmapped; + + @Nullable private Integer maxChildren; + @Nullable private Integer minChildren; + + @Nullable private ScoreMode scoreMode; + + @Nullable private InnerHitsQuery innerHitsQuery; + + private Builder(String type) { + Assert.notNull(type, "type must not be null"); + + this.type = type; + } + + /** + * Query that specifies the documents to run on child documents of the {@link #type} field. + */ + public Builder withQuery(Query query) { + this.query = query; + + return this; + } + + /** + * Indicates whether to ignore an unmapped {@link #type} and not return any documents instead of an error. Default, + * this is set to {@code false}. + */ + public Builder withIgnoreUnmapped(@Nullable Boolean ignoreUnmapped) { + this.ignoreUnmapped = ignoreUnmapped; + + return this; + } + + /** + * The Maximum number of child documents that match the {@link #query} allowed for a returned parent document. If + * the parent document exceeds this limit, it is excluded from the search results. + */ + public Builder withMaxChildren(@Nullable Integer maxChildren) { + this.maxChildren = maxChildren; + + return this; + } + + /** + * Minimum number of child documents that match the query required to match the {@link #query} for a returned parent + * document. If the parent document does not meet this limit, it is excluded from the search results. + */ + public Builder withMinChildren(@Nullable Integer minChildren) { + this.minChildren = minChildren; + + return this; + } + + /** + * Indicates how scores for matching child documents affect the root parent document’s relevance score. + */ + public Builder withScoreMode(@Nullable ScoreMode scoreMode) { + this.scoreMode = scoreMode; + + return this; + } + + /** + * Obtaining nested objects and documents that have a parent-child relationship. + */ + public Builder withInnerHitsQuery(@Nullable InnerHitsQuery innerHitsQuery) { + this.innerHitsQuery = innerHitsQuery; + + return this; + } + + public HasChildQuery build() { + Assert.notNull(query, "query must not be null."); + + return new HasChildQuery(this); + } + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/HasParentQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/HasParentQuery.java new file mode 100644 index 0000000000..7e182c7e81 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/HasParentQuery.java @@ -0,0 +1,141 @@ +/* + * 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.core.query; + +import org.jspecify.annotations.Nullable; +import org.springframework.util.Assert; + +/** + * Defines a has_parent request. + * + * @author Aouichaoui Youssef + * @see docs + * @since 5.3 + */ +public class HasParentQuery { + /** + * Name of the parent relationship mapped for the join field. + */ + private final String parentType; + + /** + * Query that specifies the documents to run on parent documents of the {@link #parentType} field. + */ + private final Query query; + + /** + * Indicates whether the relevance score of a matching parent document is aggregated into its child documents. + * Default, this is set to {@code false}. + */ + @Nullable private final Boolean score; + + /** + * Indicates whether to ignore an unmapped {@link #parentType} and not return any documents instead of an error. + * Default, this is set to {@code false}. + */ + @Nullable private final Boolean ignoreUnmapped; + + /** + * Obtaining nested objects and documents that have a parent-child relationship. + */ + @Nullable private final InnerHitsQuery innerHitsQuery; + + public static Builder builder(String parentType) { + return new Builder(parentType); + } + + private HasParentQuery(Builder builder) { + this.parentType = builder.parentType; + this.query = builder.query; + this.innerHitsQuery = builder.innerHitsQuery; + + this.score = builder.score; + this.ignoreUnmapped = builder.ignoreUnmapped; + } + + public String getParentType() { + return parentType; + } + + public Query getQuery() { + return query; + } + + @Nullable + public Boolean getScore() { + return score; + } + + @Nullable + public Boolean getIgnoreUnmapped() { + return ignoreUnmapped; + } + + @Nullable + public InnerHitsQuery getInnerHitsQuery() { + return innerHitsQuery; + } + + public static class Builder { + private final String parentType; + private Query query; + + @Nullable private Boolean score; + @Nullable private Boolean ignoreUnmapped; + + @Nullable private InnerHitsQuery innerHitsQuery; + + private Builder(String parentType) { + Assert.notNull(parentType, "parent_type must not be null."); + + this.parentType = parentType; + } + + public Builder withQuery(Query query) { + this.query = query; + + return this; + } + + public Builder withScore(@Nullable Boolean score) { + this.score = score; + + return this; + } + + public Builder withIgnoreUnmapped(@Nullable Boolean ignoreUnmapped) { + this.ignoreUnmapped = ignoreUnmapped; + + return this; + } + + /** + * Obtaining nested objects and documents that have a parent-child relationship. + */ + public Builder withInnerHitsQuery(@Nullable InnerHitsQuery innerHitsQuery) { + this.innerHitsQuery = innerHitsQuery; + + return this; + } + + public HasParentQuery build() { + Assert.notNull(query, "query must not be null."); + + return new HasParentQuery(this); + } + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/HighlightQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/HighlightQuery.java index 0bc6e2a4bb..5e8e2a5a70 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/HighlightQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/HighlightQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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.core.query; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.query.highlight.Highlight; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/IndexBoost.java b/src/main/java/org/springframework/data/elasticsearch/core/query/IndexBoost.java index 587198afd8..6ed97562bd 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/IndexBoost.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/IndexBoost.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-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,8 +23,8 @@ */ public class IndexBoost { - private String indexName; - private float boost; + private final String indexName; + private final float boost; public IndexBoost(String indexName, float boost) { this.indexName = indexName; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/IndexQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/IndexQuery.java index 37aa17170b..cad53334af 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/IndexQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/IndexQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -15,7 +15,7 @@ */ package org.springframework.data.elasticsearch.core.query; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * IndexQuery @@ -54,6 +54,13 @@ public IndexQuery(@Nullable String id, @Nullable Object object, @Nullable Long v this.indexName = indexName; } + /** + * @since 5.5 + */ + public static IndexQueryBuilder builder() { + return new IndexQueryBuilder(); + } + @Nullable public String getId() { return id; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/IndexQueryBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/query/IndexQueryBuilder.java index 77553fb077..6e45280439 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/IndexQueryBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/IndexQueryBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 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. @@ -15,8 +15,8 @@ */ package org.springframework.data.elasticsearch.core.query; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.RefreshPolicy; -import org.springframework.lang.Nullable; /** * IndexQuery Builder @@ -36,7 +36,7 @@ public class IndexQueryBuilder { @Nullable private Long seqNo; @Nullable private Long primaryTerm; @Nullable private String routing; - @Nullable private IndexQuery.OpType opType; + private IndexQuery.@Nullable OpType opType; @Nullable private RefreshPolicy refreshPolicy; @Nullable private String indexName; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/IndicesOptions.java b/src/main/java/org/springframework/data/elasticsearch/core/query/IndicesOptions.java index c2a389a0af..dd8660197f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/IndicesOptions.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/IndicesOptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,8 +25,8 @@ */ public class IndicesOptions { - private EnumSet

+ * A script is either an inline script, then the script parameters must be set + * or it refers to a stored script, then the name parameter is required. * + * @param language the language when the script is passed in the script parameter + * @param script the script to use as inline script + * @param scriptName the name when using a stored script + * @param params the script parameters * @author Peter-Josef Meisch * @since 4.4 */ -public record ScriptData(ScriptType type, @Nullable String language, @Nullable String script, - @Nullable String scriptName, @Nullable Map params) { - - public ScriptData(ScriptType type, @Nullable String language, @Nullable String script, @Nullable String scriptName, - @Nullable Map params) { - - Assert.notNull(type, "type must not be null"); - - this.type = type; - this.language = language; - this.script = script; - this.scriptName = scriptName; - this.params = params; - } - - /** - * @since 5.2 - */ - public static ScriptData of(ScriptType type, @Nullable String language, @Nullable String script, - @Nullable String scriptName, @Nullable Map params) { - return new ScriptData(type, language, script, scriptName, params); - } - - public static ScriptData of(Function builderFunction) { - - Assert.notNull(builderFunction, "f must not be null"); - - return builderFunction.apply(new Builder()).build(); - } - - /** - * @since 5.2 - */ - public static Builder builder() { - return new Builder(); - } - - /** - * @since 5.2 - */ - public static final class Builder { - @Nullable private ScriptType type; - @Nullable private String language; - @Nullable private String script; - @Nullable private String scriptName; - @Nullable private Map params; - - private Builder() {} - - public Builder withType(ScriptType type) { - - Assert.notNull(type, "type must not be null"); - - this.type = type; - return this; - } - - public Builder withLanguage(@Nullable String language) { - this.language = language; - return this; - } - - public Builder withScript(@Nullable String script) { - this.script = script; - return this; - } - - public Builder withScriptName(@Nullable String scriptName) { - this.scriptName = scriptName; - return this; - } - - public Builder withParams(@Nullable Map params) { - this.params = params; - return this; - } - - public ScriptData build() { - - Assert.notNull(type, "type must be set"); - - return new ScriptData(type, language, script, scriptName, params); - } - } +public record ScriptData(@Nullable String language, @Nullable String script, + @Nullable String scriptName, @Nullable Map params) { + + /* + * constructor overload to check the parameters + */ + public ScriptData(@Nullable String language, @Nullable String script, @Nullable String scriptName, + @Nullable Map params) { + + Assert.isTrue(script != null || scriptName != null, "script or scriptName is required"); + + this.language = language; + this.script = script; + this.scriptName = scriptName; + this.params = params; + } + + /** + * factory method to create a ScriptData object. + * + * @since 5.2 + */ + public static ScriptData of(@Nullable String language, @Nullable String script, + @Nullable String scriptName, @Nullable Map params) { + return new ScriptData(language, script, scriptName, params); + } + + /** + * factory method to create a ScriptData object using a ScriptBuilder callback. + * + * @param builderFunction function called to populate the builder + * @return + */ + public static ScriptData of(Function builderFunction) { + + Assert.notNull(builderFunction, "builderFunction must not be null"); + + return builderFunction.apply(new Builder()).build(); + } + + /** + * @since 5.2 + */ + public static Builder builder() { + return new Builder(); + } + + /** + * @since 5.2 + */ + public static final class Builder { + @Nullable + private String language; + @Nullable + private String script; + @Nullable + private String scriptName; + @Nullable + private Map params; + + private Builder() { + } + + public Builder withLanguage(@Nullable String language) { + this.language = language; + return this; + } + + public Builder withScript(@Nullable String script) { + this.script = script; + return this; + } + + public Builder withScriptName(@Nullable String scriptName) { + this.scriptName = scriptName; + return this; + } + + public Builder withParams(@Nullable Map params) { + this.params = params; + return this; + } + + public ScriptData build() { + return new ScriptData(language, script, scriptName, params); + } + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptedField.java b/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptedField.java index 2101fcfc6e..2a30a3c2e2 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptedField.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptedField.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/core/query/SearchTemplateQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/SearchTemplateQuery.java index 7403877ee1..804582c7bd 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/SearchTemplateQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/SearchTemplateQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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. @@ -17,7 +17,7 @@ import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * @author Peter-Josef Meisch diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/SearchTemplateQueryBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/query/SearchTemplateQueryBuilder.java index 3665e1bb66..225933a677 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/SearchTemplateQueryBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/SearchTemplateQueryBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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,22 +15,22 @@ */ package org.springframework.data.elasticsearch.core.query; -import org.springframework.lang.Nullable; - import java.util.Map; +import org.jspecify.annotations.Nullable; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + /** * @author Peter-Josef Meisch * @since 5.1 */ public class SearchTemplateQueryBuilder extends BaseQueryBuilder { - @Nullable - private String id; + @Nullable private String id; @Nullable String source; - @Nullable - Map params; + @Nullable Map params; @Nullable public String getId() { @@ -62,6 +62,18 @@ public SearchTemplateQueryBuilder withParams(@Nullable Map param return this; } + @Override + public SearchTemplateQueryBuilder withSort(Sort sort) { + throw new IllegalArgumentException( + "sort is not supported in a searchtemplate query. Sort values must be defined in the stored template"); + } + + @Override + public SearchTemplateQueryBuilder withPageable(Pageable pageable) { + throw new IllegalArgumentException( + "paging is not supported in a searchtemplate query. from and size values must be defined in the stored template"); + } + @Override public SearchTemplateQuery build() { return new SearchTemplateQuery(this); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/SeqNoPrimaryTerm.java b/src/main/java/org/springframework/data/elasticsearch/core/query/SeqNoPrimaryTerm.java index d034e2b352..0623128504 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/SeqNoPrimaryTerm.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/SeqNoPrimaryTerm.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/core/query/SimpleField.java b/src/main/java/org/springframework/data/elasticsearch/core/query/SimpleField.java index c151275876..298954cfa6 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/SimpleField.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/SimpleField.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -15,8 +15,10 @@ */ package org.springframework.data.elasticsearch.core.query; +import java.util.Objects; + +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.annotations.FieldType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -79,4 +81,19 @@ public String getPath() { public String toString() { return getName(); } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof SimpleField that)) + return false; + return Objects.equals(name, that.name) && Objects.equals(fieldType, that.fieldType) + && Objects.equals(path, that.path); + } + + @Override + public int hashCode() { + return Objects.hash(name, fieldType, path); + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/SourceFilter.java b/src/main/java/org/springframework/data/elasticsearch/core/query/SourceFilter.java index 7351244389..6696615964 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/SourceFilter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/SourceFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-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,12 +15,12 @@ */ package org.springframework.data.elasticsearch.core.query; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.annotations.Field; -import org.springframework.lang.Nullable; /** * SourceFilter for providing includes and excludes. Using these helps in reducing the amount of data that is returned - * from Elasticsearch especially when the stored docuements are large and only some fields from these documents are + * from Elasticsearch especially when the stored documents are large and only some fields from these documents are * needed. If the SourceFilter includes the name of a property that has a different name mapped in Elasticsearch (see * {@link Field#name()} this will automatically be mapped. * @@ -40,4 +40,15 @@ public interface SourceFilter { */ @Nullable String[] getExcludes(); + + /** + * Flag to set the _source parameter in a query to true or false. If this is not null, the values returned from + * getIncludes() and getExcludes() are ignored + * + * @since 5.5 + */ + @Nullable + default Boolean fetchSource() { + return null; + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/SqlQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/SqlQuery.java new file mode 100644 index 0000000000..332ba69627 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/SqlQuery.java @@ -0,0 +1,433 @@ +/* + * 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.core.query; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.TimeZone; + +import org.jspecify.annotations.Nullable; +import org.springframework.util.Assert; + +/** + * Defines an SQL request. + * + * @author Aouichaoui Youssef + * @see docs + * @since 5.4 + */ +public class SqlQuery { + + /** + * If true, returns partial results if there are shard request timeouts or shard failures. + *

+ * Default, this is set to {@code false}. + */ + @Nullable private final Boolean allowPartialSearchResults; + + /** + * Default catalog/cluster for queries. If unspecified, the queries are executed on the data in the local cluster + * only. + */ + @Nullable private final String catalog; + + /** + * If true, returns results in a columnar format. + *

+ * Default, this is set to {@code false}. + */ + @Nullable private final Boolean columnar; + + /** + * To retrieve a set of paginated results, ignore other request body parameters when specifying a cursor and using the + * {@link #columnar} and {@link #timeZone} parameters. + */ + @Nullable private final String cursor; + + /** + * Maximum number of rows to return in the response. + *

+ * Default, this is set to {@code 1000}. + */ + @Nullable private final Integer fetchSize; + + /** + * If false, the API returns an error for fields containing array values. + *

+ * Default, this is set to {@code false}. + */ + @Nullable private final Boolean fieldMultiValueLeniency; + + /** + * Query that filter documents for the SQL search. + */ + @Nullable private final Query filter; + + /** + * If true, the search can run on frozen indices. + *

+ * Default, this is set to {@code false}. + */ + @Nullable private final Boolean indexIncludeFrozen; + + /** + * Retention period for an async or saved synchronous search. + *

+ * Default, this is set to {@code 5 days}. + */ + @Nullable private final Duration keepAlive; + + /** + * If it is true, it will store synchronous searches when the {@link #waitForCompletionTimeout} parameter is + * specified. + */ + @Nullable private final Boolean keepOnCompletion; + + /** + * Minimum retention period for the scroll cursor. + *

+ * Default, this is set to {@code 45 seconds}. + */ + @Nullable private final Duration pageTimeout; + + /** + * Timeout before the request fails. + *

+ * Default, this is set to {@code 90 seconds}. + */ + @Nullable private final Duration requestTimeout; + + /** + * Values for parameters in the query. + */ + @Nullable private final List params; + + /** + * SQL query to run. + */ + private final String query; + + /** + * Time zone ID for the search. + *

+ * Default, this is set to {@code UTC}. + */ + @Nullable private final TimeZone timeZone; + + /** + * Period to wait for complete results. + *

+ * Default, this is set to no timeout. + */ + @Nullable private final Duration waitForCompletionTimeout; + + private SqlQuery(Builder builder) { + this.allowPartialSearchResults = builder.allowPartialSearchResults; + + this.catalog = builder.catalog; + this.columnar = builder.columnar; + this.cursor = builder.cursor; + + this.fetchSize = builder.fetchSize; + this.fieldMultiValueLeniency = builder.fieldMultiValueLeniency; + + this.filter = builder.filter; + + this.indexIncludeFrozen = builder.indexIncludeFrozen; + this.keepAlive = builder.keepAlive; + this.keepOnCompletion = builder.keepOnCompletion; + + this.pageTimeout = builder.pageTimeout; + this.requestTimeout = builder.requestTimeout; + + this.params = builder.params; + this.query = builder.query; + + this.timeZone = builder.timeZone; + this.waitForCompletionTimeout = builder.waitForCompletionTimeout; + } + + @Nullable + public Boolean getAllowPartialSearchResults() { + return allowPartialSearchResults; + } + + @Nullable + public String getCatalog() { + return catalog; + } + + @Nullable + public Boolean getColumnar() { + return columnar; + } + + @Nullable + public String getCursor() { + return cursor; + } + + @Nullable + public Integer getFetchSize() { + return fetchSize; + } + + @Nullable + public Boolean getFieldMultiValueLeniency() { + return fieldMultiValueLeniency; + } + + @Nullable + public Query getFilter() { + return filter; + } + + @Nullable + public Boolean getIndexIncludeFrozen() { + return indexIncludeFrozen; + } + + @Nullable + public Duration getKeepAlive() { + return keepAlive; + } + + @Nullable + public Boolean getKeepOnCompletion() { + return keepOnCompletion; + } + + @Nullable + public Duration getPageTimeout() { + return pageTimeout; + } + + @Nullable + public Duration getRequestTimeout() { + return requestTimeout; + } + + @Nullable + public List getParams() { + return params; + } + + public String getQuery() { + return query; + } + + @Nullable + public TimeZone getTimeZone() { + return timeZone; + } + + @Nullable + public Duration getWaitForCompletionTimeout() { + return waitForCompletionTimeout; + } + + public static Builder builder(String query) { + return new Builder(query); + } + + public static class Builder { + @Nullable private Boolean allowPartialSearchResults; + + @Nullable private String catalog; + @Nullable private Boolean columnar; + @Nullable private String cursor; + + @Nullable private Integer fetchSize; + @Nullable private Boolean fieldMultiValueLeniency; + + @Nullable private Query filter; + + @Nullable private Boolean indexIncludeFrozen; + + @Nullable private Duration keepAlive; + @Nullable private Boolean keepOnCompletion; + + @Nullable private Duration pageTimeout; + @Nullable private Duration requestTimeout; + + @Nullable private List params; + private final String query; + + @Nullable private TimeZone timeZone; + @Nullable private Duration waitForCompletionTimeout; + + private Builder(String query) { + Assert.notNull(query, "query must not be null"); + + this.query = query; + } + + /** + * If true, returns partial results if there are shard request timeouts or shard failures. + */ + public Builder withAllowPartialSearchResults(Boolean allowPartialSearchResults) { + this.allowPartialSearchResults = allowPartialSearchResults; + + return this; + } + + /** + * Default catalog/cluster for queries. If unspecified, the queries are executed on the data in the local cluster + * only. + */ + public Builder withCatalog(String catalog) { + this.catalog = catalog; + + return this; + } + + /** + * If true, returns results in a columnar format. + */ + public Builder withColumnar(Boolean columnar) { + this.columnar = columnar; + + return this; + } + + /** + * To retrieve a set of paginated results, ignore other request body parameters when specifying a cursor and using + * the {@link #columnar} and {@link #timeZone} parameters. + */ + public Builder withCursor(String cursor) { + this.cursor = cursor; + + return this; + } + + /** + * Maximum number of rows to return in the response. + */ + public Builder withFetchSize(Integer fetchSize) { + this.fetchSize = fetchSize; + + return this; + } + + /** + * If false, the API returns an error for fields containing array values. + */ + public Builder withFieldMultiValueLeniency(Boolean fieldMultiValueLeniency) { + this.fieldMultiValueLeniency = fieldMultiValueLeniency; + + return this; + } + + /** + * Query that filter documents for the SQL search. + */ + public Builder setFilter(Query filter) { + this.filter = filter; + + return this; + } + + /** + * If true, the search can run on frozen indices. + */ + public Builder withIndexIncludeFrozen(Boolean indexIncludeFrozen) { + this.indexIncludeFrozen = indexIncludeFrozen; + + return this; + } + + /** + * Retention period for an async or saved synchronous search. + */ + public Builder setKeepAlive(Duration keepAlive) { + this.keepAlive = keepAlive; + + return this; + } + + /** + * If it is true, it will store synchronous searches when the {@link #waitForCompletionTimeout} parameter is + * specified. + */ + public Builder withKeepOnCompletion(Boolean keepOnCompletion) { + this.keepOnCompletion = keepOnCompletion; + + return this; + } + + /** + * Minimum retention period for the scroll cursor. + */ + public Builder withPageTimeout(Duration pageTimeout) { + this.pageTimeout = pageTimeout; + + return this; + } + + /** + * Timeout before the request fails. + */ + public Builder withRequestTimeout(Duration requestTimeout) { + this.requestTimeout = requestTimeout; + + return this; + } + + /** + * Values for parameters in the query. + */ + public Builder withParams(List params) { + this.params = params; + + return this; + } + + /** + * Value for parameters in the query. + */ + public Builder withParam(Object param) { + if (this.params == null) { + this.params = new ArrayList<>(); + } + this.params.add(param); + + return this; + } + + /** + * Time zone ID for the search. + */ + public Builder withTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + + return this; + } + + /** + * Period to wait for complete results. + */ + public Builder withWaitForCompletionTimeout(Duration waitForCompletionTimeout) { + this.waitForCompletionTimeout = waitForCompletionTimeout; + + return this; + } + + public SqlQuery build() { + return new SqlQuery(this); + } + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/StringQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/StringQuery.java index b7a25c5d97..1b7a381475 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/StringQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/StringQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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/core/query/StringQueryBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/query/StringQueryBuilder.java index e19ccaf1e3..f688bee788 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/StringQueryBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/StringQueryBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/core/query/UpdateQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/UpdateQuery.java index 32b4c14685..75959bb56a 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/UpdateQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/UpdateQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -18,9 +18,9 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.RefreshPolicy; import org.springframework.data.elasticsearch.core.document.Document; -import org.springframework.lang.Nullable; /** * Defines an update request. @@ -76,7 +76,7 @@ private UpdateQuery(String id, @Nullable String script, @Nullable Map getFields() { return fields; } - - /** - * Creates a {@link Highlight} from an Annotation instance. - * - * @param highlight must not be {@literal null} - * @return highlight definition - */ - public static Highlight of(org.springframework.data.elasticsearch.annotations.Highlight highlight) { - - Assert.notNull(highlight, "highlight must not be null"); - - org.springframework.data.elasticsearch.annotations.HighlightParameters parameters = highlight.parameters(); - HighlightParameters highlightParameters = HighlightParameters.builder() // - .withBoundaryChars(parameters.boundaryChars()) // - .withBoundaryMaxScan(parameters.boundaryMaxScan()) // - .withBoundaryScanner(parameters.boundaryScanner()) // - .withBoundaryScannerLocale(parameters.boundaryScannerLocale()) // - .withEncoder(parameters.encoder()) // - .withForceSource(parameters.forceSource()) // - .withFragmenter(parameters.fragmenter()) // - .withFragmentSize(parameters.fragmentSize()) // - .withNoMatchSize(parameters.noMatchSize()) // - .withNumberOfFragments(parameters.numberOfFragments()) // - .withOrder(parameters.order()) // - .withPhraseLimit(parameters.phraseLimit()) // - .withPreTags(parameters.preTags()) // - .withPostTags(parameters.postTags()) // - .withRequireFieldMatch(parameters.requireFieldMatch()) // - .withTagsSchema(parameters.tagsSchema()) // - .withType(parameters.type()) // - .build(); - - List highlightFields = Arrays.stream(highlight.fields()) // - .map(HighlightField::of) // - .collect(Collectors.toList()); - - return new Highlight(highlightParameters, highlightFields); - } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/highlight/HighlightCommonParameters.java b/src/main/java/org/springframework/data/elasticsearch/core/query/highlight/HighlightCommonParameters.java index 6261fb1de8..d3b917adfa 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/highlight/HighlightCommonParameters.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/highlight/HighlightCommonParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,10 +15,13 @@ */ package org.springframework.data.elasticsearch.core.query.highlight; +import org.jspecify.annotations.Nullable; +import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.util.Assert; /** * @author Peter-Josef Meisch + * @author Haibo Liu * @since 4.3 */ public abstract class HighlightCommonParameters { @@ -31,6 +34,7 @@ public abstract class HighlightCommonParameters { private final int fragmentSize; private final int noMatchSize; private final int numberOfFragments; + @Nullable private final Query highlightQuery; private final String order; private final int phraseLimit; private final String[] preTags; @@ -51,6 +55,7 @@ protected HighlightCommonParameters(HighlightCommonParametersBuilder builder) fragmentSize = builder.fragmentSize; noMatchSize = builder.noMatchSize; numberOfFragments = builder.numberOfFragments; + highlightQuery = builder.highlightQuery; order = builder.order; phraseLimit = builder.phraseLimit; preTags = builder.preTags; @@ -75,6 +80,10 @@ public String getBoundaryScannerLocale() { return boundaryScannerLocale; } + /** + * @deprecated the underlying functionality is deprecated since Elasticsearch 8.8. + */ + @Deprecated(since = "5.5") public boolean getForceSource() { return forceSource; } @@ -95,6 +104,11 @@ public int getNumberOfFragments() { return numberOfFragments; } + @Nullable + public Query getHighlightQuery() { + return highlightQuery; + } + public String getOrder() { return order; } @@ -130,6 +144,10 @@ public static abstract class HighlightCommonParametersBuilderSQL search API. + * + * @author Aouichaoui Youssef + * @since 5.4 + */ +public interface ReactiveSqlOperations { + /** + * Execute the sql {@code query} against elasticsearch and return result as {@link SqlResponse} + * + * @param query the query to execute + * @return {@link SqlResponse} containing the list of found objects + */ + Mono search(SqlQuery query); +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlOperations.java new file mode 100644 index 0000000000..359499a9e9 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlOperations.java @@ -0,0 +1,35 @@ +/* + * 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.core.sql; + +import org.springframework.data.elasticsearch.core.query.SqlQuery; + +/** + * The operations for the + * SQL search API. + * + * @author Aouichaoui Youssef + * @since 5.4 + */ +public interface SqlOperations { + /** + * Execute the sql {@code query} against elasticsearch and return result as {@link SqlResponse} + * + * @param query the query to execute + * @return {@link SqlResponse} containing the list of found objects + */ + SqlResponse search(SqlQuery query); +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlResponse.java b/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlResponse.java new file mode 100644 index 0000000000..74db4c728b --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlResponse.java @@ -0,0 +1,217 @@ +/* + * 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.core.sql; + +import static java.util.Collections.*; + +import jakarta.json.JsonValue; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** + * Defines an SQL response. + * + * @author Aouichaoui Youssef + * @see docs + * @since 5.4 + */ +public class SqlResponse { + /** + * If {@code true}, the search is still running. + */ + private final boolean running; + + /** + * If {@code true}, the response does not contain complete search results. + */ + private final boolean partial; + + /** + * Cursor for the next set of paginated results. + */ + @Nullable private final String cursor; + + /** + * Column headings for the search results. + */ + private final List columns; + + /** + * Values for the search results. + */ + private final List rows; + + private SqlResponse(Builder builder) { + this.running = builder.running; + this.partial = builder.partial; + + this.cursor = builder.cursor; + + this.columns = unmodifiableList(builder.columns); + this.rows = unmodifiableList(builder.rows); + } + + public boolean isRunning() { + return running; + } + + public boolean isPartial() { + return partial; + } + + @Nullable + public String getCursor() { + return cursor; + } + + public List getColumns() { + return columns; + } + + public List getRows() { + return rows; + } + + public static Builder builder() { + return new Builder(); + } + + public record Column(String name, String type) { + } + + public static class Row implements Iterable> { + private final Map row; + + private Row(Builder builder) { + this.row = builder.row; + } + + public static Builder builder() { + return new Builder(); + } + + @NonNull + @Override + public Iterator> iterator() { + return row.entrySet().iterator(); + } + + @Nullable + public JsonValue get(Column column) { + return row.get(column); + } + + public static class Builder { + private final Map row = new HashMap<>(); + + public Builder withValue(Column column, JsonValue value) { + this.row.put(column, value); + + return this; + } + + public Row build() { + return new Row(this); + } + } + } + + public static class Builder { + private boolean running; + private boolean partial; + + @Nullable private String cursor; + + private final List columns = new ArrayList<>(); + private final List rows = new ArrayList<>(); + + private Builder() {} + + /** + * If {@code true}, the search is still running. + */ + public Builder withRunning(boolean running) { + this.running = running; + + return this; + } + + /** + * If {@code true}, the response does not contain complete search results. + */ + public Builder withPartial(boolean partial) { + this.partial = partial; + + return this; + } + + /** + * Cursor for the next set of paginated results. + */ + public Builder withCursor(@Nullable String cursor) { + this.cursor = cursor; + + return this; + } + + /** + * Column headings for the search results. + */ + public Builder withColumns(List columns) { + this.columns.addAll(columns); + + return this; + } + + /** + * Column heading for the search results. + */ + public Builder withColumn(Column column) { + this.columns.add(column); + + return this; + } + + /** + * Values for the search results. + */ + public Builder withRows(List rows) { + this.rows.addAll(rows); + + return this; + } + + /** + * Value for the search results. + */ + public Builder withRow(Row row) { + this.rows.add(row); + + return this; + } + + public SqlResponse build() { + return new SqlResponse(this); + } + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/sql/package-info.java b/src/main/java/org/springframework/data/elasticsearch/core/sql/package-info.java new file mode 100644 index 0000000000..b1af981669 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/sql/package-info.java @@ -0,0 +1,5 @@ +/** + * Classes and interfaces to access to SQL API of Elasticsearch. + */ +@org.jspecify.annotations.NullMarked +package org.springframework.data.elasticsearch.core.sql; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/suggest/Completion.java b/src/main/java/org/springframework/data/elasticsearch/core/suggest/Completion.java index 19c0c3f131..32e99b3bb7 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/suggest/Completion.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/suggest/Completion.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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. @@ -18,7 +18,7 @@ import java.util.List; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Based on the reference doc - diff --git a/src/main/java/org/springframework/data/elasticsearch/core/suggest/package-info.java b/src/main/java/org/springframework/data/elasticsearch/core/suggest/package-info.java index 06274cb14e..622c4bd0ee 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/suggest/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/suggest/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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. @@ -13,6 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.core.suggest; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/CompletionSuggestion.java b/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/CompletionSuggestion.java index ff57cd5670..b6aa8fa9cc 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/CompletionSuggestion.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/CompletionSuggestion.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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. @@ -20,10 +20,10 @@ import java.util.Set; import java.util.function.BiFunction; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.SearchHit; import org.springframework.data.elasticsearch.core.document.SearchDocument; import org.springframework.data.elasticsearch.support.ScoreDoc; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch diff --git a/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/PhraseSuggestion.java b/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/PhraseSuggestion.java index c38ec154c0..a96a9a4637 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/PhraseSuggestion.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/PhraseSuggestion.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,10 +15,10 @@ */ package org.springframework.data.elasticsearch.core.suggest.response; -import org.springframework.lang.Nullable; - import java.util.List; +import org.jspecify.annotations.Nullable; + /** * @author Peter-Josef Meisch * @since 4.3 diff --git a/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/SortBy.java b/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/SortBy.java index 1b6bab7ebf..3fda6cc51c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/SortBy.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/SortBy.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/core/suggest/response/Suggest.java b/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/Suggest.java index 0af01b0e5b..958e1bb8c3 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/Suggest.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/Suggest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,7 +19,7 @@ import java.util.List; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Class structure mirroring the Elasticsearch classes for a suggest response. diff --git a/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/TermSuggestion.java b/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/TermSuggestion.java index 03e6ebe7ef..4e6183a395 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/TermSuggestion.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/TermSuggestion.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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.core.suggest.response; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import java.util.List; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/package-info.java b/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/package-info.java index 5d02fba124..7507d78a2d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/package-info.java @@ -1,3 +1,2 @@ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.core.suggest.response; diff --git a/src/main/java/org/springframework/data/elasticsearch/package-info.java b/src/main/java/org/springframework/data/elasticsearch/package-info.java index c90fb32377..69b12af1f5 100644 --- a/src/main/java/org/springframework/data/elasticsearch/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/package-info.java @@ -1,3 +1,2 @@ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch; diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/ElasticsearchRepository.java b/src/main/java/org/springframework/data/elasticsearch/repository/ElasticsearchRepository.java index fb7b83c8a9..781d7c3a32 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/ElasticsearchRepository.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/ElasticsearchRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -15,13 +15,13 @@ */ package org.springframework.data.elasticsearch.repository; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.elasticsearch.core.RefreshPolicy; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.PagingAndSortingRepository; -import org.springframework.lang.Nullable; /** * @param diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/ReactiveElasticsearchRepository.java b/src/main/java/org/springframework/data/elasticsearch/repository/ReactiveElasticsearchRepository.java index f6ad0125fd..9ab2a3108d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/ReactiveElasticsearchRepository.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/ReactiveElasticsearchRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 the original author or authors. + * Copyright 2019-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License", @Nullable RefreshPolicy refreshPolicy); * you may not use this file except in compliance with the License. @@ -18,12 +18,12 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.data.elasticsearch.core.RefreshPolicy; import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.data.repository.reactive.ReactiveSortingRepository; -import org.springframework.lang.Nullable; /** * Elasticsearch specific {@link org.springframework.data.repository.Repository} interface with reactive support. diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/aot/RepositoryRuntimeHints.java b/src/main/java/org/springframework/data/elasticsearch/repository/aot/RepositoryRuntimeHints.java index 8de8ca0098..719905721d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/aot/RepositoryRuntimeHints.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/aot/RepositoryRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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,11 +19,11 @@ 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; import org.springframework.aot.hint.TypeReference; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/aot/package-info.java b/src/main/java/org/springframework/data/elasticsearch/repository/aot/package-info.java index 1a7c898aa4..270425b63b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/aot/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/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.repository.aot; diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/cdi/ElasticsearchRepositoryBean.java b/src/main/java/org/springframework/data/elasticsearch/repository/cdi/ElasticsearchRepositoryBean.java index ed5e831ca5..458a343a6e 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/cdi/ElasticsearchRepositoryBean.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/cdi/ElasticsearchRepositoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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/repository/cdi/ElasticsearchRepositoryExtension.java b/src/main/java/org/springframework/data/elasticsearch/repository/cdi/ElasticsearchRepositoryExtension.java index 7a329f632e..cb7e50ad2c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/cdi/ElasticsearchRepositoryExtension.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/cdi/ElasticsearchRepositoryExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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/repository/cdi/package-info.java b/src/main/java/org/springframework/data/elasticsearch/repository/cdi/package-info.java index f11afc98b3..b307ca9737 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/cdi/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/cdi/package-info.java @@ -1,3 +1,2 @@ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.repository.cdi; diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/config/ElasticsearchRepositoriesRegistrar.java b/src/main/java/org/springframework/data/elasticsearch/repository/config/ElasticsearchRepositoriesRegistrar.java index 2273855637..4f643ebab2 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/config/ElasticsearchRepositoriesRegistrar.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/config/ElasticsearchRepositoriesRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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/repository/config/ElasticsearchRepositoryConfigExtension.java b/src/main/java/org/springframework/data/elasticsearch/repository/config/ElasticsearchRepositoryConfigExtension.java index fa7b429ce0..b1caf1b3eb 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/config/ElasticsearchRepositoryConfigExtension.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/config/ElasticsearchRepositoryConfigExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -19,6 +19,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Locale; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -41,6 +42,7 @@ * @author Mohsin Husen * @author Mark Paluch * @author Christoph Strobl + * @author Junghoon Ban */ public class ElasticsearchRepositoryConfigExtension extends RepositoryConfigurationExtensionSupport { @@ -106,7 +108,7 @@ protected Collection> getIdentifyingAnnotations() { */ @Override protected Collection> getIdentifyingTypes() { - return Arrays.asList(ElasticsearchRepository.class, ElasticsearchRepository.class); + return List.of(ElasticsearchRepository.class); } /* diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/config/EnableElasticsearchRepositories.java b/src/main/java/org/springframework/data/elasticsearch/repository/config/EnableElasticsearchRepositories.java index 240559a3ba..120d0df50f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/config/EnableElasticsearchRepositories.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/config/EnableElasticsearchRepositories.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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/repository/config/EnableReactiveElasticsearchRepositories.java b/src/main/java/org/springframework/data/elasticsearch/repository/config/EnableReactiveElasticsearchRepositories.java index de99afc698..42e1534f02 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/config/EnableReactiveElasticsearchRepositories.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/config/EnableReactiveElasticsearchRepositories.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/repository/config/ReactiveElasticsearchRepositoriesRegistrar.java b/src/main/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoriesRegistrar.java index 601edbbd24..485f322bd1 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoriesRegistrar.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoriesRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/repository/config/ReactiveElasticsearchRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoryConfigurationExtension.java index afa141e7d5..6eb546eec6 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoryConfigurationExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/repository/config/package-info.java b/src/main/java/org/springframework/data/elasticsearch/repository/config/package-info.java index 9f8e266765..2278b3f140 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/config/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/config/package-info.java @@ -1,3 +1,2 @@ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.repository.config; diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/package-info.java b/src/main/java/org/springframework/data/elasticsearch/repository/package-info.java index 9d9e57ab26..14c68bac4f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/package-info.java @@ -1,5 +1,5 @@ /** * infrastructure to define the Elasticsearch mapping for an index. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.repository; diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java index b5814169c1..fae3b1992a 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -15,24 +15,23 @@ */ package org.springframework.data.elasticsearch.repository.query; -import java.util.Collections; - +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.PageRequest; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.SearchHitSupport; import org.springframework.data.elasticsearch.core.SearchHits; -import org.springframework.data.elasticsearch.core.SearchHitsImpl; -import org.springframework.data.elasticsearch.core.TotalHitsRelation; import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.query.BaseQuery; +import org.springframework.data.elasticsearch.core.query.DeleteQuery; import org.springframework.data.elasticsearch.core.query.Query; +import org.springframework.data.elasticsearch.core.query.SearchTemplateQuery; +import org.springframework.data.expression.ValueEvaluationContextProvider; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.util.StreamUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -42,6 +41,7 @@ * @author Rizwan Idrees * @author Mohsin Husen * @author Peter-Josef Meisch + * @author Haibo Liu */ public abstract class AbstractElasticsearchRepositoryQuery implements RepositoryQuery { @@ -50,12 +50,20 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository protected ElasticsearchQueryMethod queryMethod; protected final ElasticsearchOperations elasticsearchOperations; protected final ElasticsearchConverter elasticsearchConverter; + protected final ValueEvaluationContextProvider evaluationContextProvider; public AbstractElasticsearchRepositoryQuery(ElasticsearchQueryMethod queryMethod, - ElasticsearchOperations elasticsearchOperations) { + ElasticsearchOperations elasticsearchOperations, + ValueEvaluationContextProvider evaluationContextProvider) { + + Assert.notNull(queryMethod, "queryMethod must not be null"); + Assert.notNull(elasticsearchOperations, "elasticsearchOperations must not be null"); + Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null"); + this.queryMethod = queryMethod; this.elasticsearchOperations = elasticsearchOperations; this.elasticsearchConverter = elasticsearchOperations.getElasticsearchConverter(); + this.evaluationContextProvider = evaluationContextProvider; } @Override @@ -88,7 +96,7 @@ public Object execute(Object[] parameters) { if (isDeleteQuery()) { result = countOrGetDocumentsForDelete(query, parameterAccessor); - elasticsearchOperations.delete(query, clazz, index); + elasticsearchOperations.delete(DeleteQuery.builder(query).build(), clazz, index); elasticsearchOperations.indexOps(index).refresh(); } else if (isCountQuery()) { result = elasticsearchOperations.count(query, clazz, index); @@ -107,24 +115,17 @@ public Object execute(Object[] parameters) { : PageRequest.of(0, DEFAULT_STREAM_BATCH_SIZE)); result = StreamUtils.createStreamFromIterator(elasticsearchOperations.searchForStream(query, clazz, index)); } else if (queryMethod.isCollectionQuery()) { - - if (parameterAccessor.getPageable().isUnpaged()) { - int itemCount = (int) elasticsearchOperations.count(query, clazz, index); - - if (itemCount == 0) { - result = new SearchHitsImpl<>(0, TotalHitsRelation.EQUAL_TO, Float.NaN, null, - query.getPointInTime() != null ? query.getPointInTime().id() : null, Collections.emptyList(), null, null); - } else { + if (query instanceof SearchTemplateQuery) { + // we cannot get a count here, from and size would be in the template + } else { + if (parameterAccessor.getPageable().isUnpaged()) { + int itemCount = (int) elasticsearchOperations.count(query, clazz, index); query.setPageable(PageRequest.of(0, Math.max(1, itemCount))); + } else { + query.setPageable(parameterAccessor.getPageable()); } - } else { - query.setPageable(parameterAccessor.getPageable()); - } - - if (result == null) { - result = elasticsearchOperations.search(query, clazz, index); } - + result = elasticsearchOperations.search(query, clazz, index); } else { result = elasticsearchOperations.searchOne(query, clazz, index); } @@ -137,12 +138,13 @@ public Object execute(Object[] parameters) { public Query createQuery(Object[] parameters) { ElasticsearchParametersParameterAccessor parameterAccessor = getParameterAccessor(parameters); - ResultProcessor resultProcessor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor); var query = createQuery(parameterAccessor); Assert.notNull(query, "unsupported query"); - queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter()); + queryMethod.addSpecialMethodParameters(query, parameterAccessor, + elasticsearchOperations.getElasticsearchConverter(), + evaluationContextProvider); return query; } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java index cbb984e96b..1d72522865 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -28,9 +28,11 @@ import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.query.BaseQuery; import org.springframework.data.elasticsearch.core.query.ByQueryResponse; +import org.springframework.data.elasticsearch.core.query.DeleteQuery; import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchQueryExecution.ResultProcessingConverter; import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchQueryExecution.ResultProcessingExecution; +import org.springframework.data.expression.ValueEvaluationContextProvider; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.QueryMethod; @@ -43,18 +45,26 @@ * * @author Christoph Strobl * @author Peter-Josef Meisch + * @author Haibo Liu * @since 3.2 */ abstract class AbstractReactiveElasticsearchRepositoryQuery implements RepositoryQuery { protected final ReactiveElasticsearchQueryMethod queryMethod; private final ReactiveElasticsearchOperations elasticsearchOperations; + protected final ValueEvaluationContextProvider evaluationContextProvider; AbstractReactiveElasticsearchRepositoryQuery(ReactiveElasticsearchQueryMethod queryMethod, - ReactiveElasticsearchOperations elasticsearchOperations) { + ReactiveElasticsearchOperations elasticsearchOperations, + ValueEvaluationContextProvider evaluationContextProvider) { + + Assert.notNull(queryMethod, "queryMethod must not be null"); + Assert.notNull(elasticsearchOperations, "elasticsearchOperations must not be null"); + Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null"); this.queryMethod = queryMethod; this.elasticsearchOperations = elasticsearchOperations; + this.evaluationContextProvider = evaluationContextProvider; } /* @@ -95,7 +105,9 @@ private Object execute(ElasticsearchParametersParameterAccessor parameterAccesso var query = createQuery(parameterAccessor); Assert.notNull(query, "unsupported query"); - queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter()); + queryMethod.addSpecialMethodParameters(query, parameterAccessor, + elasticsearchOperations.getElasticsearchConverter(), + evaluationContextProvider); String indexName = queryMethod.getEntityInformation().getIndexName(); IndexCoordinates index = IndexCoordinates.of(indexName); @@ -112,7 +124,7 @@ private Object execute(ElasticsearchParametersParameterAccessor parameterAccesso * @param accessor must not be {@literal null}. * @return */ - protected abstract BaseQuery createQuery(ElasticsearchParameterAccessor accessor); + protected abstract BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor); private ReactiveElasticsearchQueryExecution getExecution(ElasticsearchParameterAccessor accessor, Converter resultProcessing) { @@ -123,7 +135,8 @@ private ReactiveElasticsearchQueryExecution getExecutionToWrap(ElasticsearchPara ReactiveElasticsearchOperations operations) { if (isDeleteQuery()) { - return (query, type, targetType, indexCoordinates) -> operations.delete(query, type, indexCoordinates) + return (query, type, targetType, indexCoordinates) -> operations + .delete(DeleteQuery.builder(query).build(), type, indexCoordinates) .map(ByQueryResponse::getDeleted); } else if (isCountQuery()) { return (query, type, targetType, indexCoordinates) -> operations.count(query, type, indexCoordinates); diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchEntityMetadata.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchEntityMetadata.java index d8e074d70e..4e041b4282 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchEntityMetadata.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchEntityMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/repository/query/ElasticsearchParameter.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameter.java index 45f4efb972..4f7039f94c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameter.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameter.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -29,7 +29,7 @@ * @author Peter-Josef Meisch * @since 5.2 */ -class ElasticsearchParameter extends Parameter { +public class ElasticsearchParameter extends Parameter { /** * Creates a new {@link ElasticsearchParameter}. diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameterAccessor.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameterAccessor.java index 88328c0c88..fd3450919c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameterAccessor.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameterAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/repository/query/ElasticsearchParameters.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameters.java index d1a76daaf6..7b2e724692 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameters.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -15,12 +15,12 @@ */ package org.springframework.data.elasticsearch.repository.query; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import org.springframework.core.MethodParameter; import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.ParametersSource; import org.springframework.data.util.TypeInformation; /** @@ -33,10 +33,13 @@ public class ElasticsearchParameters extends Parameters scriptedFields = new ArrayList<>(); private final List runtimeFields = new ArrayList<>(); - public ElasticsearchParameters(Method method, TypeInformation domainType) { + public ElasticsearchParameters(ParametersSource parametersSource) { - super(method, parameter -> new ElasticsearchParameter(parameter, domainType)); + super(parametersSource, + parameter -> new ElasticsearchParameter(parameter, parametersSource.getDomainTypeInformation())); + var domainType = parametersSource.getDomainTypeInformation(); + var method = parametersSource.getMethod(); int parameterCount = method.getParameterCount(); for (int i = 0; i < parameterCount; i++) { MethodParameter methodParameter = new MethodParameter(method, i); @@ -50,7 +53,6 @@ public ElasticsearchParameters(Method method, TypeInformation domainType) { runtimeFields.add(parameter); } } - } private ElasticsearchParameter parameterFactory(MethodParameter methodParameter, TypeInformation domainType) { diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParametersParameterAccessor.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParametersParameterAccessor.java index 67267467a7..7ff3055118 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParametersParameterAccessor.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchParametersParameterAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -21,7 +21,7 @@ * @author Christoph Strobl * @since 3.2 */ -class ElasticsearchParametersParameterAccessor extends ParametersParameterAccessor +public class ElasticsearchParametersParameterAccessor extends ParametersParameterAccessor implements ElasticsearchParameterAccessor { private final Object[] values; diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java index 3463b2b5d0..f06eba3084 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -16,14 +16,7 @@ package org.springframework.data.elasticsearch.repository.query; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; -import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; -import org.springframework.data.elasticsearch.core.query.BaseQuery; -import org.springframework.data.elasticsearch.core.query.Query; -import org.springframework.data.elasticsearch.core.query.RuntimeField; -import org.springframework.data.elasticsearch.core.query.ScriptedField; -import org.springframework.data.elasticsearch.repository.query.parser.ElasticsearchQueryCreator; -import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.data.repository.query.ValueExpressionDelegate; /** * ElasticsearchPartQuery @@ -34,41 +27,13 @@ * @author Mark Paluch * @author Rasmus Faber-Espensen * @author Peter-Josef Meisch + * @author Haibo Liu + * @deprecated since 5.5, use {@link RepositoryPartQuery} instead */ -public class ElasticsearchPartQuery extends AbstractElasticsearchRepositoryQuery { - - private final PartTree tree; - private final MappingContext mappingContext; - - public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations) { - super(method, elasticsearchOperations); - this.tree = new PartTree(queryMethod.getName(), queryMethod.getResultProcessor().getReturnedType().getDomainType()); - this.mappingContext = elasticsearchConverter.getMappingContext(); - } - - @Override - public boolean isCountQuery() { - return tree.isCountProjection(); - } - - @Override - protected boolean isDeleteQuery() { - return tree.isDelete(); - } - - @Override - protected boolean isExistsQuery() { - return tree.isExistsProjection(); - } - - protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor) { - - BaseQuery query = new ElasticsearchQueryCreator(tree, accessor, mappingContext).createQuery(); - - if (tree.getMaxResults() != null) { - query.setMaxResults(tree.getMaxResults()); - } - - return query; +@Deprecated(forRemoval = true) +public class ElasticsearchPartQuery extends RepositoryPartQuery { + public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations, + ValueExpressionDelegate valueExpressionDelegate) { + super(method, elasticsearchOperations, valueExpressionDelegate); } } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java index 6af2b618e9..2ff4bc2365 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -23,10 +23,13 @@ import java.util.List; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.convert.ConversionService; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.elasticsearch.annotations.Highlight; import org.springframework.data.elasticsearch.annotations.Query; +import org.springframework.data.elasticsearch.annotations.SearchTemplateQuery; import org.springframework.data.elasticsearch.annotations.SourceFilters; import org.springframework.data.elasticsearch.core.SearchHit; import org.springframework.data.elasticsearch.core.SearchHits; @@ -41,18 +44,17 @@ import org.springframework.data.elasticsearch.core.query.RuntimeField; import org.springframework.data.elasticsearch.core.query.ScriptedField; import org.springframework.data.elasticsearch.core.query.SourceFilter; -import org.springframework.data.elasticsearch.repository.support.StringQueryUtil; +import org.springframework.data.elasticsearch.repository.support.QueryStringProcessor; +import org.springframework.data.expression.ValueEvaluationContextProvider; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; -import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.ParametersSource; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.util.QueryExecutionConverters; import org.springframework.data.repository.util.ReactiveWrapperConverters; -import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -66,12 +68,13 @@ * @author Christoph Strobl * @author Peter-Josef Meisch * @author Alexander Torres + * @author Haibo Liu */ public class ElasticsearchQueryMethod extends QueryMethod { - // the following 2 variables exits in the base class, but are private. We need them for + // the following 2 variables exist in the base class, but are private. We need them for // correct handling of return types (SearchHits), so we have our own values here. - // Alas this means that we have to copy code that initializes these variables and in the + // This means that we have to copy code that initializes these variables and in the // base class uses them in order to use our variables protected final Method method; protected final Class unwrappedReturnType; @@ -81,9 +84,8 @@ public class ElasticsearchQueryMethod extends QueryMethod { @Nullable private ElasticsearchEntityMetadata metadata; @Nullable private final Query queryAnnotation; @Nullable private final Highlight highlightAnnotation; - private final Lazy highlightQueryLazy = Lazy.of(this::createAnnotatedHighlightQuery); - @Nullable private final SourceFilters sourceFilters; + @Nullable private final SearchTemplateQuery searchTemplateQueryAnnotation; public ElasticsearchQueryMethod(Method method, RepositoryMetadata repositoryMetadata, ProjectionFactory factory, MappingContext, ElasticsearchPersistentProperty> mappingContext) { @@ -98,13 +100,14 @@ public ElasticsearchQueryMethod(Method method, RepositoryMetadata repositoryMeta this.highlightAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Highlight.class); this.sourceFilters = AnnotatedElementUtils.findMergedAnnotation(method, SourceFilters.class); this.unwrappedReturnType = potentiallyUnwrapReturnTypeFor(repositoryMetadata, method); + this.searchTemplateQueryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, SearchTemplateQuery.class); verifyCountQueryTypes(); } @Override - protected Parameters createParameters(Method method, TypeInformation domainType) { - return new ElasticsearchParameters(method, domainType); + protected Parameters createParameters(ParametersSource parametersSource) { + return new ElasticsearchParameters(parametersSource); } protected void verifyCountQueryTypes() { @@ -118,12 +121,16 @@ protected void verifyCountQueryTypes() { } } + /** + * @return if the method is annotated with the {@link Query} annotation. + */ public boolean hasAnnotatedQuery() { return this.queryAnnotation != null; } /** - * @return the query String. Must not be {@literal null} when {@link #hasAnnotatedQuery()} returns true + * @return the query String defined in the {@link Query} annotation. Must not be {@literal null} when + * {@link #hasAnnotatedQuery()} returns true. */ @Nullable public String getAnnotatedQuery() { @@ -143,20 +150,33 @@ public boolean hasAnnotatedHighlight() { * @throws IllegalArgumentException if no {@link Highlight} annotation is present on the method * @see #hasAnnotatedHighlight() */ - public HighlightQuery getAnnotatedHighlightQuery() { + public HighlightQuery getAnnotatedHighlightQuery(HighlightConverter highlightConverter) { Assert.isTrue(hasAnnotatedHighlight(), "no Highlight annotation present on " + getName()); + Assert.notNull(highlightAnnotation, "highlightAnnotation must not be null"); - return highlightQueryLazy.get(); + return new HighlightQuery(highlightConverter.convert(highlightAnnotation), getDomainClass()); } - private HighlightQuery createAnnotatedHighlightQuery() { + /** + * @return if the method is annotated with the {@link SearchTemplateQuery} annotation. + * @since 5.5 + */ + public boolean hasAnnotatedSearchTemplateQuery() { + return this.searchTemplateQueryAnnotation != null; + } - Assert.notNull(highlightAnnotation, "highlightAnnotation must not be null"); + /** + * @return the {@link SearchTemplateQuery} annotation + * @throws IllegalArgumentException if no {@link SearchTemplateQuery} annotation is present on the method + * @since 5.5 + */ + public SearchTemplateQuery getAnnotatedSearchTemplateQuery() { + + Assert.isTrue(hasAnnotatedSearchTemplateQuery(), "no SearchTemplateQuery annotation present on " + getName()); + Assert.notNull(searchTemplateQueryAnnotation, "highlsearchTemplateQueryAnnotationightAnnotation must not be null"); - return new HighlightQuery( - org.springframework.data.elasticsearch.core.query.highlight.Highlight.of(highlightAnnotation), - getDomainClass()); + return searchTemplateQueryAnnotation; } /** @@ -282,7 +302,7 @@ public boolean isNotSearchPageMethod() { /** * @return {@literal true} if the method is annotated with - * {@link org.springframework.data.elasticsearch.annotations.CountQuery} or with {@link Query}(count =true) + * {@link org.springframework.data.elasticsearch.annotations.CountQuery} or with {@link Query}(count = true) * @since 4.2 */ public boolean hasCountQueryAnnotation() { @@ -296,42 +316,46 @@ public boolean hasCountQueryAnnotation() { * @param parameterAccessor the accessor with the query method parameter details * @param converter {@link ElasticsearchConverter} needed to convert entity property names to the Elasticsearch field * names and for parameter conversion when the includes or excludes are defined as parameters + * @param evaluationContextProvider to provide an evaluation context for SpEL evaluation * @return source filter with includes and excludes for a query, {@literal null} when no {@link SourceFilters} * annotation was set on the method. * @since 5.0 */ @Nullable - SourceFilter getSourceFilter(ParameterAccessor parameterAccessor, ElasticsearchConverter converter) { + SourceFilter getSourceFilter(ElasticsearchParametersParameterAccessor parameterAccessor, + ElasticsearchConverter converter, + ValueEvaluationContextProvider evaluationContextProvider) { if (sourceFilters == null || (sourceFilters.includes().length == 0 && sourceFilters.excludes().length == 0)) { return null; } - StringQueryUtil stringQueryUtil = new StringQueryUtil(converter.getConversionService()); + ConversionService conversionService = converter.getConversionService(); FetchSourceFilterBuilder fetchSourceFilterBuilder = new FetchSourceFilterBuilder(); if (sourceFilters.includes().length > 0) { - fetchSourceFilterBuilder - .withIncludes(mapParameters(sourceFilters.includes(), parameterAccessor, stringQueryUtil)); + fetchSourceFilterBuilder.withIncludes(mapParameters(sourceFilters.includes(), parameterAccessor, + conversionService, evaluationContextProvider)); } if (sourceFilters.excludes().length > 0) { - fetchSourceFilterBuilder - .withExcludes(mapParameters(sourceFilters.excludes(), parameterAccessor, stringQueryUtil)); + fetchSourceFilterBuilder.withExcludes(mapParameters(sourceFilters.excludes(), parameterAccessor, + conversionService, evaluationContextProvider)); } return fetchSourceFilterBuilder.build(); } - private String[] mapParameters(String[] source, ParameterAccessor parameterAccessor, - StringQueryUtil stringQueryUtil) { + private String[] mapParameters(String[] source, ElasticsearchParametersParameterAccessor parameterAccessor, + ConversionService conversionService, ValueEvaluationContextProvider evaluationContextProvider) { List fieldNames = new ArrayList<>(); for (String s : source) { if (!s.isBlank()) { - String fieldName = stringQueryUtil.replacePlaceholders(s, parameterAccessor); + String fieldName = new QueryStringProcessor(s, this, conversionService, evaluationContextProvider) + .createQuery(parameterAccessor); // this could be "[\"foo\",\"bar\"]", must be split if (fieldName.startsWith("[") && fieldName.endsWith("]")) { // noinspection RegExpRedundantEscape @@ -352,7 +376,7 @@ private String[] mapParameters(String[] source, ParameterAccessor parameterAcces /* * Copied from the QueryMethod class adding support for collections of SearchHit instances. No static method here. */ - private Class potentiallyUnwrapReturnTypeFor(RepositoryMetadata metadata, Method method) { + private Class potentiallyUnwrapReturnTypeFor(RepositoryMetadata metadata, Method method) { TypeInformation returnType = metadata.getReturnType(method); if (!QueryExecutionConverters.supports(returnType.getType()) && !ReactiveWrapperConverters.supports(returnType.getType())) { @@ -374,14 +398,17 @@ private Class potentiallyUnwrapReturnTypeFor(RepositoryMetadat } } - void addMethodParameter(BaseQuery query, ElasticsearchParametersParameterAccessor parameterAccessor, - ElasticsearchConverter elasticsearchConverter) { + void addSpecialMethodParameters(BaseQuery query, ElasticsearchParametersParameterAccessor parameterAccessor, + ElasticsearchConverter elasticsearchConverter, + ValueEvaluationContextProvider evaluationContextProvider) { if (hasAnnotatedHighlight()) { - query.setHighlightQuery(getAnnotatedHighlightQuery()); + var highlightQuery = getAnnotatedHighlightQuery(new HighlightConverter(parameterAccessor, + elasticsearchConverter.getConversionService(), evaluationContextProvider, this)); + query.setHighlightQuery(highlightQuery); } - var sourceFilter = getSourceFilter(parameterAccessor, elasticsearchConverter); + var sourceFilter = getSourceFilter(parameterAccessor, elasticsearchConverter, evaluationContextProvider); if (sourceFilter != null) { query.addSourceFilter(sourceFilter); } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java index de1417a516..32f7d32a72 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -16,11 +16,7 @@ package org.springframework.data.elasticsearch.repository.query; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; -import org.springframework.data.elasticsearch.core.query.BaseQuery; -import org.springframework.data.elasticsearch.core.query.Query; -import org.springframework.data.elasticsearch.core.query.StringQuery; -import org.springframework.data.elasticsearch.repository.support.StringQueryUtil; -import org.springframework.util.Assert; +import org.springframework.data.repository.query.ValueExpressionDelegate; /** * ElasticsearchStringQuery @@ -30,40 +26,13 @@ * @author Mark Paluch * @author Taylor Ono * @author Peter-Josef Meisch + * @author Haibo Liu + * @deprecated since 5.5, use {@link RepositoryStringQuery} */ -public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQuery { - - private final String queryString; - +@Deprecated(since = "5.5", forRemoval = true) +public class ElasticsearchStringQuery extends RepositoryStringQuery { public ElasticsearchStringQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations, - String queryString) { - super(queryMethod, elasticsearchOperations); - Assert.notNull(queryString, "Query cannot be empty"); - this.queryString = queryString; - } - - @Override - public boolean isCountQuery() { - return queryMethod.hasCountQueryAnnotation(); - } - - @Override - protected boolean isDeleteQuery() { - return false; - } - - @Override - protected boolean isExistsQuery() { - return false; - } - - protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) { - - String queryString = new StringQueryUtil(elasticsearchOperations.getElasticsearchConverter().getConversionService()) - .replacePlaceholders(this.queryString, parameterAccessor); - - var query = new StringQuery(queryString); - query.addSort(parameterAccessor.getSort()); - return query; + String queryString, ValueExpressionDelegate valueExpressionDelegate) { + super(queryMethod, elasticsearchOperations, queryString, valueExpressionDelegate); } } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/HighlightConverter.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/HighlightConverter.java new file mode 100644 index 0000000000..21b134b2bf --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/HighlightConverter.java @@ -0,0 +1,108 @@ +/* + * 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. + * 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.repository.query; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.core.convert.ConversionService; +import org.springframework.data.elasticsearch.core.query.Query; +import org.springframework.data.elasticsearch.core.query.StringQuery; +import org.springframework.data.elasticsearch.core.query.highlight.Highlight; +import org.springframework.data.elasticsearch.core.query.highlight.HighlightField; +import org.springframework.data.elasticsearch.core.query.highlight.HighlightParameters; +import org.springframework.data.elasticsearch.repository.support.QueryStringProcessor; +import org.springframework.data.expression.ValueEvaluationContextProvider; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.util.Assert; + +/** + * Convert {@link org.springframework.data.elasticsearch.annotations.Highlight} to {@link Highlight}. + * + * @author Haibo Liu + */ +public class HighlightConverter { + + private final ElasticsearchParametersParameterAccessor parameterAccessor; + private final ConversionService conversionService; + private final ValueEvaluationContextProvider evaluationContextProvider; + private final QueryMethod queryMethod; + + HighlightConverter(ElasticsearchParametersParameterAccessor parameterAccessor, + ConversionService conversionService, + ValueEvaluationContextProvider evaluationContextProvider, + QueryMethod queryMethod) { + + Assert.notNull(parameterAccessor, "parameterAccessor must not be null"); + Assert.notNull(conversionService, "conversionService must not be null"); + Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null"); + Assert.notNull(queryMethod, "queryMethod must not be null"); + + this.parameterAccessor = parameterAccessor; + this.conversionService = conversionService; + this.evaluationContextProvider = evaluationContextProvider; + this.queryMethod = queryMethod; + } + + /** + * Creates a {@link Highlight} from an Annotation instance. + * + * @param highlight must not be {@literal null} + * @return highlight definition + */ + Highlight convert(org.springframework.data.elasticsearch.annotations.Highlight highlight) { + + Assert.notNull(highlight, "highlight must not be null"); + + org.springframework.data.elasticsearch.annotations.HighlightParameters parameters = highlight.parameters(); + + // replace placeholders in highlight query with actual parameters + Query highlightQuery = null; + if (!parameters.highlightQuery().value().isEmpty()) { + String rawQuery = parameters.highlightQuery().value(); + String query = new QueryStringProcessor(rawQuery, queryMethod, conversionService, evaluationContextProvider) + .createQuery(parameterAccessor); + highlightQuery = new StringQuery(query); + } + + HighlightParameters highlightParameters = HighlightParameters.builder() // + .withBoundaryChars(parameters.boundaryChars()) // + .withBoundaryMaxScan(parameters.boundaryMaxScan()) // + .withBoundaryScanner(parameters.boundaryScanner()) // + .withBoundaryScannerLocale(parameters.boundaryScannerLocale()) // + .withEncoder(parameters.encoder()) // + .withForceSource(parameters.forceSource()) // + .withFragmenter(parameters.fragmenter()) // + .withFragmentSize(parameters.fragmentSize()) // + .withNoMatchSize(parameters.noMatchSize()) // + .withNumberOfFragments(parameters.numberOfFragments()) // + .withHighlightQuery(highlightQuery) // + .withOrder(parameters.order()) // + .withPhraseLimit(parameters.phraseLimit()) // + .withPreTags(parameters.preTags()) // + .withPostTags(parameters.postTags()) // + .withRequireFieldMatch(parameters.requireFieldMatch()) // + .withTagsSchema(parameters.tagsSchema()) // + .withType(parameters.type()) // + .build(); + + List highlightFields = Arrays.stream(highlight.fields()) // + .map(HighlightField::of) // + .toList(); + + return new Highlight(highlightParameters, highlightFields); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchParametersParameterAccessor.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchParametersParameterAccessor.java index bba53bee38..8688dcb0d8 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchParametersParameterAccessor.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchParametersParameterAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -22,7 +22,7 @@ import java.util.List; import org.springframework.data.repository.util.ReactiveWrapperConverters; -import org.springframework.data.repository.util.ReactiveWrappers; +import org.springframework.data.util.ReactiveWrappers; /** * @author Christoph Strobl diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryExecution.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryExecution.java index 3566c6e298..c166e7edd6 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryExecution.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryExecution.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -15,12 +15,12 @@ */ package org.springframework.data.elasticsearch.repository.query; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethod.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethod.java index 60ccbcd986..422eb46dbb 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethod.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -15,8 +15,6 @@ */ package org.springframework.data.elasticsearch.repository.query; -import static org.springframework.data.repository.util.ClassUtils.*; - import reactor.core.publisher.Mono; import java.lang.reflect.Method; @@ -36,6 +34,7 @@ import org.springframework.data.repository.util.ReactiveWrapperConverters; import org.springframework.data.util.Lazy; import org.springframework.data.util.ReactiveWrappers; +import org.springframework.data.util.ReflectionUtils; import org.springframework.data.util.TypeInformation; import org.springframework.util.ClassUtils; @@ -55,7 +54,7 @@ public ReactiveElasticsearchQueryMethod(Method method, RepositoryMetadata metada super(method, metadata, factory, mappingContext); - if (hasParameterOfType(method, Pageable.class)) { + if (ReflectionUtils.hasParameterOfType(method, Pageable.class)) { TypeInformation returnType = TypeInformation.fromReturnTypeOf(method); boolean multiWrapper = ReactiveWrappers.isMultiValueType(returnType.getType()); @@ -75,7 +74,7 @@ public ReactiveElasticsearchQueryMethod(Method method, RepositoryMetadata metada method)); } - if (hasParameterOfType(method, Sort.class)) { + if (ReflectionUtils.hasParameterOfType(method, Sort.class)) { throw new IllegalStateException(String.format("Method must not have Pageable *and* Sort parameter. " + "Use sorting capabilities on Pageable instead! Offending method: %s", method)); } @@ -101,11 +100,6 @@ protected void verifyCountQueryTypes() { } } - @Override - protected ElasticsearchParameters createParameters(Method method, TypeInformation domainType) { - return new ElasticsearchParameters(method, domainType); - } - /** * Check if the given {@link org.springframework.data.repository.query.QueryMethod} receives a reactive parameter * wrapper as one of its parameters. diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQuery.java index 9df6435149..f636a566d3 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -16,60 +16,25 @@ package org.springframework.data.elasticsearch.repository.query; import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; -import org.springframework.data.elasticsearch.core.query.StringQuery; -import org.springframework.data.elasticsearch.repository.support.StringQueryUtil; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.data.repository.query.ValueExpressionDelegate; /** * @author Christoph Strobl * @author Taylor Ono + * @author Haibo Liu * @since 3.2 + * @deprecated since 5.5, use {@link ReactiveRepositoryStringQuery} */ -public class ReactiveElasticsearchStringQuery extends AbstractReactiveElasticsearchRepositoryQuery { - - private final String query; +@Deprecated(since = "5.5", forRemoval = true) +public class ReactiveElasticsearchStringQuery extends ReactiveRepositoryStringQuery { public ReactiveElasticsearchStringQuery(ReactiveElasticsearchQueryMethod queryMethod, - ReactiveElasticsearchOperations operations, SpelExpressionParser expressionParser, - QueryMethodEvaluationContextProvider evaluationContextProvider) { - - this(queryMethod.getAnnotatedQuery(), queryMethod, operations, expressionParser, evaluationContextProvider); + ReactiveElasticsearchOperations operations, ValueExpressionDelegate valueExpressionDelegate) { + super(queryMethod, operations, valueExpressionDelegate); } public ReactiveElasticsearchStringQuery(String query, ReactiveElasticsearchQueryMethod queryMethod, - ReactiveElasticsearchOperations operations, SpelExpressionParser expressionParser, - QueryMethodEvaluationContextProvider evaluationContextProvider) { - - super(queryMethod, operations); - this.query = query; - } - - @Override - protected StringQuery createQuery(ElasticsearchParameterAccessor parameterAccessor) { - String queryString = new StringQueryUtil( - getElasticsearchOperations().getElasticsearchConverter().getConversionService()).replacePlaceholders(this.query, - parameterAccessor); - return new StringQuery(queryString); - } - - @Override - boolean isCountQuery() { - return queryMethod.hasCountQueryAnnotation(); - } - - @Override - boolean isDeleteQuery() { - return false; - } - - @Override - boolean isExistsQuery() { - return false; - } - - @Override - boolean isLimiting() { - return false; + ReactiveElasticsearchOperations operations, ValueExpressionDelegate valueExpressionDelegate) { + super(query, queryMethod, operations, valueExpressionDelegate); } } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactivePartTreeElasticsearchQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactivePartTreeElasticsearchQuery.java index 8563c50caf..417106457a 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactivePartTreeElasticsearchQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactivePartTreeElasticsearchQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -20,11 +20,13 @@ import org.springframework.data.elasticsearch.core.query.CriteriaQuery; import org.springframework.data.elasticsearch.repository.query.parser.ElasticsearchQueryCreator; import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.parser.PartTree; /** * @author Christoph Strobl * @author Peter-Josef Meisch + * @author Haibo Liu * @since 3.2 */ public class ReactivePartTreeElasticsearchQuery extends AbstractReactiveElasticsearchRepositoryQuery { @@ -32,15 +34,17 @@ public class ReactivePartTreeElasticsearchQuery extends AbstractReactiveElastics private final PartTree tree; public ReactivePartTreeElasticsearchQuery(ReactiveElasticsearchQueryMethod queryMethod, - ReactiveElasticsearchOperations elasticsearchOperations) { - super(queryMethod, elasticsearchOperations); + ReactiveElasticsearchOperations elasticsearchOperations, + ValueExpressionDelegate valueExpressionDelegate) { + super(queryMethod, elasticsearchOperations, + valueExpressionDelegate.createValueContextProvider(queryMethod.getParameters())); ResultProcessor processor = queryMethod.getResultProcessor(); this.tree = new PartTree(queryMethod.getName(), processor.getReturnedType().getDomainType()); } @Override - protected BaseQuery createQuery(ElasticsearchParameterAccessor accessor) { + protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor) { CriteriaQuery query = new ElasticsearchQueryCreator(tree, accessor, getMappingContext()).createQuery(); if (tree.isLimiting()) { diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositorySearchTemplateQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositorySearchTemplateQuery.java new file mode 100644 index 0000000000..12e09eebfc --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositorySearchTemplateQuery.java @@ -0,0 +1,93 @@ +/* + * 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.repository.query; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; +import org.springframework.data.elasticsearch.core.query.BaseQuery; +import org.springframework.data.elasticsearch.core.query.SearchTemplateQuery; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.util.Assert; + +/** + * A reactive repository query that uses a search template already stored in Elasticsearch. + * + * @author P.J. Meisch (pj.meisch@sothawo.com) + * @since 5.5 + */ +public class ReactiveRepositorySearchTemplateQuery extends AbstractReactiveElasticsearchRepositoryQuery { + + private String id; + private Map params; + + public ReactiveRepositorySearchTemplateQuery(ReactiveElasticsearchQueryMethod queryMethod, + ReactiveElasticsearchOperations elasticsearchOperations, + ValueExpressionDelegate valueExpressionDelegate, + String id) { + super(queryMethod, elasticsearchOperations, + valueExpressionDelegate.createValueContextProvider(queryMethod.getParameters())); + Assert.hasLength(id, "id must not be null or empty"); + this.id = id; + } + + public String getId() { + return id; + } + + public Map getParams() { + return params; + } + + @Override + public boolean isCountQuery() { + return false; + } + + @Override + protected boolean isDeleteQuery() { + return false; + } + + @Override + protected boolean isExistsQuery() { + return false; + } + + @Override + boolean isLimiting() { + return false; + } + + @Override + protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) { + + var searchTemplateParameters = new LinkedHashMap(); + var values = parameterAccessor.getValues(); + + parameterAccessor.getParameters().forEach(parameter -> { + if (!parameter.isSpecialParameter() && parameter.getName().isPresent() && parameter.getIndex() <= values.length) { + searchTemplateParameters.put(parameter.getName().get(), values[parameter.getIndex()]); + } + }); + + return SearchTemplateQuery.builder() + .withId(id) + .withParams(searchTemplateParameters) + .build(); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositoryStringQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositoryStringQuery.java new file mode 100644 index 0000000000..45b9e66e58 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositoryStringQuery.java @@ -0,0 +1,81 @@ +/* + * Copyright 2019-2024 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.repository.query; + +import org.springframework.core.convert.ConversionService; +import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; +import org.springframework.data.elasticsearch.core.query.BaseQuery; +import org.springframework.data.elasticsearch.core.query.StringQuery; +import org.springframework.data.elasticsearch.repository.support.QueryStringProcessor; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.util.Assert; + +/** + * Was originally named ReactiveElasticsearchStringQuery. + * + * @author Christoph Strobl + * @author Taylor Ono + * @author Haibo Liu + * @since 3.2 + */ +public class ReactiveRepositoryStringQuery extends AbstractReactiveElasticsearchRepositoryQuery { + + private final String query; + + public ReactiveRepositoryStringQuery(ReactiveElasticsearchQueryMethod queryMethod, + ReactiveElasticsearchOperations operations, ValueExpressionDelegate valueExpressionDelegate) { + + this(queryMethod.getAnnotatedQuery(), queryMethod, operations, valueExpressionDelegate); + } + + public ReactiveRepositoryStringQuery(String query, ReactiveElasticsearchQueryMethod queryMethod, + ReactiveElasticsearchOperations operations, ValueExpressionDelegate valueExpressionDelegate) { + super(queryMethod, operations, valueExpressionDelegate.createValueContextProvider(queryMethod.getParameters())); + + Assert.notNull(query, "query must not be null"); + + this.query = query; + } + + @Override + protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) { + ConversionService conversionService = getElasticsearchOperations().getElasticsearchConverter() + .getConversionService(); + String processed = new QueryStringProcessor(query, queryMethod, conversionService, evaluationContextProvider) + .createQuery(parameterAccessor); + return new StringQuery(processed); + } + + @Override + boolean isCountQuery() { + return queryMethod.hasCountQueryAnnotation(); + } + + @Override + boolean isDeleteQuery() { + return false; + } + + @Override + boolean isExistsQuery() { + return false; + } + + @Override + boolean isLimiting() { + return false; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/RepositoryPartQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/RepositoryPartQuery.java new file mode 100644 index 0000000000..578a30e9cb --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/RepositoryPartQuery.java @@ -0,0 +1,76 @@ +/* + * Copyright 2013-2024 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.repository.query; + +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; +import org.springframework.data.elasticsearch.core.query.BaseQuery; +import org.springframework.data.elasticsearch.repository.query.parser.ElasticsearchQueryCreator; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.data.repository.query.parser.PartTree; + +/** + * A repository query that is built from the the method name in the repository definition. Was originally named + * ElasticsearchPartQuery. + * + * @author Rizwan Idrees + * @author Mohsin Husen + * @author Kevin Leturc + * @author Mark Paluch + * @author Rasmus Faber-Espensen + * @author Peter-Josef Meisch + * @author Haibo Liu + */ +public class RepositoryPartQuery extends AbstractElasticsearchRepositoryQuery { + + private final PartTree tree; + private final MappingContext mappingContext; + + public RepositoryPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations, + ValueExpressionDelegate valueExpressionDelegate) { + super(method, elasticsearchOperations, + valueExpressionDelegate.createValueContextProvider(method.getParameters())); + this.tree = new PartTree(queryMethod.getName(), queryMethod.getResultProcessor().getReturnedType().getDomainType()); + this.mappingContext = elasticsearchConverter.getMappingContext(); + } + + @Override + public boolean isCountQuery() { + return tree.isCountProjection(); + } + + @Override + protected boolean isDeleteQuery() { + return tree.isDelete(); + } + + @Override + protected boolean isExistsQuery() { + return tree.isExistsProjection(); + } + + protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor) { + + BaseQuery query = new ElasticsearchQueryCreator(tree, accessor, mappingContext).createQuery(); + + if (tree.getMaxResults() != null) { + query.setMaxResults(tree.getMaxResults()); + } + + return query; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/RepositorySearchTemplateQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/RepositorySearchTemplateQuery.java new file mode 100644 index 0000000000..ddec6e2f07 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/RepositorySearchTemplateQuery.java @@ -0,0 +1,87 @@ +/* + * 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.repository.query; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.query.BaseQuery; +import org.springframework.data.elasticsearch.core.query.SearchTemplateQuery; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.util.Assert; + +/** + * A repository query that uses a search template already stored in Elasticsearch. + * + * @author P.J. Meisch (pj.meisch@sothawo.com) + * @since 5.5 + */ +public class RepositorySearchTemplateQuery extends AbstractElasticsearchRepositoryQuery { + + private String id; + private Map params; + + public RepositorySearchTemplateQuery(ElasticsearchQueryMethod queryMethod, + ElasticsearchOperations elasticsearchOperations, ValueExpressionDelegate valueExpressionDelegate, + String id) { + super(queryMethod, elasticsearchOperations, + valueExpressionDelegate.createValueContextProvider(queryMethod.getParameters())); + Assert.hasLength(id, "id must not be null or empty"); + this.id = id; + } + + public String getId() { + return id; + } + + public Map getParams() { + return params; + } + + @Override + public boolean isCountQuery() { + return false; + } + + @Override + protected boolean isDeleteQuery() { + return false; + } + + @Override + protected boolean isExistsQuery() { + return false; + } + + @Override + protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) { + + var searchTemplateParameters = new LinkedHashMap(); + var values = parameterAccessor.getValues(); + + parameterAccessor.getParameters().forEach(parameter -> { + if (!parameter.isSpecialParameter() && parameter.getName().isPresent() && parameter.getIndex() <= values.length) { + searchTemplateParameters.put(parameter.getName().get(), values[parameter.getIndex()]); + } + }); + + return SearchTemplateQuery.builder() + .withId(id) + .withParams(searchTemplateParameters) + .build(); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/RepositoryStringQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/RepositoryStringQuery.java new file mode 100644 index 0000000000..1b768b6ea7 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/RepositoryStringQuery.java @@ -0,0 +1,57 @@ +package org.springframework.data.elasticsearch.repository.query; + +import org.springframework.core.convert.ConversionService; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.query.BaseQuery; +import org.springframework.data.elasticsearch.core.query.StringQuery; +import org.springframework.data.elasticsearch.repository.support.QueryStringProcessor; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.util.Assert; + +/** + * A repository query that is defined by a String containing the query. Was originally named ElasticsearchStringQuery. + * + * @author Rizwan Idrees + * @author Mohsin Husen + * @author Mark Paluch + * @author Taylor Ono + * @author Peter-Josef Meisch + * @author Haibo Liu + */ +public class RepositoryStringQuery extends AbstractElasticsearchRepositoryQuery { + private final String queryString; + + public RepositoryStringQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations, + String queryString, ValueExpressionDelegate valueExpressionDelegate) { + super(queryMethod, elasticsearchOperations, + valueExpressionDelegate.createValueContextProvider(queryMethod.getParameters())); + + Assert.notNull(queryString, "Query cannot be empty"); + + this.queryString = queryString; + } + + @Override + public boolean isCountQuery() { + return queryMethod.hasCountQueryAnnotation(); + } + + @Override + protected boolean isDeleteQuery() { + return false; + } + + @Override + protected boolean isExistsQuery() { + return false; + } + + protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) { + ConversionService conversionService = elasticsearchOperations.getElasticsearchConverter().getConversionService(); + var processed = new QueryStringProcessor(queryString, queryMethod, conversionService, evaluationContextProvider) + .createQuery(parameterAccessor); + + return new StringQuery(processed) + .addSort(parameterAccessor.getSort()); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/SimpleElasticsearchEntityMetadata.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/SimpleElasticsearchEntityMetadata.java index 65b3a0b113..86c51400f8 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/SimpleElasticsearchEntityMetadata.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/SimpleElasticsearchEntityMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/repository/query/package-info.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/package-info.java index 0db3509f20..e903d1ffe1 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/package-info.java @@ -1,3 +1,2 @@ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.repository.query; diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/ElasticsearchQueryCreator.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/ElasticsearchQueryCreator.java index c8079922b6..5c1456796a 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/ElasticsearchQueryCreator.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/ElasticsearchQueryCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -18,6 +18,7 @@ import java.util.Collection; import java.util.Iterator; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Sort; import org.springframework.data.elasticsearch.core.geo.GeoBox; @@ -34,7 +35,6 @@ import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.PartTree; -import org.springframework.lang.Nullable; /** * ElasticsearchQueryCreator @@ -44,6 +44,7 @@ * @author Franck Marchand * @author Artur Konczak * @author Peter-Josef Meisch + * @author Junghoon Ban */ public class ElasticsearchQueryCreator extends AbstractQueryCreator { @@ -62,8 +63,8 @@ public ElasticsearchQueryCreator(PartTree tree, MappingContext iterator) { - PersistentPropertyPath path = context - .getPersistentPropertyPath(part.getProperty()); + PersistentPropertyPath path = context.getPersistentPropertyPath( + part.getProperty()); return new CriteriaQuery(from(part, new Criteria(path.toDotPath(ElasticsearchPersistentProperty.QueryPropertyToFieldNameConverter.INSTANCE)), iterator)); @@ -74,8 +75,8 @@ protected CriteriaQuery and(Part part, CriteriaQuery base, Iterator iter if (base == null) { return create(part, iterator); } - PersistentPropertyPath path = context - .getPersistentPropertyPath(part.getProperty()); + PersistentPropertyPath path = context.getPersistentPropertyPath( + part.getProperty()); return base.addCriteria(from(part, new Criteria(path.toDotPath(ElasticsearchPersistentProperty.QueryPropertyToFieldNameConverter.INSTANCE)), iterator)); @@ -100,104 +101,98 @@ private Criteria from(Part part, Criteria criteria, Iterator parameters) { Part.Type type = part.getType(); - switch (type) { - case TRUE: - return criteria.is(true); - case FALSE: - return criteria.is(false); - case NEGATING_SIMPLE_PROPERTY: - return criteria.is(parameters.next()).not(); - case REGEX: - return criteria.expression(parameters.next().toString()); - case LIKE: - case STARTING_WITH: - return criteria.startsWith(parameters.next().toString()); - case ENDING_WITH: - return criteria.endsWith(parameters.next().toString()); - case CONTAINING: - return criteria.contains(parameters.next().toString()); - case GREATER_THAN: - return criteria.greaterThan(parameters.next()); - case AFTER: - case GREATER_THAN_EQUAL: - return criteria.greaterThanEqual(parameters.next()); - case LESS_THAN: - return criteria.lessThan(parameters.next()); - case BEFORE: - case LESS_THAN_EQUAL: - return criteria.lessThanEqual(parameters.next()); - case BETWEEN: - return criteria.between(parameters.next(), parameters.next()); - case IN: - return criteria.in(asArray(parameters.next())); - case NOT_IN: - return criteria.notIn(asArray(parameters.next())); - case SIMPLE_PROPERTY: - case WITHIN: { - Object firstParameter = parameters.next(); - Object secondParameter = null; - if (type == Part.Type.SIMPLE_PROPERTY) { - if (part.getProperty().getType() != GeoPoint.class) { - if (firstParameter != null) { - return criteria.is(firstParameter); - } else { - // searching for null is a must_not (exists) - return criteria.exists().not(); - } - } else { - // it means it's a simple find with exact geopoint matching (e.g. findByLocation) - // and because Elasticsearch does not have any kind of query with just a geopoint - // as argument we use a "geo distance" query with a distance of one meter. - secondParameter = ".001km"; - } + return switch (type) { + case TRUE -> criteria.is(true); + case FALSE -> criteria.is(false); + case NEGATING_SIMPLE_PROPERTY -> criteria.is(parameters.next()).not(); + case REGEX -> criteria.expression(parameters.next().toString()); + case LIKE, STARTING_WITH -> criteria.startsWith(parameters.next().toString()); + case ENDING_WITH -> criteria.endsWith(parameters.next().toString()); + case CONTAINING -> criteria.contains(parameters.next().toString()); + case GREATER_THAN -> criteria.greaterThan(parameters.next()); + case AFTER, GREATER_THAN_EQUAL -> criteria.greaterThanEqual(parameters.next()); + case LESS_THAN -> criteria.lessThan(parameters.next()); + case BEFORE, LESS_THAN_EQUAL -> criteria.lessThanEqual(parameters.next()); + case BETWEEN -> criteria.between(parameters.next(), parameters.next()); + case IN -> criteria.in(asArray(parameters.next())); + case NOT_IN -> criteria.notIn(asArray(parameters.next())); + case SIMPLE_PROPERTY, WITHIN -> within(part, criteria, parameters); + case NEAR -> near(criteria, parameters); + case EXISTS, IS_NOT_NULL -> criteria.exists(); + case IS_NULL -> criteria.not().exists(); + case IS_EMPTY -> criteria.empty(); + case IS_NOT_EMPTY -> criteria.notEmpty(); + default -> throw new InvalidDataAccessApiUsageException("Illegal criteria found '" + type + "'."); + }; + } + + private Criteria within(Part part, Criteria criteria, Iterator parameters) { + + Object firstParameter = parameters.next(); + Object secondParameter; + + if (part.getType() == Part.Type.SIMPLE_PROPERTY) { + if (part.getProperty().getType() != GeoPoint.class) { + if (firstParameter != null) { + return criteria.is(firstParameter); } else { - secondParameter = parameters.next(); + // searching for null is a must_not (exists) + return criteria.exists().not(); } + } else { + // it means it's a simple find with exact geopoint matching (e.g. findByLocation) + // and because Elasticsearch does not have any kind of query with just a geopoint + // as argument we use a "geo distance" query with a distance of one meter. + secondParameter = ".001km"; + } + } else { + secondParameter = parameters.next(); + } - if (firstParameter instanceof GeoPoint && secondParameter instanceof String) - return criteria.within((GeoPoint) firstParameter, (String) secondParameter); + return doWithinIfPossible(criteria, firstParameter, secondParameter); + } - if (firstParameter instanceof Point && secondParameter instanceof Distance) - return criteria.within((Point) firstParameter, (Distance) secondParameter); + private Criteria near(Criteria criteria, Iterator parameters) { - if (firstParameter instanceof String && secondParameter instanceof String) - return criteria.within((String) firstParameter, (String) secondParameter); - } - case NEAR: { - Object firstParameter = parameters.next(); + Object firstParameter = parameters.next(); - if (firstParameter instanceof GeoBox) { - return criteria.boundedBy((GeoBox) firstParameter); - } + if (firstParameter instanceof GeoBox geoBox) { + return criteria.boundedBy(geoBox); + } - if (firstParameter instanceof Box) { - return criteria.boundedBy(GeoBox.fromBox((Box) firstParameter)); - } + if (firstParameter instanceof Box box) { + return criteria.boundedBy(GeoBox.fromBox(box)); + } - Object secondParameter = parameters.next(); + Object secondParameter = parameters.next(); - // "near" query can be the same query as the "within" query - if (firstParameter instanceof GeoPoint && secondParameter instanceof String) - return criteria.within((GeoPoint) firstParameter, (String) secondParameter); + return doWithinIfPossible(criteria, firstParameter, secondParameter); + } - if (firstParameter instanceof Point && secondParameter instanceof Distance) - return criteria.within((Point) firstParameter, (Distance) secondParameter); + /** + * Do a within query if possible, otherwise return the criteria unchanged. + * + * @param criteria must not be {@literal null} + * @param firstParameter must not be {@literal null} + * @param secondParameter must not be {@literal null} + * @return the criteria with the within query applied if possible. + * @author Junghoon Ban + */ + private Criteria doWithinIfPossible(Criteria criteria, Object firstParameter, Object secondParameter) { + + if (firstParameter instanceof GeoPoint geoPoint && secondParameter instanceof String string) { + return criteria.within(geoPoint, string); + } - if (firstParameter instanceof String && secondParameter instanceof String) - return criteria.within((String) firstParameter, (String) secondParameter); - } - case EXISTS: - case IS_NOT_NULL: - return criteria.exists(); - case IS_NULL: - return criteria.not().exists(); - case IS_EMPTY: - return criteria.empty(); - case IS_NOT_EMPTY: - return criteria.notEmpty(); - default: - throw new InvalidDataAccessApiUsageException("Illegal criteria found '" + type + "'."); + if (firstParameter instanceof Point point && secondParameter instanceof Distance distance) { + return criteria.within(point, distance); + } + + if (firstParameter instanceof String firstString && secondParameter instanceof String secondString) { + return criteria.within(firstString, secondString); } + + return criteria; } private Object[] asArray(Object o) { diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/package-info.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/package-info.java index 1654f41ae7..5f524c1b89 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/package-info.java @@ -1,3 +1,2 @@ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.repository.query.parser; diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformation.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformation.java index 77e1d98859..1320b979fa 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformation.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformation.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -15,10 +15,10 @@ */ package org.springframework.data.elasticsearch.repository.support; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.repository.core.EntityInformation; -import org.springframework.lang.Nullable; /** * @param @@ -39,6 +39,5 @@ public interface ElasticsearchEntityInformation extends EntityInformation @Nullable Long getVersion(T entity); - @Nullable - Document.VersionType getVersionType(); + Document.@Nullable VersionType getVersionType(); } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformationCreator.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformationCreator.java index af53315524..fa91061431 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformationCreator.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformationCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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/repository/support/ElasticsearchEntityInformationCreatorImpl.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformationCreatorImpl.java index 12fedba921..c571e2920e 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformationCreatorImpl.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformationCreatorImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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/repository/support/ElasticsearchRepositoryFactory.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java index a8e56c0530..b5d0b1270d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -15,11 +15,18 @@ */ package org.springframework.data.elasticsearch.repository.support; +import static org.springframework.data.querydsl.QuerydslUtils.*; + +import java.lang.reflect.Method; +import java.util.Optional; + +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; -import org.springframework.data.elasticsearch.repository.query.ElasticsearchPartQuery; import org.springframework.data.elasticsearch.repository.query.ElasticsearchQueryMethod; -import org.springframework.data.elasticsearch.repository.query.ElasticsearchStringQuery; +import org.springframework.data.elasticsearch.repository.query.RepositoryPartQuery; +import org.springframework.data.elasticsearch.repository.query.RepositorySearchTemplateQuery; +import org.springframework.data.elasticsearch.repository.query.RepositoryStringQuery; import org.springframework.data.elasticsearch.repository.support.querybyexample.QueryByExampleElasticsearchExecutor; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.querydsl.QuerydslPredicateExecutor; @@ -32,16 +39,10 @@ import org.springframework.data.repository.query.QueryByExampleExecutor; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; -import org.springframework.lang.Nullable; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.util.Assert; -import java.lang.reflect.Method; -import java.util.Optional; - -import static org.springframework.data.querydsl.QuerydslUtils.QUERY_DSL_PRESENT; - /** * Factory to create {@link ElasticsearchRepository} * @@ -54,6 +55,7 @@ * @author Sascha Woo * @author Peter-Josef Meisch * @author Ezequiel Antúnez Camacho + * @author Haibo Liu */ public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport { @@ -95,12 +97,18 @@ private static boolean isQueryDslRepository(Class repositoryInterface) { @Override protected Optional getQueryLookupStrategy(@Nullable Key key, - QueryMethodEvaluationContextProvider evaluationContextProvider) { - return Optional.of(new ElasticsearchQueryLookupStrategy()); + ValueExpressionDelegate valueExpressionDelegate) { + return Optional.of(new ElasticsearchQueryLookupStrategy(valueExpressionDelegate)); } private class ElasticsearchQueryLookupStrategy implements QueryLookupStrategy { + private final ValueExpressionDelegate valueExpressionDelegate; + + ElasticsearchQueryLookupStrategy(ValueExpressionDelegate valueExpressionDelegate) { + this.valueExpressionDelegate = valueExpressionDelegate; + } + /* * (non-Javadoc) * @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.projection.ProjectionFactory, org.springframework.data.repository.core.NamedQueries) @@ -115,11 +123,17 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, if (namedQueries.hasQuery(namedQueryName)) { String namedQuery = namedQueries.getQuery(namedQueryName); - return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, namedQuery); + return new RepositoryStringQuery(queryMethod, elasticsearchOperations, namedQuery, + valueExpressionDelegate); } else if (queryMethod.hasAnnotatedQuery()) { - return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, queryMethod.getAnnotatedQuery()); + return new RepositoryStringQuery(queryMethod, elasticsearchOperations, queryMethod.getAnnotatedQuery(), + valueExpressionDelegate); + } else if (queryMethod.hasAnnotatedSearchTemplateQuery()) { + var searchTemplateQuery = queryMethod.getAnnotatedSearchTemplateQuery(); + return new RepositorySearchTemplateQuery(queryMethod, elasticsearchOperations, valueExpressionDelegate, + searchTemplateQuery.id()); } - return new ElasticsearchPartQuery(queryMethod, elasticsearchOperations); + return new RepositoryPartQuery(queryMethod, elasticsearchOperations, valueExpressionDelegate); } } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactoryBean.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactoryBean.java index ce28b8a126..98cf29d0fc 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -17,11 +17,11 @@ import java.io.Serializable; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.core.support.RepositoryFactorySupport; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryMetadata.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryMetadata.java index c43b3637ff..46cea4961f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryMetadata.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/repository/support/MappingElasticsearchEntityInformation.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/MappingElasticsearchEntityInformation.java index e6b85cb04b..7c3152ed3f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/MappingElasticsearchEntityInformation.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/MappingElasticsearchEntityInformation.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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/repository/support/StringQueryUtil.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/QueryStringPlaceholderReplacer.java similarity index 58% rename from src/main/java/org/springframework/data/elasticsearch/repository/support/StringQueryUtil.java rename to src/main/java/org/springframework/data/elasticsearch/repository/support/QueryStringPlaceholderReplacer.java index e2301754f8..82bf414afc 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/StringQueryUtil.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/QueryStringPlaceholderReplacer.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,38 +15,52 @@ */ package org.springframework.data.elasticsearch.repository.support; -import java.util.Collection; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; import org.springframework.core.convert.ConversionService; +import org.springframework.data.elasticsearch.core.convert.ConversionException; +import org.springframework.data.elasticsearch.repository.support.value.ElasticsearchQueryValueConversionService; import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.util.Assert; import org.springframework.util.NumberUtils; /** + * To replace the placeholders like `?0`, `?1, `?2` of the query string. + * * @author Peter-Josef Meisch * @author Niklas Herder + * @author Haibo Liu */ -final public class StringQueryUtil { +final public class QueryStringPlaceholderReplacer { private static final Pattern PARAMETER_PLACEHOLDER = Pattern.compile("\\?(\\d+)"); private final ConversionService conversionService; - public StringQueryUtil(ConversionService conversionService) { - this.conversionService = conversionService; + public QueryStringPlaceholderReplacer(ConversionService conversionService) { + + Assert.notNull(conversionService, "conversionService must not be null"); + + this.conversionService = ElasticsearchQueryValueConversionService.getInstance(conversionService); } + /** + * Replace the placeholders of the query string. + * + * @param input raw query string + * @param accessor parameter info + * @return a plain string with placeholders replaced + */ public String replacePlaceholders(String input, ParameterAccessor accessor) { Matcher matcher = PARAMETER_PLACEHOLDER.matcher(input); String result = input; - while (matcher.find()) { + while (matcher.find()) { String placeholder = Pattern.quote(matcher.group()) + "(?!\\d+)"; int index = NumberUtils.parseNumber(matcher.group(1), Integer.class); - String replacement = Matcher.quoteReplacement(getParameterWithIndex(accessor, index)); + String replacement = Matcher.quoteReplacement(getParameterWithIndex(accessor, index, input)); result = result.replaceAll(placeholder, replacement); // need to escape backslashes that are not escapes for quotes so that they are sent as double-backslashes // to Elasticsearch @@ -55,47 +69,17 @@ public String replacePlaceholders(String input, ParameterAccessor accessor) { return result; } - private String getParameterWithIndex(ParameterAccessor accessor, int index) { + private String getParameterWithIndex(ParameterAccessor accessor, int index, String input) { Object parameter = accessor.getBindableValue(index); - String parameterValue = "null"; - - if (parameter != null) { - parameterValue = convert(parameter); - } - - return parameterValue; - - } - - private String convert(Object parameter) { - if (Collection.class.isAssignableFrom(parameter.getClass())) { - Collection collectionParam = (Collection) parameter; - StringBuilder sb = new StringBuilder("["); - sb.append(collectionParam.stream().map(o -> { - if (o instanceof String) { - return "\"" + convert(o) + "\""; - } else { - return convert(o); - } - }).collect(Collectors.joining(","))); - sb.append("]"); - return sb.toString(); - } else { - String parameterValue = "null"; - if (conversionService.canConvert(parameter.getClass(), String.class)) { - String converted = conversionService.convert(parameter, String.class); - - if (converted != null) { - parameterValue = converted; - } - } else { - parameterValue = parameter.toString(); - } + String value = conversionService.convert(parameter, String.class); - parameterValue = parameterValue.replaceAll("\"", Matcher.quoteReplacement("\\\"")); - return parameterValue; + if (value == null) { + throw new ConversionException(String.format( + "Parameter value can't be null for placeholder at index '%s' in query '%s' when querying elasticsearch", + index, input)); } + return value; } } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/QueryStringProcessor.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/QueryStringProcessor.java new file mode 100644 index 0000000000..b24cc17627 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/QueryStringProcessor.java @@ -0,0 +1,67 @@ +/* + * 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.repository.support; + +import org.springframework.core.convert.ConversionService; +import org.springframework.data.elasticsearch.repository.query.ElasticsearchParametersParameterAccessor; +import org.springframework.data.elasticsearch.repository.support.spel.QueryStringSpELEvaluator; +import org.springframework.data.expression.ValueEvaluationContextProvider; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.util.Assert; + +/** + * To process query strings with placeholder replacement and SpEL evaluation by {@link QueryStringPlaceholderReplacer} + * and {@link QueryStringSpELEvaluator}. + * + * @since 5.3 + * @author Haibo Liu + */ +public class QueryStringProcessor { + + private final String query; + private final QueryMethod queryMethod; + private final ConversionService conversionService; + private final ValueEvaluationContextProvider evaluationContextProvider; + + public QueryStringProcessor(String query, QueryMethod queryMethod, ConversionService conversionService, + ValueEvaluationContextProvider evaluationContextProvider) { + + Assert.notNull(query, "query must not be null"); + Assert.notNull(queryMethod, "queryMethod must not be null"); + Assert.notNull(conversionService, "conversionService must not be null"); + Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null"); + + this.query = query; + this.queryMethod = queryMethod; + this.conversionService = conversionService; + this.evaluationContextProvider = evaluationContextProvider; + } + + /** + * Process the query string with placeholder replacement and SpEL evaluation. + * + * @param parameterAccessor parameter info + * @return processed string + */ + public String createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) { + String queryString = new QueryStringPlaceholderReplacer(conversionService) + .replacePlaceholders(query, parameterAccessor); + + QueryStringSpELEvaluator evaluator = new QueryStringSpELEvaluator(queryString, parameterAccessor, queryMethod, + evaluationContextProvider, conversionService); + return evaluator.evaluate(); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java index 226f2e2c90..368df504cf 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -19,12 +19,14 @@ import java.lang.reflect.Method; import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchQueryMethod; -import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchStringQuery; import org.springframework.data.elasticsearch.repository.query.ReactivePartTreeElasticsearchQuery; +import org.springframework.data.elasticsearch.repository.query.ReactiveRepositorySearchTemplateQuery; +import org.springframework.data.elasticsearch.repository.query.ReactiveRepositoryStringQuery; import org.springframework.data.elasticsearch.repository.support.querybyexample.ReactiveQueryByExampleElasticsearchExecutor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; @@ -36,11 +38,9 @@ import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; import org.springframework.data.repository.query.RepositoryQuery; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.util.Assert; /** @@ -50,12 +50,11 @@ * @author Christoph Strobl * @author Ivan Greene * @author Ezequiel Antúnez Camacho + * @author Haibo Liu * @since 3.2 */ public class ReactiveElasticsearchRepositoryFactory extends ReactiveRepositoryFactorySupport { - private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); - private final ReactiveElasticsearchOperations operations; private final MappingContext, ElasticsearchPersistentProperty> mappingContext; @@ -94,14 +93,10 @@ protected Object getTargetRepository(RepositoryInformation information) { return getTargetRepositoryViaReflection(information, entityInformation, operations); } - /* - * (non-Javadoc) - * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getQueryLookupStrategy(org.springframework.data.repository.query.QueryLookupStrategy.Key, org.springframework.data.repository.query.EvaluationContextProvider) - */ @Override protected Optional getQueryLookupStrategy(@Nullable Key key, - QueryMethodEvaluationContextProvider evaluationContextProvider) { - return Optional.of(new ElasticsearchQueryLookupStrategy(operations, evaluationContextProvider, mappingContext)); + ValueExpressionDelegate valueExpressionDelegate) { + return Optional.of(new ElasticsearchQueryLookupStrategy(operations, valueExpressionDelegate, mappingContext)); } /* @@ -132,19 +127,19 @@ protected RepositoryMetadata getRepositoryMetadata(Class repositoryInterface) private static class ElasticsearchQueryLookupStrategy implements QueryLookupStrategy { private final ReactiveElasticsearchOperations operations; - private final QueryMethodEvaluationContextProvider evaluationContextProvider; + private final ValueExpressionDelegate valueExpressionDelegate; private final MappingContext, ElasticsearchPersistentProperty> mappingContext; public ElasticsearchQueryLookupStrategy(ReactiveElasticsearchOperations operations, - QueryMethodEvaluationContextProvider evaluationContextProvider, + ValueExpressionDelegate valueExpressionDelegate, MappingContext, ElasticsearchPersistentProperty> mappingContext) { Assert.notNull(operations, "operations must not be null"); - Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null"); + Assert.notNull(valueExpressionDelegate, "evaluationContextProvider must not be null"); Assert.notNull(mappingContext, "mappingContext must not be null"); this.operations = operations; - this.evaluationContextProvider = evaluationContextProvider; + this.valueExpressionDelegate = valueExpressionDelegate; this.mappingContext = mappingContext; } @@ -163,13 +158,16 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, if (namedQueries.hasQuery(namedQueryName)) { String namedQuery = namedQueries.getQuery(namedQueryName); - return new ReactiveElasticsearchStringQuery(namedQuery, queryMethod, operations, EXPRESSION_PARSER, - evaluationContextProvider); + return new ReactiveRepositoryStringQuery(namedQuery, queryMethod, operations, + valueExpressionDelegate); } else if (queryMethod.hasAnnotatedQuery()) { - return new ReactiveElasticsearchStringQuery(queryMethod, operations, EXPRESSION_PARSER, - evaluationContextProvider); + return new ReactiveRepositoryStringQuery(queryMethod, operations, valueExpressionDelegate); + } else if (queryMethod.hasAnnotatedSearchTemplateQuery()) { + var searchTemplateQuery = queryMethod.getAnnotatedSearchTemplateQuery(); + return new ReactiveRepositorySearchTemplateQuery(queryMethod, operations, valueExpressionDelegate, + searchTemplateQuery.id()); } else { - return new ReactivePartTreeElasticsearchQuery(queryMethod, operations); + return new ReactivePartTreeElasticsearchQuery(queryMethod, operations, valueExpressionDelegate); } } } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactoryBean.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactoryBean.java index 72421b9132..3f30545bfb 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -15,12 +15,12 @@ */ package org.springframework.data.elasticsearch.repository.support; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.core.support.RepositoryFactorySupport; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryMetadata.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryMetadata.java index 9b063ce426..29b540dd6c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryMetadata.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/repository/support/SimpleElasticsearchRepository.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleElasticsearchRepository.java index a123428468..d48a3c80e5 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleElasticsearchRepository.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleElasticsearchRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -22,6 +22,7 @@ import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; @@ -38,13 +39,13 @@ import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.query.BaseQuery; +import org.springframework.data.elasticsearch.core.query.DeleteQuery; import org.springframework.data.elasticsearch.core.query.MoreLikeThisQuery; import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.routing.RoutingResolver; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.data.util.StreamUtils; import org.springframework.data.util.Streamable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -82,6 +83,13 @@ public SimpleElasticsearchRepository(ElasticsearchEntityInformation metad this.entityClass = this.entityInformation.getJavaType(); this.indexOperations = operations.indexOps(this.entityClass); + if (!"true".equals(System.getenv("SPRING_DATA_ELASTICSEARCH_SKIP_REPOSITORY_INIT"))) { + createIndexAndMappingIfNeeded(); + } + } + + public void createIndexAndMappingIfNeeded() { + if (shouldCreateIndexAndMapping() && !indexOperations.exists()) { indexOperations.createWithMapping(); } else if (shouldAlwaysWriteMapping()) { @@ -179,7 +187,6 @@ public S save(S entity) { Assert.notNull(entity, "Cannot save 'null' entity."); - // noinspection DataFlowIssue return executeAndRefresh(operations -> operations.save(entity, getIndexCoordinates())); } @@ -188,7 +195,6 @@ public S save(S entity, @Nullable RefreshPolicy refreshPolicy) { Assert.notNull(entity, "entity must not be null"); - // noinspection DataFlowIssue return executeAndRefresh(operations -> operations.save(entity, getIndexCoordinates()), refreshPolicy); } @@ -302,7 +308,7 @@ public void deleteAllById(Iterable ids) { Query query = operations.idsQuery(idStrings); executeAndRefresh((OperationsCallback) operations -> { - operations.delete(query, entityClass, getIndexCoordinates()); + operations.delete(DeleteQuery.builder(query).build(), entityClass, getIndexCoordinates()); return null; }); } @@ -324,7 +330,7 @@ public void deleteAllById(Iterable ids, @Nullable RefreshPolicy re Query query = operations.idsQuery(idStrings); executeAndRefresh((OperationsCallback) operations -> { - operations.delete(query, entityClass, getIndexCoordinates()); + operations.delete(DeleteQuery.builder(query).build(), entityClass, getIndexCoordinates()); return null; }); } @@ -358,7 +364,6 @@ private void doDelete(@Nullable ID id, @Nullable String routing, IndexCoordinate if (id != null) { executeAndRefresh(operations -> { var ops = routing != null ? operations.withRouting(RoutingResolver.just(routing)) : operations; - // noinspection DataFlowIssue return ops.delete(stringIdRepresentation(id), indexCoordinates); }); } @@ -370,7 +375,6 @@ private void doDelete(@Nullable ID id, @Nullable String routing, IndexCoordinate if (id != null) { executeAndRefresh(operations -> { var ops = routing != null ? operations.withRouting(RoutingResolver.just(routing)) : operations; - // noinspection DataFlowIssue return ops.delete(stringIdRepresentation(id), indexCoordinates); }, refreshPolicy); } @@ -380,7 +384,7 @@ private void doDelete(@Nullable ID id, @Nullable String routing, IndexCoordinate public void deleteAll() { executeAndRefresh((OperationsCallback) operations -> { - operations.delete(Query.findAll(), entityClass, getIndexCoordinates()); + operations.delete(DeleteQuery.builder(Query.findAll()).build(), entityClass, getIndexCoordinates()); return null; }); } @@ -388,7 +392,7 @@ public void deleteAll() { @Override public void deleteAll(@Nullable RefreshPolicy refreshPolicy) { executeAndRefresh((OperationsCallback) operations -> { - operations.delete(Query.findAll(), entityClass, getIndexCoordinates()); + operations.delete(DeleteQuery.builder(Query.findAll()).build(), entityClass, getIndexCoordinates()); return null; }, refreshPolicy); } @@ -453,9 +457,7 @@ public R executeAndRefresh(OperationsCallback callback) { @Nullable public R executeAndRefresh(OperationsCallback callback, @Nullable RefreshPolicy refreshPolicy) { - R result = callback.doWithOperations(operations.withRefreshPolicy(refreshPolicy)); - doRefresh(); - return result; + return callback.doWithOperations(operations.withRefreshPolicy(refreshPolicy)); } // endregion } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepository.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepository.java index 4a75c356cb..ae3e1a4d00 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepository.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -20,6 +20,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -32,10 +33,10 @@ import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.query.BaseQuery; +import org.springframework.data.elasticsearch.core.query.DeleteQuery; import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.routing.RoutingResolver; import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -61,10 +62,12 @@ public SimpleReactiveElasticsearchRepository(ElasticsearchEntityInformation deleteAllById(Iterable ids) { .collectList() // .map(operations::idsQuery) // .flatMap( - query -> operations.delete(query, entityInformation.getJavaType(), entityInformation.getIndexCoordinates())) // + query -> operations.delete(DeleteQuery.builder(query).build(), entityInformation.getJavaType(), + entityInformation.getIndexCoordinates())) // .then(doRefresh()); } @@ -299,7 +303,8 @@ public Mono deleteAllById(Iterable ids, @Nullable RefreshPol .collectList() // .map(operations::idsQuery) // .flatMap( - query -> operationsWithRefreshPolicy.delete(query, entityInformation.getJavaType(), + query -> operationsWithRefreshPolicy.delete(DeleteQuery.builder(query).build(), + entityInformation.getJavaType(), entityInformation.getIndexCoordinates())) // .then(doRefresh()); } @@ -331,7 +336,8 @@ public Mono deleteAll(Publisher entityStream) { .collectList() // .map(operations::idsQuery) .flatMap( - query -> operations.delete(query, entityInformation.getJavaType(), entityInformation.getIndexCoordinates())) // + query -> operations.delete(DeleteQuery.builder(query).build(), entityInformation.getJavaType(), + entityInformation.getIndexCoordinates())) // .then(doRefresh()); } @@ -347,21 +353,25 @@ public Mono deleteAll(Publisher entityStream, @Nullable Refre .collectList() // .map(operations::idsQuery) .flatMap( - query -> operationsWithRefreshPolicy.delete(query, entityInformation.getJavaType(), + query -> operationsWithRefreshPolicy.delete(DeleteQuery.builder(query).build(), + entityInformation.getJavaType(), entityInformation.getIndexCoordinates())) // .then(doRefresh()); } @Override public Mono deleteAll() { - return operations.delete(Query.findAll(), entityInformation.getJavaType(), entityInformation.getIndexCoordinates()) // + return operations + .delete(DeleteQuery.builder(Query.findAll()).build(), entityInformation.getJavaType(), + entityInformation.getIndexCoordinates()) // .then(doRefresh()); } @Override public Mono deleteAll(@Nullable RefreshPolicy refreshPolicy) { return operations.withRefreshPolicy(refreshPolicy) - .delete(Query.findAll(), entityInformation.getJavaType(), entityInformation.getIndexCoordinates()) // + .delete(DeleteQuery.builder(Query.findAll()).build(), entityInformation.getJavaType(), + entityInformation.getIndexCoordinates()) // .then(doRefresh()); } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/package-info.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/package-info.java index 17be692af3..7f6a0f13f1 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/package-info.java @@ -1,3 +1,2 @@ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.repository.support; diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ExampleCriteriaMapper.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ExampleCriteriaMapper.java index 45ad8d0ba1..432d1883e5 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ExampleCriteriaMapper.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ExampleCriteriaMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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. @@ -18,6 +18,7 @@ import java.util.Map; import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; @@ -27,7 +28,6 @@ import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.support.ExampleMatcherAccessor; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutor.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutor.java index c64c8faf32..4386581bc0 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutor.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutor.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutor.java index 37eade7b3d..de6b1e07ce 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutor.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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/repository/support/querybyexample/package-info.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/package-info.java new file mode 100644 index 0000000000..c2b3be5cca --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/package-info.java @@ -0,0 +1,2 @@ +@org.jspecify.annotations.NullMarked +package org.springframework.data.elasticsearch.repository.support.querybyexample; diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/QueryStringSpELEvaluator.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/QueryStringSpELEvaluator.java new file mode 100644 index 0000000000..9d982d9281 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/QueryStringSpELEvaluator.java @@ -0,0 +1,144 @@ +/* + * 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.repository.support.spel; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.jspecify.annotations.Nullable; +import org.springframework.core.convert.ConversionService; +import org.springframework.data.elasticsearch.core.convert.ConversionException; +import org.springframework.data.elasticsearch.repository.query.ElasticsearchParametersParameterAccessor; +import org.springframework.data.elasticsearch.repository.support.value.ElasticsearchCollectionValueToStringConverter; +import org.springframework.data.elasticsearch.repository.support.value.ElasticsearchQueryValueConversionService; +import org.springframework.data.elasticsearch.repository.support.value.ElasticsearchStringValueToStringConverter; +import org.springframework.data.expression.ValueEvaluationContextProvider; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ParserContext; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.common.CompositeStringExpression; +import org.springframework.expression.common.LiteralExpression; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.support.StandardTypeConverter; +import org.springframework.util.Assert; + +/** + * To evaluate the SpEL expressions of the query string. + * + * @author Haibo Liu + * @since 5.3 + */ +public class QueryStringSpELEvaluator { + + private static final SpelExpressionParser PARSER = new SpelExpressionParser(); + private static final Map QUERY_EXPRESSIONS = new ConcurrentHashMap<>(); + + private final String queryString; + private final ElasticsearchParametersParameterAccessor parameterAccessor; + private final QueryMethod queryMethod; + private final ValueEvaluationContextProvider evaluationContextProvider; + private final TypeConverter elasticsearchSpELTypeConverter; + + public QueryStringSpELEvaluator(String queryString, ElasticsearchParametersParameterAccessor parameterAccessor, + QueryMethod queryMethod, ValueEvaluationContextProvider evaluationContextProvider, + ConversionService conversionService) { + + Assert.notNull(queryString, "queryString must not be null"); + Assert.notNull(parameterAccessor, "parameterAccessor must not be null"); + Assert.notNull(queryMethod, "queryMethod must not be null"); + Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null"); + Assert.notNull(conversionService, "conversionService must not be null"); + + this.queryString = queryString; + this.parameterAccessor = parameterAccessor; + this.queryMethod = queryMethod; + this.evaluationContextProvider = evaluationContextProvider; + this.elasticsearchSpELTypeConverter = new StandardTypeConverter( + ElasticsearchQueryValueConversionService.getInstance(conversionService)); + } + + /** + * Evaluate the SpEL parts of the query string. + * + * @return a plain string with values evaluated + */ + public String evaluate() { + Expression expr = getQueryExpression(queryString); + + if (expr != null) { + EvaluationContext context = evaluationContextProvider.getEvaluationContext(parameterAccessor.getValues()) + .getRequiredEvaluationContext(); + + if (context instanceof StandardEvaluationContext standardEvaluationContext) { + standardEvaluationContext.setTypeConverter(elasticsearchSpELTypeConverter); + } + + String parsed = parseExpressions(expr, context); + Assert.notNull(parsed, "Query parsed by SpEL should not be null"); + return parsed; + } + return queryString; + } + + /** + * {@link Expression#getValue(EvaluationContext, Class)} is not used because the value part in SpEL should be + * converted by {@link ElasticsearchStringValueToStringConverter} or + * {@link ElasticsearchCollectionValueToStringConverter} to escape the quotations, but other literal parts in SpEL + * expression should not be processed with these converters. So we just get the string value from + * {@link LiteralExpression} directly rather than {@link LiteralExpression#getValue(EvaluationContext, Class)}. + */ + private String parseExpressions(Expression rootExpr, EvaluationContext context) { + StringBuilder parsed = new StringBuilder(); + + if (rootExpr instanceof LiteralExpression literalExpression) { + // get the string literal directly + parsed.append(literalExpression.getExpressionString()); + } else if (rootExpr instanceof SpelExpression spelExpression) { + // evaluate the value + String value = spelExpression.getValue(context, String.class); + + if (value == null) { + throw new ConversionException(String.format( + "Parameter value can't be null for SpEL expression '%s' in method '%s' when querying elasticsearch", + spelExpression.getExpressionString(), queryMethod.getName())); + } + parsed.append(value); + } else if (rootExpr instanceof CompositeStringExpression compositeStringExpression) { + // parse one by one for composite expression + Expression[] expressions = compositeStringExpression.getExpressions(); + + for (Expression exp : expressions) { + parsed.append(parseExpressions(exp, context)); + } + } else { + // no more + parsed.append(rootExpr.getValue(context, String.class)); + } + return parsed.toString(); + } + + @Nullable + private Expression getQueryExpression(String queryString) { + return QUERY_EXPRESSIONS.computeIfAbsent(queryString, f -> { + Expression expr = PARSER.parseExpression(queryString, ParserContext.TEMPLATE_EXPRESSION); + return expr instanceof LiteralExpression ? null : expr; + }); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/package-info.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/package-info.java new file mode 100644 index 0000000000..0388eba7c4 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/package-info.java @@ -0,0 +1,2 @@ +@org.jspecify.annotations.NullMarked +package org.springframework.data.elasticsearch.repository.support.spel; diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/value/ElasticsearchCollectionValueToStringConverter.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/value/ElasticsearchCollectionValueToStringConverter.java new file mode 100644 index 0000000000..2e3441fd0e --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/value/ElasticsearchCollectionValueToStringConverter.java @@ -0,0 +1,102 @@ +/* + * 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.repository.support.value; + +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import java.util.Set; +import java.util.StringJoiner; + +import org.jspecify.annotations.Nullable; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.GenericConverter; + +/** + * Convert a collection into string for value part of the elasticsearch query. + *

+ * If the value is type {@link String}, it should be wrapped with square brackets, with each element quoted therefore + * escaped(by {@link ElasticsearchStringValueToStringConverter}) if quotations exist in the original element. + *

+ * eg: The value part of an elasticsearch terms query should looks like {@code ["hello \"Stranger\"","Another string"]} + * for query + * + *

+ * {@code
+ *  {
+ *    "bool":{
+ *      "must":{
+ *        "terms":{
+ *          "name": ["hello \"Stranger\"", "Another string"]
+ *        }
+ *      }
+ *    }
+ *  }
+ * }
+ * 
+ * + * @since 5.3 + * @author Haibo Liu + */ +public class ElasticsearchCollectionValueToStringConverter implements GenericConverter { + + private static final String DELIMITER = ","; + + private final ConversionService conversionService; + + public ElasticsearchCollectionValueToStringConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Collection.class, String.class)); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + + if (source == null) { + return "[]"; + } + Collection sourceCollection = (Collection) source; + + if (sourceCollection.isEmpty()) { + return "[]"; + } + + StringJoiner sb = new StringJoiner(DELIMITER, "[", "]"); + + for (Object sourceElement : sourceCollection) { + // ignore the null value in collection + if (Objects.isNull(sourceElement)) { + continue; + } + + Object targetElement = this.conversionService.convert( + sourceElement, sourceType.elementTypeDescriptor(sourceElement), targetType); + + if (sourceElement instanceof String) { + sb.add("\"" + targetElement + '"'); + } else { + sb.add(String.valueOf(targetElement)); + } + } + return sb.toString(); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/value/ElasticsearchQueryValueConversionService.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/value/ElasticsearchQueryValueConversionService.java new file mode 100644 index 0000000000..a611311c6f --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/value/ElasticsearchQueryValueConversionService.java @@ -0,0 +1,98 @@ +/* + * 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.repository.support.value; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.jspecify.annotations.Nullable; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.util.Assert; + +/** + * A {@link ConversionService} using custom converters to handle query values in elasticsearch query. If the value to be + * converted beyond the scope of custom converters, it'll delegate to the {@link #delegate delegated conversion + * service}. + *

+ * This is a better solution for converting query values in elasticsearch query, because it has all the capability the + * {@link #delegate delegated conversion service} has, especially for user-registered {@link Converter}s. + * + * @since 5.3 + * @author Haibo Liu + */ +public class ElasticsearchQueryValueConversionService implements ConversionService { + + private static final Map CACHE = new ConcurrentHashMap<>(); + + private final GenericConversionService valueConversionService = new GenericConversionService(); + + private final ConversionService delegate; + + private ElasticsearchQueryValueConversionService(ConversionService delegate) { + + Assert.notNull(delegate, "delegated ConversionService must not be null"); + + this.delegate = delegate; + + // register elasticsearch custom type converters for conversion service + valueConversionService.addConverter(new ElasticsearchCollectionValueToStringConverter(this)); + valueConversionService.addConverter(new ElasticsearchStringValueToStringConverter()); + } + + /** + * Get a {@link ElasticsearchQueryValueConversionService} with this conversion service as delegated. + * + * @param conversionService conversion service as delegated + * @return a conversion service having the capability to convert query values in elasticsearch query + */ + public static ElasticsearchQueryValueConversionService getInstance(ConversionService conversionService) { + return CACHE.computeIfAbsent(conversionService, ElasticsearchQueryValueConversionService::new); + } + + @Override + public boolean canConvert(@Nullable Class sourceType, Class targetType) { + Assert.notNull(targetType, "Target type to convert to cannot be null"); + return canConvert(TypeDescriptor.valueOf(sourceType), TypeDescriptor.valueOf(targetType)); + } + + @Override + public boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { + return valueConversionService.canConvert(sourceType, targetType) + || delegate.canConvert(sourceType, targetType); + } + + @SuppressWarnings("unchecked") + @Override + @Nullable + public T convert(@Nullable Object source, Class targetType) { + Assert.notNull(targetType, "Target type to convert to cannot be null"); + return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType)); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { + + if (valueConversionService.canConvert(sourceType, targetType)) { + return valueConversionService.convert(source, sourceType, targetType); + } else { + return delegate.convert(source, sourceType, targetType); + } + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/value/ElasticsearchStringValueToStringConverter.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/value/ElasticsearchStringValueToStringConverter.java new file mode 100644 index 0000000000..ee03ccf3fa --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/value/ElasticsearchStringValueToStringConverter.java @@ -0,0 +1,50 @@ +/* + * 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.repository.support.value; + +import java.util.Collections; +import java.util.Set; +import java.util.regex.Matcher; + +import org.jspecify.annotations.Nullable; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.GenericConverter; + +/** + * Values in elasticsearch query may contain quotations and should be escaped when converting. Note that the converter + * should only be used in this situation, rather than common string to string conversions. + * + * @since 5.3 + * @author Haibo Liu + */ +public class ElasticsearchStringValueToStringConverter implements GenericConverter { + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, String.class)); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return source != null ? escape(source) : null; + } + + private String escape(@Nullable Object source) { + // escape the quotes in the string, because the string should already be quoted manually + return String.valueOf(source).replaceAll("\"", Matcher.quoteReplacement("\\\"")); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/value/package-info.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/value/package-info.java new file mode 100644 index 0000000000..18e794112c --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/value/package-info.java @@ -0,0 +1,2 @@ +@org.jspecify.annotations.NullMarked +package org.springframework.data.elasticsearch.repository.support.value; diff --git a/src/main/java/org/springframework/data/elasticsearch/support/DefaultStringObjectMap.java b/src/main/java/org/springframework/data/elasticsearch/support/DefaultStringObjectMap.java index ba8f56ed46..f171d86048 100644 --- a/src/main/java/org/springframework/data/elasticsearch/support/DefaultStringObjectMap.java +++ b/src/main/java/org/springframework/data/elasticsearch/support/DefaultStringObjectMap.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,7 +22,7 @@ import java.util.Set; import java.util.function.BiConsumer; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import com.fasterxml.jackson.core.JsonProcessingException; @@ -40,7 +40,7 @@ public DefaultStringObjectMap() { this(new LinkedHashMap<>()); } - public DefaultStringObjectMap(Map map) { + public DefaultStringObjectMap(Map map) { this.delegate = new LinkedHashMap<>(map); } diff --git a/src/main/java/org/springframework/data/elasticsearch/support/ExceptionUtils.java b/src/main/java/org/springframework/data/elasticsearch/support/ExceptionUtils.java index 3bac853ba0..4fcc56ffad 100644 --- a/src/main/java/org/springframework/data/elasticsearch/support/ExceptionUtils.java +++ b/src/main/java/org/springframework/data/elasticsearch/support/ExceptionUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/support/HttpHeaders.java b/src/main/java/org/springframework/data/elasticsearch/support/HttpHeaders.java index 8dedf0d6b5..742ce9d041 100644 --- a/src/main/java/org/springframework/data/elasticsearch/support/HttpHeaders.java +++ b/src/main/java/org/springframework/data/elasticsearch/support/HttpHeaders.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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. @@ -25,7 +25,7 @@ import java.util.Map; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; import org.springframework.util.MultiValueMap; diff --git a/src/main/java/org/springframework/data/elasticsearch/support/ScoreDoc.java b/src/main/java/org/springframework/data/elasticsearch/support/ScoreDoc.java index 680970a253..4de876f8ad 100644 --- a/src/main/java/org/springframework/data/elasticsearch/support/ScoreDoc.java +++ b/src/main/java/org/springframework/data/elasticsearch/support/ScoreDoc.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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.support; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * @author Peter-Josef Meisch diff --git a/src/main/java/org/springframework/data/elasticsearch/support/StringObjectMap.java b/src/main/java/org/springframework/data/elasticsearch/support/StringObjectMap.java index 566cbbc4de..f3a7c86c9b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/support/StringObjectMap.java +++ b/src/main/java/org/springframework/data/elasticsearch/support/StringObjectMap.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,8 +21,8 @@ import java.util.function.LongSupplier; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; import org.springframework.data.elasticsearch.core.document.Document; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/data/elasticsearch/support/Version.java b/src/main/java/org/springframework/data/elasticsearch/support/Version.java index 5761982bb4..b8aae6a70e 100644 --- a/src/main/java/org/springframework/data/elasticsearch/support/Version.java +++ b/src/main/java/org/springframework/data/elasticsearch/support/Version.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/support/VersionInfo.java b/src/main/java/org/springframework/data/elasticsearch/support/VersionInfo.java index 6ecc716096..d62ea73f87 100644 --- a/src/main/java/org/springframework/data/elasticsearch/support/VersionInfo.java +++ b/src/main/java/org/springframework/data/elasticsearch/support/VersionInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -21,7 +21,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * This class is used to log the versions of Spring Data Elasticsearch, the Elasticsearch client libs used to built, the diff --git a/src/main/java/org/springframework/data/elasticsearch/support/package-info.java b/src/main/java/org/springframework/data/elasticsearch/support/package-info.java index 0ad015c73d..7c65e45ba3 100644 --- a/src/main/java/org/springframework/data/elasticsearch/support/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/support/package-info.java @@ -1,3 +1,2 @@ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.support; diff --git a/src/test/java/org/springframework/data/elasticsearch/utils/geohash/BitUtil.java b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/BitUtil.java similarity index 94% rename from src/test/java/org/springframework/data/elasticsearch/utils/geohash/BitUtil.java rename to src/main/java/org/springframework/data/elasticsearch/utils/geohash/BitUtil.java index 8666ef062a..175983a740 100644 --- a/src/test/java/org/springframework/data/elasticsearch/utils/geohash/BitUtil.java +++ b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/BitUtil.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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. @@ -22,6 +22,7 @@ *
* Utilities for common Bit twiddling methods. Borrowed heavily from Lucene (org.apache.lucene.util.BitUtil). */ +@SuppressWarnings("CStyleArrayDeclaration") public class BitUtil { // magic numbers for bit interleaving private static final long MAGIC[] = { 0x5555555555555555L, 0x3333333333333333L, 0x0F0F0F0F0F0F0F0FL, 0x00FF00FF00FF00FFL, 0x0000FFFF0000FFFFL, 0x00000000FFFFFFFFL, 0xAAAAAAAAAAAAAAAAL }; @@ -65,7 +66,7 @@ public static long deinterleave(long b) { /** * flip flops odd with even bits */ - public static final long flipFlop(final long b) { + public static long flipFlop(final long b) { return ((b & MAGIC[6]) >>> 1) | ((b & MAGIC[0]) << 1); } } diff --git a/src/test/java/org/springframework/data/elasticsearch/utils/geohash/Geohash.java b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/Geohash.java similarity index 90% rename from src/test/java/org/springframework/data/elasticsearch/utils/geohash/Geohash.java rename to src/main/java/org/springframework/data/elasticsearch/utils/geohash/Geohash.java index b1c1eff16f..cf444bfc48 100644 --- a/src/test/java/org/springframework/data/elasticsearch/utils/geohash/Geohash.java +++ b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/Geohash.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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. @@ -17,6 +17,9 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Locale; + +import org.springframework.util.Assert; /** * Code copied from Elasticsearch 7.10, Apache License V2 @@ -70,6 +73,21 @@ public static Point toPoint(final String geohash) throws IllegalArgumentExceptio return new Point(decodeLongitude(hash), decodeLatitude(hash)); } + /** + * Converts a geohash to a string in the format "lat,lon" + * + * @param geohash the geohash to convert + * @return the lat lon pair in a String + * @since 5.3 + */ + public static String toLatLon(final String geohash) { + + Assert.notNull(geohash, "geohash must not be null"); + + var point = Geohash.toPoint(geohash); + return String.format(Locale.ROOT, "%f,%f", point.getLat(), point.getLon()); + } + /** * Computes the bounding box coordinates from a given geohash * @@ -130,7 +148,7 @@ public static Collection getNeighbors(String geohash) { * @param neighbors list to add the neighbors to * @return the given list */ - public static final > E addNeighbors(String geohash, E neighbors) { + public static > E addNeighbors(String geohash, E neighbors) { return addNeighborsAtLevel(geohash, geohash.length(), neighbors); } @@ -142,7 +160,7 @@ public static final > E addNeighbors(String * @param neighbors list to add the neighbors to * @return the given list */ - public static final > E addNeighborsAtLevel(String geohash, int level, + public static > E addNeighborsAtLevel(String geohash, int level, E neighbors) { String south = getNeighbor(geohash, level, 0, -1); String north = getNeighbor(geohash, level, 0, +1); @@ -173,7 +191,7 @@ public static final > E addNeighborsAtLevel * @param dy delta of the second grid coordinate (must be -1, 0 or +1) * @return geohash of the defined cell */ - public static final String getNeighbor(String geohash, int level, int dx, int dy) { + public static String getNeighbor(String geohash, int level, int dx, int dy) { int cell = BASE_32_STRING.indexOf(geohash.charAt(level - 1)); // Decoding the Geohash bit pattern to determine grid coordinates @@ -221,14 +239,14 @@ public static final String getNeighbor(String geohash, int level, int dx, int dy /** * Encode a string geohash to the geohash based long format (lon/lat interleaved, 4 least significant bits = level) */ - public static final long longEncode(String hash) { + public static long longEncode(String hash) { return longEncode(hash, hash.length()); } /** * Encode lon/lat to the geohash based long format (lon/lat interleaved, 4 least significant bits = level) */ - public static final long longEncode(final double lon, final double lat, final int level) { + public static long longEncode(final double lon, final double lat, final int level) { // shift to appropriate level final short msf = (short) (((12 - level) * 5) + (MORTON_OFFSET - 2)); return ((encodeLatLon(lat, lon) >>> msf) << 4) | level; @@ -237,14 +255,14 @@ public static final long longEncode(final double lon, final double lat, final in /** * Encode to a geohash string from full resolution longitude, latitude) */ - public static final String stringEncode(final double lon, final double lat) { + public static String stringEncode(final double lon, final double lat) { return stringEncode(lon, lat, 12); } /** * Encode to a level specific geohash string from full resolution longitude, latitude */ - public static final String stringEncode(final double lon, final double lat, final int level) { + public static String stringEncode(final double lon, final double lat, final int level) { // convert to geohashlong long interleaved = encodeLatLon(lat, lon); interleaved >>>= (((PRECISION - level) * 5) + (MORTON_OFFSET - 2)); @@ -255,7 +273,7 @@ public static final String stringEncode(final double lon, final double lat, fina /** * Encode to a geohash string from the geohash based long format */ - public static final String stringEncode(long geoHashLong) { + public static String stringEncode(long geoHashLong) { int level = (int) geoHashLong & 15; geoHashLong >>>= 4; char[] chars = new char[level]; @@ -350,12 +368,12 @@ public static int encodeLongitude(double longitude) { } /** returns the latitude value from the string based geohash */ - public static final double decodeLatitude(final String geohash) { + public static double decodeLatitude(final String geohash) { return decodeLatitude(Geohash.mortonEncode(geohash)); } /** returns the latitude value from the string based geohash */ - public static final double decodeLongitude(final String geohash) { + public static double decodeLongitude(final String geohash) { return decodeLongitude(Geohash.mortonEncode(geohash)); } diff --git a/src/test/java/org/springframework/data/elasticsearch/utils/geohash/Geometry.java b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/Geometry.java similarity index 95% rename from src/test/java/org/springframework/data/elasticsearch/utils/geohash/Geometry.java rename to src/main/java/org/springframework/data/elasticsearch/utils/geohash/Geometry.java index 3cb9735f85..0ca757d4f3 100644 --- a/src/test/java/org/springframework/data/elasticsearch/utils/geohash/Geometry.java +++ b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/Geometry.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/utils/geohash/GeometryValidator.java b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/GeometryValidator.java similarity index 95% rename from src/test/java/org/springframework/data/elasticsearch/utils/geohash/GeometryValidator.java rename to src/main/java/org/springframework/data/elasticsearch/utils/geohash/GeometryValidator.java index 0e5fb71aaa..c06f3ddc10 100644 --- a/src/test/java/org/springframework/data/elasticsearch/utils/geohash/GeometryValidator.java +++ b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/GeometryValidator.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/utils/geohash/GeometryVisitor.java b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/GeometryVisitor.java similarity index 82% rename from src/test/java/org/springframework/data/elasticsearch/utils/geohash/GeometryVisitor.java rename to src/main/java/org/springframework/data/elasticsearch/utils/geohash/GeometryVisitor.java index a8035c0aba..e2daa06595 100644 --- a/src/test/java/org/springframework/data/elasticsearch/utils/geohash/GeometryVisitor.java +++ b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/GeometryVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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. @@ -44,25 +44,7 @@ */ public interface GeometryVisitor { - /* - T visit(Circle circle) throws E; - - T visit(GeometryCollection collection) throws E; - - T visit(Line line) throws E; - - T visit(LinearRing ring) throws E; - - T visit(MultiLine multiLine) throws E; - - T visit(MultiPoint multiPoint) throws E; - - T visit(MultiPolygon multiPolygon) throws E; - */ T visit(Point point) throws E; - /* - T visit(Polygon polygon) throws E; - */ T visit(Rectangle rectangle) throws E; } diff --git a/src/test/java/org/springframework/data/elasticsearch/utils/geohash/Point.java b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/Point.java similarity index 98% rename from src/test/java/org/springframework/data/elasticsearch/utils/geohash/Point.java rename to src/main/java/org/springframework/data/elasticsearch/utils/geohash/Point.java index 47014c9d2f..7e979b3657 100644 --- a/src/test/java/org/springframework/data/elasticsearch/utils/geohash/Point.java +++ b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/Point.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/utils/geohash/Rectangle.java b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/Rectangle.java similarity index 98% rename from src/test/java/org/springframework/data/elasticsearch/utils/geohash/Rectangle.java rename to src/main/java/org/springframework/data/elasticsearch/utils/geohash/Rectangle.java index 8bfe0aa206..c8145e7ab9 100644 --- a/src/test/java/org/springframework/data/elasticsearch/utils/geohash/Rectangle.java +++ b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/Rectangle.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/utils/geohash/ShapeType.java b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/ShapeType.java similarity index 95% rename from src/test/java/org/springframework/data/elasticsearch/utils/geohash/ShapeType.java rename to src/main/java/org/springframework/data/elasticsearch/utils/geohash/ShapeType.java index d0472af342..386fac749f 100644 --- a/src/test/java/org/springframework/data/elasticsearch/utils/geohash/ShapeType.java +++ b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/ShapeType.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/utils/geohash/StandardValidator.java b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/StandardValidator.java similarity index 97% rename from src/test/java/org/springframework/data/elasticsearch/utils/geohash/StandardValidator.java rename to src/main/java/org/springframework/data/elasticsearch/utils/geohash/StandardValidator.java index cc0430bd79..98659e7c74 100644 --- a/src/test/java/org/springframework/data/elasticsearch/utils/geohash/StandardValidator.java +++ b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/StandardValidator.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/utils/geohash/WellKnownText.java b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/WellKnownText.java similarity index 90% rename from src/test/java/org/springframework/data/elasticsearch/utils/geohash/WellKnownText.java rename to src/main/java/org/springframework/data/elasticsearch/utils/geohash/WellKnownText.java index 8a9638ce5d..1b72c579bc 100644 --- a/src/test/java/org/springframework/data/elasticsearch/utils/geohash/WellKnownText.java +++ b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/WellKnownText.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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. @@ -36,11 +36,11 @@ public class WellKnownText { public static final String COMMA = ","; public static final String NAN = "NaN"; - private final String NUMBER = ""; - private final String EOF = "END-OF-STREAM"; - private final String EOL = "END-OF-LINE"; + private static final String NUMBER = ""; + private static final String EOF = "END-OF-STREAM"; + private static final String EOL = "END-OF-LINE"; - private final boolean coerce; + @SuppressWarnings("FieldCanBeLocal") private final boolean coerce; private final GeometryValidator validator; public WellKnownText(boolean coerce, GeometryValidator validator) { @@ -76,7 +76,7 @@ public Void visit(Point point) { private void visitPoint(double lon, double lat, double alt) { sb.append(lon).append(SPACE).append(lat); - if (Double.isNaN(alt) == false) { + if (!Double.isNaN(alt)) { sb.append(SPACE).append(alt); } } @@ -138,13 +138,11 @@ public Geometry fromWKT(String wkt) throws IOException, ParseException { */ private Geometry parseGeometry(StreamTokenizer stream) throws IOException, ParseException { final String type = nextWord(stream).toLowerCase(Locale.ROOT); - switch (type) { - case "point": - return parsePoint(stream); - case "bbox": - return parseBBox(stream); - } - throw new IllegalArgumentException("Unknown geometry type: " + type); + return switch (type) { + case "point" -> parsePoint(stream); + case "bbox" -> parseBBox(stream); + default -> throw new IllegalArgumentException("Unknown geometry type: " + type); + }; } private Point parsePoint(StreamTokenizer stream) throws IOException, ParseException { @@ -178,7 +176,7 @@ private void parseCoordinate(StreamTokenizer stream, ArrayList lats, Arr if (isNumberNext(stream)) { alts.add(nextNumber(stream)); } - if (alts.isEmpty() == false && alts.size() != lons.size()) { + if (!alts.isEmpty() && alts.size() != lons.size()) { throw new ParseException("coordinate dimensions do not match: " + tokenString(stream), stream.lineno()); } } @@ -187,7 +185,6 @@ private Rectangle parseBBox(StreamTokenizer stream) throws IOException, ParseExc if (nextEmptyOrOpen(stream).equals(EMPTY)) { return Rectangle.EMPTY; } - // TODO: Add 3D support double minLon = nextNumber(stream); nextComma(stream); double maxLon = nextNumber(stream); @@ -203,18 +200,16 @@ private Rectangle parseBBox(StreamTokenizer stream) throws IOException, ParseExc * next word in the stream */ private String nextWord(StreamTokenizer stream) throws ParseException, IOException { - switch (stream.nextToken()) { - case StreamTokenizer.TT_WORD: + return switch (stream.nextToken()) { + case StreamTokenizer.TT_WORD -> { final String word = stream.sval; - return word.equalsIgnoreCase(EMPTY) ? EMPTY : word; - case '(': - return LPAREN; - case ')': - return RPAREN; - case ',': - return COMMA; - } - throw new ParseException("expected word but found: " + tokenString(stream), stream.lineno()); + yield word.equalsIgnoreCase(EMPTY) ? EMPTY : word; + } + case '(' -> LPAREN; + case ')' -> RPAREN; + case ',' -> COMMA; + default -> throw new ParseException("expected word but found: " + tokenString(stream), stream.lineno()); + }; } private double nextNumber(StreamTokenizer stream) throws IOException, ParseException { @@ -238,7 +233,7 @@ private String tokenString(StreamTokenizer stream) { case StreamTokenizer.TT_EOF -> EOF; case StreamTokenizer.TT_EOL -> EOL; case StreamTokenizer.TT_NUMBER -> NUMBER; - default -> "'" + (char) stream.ttype + "'"; + default -> "'" + (char) stream.ttype + '\''; }; } diff --git a/src/test/java/org/springframework/data/elasticsearch/utils/geohash/package-info.java b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/package-info.java similarity index 93% rename from src/test/java/org/springframework/data/elasticsearch/utils/geohash/package-info.java rename to src/main/java/org/springframework/data/elasticsearch/utils/geohash/package-info.java index ec82a9f5d0..dbcea229e9 100644 --- a/src/test/java/org/springframework/data/elasticsearch/utils/geohash/package-info.java +++ b/src/main/java/org/springframework/data/elasticsearch/utils/geohash/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/kotlin/org/springframework/data/elasticsearch/core/DocumentOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/elasticsearch/core/DocumentOperationsExtensions.kt index c3c1a63b00..1a7d805479 100644 --- a/src/main/kotlin/org/springframework/data/elasticsearch/core/DocumentOperationsExtensions.kt +++ b/src/main/kotlin/org/springframework/data/elasticsearch/core/DocumentOperationsExtensions.kt @@ -1,11 +1,9 @@ +@file:Suppress("unused") + package org.springframework.data.elasticsearch.core import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates -import org.springframework.data.elasticsearch.core.query.BulkOptions -import org.springframework.data.elasticsearch.core.query.ByQueryResponse -import org.springframework.data.elasticsearch.core.query.IndexQuery -import org.springframework.data.elasticsearch.core.query.Query -import org.springframework.data.elasticsearch.core.query.UpdateQuery +import org.springframework.data.elasticsearch.core.query.* /** * Extension functions for [DocumentOperations] methods that take a Class parameter leveraging reified type parameters. @@ -39,8 +37,8 @@ inline fun DocumentOperations.bulkIndex( inline fun DocumentOperations.bulkUpdate(queries: List) = bulkUpdate(queries, T::class.java) -inline fun DocumentOperations.delete(id: String): String = +inline fun DocumentOperations.deleteById(id: String): String = delete(id, T::class.java) -inline fun DocumentOperations.delete(query: Query): ByQueryResponse = - delete(query, T::class.java) +inline fun DocumentOperations.deleteByQuery(query: Query): ByQueryResponse = + delete(DeleteQuery.builder(query).build(), T::class.java) diff --git a/src/main/kotlin/org/springframework/data/elasticsearch/core/ReactiveElasticsearchOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/elasticsearch/core/ReactiveElasticsearchOperationsExtensions.kt index ea75056f1d..f80b0c1097 100644 --- a/src/main/kotlin/org/springframework/data/elasticsearch/core/ReactiveElasticsearchOperationsExtensions.kt +++ b/src/main/kotlin/org/springframework/data/elasticsearch/core/ReactiveElasticsearchOperationsExtensions.kt @@ -3,13 +3,13 @@ package org.springframework.data.elasticsearch.core import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates /** - * Extension functions for [ReacctiveElasticsearchOperations] methods that take a Class parameter leveraging reified type parameters. + * Extension functions for [ReactiveElasticsearchOperations] methods that take a Class parameter leveraging reified type parameters. * @author Peter-Josef Meisch * @since 5.2 */ inline fun ReactiveElasticsearchOperations.indexOps(): ReactiveIndexOperations = - indexOps(T::class.java) + indexOps(T::class.java) inline fun ReactiveElasticsearchOperations.getIndexCoordinatesFor(): IndexCoordinates = - getIndexCoordinatesFor(T::class.java) + getIndexCoordinatesFor(T::class.java) diff --git a/src/main/kotlin/org/springframework/data/elasticsearch/core/ReactiveSearchOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/elasticsearch/core/ReactiveSearchOperationsExtensions.kt index e4983c1d36..c8e91ff834 100644 --- a/src/main/kotlin/org/springframework/data/elasticsearch/core/ReactiveSearchOperationsExtensions.kt +++ b/src/main/kotlin/org/springframework/data/elasticsearch/core/ReactiveSearchOperationsExtensions.kt @@ -1,3 +1,5 @@ +@file:Suppress("unused") + package org.springframework.data.elasticsearch.core import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates @@ -14,13 +16,38 @@ import reactor.core.publisher.Mono inline fun ReactiveSearchOperations.count(): Mono = count(T::class.java) inline fun ReactiveSearchOperations.count(query: Query): Mono = count(query, T::class.java) -inline fun ReactiveSearchOperations.count(query: Query, index: IndexCoordinates): Mono = count(query, T::class.java, index) -inline fun ReactiveSearchOperations.search(query: Query): Flux> = search(query, T::class.java) -inline fun ReactiveSearchOperations.search(query: Query, index: IndexCoordinates): Flux> = search(query, T::class.java, index) -inline fun ReactiveSearchOperations.searchForPage(query: Query): Mono> = searchForPage(query, T::class.java) -inline fun ReactiveSearchOperations.searchForPage(query: Query, index: IndexCoordinates): Mono> = searchForPage(query, T::class.java, index) -inline fun ReactiveSearchOperations.searchForHits(query: Query): Mono> = searchForHits(query, T::class.java) -inline fun ReactiveSearchOperations.aggregate(query: Query): Flux> = aggregate(query, T::class.java) -inline fun ReactiveSearchOperations.aggregate(query: Query, index: IndexCoordinates): Flux> = aggregate(query, T::class.java, index) -inline fun ReactiveSearchOperations.suggest(query: Query): Mono = suggest(query, T::class.java) -inline fun ReactiveSearchOperations.suggest(query: Query, index: IndexCoordinates): Mono = suggest(query, T::class.java, index) +inline fun ReactiveSearchOperations.count(query: Query, index: IndexCoordinates): Mono = + count(query, T::class.java, index) + +inline fun ReactiveSearchOperations.search(query: Query): Flux> = + search(query, T::class.java) + +inline fun ReactiveSearchOperations.search( + query: Query, + index: IndexCoordinates +): Flux> = search(query, T::class.java, index) + +inline fun ReactiveSearchOperations.searchForPage(query: Query): Mono> = + searchForPage(query, T::class.java) + +inline fun ReactiveSearchOperations.searchForPage( + query: Query, + index: IndexCoordinates +): Mono> = searchForPage(query, T::class.java, index) + +inline fun ReactiveSearchOperations.searchForHits(query: Query): Mono> = + searchForHits(query, T::class.java) + +inline fun ReactiveSearchOperations.aggregate(query: Query): Flux> = + aggregate(query, T::class.java) + +inline fun ReactiveSearchOperations.aggregate( + query: Query, + index: IndexCoordinates +): Flux> = aggregate(query, T::class.java, index) + +inline fun ReactiveSearchOperations.suggest(query: Query): Mono = + suggest(query, T::class.java) + +inline fun ReactiveSearchOperations.suggest(query: Query, index: IndexCoordinates): Mono = + suggest(query, T::class.java, index) diff --git a/src/main/kotlin/org/springframework/data/elasticsearch/core/SearchOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/elasticsearch/core/SearchOperationsExtensions.kt index dd797e4cf5..b71e4acf8a 100644 --- a/src/main/kotlin/org/springframework/data/elasticsearch/core/SearchOperationsExtensions.kt +++ b/src/main/kotlin/org/springframework/data/elasticsearch/core/SearchOperationsExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 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. @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:Suppress("unused") + package org.springframework.data.elasticsearch.core import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates @@ -28,27 +30,35 @@ import org.springframework.data.elasticsearch.core.query.Query inline fun SearchOperations.count(query: Query): Long = count(query, T::class.java) inline fun SearchOperations.searchOne(query: Query): SearchHit? = searchOne(query, T::class.java) -inline fun SearchOperations.searchOne(query: Query, index: IndexCoordinates): SearchHit? = searchOne(query, T::class.java, index) -inline fun SearchOperations.multiSearch(queries: List): List> = - multiSearch(queries, T::class.java) +inline fun SearchOperations.searchOne(query: Query, index: IndexCoordinates): SearchHit? = + searchOne(query, T::class.java, index) + +inline fun SearchOperations.multiSearch(queries: List): List> = + multiSearch(queries, T::class.java) -inline fun SearchOperations.multiSearch(queries: List, index: IndexCoordinates): List> = - multiSearch(queries, T::class.java, index) +inline fun SearchOperations.multiSearch( + queries: List, + index: IndexCoordinates +): List> = + multiSearch(queries, T::class.java, index) inline fun SearchOperations.search(query: Query): SearchHits = - search(query, T::class.java) + search(query, T::class.java) inline fun SearchOperations.search(query: Query, index: IndexCoordinates): SearchHits = - search(query, T::class.java, index) + search(query, T::class.java, index) inline fun SearchOperations.search(query: MoreLikeThisQuery): SearchHits = - search(query, T::class.java) + search(query, T::class.java) inline fun SearchOperations.search(query: MoreLikeThisQuery, index: IndexCoordinates): SearchHits = - search(query, T::class.java, index) + search(query, T::class.java, index) inline fun SearchOperations.searchForStream(query: Query): SearchHitsIterator = - searchForStream(query, T::class.java) + searchForStream(query, T::class.java) -inline fun SearchOperations.searchForStream(query: Query, index: IndexCoordinates): SearchHitsIterator = - searchForStream(query, T::class.java, index) +inline fun SearchOperations.searchForStream( + query: Query, + index: IndexCoordinates +): SearchHitsIterator = + searchForStream(query, T::class.java, index) diff --git a/src/main/kotlin/org/springframework/data/elasticsearch/repository/CoroutineElasticsearchRepository.kt b/src/main/kotlin/org/springframework/data/elasticsearch/repository/CoroutineElasticsearchRepository.kt index 994eb49d5c..79c932feab 100644 --- a/src/main/kotlin/org/springframework/data/elasticsearch/repository/CoroutineElasticsearchRepository.kt +++ b/src/main/kotlin/org/springframework/data/elasticsearch/repository/CoroutineElasticsearchRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 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. @@ -24,4 +24,4 @@ import org.springframework.data.repository.kotlin.CoroutineSortingRepository * @since 5.2 */ @NoRepositoryBean -interface CoroutineElasticsearchRepository : CoroutineCrudRepository, CoroutineSortingRepository +interface CoroutineElasticsearchRepository : CoroutineCrudRepository, CoroutineSortingRepository diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt index e14bb2735b..1966e7743c 100644 --- a/src/main/resources/notice.txt +++ b/src/main/resources/notice.txt @@ -1,4 +1,4 @@ -Spring Data Elasticsearch 5.2 GA (2023.1.0) +Spring Data Elasticsearch 6.0 M3 (2025.1.0) Copyright (c) [2013-2022] Pivotal Software, Inc. This product is licensed to you under the Apache License, Version 2.0 (the "License"). @@ -10,11 +10,3 @@ code for the these subcomponents is subject to the terms and conditions of the subcomponent's license, as noted in the LICENSE file. - - - - - - - - diff --git a/src/test/java/org/elasticsearch/bootstrap/JarHell.java b/src/test/java/org/elasticsearch/bootstrap/JarHell.java index 73305d7363..a4369f3c63 100644 --- a/src/test/java/org/elasticsearch/bootstrap/JarHell.java +++ b/src/test/java/org/elasticsearch/bootstrap/JarHell.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 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/test/java/org/springframework/data/elasticsearch/BulkFailureExceptionTest.java b/src/test/java/org/springframework/data/elasticsearch/BulkFailureExceptionTest.java index 4d90ea141c..88743e4477 100644 --- a/src/test/java/org/springframework/data/elasticsearch/BulkFailureExceptionTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/BulkFailureExceptionTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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/test/java/org/springframework/data/elasticsearch/ClientArchitectureTests.java b/src/test/java/org/springframework/data/elasticsearch/ClientArchitectureTests.java new file mode 100644 index 0000000000..d71707572c --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/ClientArchitectureTests.java @@ -0,0 +1,23 @@ +package org.springframework.data.elasticsearch; + +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; + +@AnalyzeClasses( + packages = { "org.springframework.data.elasticsearch", "co.elastic" }, + importOptions = ImportOption.DoNotIncludeTests.class) +class ClientArchitectureTests { + + @ArchTest public static final ArchRule elasticLibrariesShouldOnlyBeUsedInClientElc = ArchRuleDefinition + .noClasses() + .that() + .resideInAPackage("org.springframework.data.elasticsearch..") + .and() + .resideOutsideOfPackage("org.springframework.data.elasticsearch.client.elc..") + .should() + .dependOnClassesThat() + .resideInAnyPackage("co.elastic.clients..", "org.elasticsearch.client.."); +} diff --git a/src/test/java/org/springframework/data/elasticsearch/DocumentUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/DocumentUnitTests.java index 0c2fd9fc49..d083be144b 100644 --- a/src/test/java/org/springframework/data/elasticsearch/DocumentUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/DocumentUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/test/java/org/springframework/data/elasticsearch/ELCQueries.java b/src/test/java/org/springframework/data/elasticsearch/ELCQueries.java deleted file mode 100644 index 8824271d7c..0000000000 --- a/src/test/java/org/springframework/data/elasticsearch/ELCQueries.java +++ /dev/null @@ -1,59 +0,0 @@ -/* -// * Copyright 2022-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.elasticsearch; - -import static org.springframework.data.elasticsearch.client.elc.Queries.*; - -import co.elastic.clients.elasticsearch._types.aggregations.Aggregation; - -import org.springframework.data.elasticsearch.client.elc.NativeQuery; -import org.springframework.data.elasticsearch.client.elc.Queries; -import org.springframework.data.elasticsearch.core.query.BaseQueryBuilder; -import org.springframework.data.elasticsearch.core.query.Query; - -/** - * Class providing some queries for the new Elasticsearch client needed in different tests. - * - * @author Peter-Josef Meisch - * @since 4.4 - * @deprecated since 5.1, use the corresponding methods from {@link Queries}. - */ -@Deprecated(forRemoval = true) -public final class ELCQueries { - - private ELCQueries() {} - - public static Query getTermsAggsQuery(String aggsName, String aggsField) { - return NativeQuery.builder() // - .withQuery(Queries.matchAllQueryAsQuery()) // - .withAggregation(aggsName, Aggregation.of(a -> a // - .terms(ta -> ta.field(aggsField)))) // - .withMaxResults(0) // - .build(); - } - - public static Query queryWithIds(String... ids) { - return NativeQuery.builder().withIds(ids).build(); - } - - public static BaseQueryBuilder getBuilderWithMatchAllQuery() { - return NativeQuery.builder().withQuery(matchAllQueryAsQuery()); - } - - public static BaseQueryBuilder getBuilderWithTermQuery(String field, String value) { - return NativeQuery.builder().withQuery(termQueryAsQuery(field, value)); - } -} diff --git a/src/test/java/org/springframework/data/elasticsearch/JUnit5ClusterConnectionTests.java b/src/test/java/org/springframework/data/elasticsearch/JUnit5ClusterConnectionTests.java index 2a17ad218c..acbe9bfbb9 100644 --- a/src/test/java/org/springframework/data/elasticsearch/JUnit5ClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/JUnit5ClusterConnectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/test/java/org/springframework/data/elasticsearch/JUnit5SampleElasticsearchTemplateTests.java b/src/test/java/org/springframework/data/elasticsearch/JUnit5SampleElasticsearchTemplateTests.java index e152685091..363f93906b 100644 --- a/src/test/java/org/springframework/data/elasticsearch/JUnit5SampleElasticsearchTemplateTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/JUnit5SampleElasticsearchTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/JUnit5SampleReactiveELCTests.java b/src/test/java/org/springframework/data/elasticsearch/JUnit5SampleReactiveELCTests.java index 5008eb3e57..d5b6f48e11 100644 --- a/src/test/java/org/springframework/data/elasticsearch/JUnit5SampleReactiveELCTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/JUnit5SampleReactiveELCTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/NestedObjectELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/NestedObjectELCIntegrationTests.java index 9353999725..e99b5e72fb 100644 --- a/src/test/java/org/springframework/data/elasticsearch/NestedObjectELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/NestedObjectELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/NestedObjectIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/NestedObjectIntegrationTests.java index 7594bb23a6..2682274550 100644 --- a/src/test/java/org/springframework/data/elasticsearch/NestedObjectIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/NestedObjectIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -15,6 +15,8 @@ */ package org.springframework.data.elasticsearch; +import static co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders.*; +import static java.util.UUID.*; import static org.assertj.core.api.Assertions.*; import static org.springframework.data.elasticsearch.utils.IdGenerator.*; @@ -27,7 +29,9 @@ import java.util.Map; import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -37,6 +41,7 @@ import org.springframework.data.elasticsearch.annotations.FieldType; import org.springframework.data.elasticsearch.annotations.InnerField; import org.springframework.data.elasticsearch.annotations.MultiField; +import org.springframework.data.elasticsearch.client.elc.NativeQuery; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.IndexOperations; import org.springframework.data.elasticsearch.core.SearchHits; @@ -45,7 +50,6 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Rizwan Idrees @@ -249,6 +253,40 @@ private List createPerson() { public void shouldSearchBooksForPersonInitialLevelNestedType() { // given + var foo = getPerson(); + + Car car = new Car(); + car.setName("Saturn"); + car.setModel("Imprezza"); + + Person bar = new Person(); + bar.setId("2"); + bar.setName("Bar"); + bar.setCar(Collections.singletonList(car)); + + List indexQueries = new ArrayList<>(); + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(foo.getId()); + indexQuery1.setObject(foo); + + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(bar.getId()); + indexQuery2.setObject(bar); + + indexQueries.add(indexQuery1); + indexQueries.add(indexQuery2); + + operations.bulkIndex(indexQueries, Person.class); + + // when + Query searchQuery = getNestedQuery3(); + SearchHits persons = operations.search(searchQuery, Person.class); + + // then + assertThat(persons).hasSize(1); + } + + private static Person getPerson() { List cars = new ArrayList<>(); Car saturn = new Car(); @@ -288,36 +326,7 @@ public void shouldSearchBooksForPersonInitialLevelNestedType() { foo.setId("1"); foo.setCar(cars); foo.setBooks(Arrays.asList(java, spring)); - - Car car = new Car(); - car.setName("Saturn"); - car.setModel("Imprezza"); - - Person bar = new Person(); - bar.setId("2"); - bar.setName("Bar"); - bar.setCar(Collections.singletonList(car)); - - List indexQueries = new ArrayList<>(); - IndexQuery indexQuery1 = new IndexQuery(); - indexQuery1.setId(foo.getId()); - indexQuery1.setObject(foo); - - IndexQuery indexQuery2 = new IndexQuery(); - indexQuery2.setId(bar.getId()); - indexQuery2.setObject(bar); - - indexQueries.add(indexQuery1); - indexQueries.add(indexQuery2); - - operations.bulkIndex(indexQueries, Person.class); - - // when - Query searchQuery = getNestedQuery3(); - SearchHits persons = operations.search(searchQuery, Person.class); - - // then - assertThat(persons).hasSize(1); + return foo; } @NotNull @@ -368,6 +377,42 @@ public void shouldIndexAndSearchMapAsNestedType() { assertThat(books.getSearchHit(0).getContent().getId()).isEqualTo(book2.getId()); } + @Test // #2952 + @DisplayName("should handle null and empty field parameters in the mapping process") + void shouldSupportMappingNullAndEmptyFieldParameter() { + // Given + operations.indexOps(MultiFieldWithNullEmptyParameters.class).createWithMapping(); + List indexQueries = new ArrayList<>(); + MultiFieldWithNullEmptyParameters nullObj = new MultiFieldWithNullEmptyParameters(); + nullObj.addFieldWithInner(randomUUID().toString()); + MultiFieldWithNullEmptyParameters objWithValue = new MultiFieldWithNullEmptyParameters(); + objWithValue.addEmptyField(randomUUID().toString()); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(nextIdAsString()); + indexQuery1.setObject(nullObj); + indexQueries.add(indexQuery1); + + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(nextIdAsString()); + indexQuery2.setObject(objWithValue); + indexQueries.add(indexQuery2); + + // When + operations.bulkIndex(indexQueries, MultiFieldWithNullEmptyParameters.class); + + // Then + SearchHits nullResults = operations.search( + NativeQuery.builder().withQuery(match(bm -> bm.field("empty-field").query("EMPTY"))).build(), + MultiFieldWithNullEmptyParameters.class); + assertThat(nullResults.getSearchHits()).hasSize(1); + + nullResults = operations.search( + NativeQuery.builder().withQuery(match(bm -> bm.field("inner-field.prefix").query("EMPTY"))).build(), + MultiFieldWithNullEmptyParameters.class); + assertThat(nullResults.getSearchHits()).hasSize(1); + } + @NotNull abstract protected Query getNestedQuery4(); @@ -617,4 +662,40 @@ public void setName(@Nullable String name) { } } + @Document(indexName = "#{@indexNameProvider.indexName()}-multi-field") + static class MultiFieldWithNullEmptyParameters { + @Nullable + @MultiField(mainField = @Field(name = "empty-field", type = FieldType.Keyword, nullValue = "EMPTY", + storeNullValue = true)) private List emptyField; + + @Nullable + @MultiField(mainField = @Field(name = "inner-field", type = FieldType.Text, storeNullValue = true), + otherFields = { @InnerField(suffix = "prefix", type = FieldType.Keyword, + nullValue = "EMPTY") }) private List fieldWithInner; + + public List getEmptyField() { + if (emptyField == null) { + emptyField = new ArrayList<>(); + } + + return emptyField; + } + + public void addEmptyField(String value) { + getEmptyField().add(value); + } + + public List getFieldWithInner() { + if (fieldWithInner == null) { + fieldWithInner = new ArrayList<>(); + } + + return fieldWithInner; + } + + public void addFieldWithInner(@Nullable String value) { + getFieldWithInner().add(value); + } + } + } diff --git a/src/test/java/org/springframework/data/elasticsearch/NullabilityArchitectureTests.java b/src/test/java/org/springframework/data/elasticsearch/NullabilityArchitectureTests.java new file mode 100644 index 0000000000..a086f251e3 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/NullabilityArchitectureTests.java @@ -0,0 +1,21 @@ +package org.springframework.data.elasticsearch; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; + +@AnalyzeClasses(packages = { "org.springframework.data.elasticsearch" }) +class NullabilityArchitectureTests { + + @ArchTest public static final ArchRule shouldNotUseSpringNullability = ArchRuleDefinition + .noClasses() + .that() + .resideInAPackage("org.springframework.data.elasticsearch..") + .should() + .dependOnClassesThat() + .haveFullyQualifiedName("org.springframework.lang.NonNull") + .orShould() + .dependOnClassesThat() + .haveFullyQualifiedName("org.springframework.lang.Nullable"); +} diff --git a/src/test/java/org/springframework/data/elasticsearch/annotations/ComposableAnnotationsUnitTest.java b/src/test/java/org/springframework/data/elasticsearch/annotations/ComposableAnnotationsUnitTest.java index e1647bcbb4..8c64053a8c 100644 --- a/src/test/java/org/springframework/data/elasticsearch/annotations/ComposableAnnotationsUnitTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/annotations/ComposableAnnotationsUnitTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -28,6 +28,7 @@ import java.time.LocalDate; import org.json.JSONException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.core.annotation.AliasFor; @@ -39,16 +40,15 @@ import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.suggest.Completion; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch */ public class ComposableAnnotationsUnitTest { - private static SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext(); - private static MappingElasticsearchConverter converter = new MappingElasticsearchConverter(mappingContext); - private static MappingBuilder mappingBuilder = new MappingBuilder(converter); + private static final SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext(); + private static final MappingElasticsearchConverter converter = new MappingElasticsearchConverter(mappingContext); + private static final MappingBuilder mappingBuilder = new MappingBuilder(converter); @Test // DATAES-362 @DisplayName("Document annotation should be composable") diff --git a/src/test/java/org/springframework/data/elasticsearch/blockhound/BlockHoundIntegrationCustomizer.java b/src/test/java/org/springframework/data/elasticsearch/blockhound/BlockHoundIntegrationCustomizer.java deleted file mode 100644 index 0c20785124..0000000000 --- a/src/test/java/org/springframework/data/elasticsearch/blockhound/BlockHoundIntegrationCustomizer.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2021-2023 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.blockhound; - -import reactor.blockhound.BlockHound; -import reactor.blockhound.BlockingOperationError; -import reactor.blockhound.integration.BlockHoundIntegration; - -/** - * @author Peter-Josef Meisch - */ -public class BlockHoundIntegrationCustomizer implements BlockHoundIntegration { - - @Override - public void applyTo(BlockHound.Builder builder) { - // Elasticsearch classes reading from the classpath on initialization, needed for parsing Elasticsearch responses - builder // - .allowBlockingCallsInside("org.elasticsearch.Build", "") // - .allowBlockingCallsInside("org.elasticsearch.common.xcontent.XContentBuilder", "") // pre 7.16 - .allowBlockingCallsInside("org.elasticsearch.common.XContentBuilder", "") // from 7.16 on - .allowBlockingCallsInside("org.elasticsearch.xcontent.json.JsonXContent", "contentBuilder") // from 7.16 on - .allowBlockingCallsInside("jakarta.json.spi.JsonProvider", "provider") // - ; - builder.blockingMethodCallback(it -> { - new Error(it.toString()).printStackTrace(); - throw new BlockingOperationError(it); - }); - - } -} diff --git a/src/test/java/org/springframework/data/elasticsearch/blockhound/BlockHoundTests.java b/src/test/java/org/springframework/data/elasticsearch/blockhound/BlockHoundTests.java deleted file mode 100644 index dd99d735dc..0000000000 --- a/src/test/java/org/springframework/data/elasticsearch/blockhound/BlockHoundTests.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2021-2023 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.blockhound; - -import static org.assertj.core.api.Assertions.*; - -import reactor.blockhound.BlockingOperationError; -import reactor.core.publisher.Mono; - -import java.time.Duration; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -/** - * @author Peter-Josef Meisch - */ -public class BlockHoundTests { - - @Test // #1822 - @DisplayName("should fail if BlockHound is not installed") - void shouldFailIfBlockHoundIsNotInstalled() { - - assertThatThrownBy(() -> { - Mono.delay(Duration.ofMillis(1)).doOnNext(it -> { - try { - Thread.sleep(10); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - }).block(); // should throw an exception about Thread.sleep - }).hasCauseInstanceOf(BlockingOperationError.class); - } -} diff --git a/src/test/java/org/springframework/data/elasticsearch/client/ClientConfigurationUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/client/ClientConfigurationUnitTests.java index c73b864c38..aa61213a20 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/ClientConfigurationUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/ClientConfigurationUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 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/test/java/org/springframework/data/elasticsearch/client/InetSocketAddressParserUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/client/InetSocketAddressParserUnitTests.java index f138ffa077..8bbef09c18 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/InetSocketAddressParserUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/InetSocketAddressParserUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 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/test/java/org/springframework/data/elasticsearch/client/RestClientsTest.java b/src/test/java/org/springframework/data/elasticsearch/client/RestClientsTest.java index 4f01f1c1a3..f8c9b6e495 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/RestClientsTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/RestClientsTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -17,9 +17,9 @@ import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; -import static io.specto.hoverfly.junit.dsl.HoverflyDsl.service; -import static io.specto.hoverfly.junit.verification.HoverflyVerifications.atLeast; -import static org.assertj.core.api.Assertions.assertThat; +import static io.specto.hoverfly.junit.dsl.HoverflyDsl.*; +import static io.specto.hoverfly.junit.verification.HoverflyVerifications.*; +import static org.assertj.core.api.Assertions.*; import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.transport.endpoints.BooleanResponse; @@ -50,10 +50,13 @@ /** * We need hoverfly for testing the reactive code to use a proxy. Wiremock cannot intercept the proxy calls as WebClient - * uses HTTP CONNECT on proxy requests which wiremock does not support. + * uses HTTP CONNECT on proxy requests which wiremock does not support.
+ * Note: since 5.0 we do not use the WebClient for the reactive code anymore, so this might be handled with two + * wiremocks, but there is no real need to change this test setup. * * @author Peter-Josef Meisch */ +@SuppressWarnings("UastIncorrectHttpHeaderInspection") @HoverflyCapture(path = "target/hoverfly", config = @HoverflyConfig(proxyLocalHost = true, plainHttpTunneling = true)) @ExtendWith(HoverflyExtension.class) public class RestClientsTest { @@ -204,15 +207,14 @@ void shouldSetCompatibilityHeaders(ClientUnderTestFactory clientUnderTestFactory ClientUnderTest clientUnderTest = clientUnderTestFactory.create(clientConfiguration); class Foo { - public String id; + public final String id; Foo(String id) { this.id = id; } } - ; - clientUnderTest.index(new Foo("42")); + clientUnderTest.index(new Foo("42")); verify(putRequestedFor(urlMatching(urlPattern)) // .withHeader("Accept", new EqualToPattern("application/vnd.elasticsearch+json;compatible-with=7")) // diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/AutoCloseableElasticsearchClientTest.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/AutoCloseableElasticsearchClientTest.java index 18d632320c..279d64936c 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/elc/AutoCloseableElasticsearchClientTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/AutoCloseableElasticsearchClientTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryMappingUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryMappingUnitTests.java index adb51c257b..c0f3641dbe 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryMappingUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryMappingUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -15,6 +15,7 @@ */ package org.springframework.data.elasticsearch.client.elc; +import static org.assertj.core.api.Assertions.*; import static org.skyscreamer.jsonassert.JSONAssert.*; import static org.springframework.data.elasticsearch.client.elc.JsonUtils.*; @@ -22,12 +23,14 @@ import co.elastic.clients.json.jackson.JacksonJsonpMapper; import java.time.LocalDate; +import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import org.assertj.core.api.SoftAssertions; import org.json.JSONException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -47,7 +50,6 @@ import org.springframework.data.elasticsearch.core.query.FetchSourceFilterBuilder; import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.SourceFilter; -import org.springframework.lang.Nullable; /** * Tests for the mapping of {@link CriteriaQuery} by a {@link MappingElasticsearchConverter}. In the same package as @@ -60,7 +62,7 @@ */ public class CriteriaQueryMappingUnitTests { - private JsonpMapper mapper = new JacksonJsonpMapper(); + private final JsonpMapper mapper = new JacksonJsonpMapper(); MappingElasticsearchConverter mappingElasticsearchConverter; @@ -445,6 +447,108 @@ void shouldMapNamesInSourceStoredFields() { softly.assertAll(); } + // the following test failed because of a wrong implementation in Criteria + // equals and hscode methods. + @Test // #3083 + @DisplayName("should map correct subcriteria") + void shouldMapCorrectSubcriteria() throws JSONException { + Criteria criteria = new Criteria("first").is("hello"); + + List criterias = new ArrayList<>(); + criterias.add(new Criteria().or("second").exists()); + + List subCriterias = new ArrayList<>(); + subCriterias.add(new Criteria("third").exists() + .and(new Criteria("fourth").is("ciao"))); + subCriterias.add(new Criteria("third").exists() + .and(new Criteria("fourth").is("hi"))); + + Criteria result = Criteria.or(); + + for (Criteria c : criterias) { + result = result.or(c); + } + + for (Criteria c : subCriterias) { + result = result.subCriteria(c); + } + criteria = criteria.subCriteria(result); + CriteriaQuery criteriaQuery = new CriteriaQuery(criteria); + + String expected = """ + { + "bool": { + "must": [ + { + "query_string": { + "default_operator": "and", + "fields": [ + "first" + ], + "query": "hello" + } + }, + { + "bool": { + "should": [ + { + "exists": { + "field": "second" + } + }, + { + "bool": { + "must": [ + { + "exists": { + "field": "third" + } + }, + { + "query_string": { + "default_operator": "and", + "fields": [ + "fourth" + ], + "query": "ciao" + } + } + ] + } + }, + { + "bool": { + "must": [ + { + "exists": { + "field": "third" + } + }, + { + "query_string": { + "default_operator": "and", + "fields": [ + "fourth" + ], + "query": "hi" + } + } + ] + } + } + ] + } + } + ] + } + } + """; + + mappingElasticsearchConverter.updateQuery(criteriaQuery, Person.class); + var queryString = queryToJson(CriteriaQueryProcessor.createQuery(criteriaQuery.getCriteria()), mapper); + + assertEquals(expected, queryString, false); + } // endregion // region helper functions diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessorUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessorUnitTests.java index 7307fdb098..9ef5bd13f2 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessorUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessorUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -33,7 +33,7 @@ @SuppressWarnings("ConstantConditions") class CriteriaQueryProcessorUnitTests { - private JsonpMapper mapper = new JacksonJsonpMapper(); + private final JsonpMapper mapper = new JacksonJsonpMapper(); private final CriteriaQueryProcessor queryProcessor = new CriteriaQueryProcessor(); @@ -99,7 +99,7 @@ void shouldProcessTwoCriteriaWithOr() throws JSONException { ] } } - """; + """; Criteria criteria = new Criteria("field1").is("value1").or("field2").is("value2"); @@ -152,7 +152,7 @@ void shouldProcessMixedCriteriaWithOrAnd() throws JSONException { ] } } - """; + """; Criteria criteria = new Criteria("field1").is("value1") // .or("field2").is("value2") // @@ -295,7 +295,7 @@ void shouldProcessNestedSubCriteria() throws JSONException { ] } } - """; + """; Criteria criteria = Criteria.or() .subCriteria(new Criteria("lastName").is("Miller") @@ -483,4 +483,74 @@ void shouldBuildRegexpQuery() throws JSONException { assertEquals(expected, queryString, false); } + @Test + void shouldWrapOrCriteria() throws JSONException { + // Given + String expected = """ + { + "bool": { + "should": [ + { + "query_string": { + "analyze_wildcard": true, + "fields": [ + "field1" + ], + "query": "*xyz*" + } + }, + { + "bool": { + "must_not": [ + { + "query_string": { + "boost": 1.5, + "default_operator": "and", + "fields": [ + "field1" + ], + "query": "abc" + } + } + ] + } + }, + { + "bool": { + "must": [ + { + "query_string": { + "analyze_wildcard": true, + "fields": [ + "field2" + ], + "query": "elastic*" + } + } + ] + } + } + ] + } + } + """; + + Criteria criteria = Criteria.where("field1") + .contains("xyz") + .or( + Criteria.where("field1") + .is("abc").not() + .boost(1.5f) + .subCriteria( + Criteria.where("field2") + .startsWith("elastic") + ) + ); + + // Then + String queryString = queryToJson(CriteriaQueryProcessor.createQuery(criteria), mapper); + + assertEquals(expected, queryString, false); + } + } diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/DevTests.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/DevTests.java index a6202849e9..25308b569a 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/elc/DevTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/DevTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,6 +22,8 @@ import co.elastic.clients.elasticsearch._types.mapping.TypeMapping; import co.elastic.clients.elasticsearch.cluster.HealthRequest; import co.elastic.clients.elasticsearch.cluster.HealthResponse; +import co.elastic.clients.elasticsearch.core.CountRequest; +import co.elastic.clients.elasticsearch.core.CountResponse; import co.elastic.clients.elasticsearch.core.IndexRequest; import co.elastic.clients.elasticsearch.core.IndexResponse; import co.elastic.clients.elasticsearch.core.SearchRequest; @@ -46,6 +48,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.elasticsearch.client.RequestOptions; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; @@ -55,26 +58,26 @@ import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter; import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; import org.springframework.data.elasticsearch.support.HttpHeaders; -import org.springframework.lang.Nullable; /** * Not really tests, but a class to check the first implementations of the new Elasticsearch client. Needs Elasticsearch * on port 9200 and an intercepting proxy on port 8080. * * @author Peter-Josef Meisch + * @author maryantocinn */ @Disabled @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class DevTests { - private static Log LOGGER = LogFactory.getLog(DevTests.class); + private static final Log LOGGER = LogFactory.getLog(DevTests.class); private static final String INDEX = "appdata-index"; private static final SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext(); private static final MappingElasticsearchConverter converter = new MappingElasticsearchConverter(mappingContext); - private final TransportOptions transportOptions = new RestClientOptions(RequestOptions.DEFAULT).toBuilder() + private final TransportOptions transportOptions = new RestClientOptions(RequestOptions.DEFAULT, false).toBuilder() .addHeader("X-SpringDataElasticsearch-AlwaysThere", "true").setParameter("pretty", "true").build(); private final JsonpMapper jsonpMapper = new JacksonJsonpMapper(); @@ -218,13 +221,13 @@ void clusterHealth() { try { HealthResponse healthResponse = clusterHealthImperative(healthRequest); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("error", e); } try { HealthResponse healthResponse = clusterHealthReactive(healthRequest); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("error", e); } } @@ -301,12 +304,12 @@ void save() { try { indexImperative(indexRequestBuilder.apply(1)); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("error", e); } try { indexReactive(indexRequestBuilder.apply(2)); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("error", e); } } @@ -334,14 +337,14 @@ void search() { searchResponse = searchImperative(searchRequest); assertThat(searchResponse).isNotNull(); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("error", e); } try { searchResponse = searchReactive(searchRequest); assertThat(searchResponse).isNotNull(); } catch (Exception e) { - e.printStackTrace(); + LOGGER.error("error", e); } } @@ -352,6 +355,43 @@ private ResponseBody searchImperative(SearchRequest searchRequest) private ResponseBody searchReactive(SearchRequest searchRequest) { return Objects.requireNonNull(reactiveElasticsearchClient.search(searchRequest, EntityAsMap.class).block()); } + + // endregion + // region count + @Test + @Order(40) + void count() { + + CountRequest countRequest = new CountRequest.Builder().index(INDEX) + .query(query -> query.match(matchQuery -> matchQuery.field("content").query(FieldValue.of("content1")))) + .build(); + + CountResponse countResponse = null; + + try { + countResponse = countImperative(countRequest); + assertThat(countResponse).isNotNull(); + assertThat(countResponse.count()).isEqualTo(1); + } catch (IOException e) { + LOGGER.error("error", e); + } + + try { + countResponse = countReactive(countRequest); + assertThat(countResponse).isNotNull(); + assertThat(countResponse.count()).isEqualTo(1); + } catch (Exception e) { + LOGGER.error("error", e); + } + } + + private CountResponse countImperative(CountRequest countRequest) throws IOException { + return imperativeElasticsearchClient.count(countRequest); + } + + private CountResponse countReactive(CountRequest countRequest) { + return Objects.requireNonNull(reactiveElasticsearchClient.count(countRequest).block()); + } // endregion private ClientConfiguration clientConfiguration() { diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/DocumentAdaptersUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/DocumentAdaptersUnitTests.java index 37f800ad83..68dc1db7c4 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/elc/DocumentAdaptersUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/DocumentAdaptersUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import org.assertj.core.api.SoftAssertions; import org.assertj.core.data.Offset; @@ -144,17 +145,17 @@ void shouldAdaptReturnedMatchedQueries() { Hit searchHit = new Hit.Builder() // .index("index") // .id("42") // - .matchedQueries("query1", "query2") // + .matchedQueries("query1", 1D) // .build(); SearchDocument searchDocument = DocumentAdapters.from(searchHit, jsonpMapper); SoftAssertions softly = new SoftAssertions(); - List matchedQueries = searchDocument.getMatchedQueries(); + Map matchedQueries = searchDocument.getMatchedQueries(); softly.assertThat(matchedQueries).isNotNull(); - softly.assertThat(matchedQueries).hasSize(2); - softly.assertThat(matchedQueries).isEqualTo(Arrays.asList("query1", "query2")); + softly.assertThat(matchedQueries).hasSize(1); + softly.assertThat(matchedQueries).isEqualTo(Map.of("query1",1D)); softly.assertAll(); } } diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/ELCWiremockTests.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/ELCWiremockTests.java new file mode 100644 index 0000000000..875fd752eb --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/ELCWiremockTests.java @@ -0,0 +1,142 @@ +/* + * 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 static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.*; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; + +/** + * Tests that need to check the data produced by the Elasticsearch client + * + * @author Peter-Josef Meisch + */ +@SuppressWarnings("UastIncorrectHttpHeaderInspection") +@ExtendWith(SpringExtension.class) +public class ELCWiremockTests { + + @RegisterExtension static WireMockExtension wireMock = WireMockExtension.newInstance() + .options(wireMockConfig() + .dynamicPort() + // needed, otherwise Wiremock goes to test/resources/mappings + .usingFilesUnderDirectory("src/test/resources/wiremock-mappings")) + .build(); + + @Configuration + static class Config extends ElasticsearchConfiguration { + @Override + public ClientConfiguration clientConfiguration() { + return ClientConfiguration.builder() + .connectedTo("localhost:" + wireMock.getPort()) + .build(); + } + } + + @Autowired ElasticsearchOperations operations; + + @Test // #2839 + @DisplayName("should store null values if configured") + void shouldStoreNullValuesIfConfigured() { + + wireMock.stubFor(put(urlPathEqualTo("/null-fields/_doc/42")) + .withRequestBody(equalToJson(""" + { + "_class": "org.springframework.data.elasticsearch.client.elc.ELCWiremockTests$EntityWithNullFields", + "id": "42", + "field1": null + } + """)) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("X-elastic-product", "Elasticsearch") + .withHeader("content-type", "application/vnd.elasticsearch+json;compatible-with=8") + .withBody(""" + { + "_index": "null-fields", + "_id": "42", + "_version": 1, + "result": "created", + "forced_refresh": true, + "_shards": { + "total": 2, + "successful": 1, + "failed": 0 + }, + "_seq_no": 1, + "_primary_term": 1 + } + """))); + + var entity = new EntityWithNullFields(); + entity.setId("42"); + + operations.save(entity); + // no need to assert anything, if the field1:null is not sent, we run into a 404 error + } + + @Document(indexName = "null-fields") + static class EntityWithNullFields { + @Nullable + @Id private String id; + @Nullable + @Field(storeNullValue = true) private String field1; + @Nullable + @Field private String field2; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public String getField1() { + return field1; + } + + public void setField1(@Nullable String field1) { + this.field1 = field1; + } + + @Nullable + public String getField2() { + return field2; + } + + public void setField2(@Nullable String field2) { + this.field2 = field2; + } + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchPartQueryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchPartQueryELCIntegrationTests.java index 6fe225e94c..40f7dd7cee 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchPartQueryELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchPartQueryELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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. @@ -21,7 +21,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; -import org.springframework.data.elasticsearch.core.query.ElasticsearchPartQueryIntegrationTests; +import org.springframework.data.elasticsearch.core.query.RepositoryPartQueryIntegrationTests; import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration; @@ -29,7 +29,7 @@ * @author Peter-Josef Meisch * @since 4.4 */ -public class ElasticsearchPartQueryELCIntegrationTests extends ElasticsearchPartQueryIntegrationTests { +public class ElasticsearchPartQueryELCIntegrationTests extends RepositoryPartQueryIntegrationTests { @Configuration @Import({ ElasticsearchTemplateConfiguration.class }) diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/ReactiveIndicesTemplateTest.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/ReactiveIndicesTemplateTest.java index 3a63cb109b..8e44ef0069 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/elc/ReactiveIndicesTemplateTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/ReactiveIndicesTemplateTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/client/elc/RequestConverterTest.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/RequestConverterTest.java index c20ab8da8f..cdb597e383 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/elc/RequestConverterTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/RequestConverterTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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,9 @@ import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import java.util.List; + +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; @@ -28,30 +31,30 @@ import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; +import org.springframework.data.elasticsearch.core.query.Criteria; +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; +import org.springframework.data.elasticsearch.core.query.DeleteQuery; import org.springframework.data.elasticsearch.core.query.DocValueField; -import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.StringQuery; -import org.springframework.lang.Nullable; - -import java.util.List; /** * @author Peter-Josef Meisch + * @author Han Seungwoo */ class RequestConverterTest { private static final SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext(); private static final MappingElasticsearchConverter converter = new MappingElasticsearchConverter(mappingContext); - private JacksonJsonpMapper jsonpMapper = new JacksonJsonpMapper(); - private RequestConverter requestConverter = new RequestConverter(converter, jsonpMapper); + private final JacksonJsonpMapper jsonpMapper = new JacksonJsonpMapper(); + private final RequestConverter requestConverter = new RequestConverter(converter, jsonpMapper); @Test // #2316 @DisplayName("should add docvalue_fields") void shouldAddDocvalueFields() { var docValueFields = List.of( // - new DocValueField("field1"), // - new DocValueField("field2", "format2") // + new DocValueField("field1"), // + new DocValueField("field2", "format2") // ); // doesn't matter what type of query is used, the relevant part for docvalue_fields is in the base builder. var query = StringQuery.builder(""" @@ -59,10 +62,11 @@ void shouldAddDocvalueFields() { "match_all":{} } """) // - .withDocValueFields(docValueFields) // - .build(); + .withDocValueFields(docValueFields) // + .build(); - var searchRequest = requestConverter.searchRequest(query,null, SampleEntity.class, IndexCoordinates.of("foo"), true); + var searchRequest = requestConverter.searchRequest(query, null, SampleEntity.class, IndexCoordinates.of("foo"), + true); var fieldAndFormats = searchRequest.docvalueFields(); assertThat(fieldAndFormats).hasSize(2); @@ -72,6 +76,19 @@ void shouldAddDocvalueFields() { assertThat(fieldAndFormats.get(1).format()).isEqualTo("format2"); } + @Test // #2973 + @DisplayName("should set refresh based on deleteRequest") + void refreshSetByDeleteRequest() { + var query = new CriteriaQuery(new Criteria("text").contains("test")); + var deleteQuery = DeleteQuery.builder(query).withRefresh(true).build(); + + var deleteByQueryRequest = requestConverter.documentDeleteByQueryRequest(deleteQuery, null, SampleEntity.class, + IndexCoordinates.of("foo"), + null); + + assertThat(deleteByQueryRequest.refresh()).isTrue(); + } + @Document(indexName = "does-not-matter") static class SampleEntity { @Nullable diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/SearchDocumentResponseBuilderUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/SearchDocumentResponseBuilderUnitTests.java index d53e54079a..daa4070452 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/elc/SearchDocumentResponseBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/SearchDocumentResponseBuilderUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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,17 +15,25 @@ */ package org.springframework.data.elasticsearch.client.elc; +import static org.assertj.core.api.Assertions.*; + +import co.elastic.clients.elasticsearch._types.ShardFailure; +import co.elastic.clients.elasticsearch._types.ShardStatistics; import co.elastic.clients.elasticsearch.core.search.HitsMetadata; import co.elastic.clients.elasticsearch.core.search.Suggestion; import co.elastic.clients.elasticsearch.core.search.TotalHitsRelation; +import co.elastic.clients.json.JsonData; import co.elastic.clients.json.jackson.JacksonJsonpMapper; import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.assertj.core.api.SoftAssertions; import org.json.JSONException; import org.junit.jupiter.api.Test; +import org.springframework.data.elasticsearch.ElasticsearchErrorCause; +import org.springframework.data.elasticsearch.core.SearchShardStatistics; import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse; import com.google.common.collect.ImmutableList; @@ -35,45 +43,33 @@ * Tests for the factory class to create {@link SearchDocumentResponse} instances. * * @author Sébastien Comeau + * @author Haibo Liu + * @author Mohamed El Harrougui * @since 5.2 */ class SearchDocumentResponseBuilderUnitTests { - private JacksonJsonpMapper jsonpMapper = new JacksonJsonpMapper(); + private final JacksonJsonpMapper jsonpMapper = new JacksonJsonpMapper(); @Test // #2681 void shouldGetPhraseSuggestion() throws JSONException { // arrange final var hitsMetadata = new HitsMetadata.Builder() - .total(total -> total - .value(0) - .relation(TotalHitsRelation.Eq)) - .hits(new ArrayList<>()) - .build(); - - final var suggestionTest = new Suggestion.Builder() - .phrase(phrase -> phrase - .text("National") - .offset(0) - .length(8) - .options(option -> option - .text("nations") - .highlighted("highlighted-nations") - .score(0.11480146) - .collateMatch(false)) - .options(option -> option - .text("national") - .highlighted("highlighted-national") - .score(0.08063514) - .collateMatch(false))) + .total(total -> total.value(0).relation(TotalHitsRelation.Eq)).hits(new ArrayList<>()).build(); + + final var suggestionTest = new Suggestion.Builder().phrase(phrase -> phrase.text("National").offset(0) + .length(8) + .options( + option -> option.text("nations").highlighted("highlighted-nations").score(0.11480146).collateMatch(false)) + .options(option -> option.text("national").highlighted("highlighted-national").score(0.08063514) + .collateMatch(false))) .build(); final var sortProperties = ImmutableMap.>> builder() - .put("suggestionTest", ImmutableList.of(suggestionTest)) - .build(); + .put("suggestionTest", ImmutableList.of(suggestionTest)).build(); // act - final var actual = SearchDocumentResponseBuilder.from(hitsMetadata, null, null, null, sortProperties, null, + final var actual = SearchDocumentResponseBuilder.from(hitsMetadata, null, null, null, 0, null, sortProperties, null, jsonpMapper); // assert @@ -108,4 +104,40 @@ void shouldGetPhraseSuggestion() throws JSONException { softly.assertAll(); } + + @Test // #2605 + void shouldGetShardStatisticsInfo() { + // arrange + HitsMetadata hitsMetadata = new HitsMetadata.Builder() + .total(t -> t.value(0).relation(TotalHitsRelation.Eq)).hits(new ArrayList<>()).build(); + + ShardStatistics shards = new ShardStatistics.Builder().total(15).successful(14).skipped(0).failed(1) + .failures(List.of(ShardFailure.of(sfb -> sfb.index("test-index").node("test-node").shard(1) + .reason(rb -> rb.reason("this is a mock failure in shards") + .causedBy(cbb -> cbb.reason("inner reason").metadata(Map.of("hello", JsonData.of("world")))) + .type("reason-type") + + ).status("fail")))).build(); + + // act + SearchDocumentResponse response = SearchDocumentResponseBuilder.from(hitsMetadata, shards, null, null, 0, null, + null, null, jsonpMapper); + + // assert + SearchShardStatistics shardStatistics = response.getSearchShardStatistics(); + assertThat(shardStatistics).isNotNull(); + assertThat(shardStatistics.getTotal()).isEqualTo(15); + assertThat(shardStatistics.getSuccessful()).isEqualTo(14); + assertThat(shardStatistics.getSkipped()).isEqualTo(0); + assertThat(shardStatistics.getFailed()).isEqualTo(1); + // assert failure + List failures = shardStatistics.getFailures(); + assertThat(failures.size()).isEqualTo(1); + assertThat(failures).extracting(SearchShardStatistics.Failure::getIndex).containsExactly("test-index"); + assertThat(failures).extracting(SearchShardStatistics.Failure::getElasticsearchErrorCause) + .extracting(ElasticsearchErrorCause::getReason).containsExactly("this is a mock failure in shards"); + assertThat(failures).extracting(SearchShardStatistics.Failure::getElasticsearchErrorCause) + .extracting(ElasticsearchErrorCause::getCausedBy).extracting(ElasticsearchErrorCause::getReason) + .containsExactly("inner reason"); + } } diff --git a/src/test/java/org/springframework/data/elasticsearch/client/util/ScrollStateTest.java b/src/test/java/org/springframework/data/elasticsearch/client/util/ScrollStateTest.java index 5339419a6c..562fc26c45 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/util/ScrollStateTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/util/ScrollStateTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/test/java/org/springframework/data/elasticsearch/client/util/package-info.java b/src/test/java/org/springframework/data/elasticsearch/client/util/package-info.java index 79701328cb..3e0be80923 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/util/package-info.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/util/package-info.java @@ -1,3 +1,2 @@ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.client.util; diff --git a/src/test/java/org/springframework/data/elasticsearch/config/AuditingELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/config/AuditingELCIntegrationTests.java index 29df44ecd4..1b73f5dcee 100644 --- a/src/test/java/org/springframework/data/elasticsearch/config/AuditingELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/config/AuditingELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/config/AuditingIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/config/AuditingIntegrationTests.java index 6958e47dbf..eb27851084 100644 --- a/src/test/java/org/springframework/data/elasticsearch/config/AuditingIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/config/AuditingIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -20,6 +20,7 @@ import java.time.LocalDateTime; import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -37,7 +38,6 @@ import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.mapping.callback.EntityCallbacks; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch diff --git a/src/test/java/org/springframework/data/elasticsearch/config/AuditingReactiveELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/config/AuditingReactiveELCIntegrationTests.java index c7b4084c67..30c514ad66 100644 --- a/src/test/java/org/springframework/data/elasticsearch/config/AuditingReactiveELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/config/AuditingReactiveELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/config/AuditingReactiveIntegrationTest.java b/src/test/java/org/springframework/data/elasticsearch/config/AuditingReactiveIntegrationTest.java index 028a87642a..84a308166d 100644 --- a/src/test/java/org/springframework/data/elasticsearch/config/AuditingReactiveIntegrationTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/config/AuditingReactiveIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -21,6 +21,7 @@ import java.time.LocalDateTime; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -38,7 +39,6 @@ import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch diff --git a/src/test/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingRegistrarUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingRegistrarUnitTests.java index b4a015aa8e..a2e5f3b5a7 100644 --- a/src/test/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingRegistrarUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingRegistrarUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/test/java/org/springframework/data/elasticsearch/config/ElasticsearchConfigurationSupportUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/config/ElasticsearchConfigurationSupportUnitTests.java index ffccf7a265..3470b6a79a 100644 --- a/src/test/java/org/springframework/data/elasticsearch/config/ElasticsearchConfigurationSupportUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/config/ElasticsearchConfigurationSupportUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 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/test/java/org/springframework/data/elasticsearch/config/configuration/ElasticsearchConfigurationELCTests.java b/src/test/java/org/springframework/data/elasticsearch/config/configuration/ElasticsearchConfigurationELCTests.java index bff694c154..973f129b3a 100644 --- a/src/test/java/org/springframework/data/elasticsearch/config/configuration/ElasticsearchConfigurationELCTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/config/configuration/ElasticsearchConfigurationELCTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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. @@ -20,6 +20,7 @@ import co.elastic.clients.elasticsearch.ElasticsearchClient; import org.elasticsearch.client.RestClient; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -31,8 +32,6 @@ import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -50,7 +49,6 @@ public class ElasticsearchConfigurationELCTests { @EnableElasticsearchRepositories(basePackages = { "org.springframework.data.elasticsearch.config.configuration" }, considerNestedRepositories = true) static class Config extends ElasticsearchConfiguration { - @NonNull @Override public ClientConfiguration clientConfiguration() { return ClientConfiguration.builder() // diff --git a/src/test/java/org/springframework/data/elasticsearch/config/configuration/ReactiveElasticsearchConfigurationELCTests.java b/src/test/java/org/springframework/data/elasticsearch/config/configuration/ReactiveElasticsearchConfigurationELCTests.java index 175449d6da..77b290eeca 100644 --- a/src/test/java/org/springframework/data/elasticsearch/config/configuration/ReactiveElasticsearchConfigurationELCTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/config/configuration/ReactiveElasticsearchConfigurationELCTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -17,6 +17,7 @@ import static org.assertj.core.api.Assertions.*; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -29,8 +30,6 @@ import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository; import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -38,6 +37,7 @@ * @author Peter-Josef Meisch * @since 4.4 */ +@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") @ExtendWith(SpringExtension.class) @ContextConfiguration public class ReactiveElasticsearchConfigurationELCTests { @@ -48,7 +48,6 @@ public class ReactiveElasticsearchConfigurationELCTests { considerNestedRepositories = true) static class Config extends ReactiveElasticsearchConfiguration { - @NonNull @Override public ClientConfiguration clientConfiguration() { return ClientConfiguration.builder() // @@ -61,7 +60,6 @@ public ClientConfiguration clientConfiguration() { * using a repository with an entity that is set to createIndex = false as we have no elastic running for this test * and just check that all the necessary beans are created. */ - // @Autowired private WebClient webClient; @Autowired private ReactiveElasticsearchClient reactiveElasticsearchClient; @Autowired private ReactiveElasticsearchOperations reactiveElasticsearchOperations; @Autowired private CreateIndexFalseRepository repository; diff --git a/src/test/java/org/springframework/data/elasticsearch/config/namespace/ElasticsearchNamespaceHandlerTests.java b/src/test/java/org/springframework/data/elasticsearch/config/namespace/ElasticsearchNamespaceHandlerTests.java index c154b88536..0472e4d636 100644 --- a/src/test/java/org/springframework/data/elasticsearch/config/namespace/ElasticsearchNamespaceHandlerTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/config/namespace/ElasticsearchNamespaceHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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/test/java/org/springframework/data/elasticsearch/config/nested/EnableNestedRepositoriesELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/config/nested/EnableNestedRepositoriesELCIntegrationTests.java index e5655dc1c9..ca749ffe21 100644 --- a/src/test/java/org/springframework/data/elasticsearch/config/nested/EnableNestedRepositoriesELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/config/nested/EnableNestedRepositoriesELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/config/nested/EnableNestedRepositoriesIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/config/nested/EnableNestedRepositoriesIntegrationTests.java index 1785a40bf7..76bf50ccfe 100644 --- a/src/test/java/org/springframework/data/elasticsearch/config/nested/EnableNestedRepositoriesIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/config/nested/EnableNestedRepositoriesIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2023 the original author or authors. + * Copyright 2015-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. @@ -18,9 +18,7 @@ import static org.assertj.core.api.Assertions.*; import static org.springframework.data.elasticsearch.annotations.FieldType.*; -import java.lang.Double; -import java.lang.Long; - +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -36,7 +34,6 @@ import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; import org.springframework.data.repository.Repository; -import org.springframework.lang.Nullable; /** * @author Kevin Leturc diff --git a/src/test/java/org/springframework/data/elasticsearch/config/notnested/EnableRepositoriesELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/config/notnested/EnableRepositoriesELCIntegrationTests.java index c9569be983..9a56b59526 100644 --- a/src/test/java/org/springframework/data/elasticsearch/config/notnested/EnableRepositoriesELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/config/notnested/EnableRepositoriesELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/config/notnested/EnableRepositoriesIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/config/notnested/EnableRepositoriesIntegrationTests.java index d228be8b51..8222e2ef82 100644 --- a/src/test/java/org/springframework/data/elasticsearch/config/notnested/EnableRepositoriesIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/config/notnested/EnableRepositoriesIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -18,10 +18,9 @@ import static org.assertj.core.api.Assertions.*; import static org.springframework.data.elasticsearch.annotations.FieldType.*; -import java.lang.Double; -import java.lang.Long; import java.util.UUID; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -42,7 +41,6 @@ import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.data.elasticsearch.utils.IndexNameProvider; import org.springframework.data.repository.Repository; -import org.springframework.lang.Nullable; /** * @author Rizwan Idrees @@ -51,6 +49,7 @@ * @author Gad Akuka * @author Peter-Josef Meisch */ +@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") @SpringIntegrationTest public abstract class EnableRepositoriesIntegrationTests implements ApplicationContextAware { @@ -159,12 +158,11 @@ public void setRate(int rate) { this.rate = rate; } - @Nullable - public java.lang.Double getScriptedRate() { + public java.lang.@Nullable Double getScriptedRate() { return scriptedRate; } - public void setScriptedRate(@Nullable java.lang.Double scriptedRate) { + public void setScriptedRate(java.lang.@Nullable Double scriptedRate) { this.scriptedRate = scriptedRate; } @@ -194,12 +192,11 @@ public void setLocation(@Nullable GeoPoint location) { this.location = location; } - @Nullable - public java.lang.Long getVersion() { + public java.lang.@Nullable Long getVersion() { return version; } - public void setVersion(@Nullable java.lang.Long version) { + public void setVersion(java.lang.@Nullable Long version) { this.version = version; } } @@ -255,12 +252,11 @@ public void setRate(int rate) { this.rate = rate; } - @Nullable - public java.lang.Long getScriptedRate() { + public java.lang.@Nullable Long getScriptedRate() { return scriptedRate; } - public void setScriptedRate(@Nullable java.lang.Long scriptedRate) { + public void setScriptedRate(java.lang.@Nullable Long scriptedRate) { this.scriptedRate = scriptedRate; } @@ -290,12 +286,11 @@ public void setLocation(@Nullable GeoPoint location) { this.location = location; } - @Nullable - public java.lang.Long getVersion() { + public java.lang.@Nullable Long getVersion() { return version; } - public void setVersion(@Nullable java.lang.Long version) { + public void setVersion(java.lang.@Nullable Long version) { this.version = version; } } diff --git a/src/test/java/org/springframework/data/elasticsearch/config/notnested/SampleElasticsearchRepository.java b/src/test/java/org/springframework/data/elasticsearch/config/notnested/SampleElasticsearchRepository.java index 60cd657a50..25bfb7c1f8 100644 --- a/src/test/java/org/springframework/data/elasticsearch/config/notnested/SampleElasticsearchRepository.java +++ b/src/test/java/org/springframework/data/elasticsearch/config/notnested/SampleElasticsearchRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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/test/java/org/springframework/data/elasticsearch/config/notnested/SampleUUIDKeyedElasticsearchRepository.java b/src/test/java/org/springframework/data/elasticsearch/config/notnested/SampleUUIDKeyedElasticsearchRepository.java index a4d33aedc5..ec355e0981 100644 --- a/src/test/java/org/springframework/data/elasticsearch/config/notnested/SampleUUIDKeyedElasticsearchRepository.java +++ b/src/test/java/org/springframework/data/elasticsearch/config/notnested/SampleUUIDKeyedElasticsearchRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 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/test/java/org/springframework/data/elasticsearch/config/notnested/package-info.java b/src/test/java/org/springframework/data/elasticsearch/config/notnested/package-info.java index 55d936ed9f..c32f6a26e6 100644 --- a/src/test/java/org/springframework/data/elasticsearch/config/notnested/package-info.java +++ b/src/test/java/org/springframework/data/elasticsearch/config/notnested/package-info.java @@ -1,3 +1,2 @@ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.config.notnested; diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchELCIntegrationTests.java index 6ed2bd9bcb..826c2e4166 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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. @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Bean; @@ -45,11 +46,9 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.RescorerQuery; import org.springframework.data.elasticsearch.core.query.ScriptData; -import org.springframework.data.elasticsearch.core.query.ScriptType; import org.springframework.data.elasticsearch.core.query.ScriptedField; import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfiguration; /** @@ -183,51 +182,49 @@ protected Query getMatchAllQueryWithIncludesAndInlineExpressionScript(@Nullable return nativeQueryBuilder.withScriptedField(new ScriptedField( // fieldName, // - new ScriptData(ScriptType.INLINE, "expression", script, null, params))) // + new ScriptData( "expression", script, null, params))) // .build(); } @Override protected Query getQueryWithRescorer() { - return NativeQuery.builder() // - .withQuery(q -> q // - .bool(b -> b // - .filter(f -> f.exists(e -> e.field("rate"))) // - .should(s -> s.term(t -> t.field("message").value("message"))) // - )) // - .withRescorerQuery( // - new RescorerQuery(NativeQuery.builder() // - .withQuery(q -> q // - .functionScore(fs -> fs // - .functions(f1 -> f1 // - .filter(matchAllQueryAsQuery()) // - .weight(1.0) // - .gauss(d -> d // - .field("rate") // - .placement(dp -> dp // - .origin(JsonData.of(0)) // - .scale(JsonData.of(10)) // - .decay(0.5)) // - )) // - .functions(f2 -> f2 // - .filter(matchAllQueryAsQuery()).weight(100.0) // - .gauss(d -> d // - .field("rate") // - .placement(dp -> dp // - .origin(JsonData.of(0)) // - .scale(JsonData.of(10)) // - .decay(0.5)) // - - )) // - .scoreMode(FunctionScoreMode.Sum) // - .maxBoost(80.0) // - .boostMode(FunctionBoostMode.Replace)) // - ) // - .build() // - ) // - .withScoreMode(RescorerQuery.ScoreMode.Max) // - .withWindowSize(100)) // + return NativeQuery.builder() + .withQuery(q -> q + .bool(b -> b + .filter(f -> f.exists(e -> e.field("rate"))) + .should(s -> s.term(t -> t.field("message").value("message"))))) + .withRescorerQuery( + new RescorerQuery(NativeQuery.builder() + .withQuery(q -> q + .functionScore(fs -> fs + .functions(f1 -> f1 + .filter(matchAllQueryAsQuery()) + .weight(1.0) + .gauss(d -> d + .untyped(ut -> ut + .field("rate") + .placement(dp -> dp + .origin(JsonData.of(0)) + .scale(JsonData.of(10)) + .decay(0.5))))) + .functions(f2 -> f2 + .filter(matchAllQueryAsQuery()).weight(100.0) + .gauss(d -> d + .untyped(ut -> ut + .field("rate") + .placement(dp -> dp + .origin(JsonData.of(0)) + .scale(JsonData.of(10)) + .decay(0.5))) + + )) + .scoreMode(FunctionScoreMode.Sum) + .maxBoost(80.0) + .boostMode(FunctionBoostMode.Replace))) + .build()) + .withScoreMode(RescorerQuery.ScoreMode.Max) + .withWindowSize(100)) .build(); } } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchIntegrationTests.java index a89fe30fe5..195b11b3d2 100755 --- a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 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. @@ -19,22 +19,18 @@ import static org.assertj.core.api.Assertions.*; import static org.springframework.data.elasticsearch.annotations.Document.VersionType.*; import static org.springframework.data.elasticsearch.annotations.FieldType.*; -import static org.springframework.data.elasticsearch.annotations.FieldType.Integer; import static org.springframework.data.elasticsearch.core.document.Document.*; import static org.springframework.data.elasticsearch.core.query.StringQuery.*; import static org.springframework.data.elasticsearch.utils.IdGenerator.*; import static org.springframework.data.elasticsearch.utils.IndexBuilder.*; -import java.lang.Double; -import java.lang.Integer; -import java.lang.Long; -import java.lang.Object; import java.util.*; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.assertj.core.api.InstanceOfAssertFactories; import org.assertj.core.api.SoftAssertions; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -66,10 +62,11 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.highlight.Highlight; import org.springframework.data.elasticsearch.core.query.highlight.HighlightField; +import org.springframework.data.elasticsearch.core.query.highlight.HighlightFieldParameters; +import org.springframework.data.elasticsearch.core.query.highlight.HighlightParameters; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; import org.springframework.data.util.StreamUtils; -import org.springframework.lang.Nullable; /** * All the integration tests that are not in separate files. @@ -102,6 +99,7 @@ * @author scoobyzhang * @author Hamid Rahimi * @author Illia Ulianov + * @author Mohamed El Harrougui */ @SpringIntegrationTest public abstract class ElasticsearchIntegrationTests { @@ -615,7 +613,8 @@ public void shouldDeleteDocumentForGivenQuery() { // when Query query = getTermQuery("id", documentId); - operations.delete(query, SampleEntity.class, IndexCoordinates.of(indexNameProvider.indexName())); + operations.delete(DeleteQuery.builder(query).build(), SampleEntity.class, + IndexCoordinates.of(indexNameProvider.indexName())); // then Query searchQuery = getTermQuery("id", documentId); @@ -646,7 +645,7 @@ public void shouldDeleteAcrossIndex() { // when Query query = getTermQuery("message", "foo"); - operations.delete(query, SampleEntity.class, IndexCoordinates.of(MULTI_INDEX_ALL)); + operations.delete(DeleteQuery.builder(query).build(), SampleEntity.class, IndexCoordinates.of(MULTI_INDEX_ALL)); // then assertThat(operations.count(query, IndexCoordinates.of(MULTI_INDEX_1_NAME, MULTI_INDEX_2_NAME))).isEqualTo(0); @@ -677,7 +676,7 @@ public void shouldDeleteAcrossIndexWhenNoMatchingDataPresent() { // when Query query = getTermQuery("message", "negative"); - operations.delete(query, SampleEntity.class, IndexCoordinates.of("test-index-*")); + operations.delete(DeleteQuery.builder(query).build(), SampleEntity.class, IndexCoordinates.of("test-index-*")); operations.indexOps(IndexCoordinates.of(MULTI_INDEX_1_NAME)).refresh(); operations.indexOps(IndexCoordinates.of(MULTI_INDEX_2_NAME)).refresh(); @@ -1039,7 +1038,8 @@ public void shouldDeleteGivenCriteriaQuery() { CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria("message").contains("test")); // when - operations.delete(criteriaQuery, SampleEntity.class, IndexCoordinates.of(indexNameProvider.indexName())); + operations.delete(DeleteQuery.builder(criteriaQuery).build(), SampleEntity.class, + IndexCoordinates.of(indexNameProvider.indexName())); // then StringQuery stringQuery = new StringQuery(MATCH_ALL); @@ -1630,12 +1630,13 @@ void shouldDoUpdateByQueryForExistingDocument() { final Query query = operations.matchAllQuery(); final UpdateQuery updateQuery = UpdateQuery.builder(query) - .withScriptType(ScriptType.INLINE) .withScript("ctx._source['message'] = params['newMessage']").withLang("painless") .withParams(Collections.singletonMap("newMessage", messageAfterUpdate)).withAbortOnVersionConflict(true) .build(); - operations.updateByQuery(updateQuery, IndexCoordinates.of(indexNameProvider.indexName())); + var byQueryResponse = operations.updateByQuery(updateQuery, IndexCoordinates.of(indexNameProvider.indexName())); + + assertThat(byQueryResponse.getUpdated()).isEqualTo(1); SampleEntity indexedEntity = operations.get(documentId, SampleEntity.class, IndexCoordinates.of(indexNameProvider.indexName())); @@ -1856,7 +1857,7 @@ public void shouldReturnDocumentAboveMinimalScoreGivenQuery() { protected abstract Query getBoolQueryWithWildcardsFirstMustSecondShouldAndMinScore(String firstField, String firstValue, String secondField, String secondValue, float minScore); - @Test // DATAES-462 + @Test // DATAES-462, #2986 public void shouldReturnScores() { List indexQueries = new ArrayList<>(); @@ -1873,6 +1874,7 @@ public void shouldReturnScores() { IndexCoordinates.of(indexNameProvider.indexName())); assertThat(searchHits.getMaxScore()).isGreaterThan(0f); + assertThat(searchHits.getExecutionDuration().toMillis()).isGreaterThan(0); assertThat(searchHits.getSearchHit(0).getScore()).isGreaterThan(0f); } @@ -2497,7 +2499,8 @@ public void shouldDeleteOnlyDocumentsMatchedByDeleteQuery() { // when Query query = operations.idsQuery(Arrays.asList(documentIdToDelete)); - operations.delete(query, SampleEntity.class, IndexCoordinates.of(indexNameProvider.indexName())); + operations.delete(DeleteQuery.builder(query).build(), SampleEntity.class, + IndexCoordinates.of(indexNameProvider.indexName())); // then // document with id "remainingDocumentId" should still be indexed @@ -2527,7 +2530,8 @@ public void shouldDeleteOnlyDocumentsMatchedByCriteriaQuery() { // when CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria("id").is(documentIdToDelete)); - operations.delete(criteriaQuery, SampleEntity.class, IndexCoordinates.of(indexNameProvider.indexName())); + operations.delete(DeleteQuery.builder(criteriaQuery).build(), SampleEntity.class, + IndexCoordinates.of(indexNameProvider.indexName())); // then // document with id "remainingDocumentId" should still be indexed @@ -2864,17 +2868,29 @@ private List getIndexQueries(List sampleEntities) { } @Document(indexName = MULTI_INDEX_2_NAME) - class ResultAggregator { + static class ResultAggregator { - private String id; - private String firstName; - private String lastName; + private final String id; + private final String firstName; + private final String lastName; ResultAggregator(String id, String firstName, String lastName) { this.id = id; this.firstName = firstName; this.lastName = lastName; } + + public String getId() { + return id; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } } @Test // DATAES-709 @@ -2964,6 +2980,130 @@ void shouldReturnHighlightFieldsInSearchHit() { assertThat(highlightField.get(1)).contains("message"); } + @Test // #2636 + void shouldReturnHighlightFieldsWithHighlightQueryInSearchHit() { + IndexCoordinates index = createIndexCoordinatesWithHighlightMessage(); + + // a highlight query equals to the search query + var sameHighlightQuery = HighlightFieldParameters.builder() + .withHighlightQuery(getBuilderWithTermQuery("message", "message").build()) + .build(); + Query query = getBuilderWithTermQuery("message", "message") // + .withHighlightQuery( + new HighlightQuery(new Highlight(singletonList(new HighlightField("message", sameHighlightQuery))), + HighlightEntity.class)) + .build(); + SearchHits searchHits = operations.search(query, HighlightEntity.class, index); + + assertThat(searchHits).isNotNull(); + assertThat(searchHits.getSearchHits()).hasSize(1); + + SearchHit searchHit = searchHits.getSearchHit(0); + List highlightField = searchHit.getHighlightField("message"); + assertThat(highlightField).hasSize(2); + assertThat(highlightField.get(0)).contains("message"); + assertThat(highlightField.get(1)).contains("message"); + } + + @Test // #2636 + void shouldReturnDifferentHighlightFieldsWithDifferentHighlightQueryInSearchHit() { + IndexCoordinates index = createIndexCoordinatesWithHighlightMessage(); + + // a different highlight query from the search query + var differentHighlightQueryInField = HighlightFieldParameters.builder() + .withHighlightQuery(getBuilderWithTermQuery("message", "initial").build()) + .build(); + // highlight_query in field + Query highlightQueryInField = getBuilderWithTermQuery("message", "message") // + .withHighlightQuery( + new HighlightQuery( + new Highlight(singletonList(new HighlightField("message", differentHighlightQueryInField))), + HighlightEntity.class)) + .build(); + assertThatHighlightFieldsIsDifferentFromHighlightQuery(highlightQueryInField, index); + } + + @Test // #2636 + void shouldReturnDifferentHighlightFieldsWithDifferentParamHighlightQueryInSearchHit() { + IndexCoordinates index = createIndexCoordinatesWithHighlightMessage(); + + // a different highlight query from the search query and used in highlight param rather than field + var differentHighlightQueryInParam = HighlightParameters.builder() + .withHighlightQuery(getBuilderWithTermQuery("message", "initial").build()) + .build(); + // highlight_query in param + Query highlightQueryInParam = getBuilderWithTermQuery("message", "message") // + .withHighlightQuery( + new HighlightQuery( + new Highlight(differentHighlightQueryInParam, singletonList(new HighlightField("message"))), + HighlightEntity.class)) + .build(); + assertThatHighlightFieldsIsDifferentFromHighlightQuery(highlightQueryInParam, index); + } + + @Test // #2636 + void shouldReturnDifferentHighlightFieldsWithDifferentHighlightCriteriaQueryInSearchHit() { + IndexCoordinates index = createIndexCoordinatesWithHighlightMessage(); + // a different highlight query from the search query, written by CriteriaQuery rather than NativeQuery + var criteriaHighlightQueryInParam = HighlightParameters.builder() + .withHighlightQuery(new CriteriaQuery(new Criteria("message").is("initial"))) + .build(); + // highlight_query in param + Query differentHighlightQueryUsingCriteria = getBuilderWithTermQuery("message", "message") // + .withHighlightQuery( + new HighlightQuery( + new Highlight(criteriaHighlightQueryInParam, singletonList(new HighlightField("message"))), + HighlightEntity.class)) + .build(); + assertThatHighlightFieldsIsDifferentFromHighlightQuery(differentHighlightQueryUsingCriteria, index); + } + + @Test // #2636 + void shouldReturnDifferentHighlightFieldsWithDifferentHighlightStringQueryInSearchHit() { + IndexCoordinates index = createIndexCoordinatesWithHighlightMessage(); + // a different highlight query from the search query, written by StringQuery + var stringHighlightQueryInParam = HighlightParameters.builder() + .withHighlightQuery(new StringQuery( + """ + { + "term": { + "message": { + "value": "initial" + } + } + } + """)) + .build(); + // highlight_query in param + Query differentHighlightQueryUsingStringQuery = getBuilderWithTermQuery("message", "message") // + .withHighlightQuery( + new HighlightQuery(new Highlight(stringHighlightQueryInParam, singletonList(new HighlightField("message"))), + HighlightEntity.class)) + .build(); + assertThatHighlightFieldsIsDifferentFromHighlightQuery(differentHighlightQueryUsingStringQuery, index); + } + + private IndexCoordinates createIndexCoordinatesWithHighlightMessage() { + IndexCoordinates index = IndexCoordinates.of("test-index-highlight-entity-template"); + HighlightEntity entity = new HighlightEntity("1", + "This message is a long text which contains the word to search for " + + "in two places, the first being near the beginning and the second near the end of the message. " + + "However, i'll use a different highlight query from the initial search query"); + IndexQuery indexQuery = new IndexQueryBuilder().withId(entity.getId()).withObject(entity).build(); + operations.index(indexQuery, index); + operations.indexOps(index).refresh(); + return index; + } + + private void assertThatHighlightFieldsIsDifferentFromHighlightQuery(Query query, IndexCoordinates index) { + SearchHits searchHits = operations.search(query, HighlightEntity.class, index); + + SearchHit searchHit = searchHits.getSearchHit(0); + List highlightField = searchHit.getHighlightField("message"); + assertThat(highlightField).hasSize(1); + assertThat(highlightField.get(0)).contains("initial"); + } + @Test // #1686 void shouldRunRescoreQueryInSearchQuery() { IndexCoordinates index = IndexCoordinates.of(indexNameProvider.getPrefix() + "rescore-entity"); @@ -3336,7 +3476,7 @@ private void shouldUpdateEntityWithJoinFields(String qId1, String qId2, String a private void shouldDeleteEntityWithJoinFields(String qId2, String aId2) throws Exception { - operations.delete(getQueryForParentId("answer", qId2, qId2), SampleJoinEntity.class, + operations.delete(DeleteQuery.builder(getQueryForParentId("answer", qId2, qId2)).build(), SampleJoinEntity.class, IndexCoordinates.of(indexNameProvider.indexName())); SearchHits deletedHits = operations.search(getQueryForParentId("answer", qId2, null), @@ -3652,6 +3792,106 @@ void shouldThrowVersionConflictExceptionWhenSavingInvalidVersion() { }).isInstanceOf(VersionConflictException.class); } + @Test // GH-2865 + public void shouldDeleteDocumentForGivenQueryUsingParameters() { + // Given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = SampleEntity.builder().id(documentId).message("some message") + .version(System.currentTimeMillis()).build(); + + IndexQuery indexQuery = getIndexQuery(sampleEntity); + String indexName = indexNameProvider.indexName(); + + operations.index(indexQuery, IndexCoordinates.of(indexName)); + + // When + final Query query = getTermQuery("id", documentId); + final DeleteQuery deleteQuery = DeleteQuery.builder(query).withSlices(2).build(); + ByQueryResponse result = operations.delete(deleteQuery, SampleEntity.class, IndexCoordinates.of(indexName)); + + // Then + assertThat(result.getDeleted()).isEqualTo(1); + SearchHits searchHits = operations.search(query, SampleEntity.class, + IndexCoordinates.of(indexName)); + assertThat(searchHits.getTotalHits()).isEqualTo(0); + } + + @Test + public void shouldDeleteDocumentForGivenQueryAndUnavailableIndex() { + // Given + String indexName = UUID.randomUUID().toString(); + + // When + final Query query = operations.matchAllQuery(); + final DeleteQuery deleteQuery = DeleteQuery.builder(query).withIgnoreUnavailable(true).build(); + ByQueryResponse result = operations.delete(deleteQuery, SampleEntity.class, IndexCoordinates.of(indexName)); + + // Then + assertThat(result.getDeleted()).isEqualTo(0); + } + + @Test + public void shouldGetOnlyDocumentsThatHasChild() { + // Given + String indexName = indexNameProvider.indexName() + "-join"; + operations.indexOps(RootEntity.class).createWithMapping(); + + RootEntity parentEntity = RootEntity.builder() + .withId(nextIdAsString()) + .withParent(new RootEntity.Parent()) + .build(); + IndexQuery indexQuery = new IndexQueryBuilder().withId(parentEntity.id).withObject(parentEntity).build(); + operations.index(indexQuery, IndexCoordinates.of(indexName)); + + RootEntity childEntity = RootEntity.builder() + .withId(nextIdAsString()) + .withChild(new RootEntity.Child()) + .withRelation(new JoinField<>("child", parentEntity.id)) + .build(); + indexQuery = new IndexQueryBuilder().withId(childEntity.id).withObject(childEntity).build(); + operations.index(indexQuery, IndexCoordinates.of(indexName)); + + HasChildQuery childQuery = HasChildQuery.builder("child").withQuery(operations.matchAllQuery()).build(); + Query query = CriteriaQuery.builder(Criteria.where("child").hasChild(childQuery)).build(); + + // When + SearchHits hits = operations.search(query, RootEntity.class); + + // Then + assertThat(hits.getTotalHits()).isEqualTo(1); + } + + @Test + public void shouldGetOnlyDocumentsThatHasParent() { + // Given + String indexName = indexNameProvider.indexName() + "-join"; + operations.indexOps(RootEntity.class).createWithMapping(); + + RootEntity parentEntity = RootEntity.builder() + .withId(nextIdAsString()) + .withParent(new RootEntity.Parent()) + .build(); + IndexQuery indexQuery = new IndexQueryBuilder().withId(parentEntity.id).withObject(parentEntity).build(); + operations.index(indexQuery, IndexCoordinates.of(indexName)); + + RootEntity childEntity = RootEntity.builder() + .withId(nextIdAsString()) + .withChild(new RootEntity.Child()) + .withRelation(new JoinField<>("child", parentEntity.id)) + .build(); + indexQuery = new IndexQueryBuilder().withId(childEntity.id).withObject(childEntity).build(); + operations.index(indexQuery, IndexCoordinates.of(indexName)); + + HasParentQuery childQuery = HasParentQuery.builder("parent").withQuery(operations.matchAllQuery()).build(); + Query query = CriteriaQuery.builder(Criteria.where("parent").hasParent(childQuery)).build(); + + // When + SearchHits hits = operations.search(query, RootEntity.class); + + // Then + assertThat(hits.getTotalHits()).isEqualTo(1); + } + // region entities @Document(indexName = "#{@indexNameProvider.indexName()}") @Setting(shards = 1, replicas = 0, refreshInterval = "-1") @@ -3762,12 +4002,11 @@ public void setRate(int rate) { this.rate = rate; } - @Nullable - public java.lang.Double getScriptedRate() { + public java.lang.@Nullable Double getScriptedRate() { return scriptedRate; } - public void setScriptedRate(@Nullable java.lang.Double scriptedRate) { + public void setScriptedRate(java.lang.@Nullable Double scriptedRate) { this.scriptedRate = scriptedRate; } @@ -3788,12 +4027,11 @@ public void setLocation(@Nullable GeoPoint location) { this.location = location; } - @Nullable - public java.lang.Long getVersion() { + public java.lang.@Nullable Long getVersion() { return version; } - public void setVersion(@Nullable java.lang.Long version) { + public void setVersion(java.lang.@Nullable Long version) { this.version = version; } @@ -3922,12 +4160,11 @@ public void setRate(int rate) { this.rate = rate; } - @Nullable - public java.lang.Long getScriptedRate() { + public java.lang.@Nullable Long getScriptedRate() { return scriptedRate; } - public void setScriptedRate(@Nullable java.lang.Long scriptedRate) { + public void setScriptedRate(java.lang.@Nullable Long scriptedRate) { this.scriptedRate = scriptedRate; } @@ -3948,12 +4185,11 @@ public void setLocation(@Nullable GeoPoint location) { this.location = location; } - @Nullable - public java.lang.Long getVersion() { + public java.lang.@Nullable Long getVersion() { return version; } - public void setVersion(@Nullable java.lang.Long version) { + public void setVersion(java.lang.@Nullable Long version) { this.version = version; } } @@ -4099,12 +4335,11 @@ private static class GTEVersionEntity { @Id private String id; @Nullable private String name; - @Nullable - public java.lang.Long getVersion() { + public java.lang.@Nullable Long getVersion() { return version; } - public void setVersion(@Nullable java.lang.Long version) { + public void setVersion(java.lang.@Nullable Long version) { this.version = version; } @@ -4159,12 +4394,11 @@ public void setFirstName(@Nullable String firstName) { this.firstName = firstName; } - @Nullable - public java.lang.Long getVersion() { + public java.lang.@Nullable Long getVersion() { return version; } - public void setVersion(@Nullable java.lang.Long version) { + public void setVersion(java.lang.@Nullable Long version) { this.version = version; } } @@ -4202,12 +4436,11 @@ public void setLastName(@Nullable String lastName) { this.lastName = lastName; } - @Nullable - public java.lang.Long getVersion() { + public java.lang.@Nullable Long getVersion() { return version; } - public void setVersion(@Nullable java.lang.Long version) { + public void setVersion(java.lang.@Nullable Long version) { this.version = version; } } @@ -4292,7 +4525,7 @@ static class SearchHitsEntity { public SearchHitsEntity() {} - public SearchHitsEntity(@Nullable String id, @Nullable java.lang.Long number, @Nullable String keyword) { + public SearchHitsEntity(@Nullable String id, java.lang.@Nullable Long number, @Nullable String keyword) { this.id = id; this.number = number; this.keyword = keyword; @@ -4307,12 +4540,11 @@ public void setId(@Nullable String id) { this.id = id; } - @Nullable - public java.lang.Long getNumber() { + public java.lang.@Nullable Long getNumber() { return number; } - public void setNumber(@Nullable java.lang.Long number) { + public void setNumber(java.lang.@Nullable Long number) { this.number = number; } @@ -4427,12 +4659,11 @@ public void setSeqNoPrimaryTerm(@Nullable SeqNoPrimaryTerm seqNoPrimaryTerm) { this.seqNoPrimaryTerm = seqNoPrimaryTerm; } - @Nullable - public java.lang.Long getVersion() { + public java.lang.@Nullable Long getVersion() { return version; } - public void setVersion(@Nullable java.lang.Long version) { + public void setVersion(java.lang.@Nullable Long version) { this.version = version; } } @@ -4446,7 +4677,7 @@ static class VersionedEntity { public VersionedEntity() {} - public VersionedEntity(@Nullable String id, @Nullable java.lang.Long version) { + public VersionedEntity(@Nullable String id, java.lang.@Nullable Long version) { this.id = id; this.version = version; } @@ -4460,12 +4691,11 @@ public void setId(@Nullable String id) { this.id = id; } - @Nullable - public java.lang.Long getVersion() { + public java.lang.@Nullable Long getVersion() { return version; } - public void setVersion(@Nullable java.lang.Long version) { + public void setVersion(java.lang.@Nullable Long version) { this.version = version; } } @@ -4583,7 +4813,7 @@ public static final class ImmutableWithScriptedEntity { @Nullable @ScriptedField private final Double scriptedRate; - public ImmutableWithScriptedEntity(String id, int rate, @Nullable java.lang.Double scriptedRate) { + public ImmutableWithScriptedEntity(String id, int rate, java.lang.@Nullable Double scriptedRate) { this.id = id; this.rate = rate; this.scriptedRate = scriptedRate; @@ -4773,5 +5003,106 @@ public void setIndexedIndexName(@Nullable String indexedIndexName) { this.indexedIndexName = indexedIndexName; } } + + @Document(indexName = "#{@indexNameProvider.indexName()}-join") + private static class RootEntity { + @Id private String id; + + @Field(type = FieldType.Object) private Child child; + + @Field(type = FieldType.Object) private Parent parent; + + @JoinTypeRelations(relations = { + @JoinTypeRelation(parent = "parent", children = { "child" }) + }) private JoinField relation = new JoinField<>("parent"); + + private static final class Child {} + + private static final class Parent {} + + public static Builder builder() { + return new Builder(); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Child getChild() { + return child; + } + + public void setChild(@Nullable Child child) { + this.child = child; + } + + public Parent getParent() { + return parent; + } + + public void setParent(@Nullable Parent parent) { + this.parent = parent; + } + + public JoinField getRelation() { + if (relation == null) { + relation = new JoinField<>("parent"); + } + + return relation; + } + + public void setRelation(JoinField relation) { + this.relation = relation; + } + + public static final class Builder { + @Nullable private String id; + + @Nullable private Parent parent; + @Nullable private Child child; + private JoinField relation = new JoinField<>("parent"); + + private Builder() {} + + public Builder withId(@Nullable String id) { + this.id = id; + + return this; + } + + public Builder withParent(@Nullable Parent parent) { + this.parent = parent; + + return this; + } + + public Builder withChild(@Nullable Child child) { + this.child = child; + + return this; + } + + public Builder withRelation(JoinField relation) { + this.relation = relation; + + return this; + } + + public RootEntity build() { + RootEntity root = new RootEntity(); + root.setId(id); + root.setParent(parent); + root.setChild(child); + root.setRelation(relation); + + return root; + } + } + } // endregion } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/EntityOperationsUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/EntityOperationsUnitTests.java index 78fcebe2c5..c85621b8bb 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/EntityOperationsUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/EntityOperationsUnitTests.java @@ -21,6 +21,7 @@ import java.util.HashSet; import org.assertj.core.api.SoftAssertions; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -39,7 +40,6 @@ import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm; import org.springframework.data.elasticsearch.core.routing.DefaultRoutingResolver; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch @@ -291,7 +291,7 @@ public void setIndexName(@Nullable String indexName) { } @Document(indexName = "entity-operations-test") - static record EntityFromRecord( + record EntityFromRecord( @Id @Nullable String id, @Field(type = FieldType.Text) @Nullable String text, @Version @Nullable Long version, diff --git a/src/test/java/org/springframework/data/elasticsearch/core/IndexCoordinatesUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/IndexCoordinatesUnitTests.java index 662493cb67..5a404f19e0 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/IndexCoordinatesUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/IndexCoordinatesUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -23,11 +23,12 @@ /** * @author Peter-Josef Meisch */ +@SuppressWarnings("DataFlowIssue") class IndexCoordinatesUnitTests { @Test void cannotBeInitializedWithNullIndexName() { - assertThatThrownBy(() -> IndexCoordinates.of(null)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> IndexCoordinates.of((String) null)).isInstanceOf(IllegalArgumentException.class); } @Test diff --git a/src/test/java/org/springframework/data/elasticsearch/core/InnerHitsELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/InnerHitsELCIntegrationTests.java index 82e27ecb83..3d28a95c11 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/InnerHitsELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/InnerHitsELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/test/java/org/springframework/data/elasticsearch/core/InnerHitsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/InnerHitsIntegrationTests.java index 50a9dede02..6dcb12cb7e 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/InnerHitsIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/InnerHitsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -23,6 +23,7 @@ import java.util.List; import org.assertj.core.api.SoftAssertions; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -36,7 +37,6 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * Testing the querying and parsing of inner_hits. diff --git a/src/test/java/org/springframework/data/elasticsearch/core/LogEntityELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/LogEntityELCIntegrationTests.java index 78223049de..3f9bd74b69 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/LogEntityELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/LogEntityELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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. @@ -54,12 +54,13 @@ Query termQueryForIp(String ip) { @Override Query rangeQueryForIp(String from, String to) { - return NativeQuery.builder() // - .withQuery(qb -> qb // - .range(rqb -> rqb // - .field("ip") // - .gte(JsonData.of(from))// - .lte(JsonData.of(to))// - )).build(); + return NativeQuery.builder() + .withQuery(qb -> qb + .range(rqb -> rqb + .untyped(ut -> ut + .field("ip") + .gte(JsonData.of(from)) + .lte(JsonData.of(to))))) + .build(); } } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/LogEntityIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/LogEntityIntegrationTests.java index df006a05ec..c7c7c463cf 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/LogEntityIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/LogEntityIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 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. @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.Date; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -37,7 +38,6 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * LogEntityIntegrationTests @@ -137,8 +137,7 @@ static class LogEntity { @Nullable private long sequenceCode; @Nullable @Field(type = Ip) private String ip; - @Nullable - @Field(type = Date, format = DateFormat.date_time) private java.util.Date date; + @Field(type = Date, format = DateFormat.date_time) private java.util.@Nullable Date date; private LogEntity() {} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/MappingContextBaseTests.java b/src/test/java/org/springframework/data/elasticsearch/core/MappingContextBaseTests.java index 9ddf9f78c5..fa54bf2c4b 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/MappingContextBaseTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/MappingContextBaseTests.java @@ -1,4 +1,4 @@ -/* Copyright 2019-2023 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/test/java/org/springframework/data/elasticsearch/core/PointInTimeELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/PointInTimeELCIntegrationTests.java index f36913c3cc..ed1ccf162c 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/PointInTimeELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/PointInTimeELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/core/PointInTimeIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/PointInTimeIntegrationTests.java index 2cba61f150..4f640f8432 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/PointInTimeIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/PointInTimeIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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,6 +19,7 @@ import java.time.Duration; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -35,7 +36,6 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchELCIntegrationTests.java index b25e5e9861..4e465de688 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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. @@ -28,6 +28,7 @@ import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Bean; @@ -42,7 +43,6 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchTemplateConfiguration; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfiguration; /** diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchIntegrationTests.java index 0ceefea94d..7090ea5073 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 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,10 +25,6 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import java.lang.Boolean; -import java.lang.Integer; -import java.lang.Long; -import java.lang.Object; import java.time.Duration; import java.time.LocalDate; import java.time.format.DateTimeFormatter; @@ -46,6 +42,7 @@ import org.assertj.core.api.InstanceOfAssertFactories; import org.json.JSONException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -78,7 +75,6 @@ import org.springframework.data.elasticsearch.core.query.*; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -608,7 +604,7 @@ public void deleteByQueryShouldReturnZeroWhenIndexDoesNotExist() { CriteriaQuery query = new CriteriaQuery(new Criteria("message").contains("test")); - operations.delete(query, SampleEntity.class) // + operations.delete(DeleteQuery.builder(query).build(), SampleEntity.class) // .as(StepVerifier::create) // .consumeNextWith(byQueryResponse -> assertThat(byQueryResponse.getDeleted()).isEqualTo(0L)).verifyComplete(); } @@ -631,7 +627,7 @@ public void shouldDeleteAcrossIndex() { Query query = getBuilderWithTermQuery("message", "test").build(); - operations.delete(query, SampleEntity.class, IndexCoordinates.of(indexPrefix + '*')) // + operations.delete(DeleteQuery.builder(query).build(), SampleEntity.class, IndexCoordinates.of(indexPrefix + '*')) // .map(ByQueryResponse::getDeleted) // .as(StepVerifier::create) // .expectNext(2L) // @@ -658,7 +654,7 @@ public void shouldDeleteAcrossIndexWhenNoMatchingDataPresent() { Query query = getBuilderWithTermQuery("message", "negative").build(); - operations.delete(query, SampleEntity.class, IndexCoordinates.of(indexPrefix + '*')) // + operations.delete(DeleteQuery.builder(query).build(), SampleEntity.class, IndexCoordinates.of(indexPrefix + '*')) // .map(ByQueryResponse::getDeleted) // .as(StepVerifier::create) // .expectNext(0L) // @@ -674,7 +670,7 @@ public void deleteByQueryShouldReturnNumberOfDeletedDocuments() { CriteriaQuery query = new CriteriaQuery(new Criteria("message").contains("test")); - operations.delete(query, SampleEntity.class) // + operations.delete(DeleteQuery.builder(query).build(), SampleEntity.class) // .map(ByQueryResponse::getDeleted) // .as(StepVerifier::create) // .expectNext(2L) // @@ -688,7 +684,7 @@ public void deleteByQueryShouldReturnZeroIfNothingDeleted() { CriteriaQuery query = new CriteriaQuery(new Criteria("message").contains("luke")); - operations.delete(query, SampleEntity.class) // + operations.delete(DeleteQuery.builder(query).build(), SampleEntity.class) // .map(ByQueryResponse::getDeleted) // .as(StepVerifier::create) // .expectNext(0L) // @@ -1114,6 +1110,7 @@ void shouldReturnInformationListOfAllIndices() { try { JSONAssert.assertEquals(expectedMappings, indexInformation.getMapping().toJson(), false); } catch (JSONException e) { + // noinspection CallToPrintStackTrace e.printStackTrace(); } }).verifyComplete(); @@ -1338,12 +1335,11 @@ public void setRate(int rate) { this.rate = rate; } - @Nullable - public java.lang.Long getVersion() { + public java.lang.@Nullable Long getVersion() { return version; } - public void setVersion(@Nullable java.lang.Long version) { + public void setVersion(java.lang.@Nullable Long version) { this.version = version; } @@ -1451,12 +1447,11 @@ public void setSeqNoPrimaryTerm(@Nullable SeqNoPrimaryTerm seqNoPrimaryTerm) { this.seqNoPrimaryTerm = seqNoPrimaryTerm; } - @Nullable - public java.lang.Long getVersion() { + public java.lang.@Nullable Long getVersion() { return version; } - public void setVersion(@Nullable java.lang.Long version) { + public void setVersion(java.lang.@Nullable Long version) { this.version = version; } } @@ -1477,12 +1472,11 @@ public void setId(@Nullable String id) { this.id = id; } - @Nullable - public java.lang.Long getVersion() { + public java.lang.@Nullable Long getVersion() { return version; } - public void setVersion(@Nullable java.lang.Long version) { + public void setVersion(java.lang.@Nullable Long version) { this.version = version; } } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ReactivePointInTimeELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ReactivePointInTimeELCIntegrationTests.java index b898ae4961..4cb9366e44 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ReactivePointInTimeELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ReactivePointInTimeELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/core/ReactivePointInTimeIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ReactivePointInTimeIntegrationTests.java index 12cf3d0a4e..e8163f8999 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ReactivePointInTimeIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ReactivePointInTimeIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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. @@ -21,6 +21,7 @@ import java.time.Duration; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -37,7 +38,6 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ReactiveReindexELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveReindexELCIntegrationTests.java index e92d0a0822..8c1843082a 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ReactiveReindexELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveReindexELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/core/ReactiveReindexIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveReindexIntegrationTests.java index 82433cf776..43e5019363 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ReactiveReindexIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveReindexIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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. @@ -23,6 +23,7 @@ import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -35,7 +36,6 @@ import org.springframework.data.elasticsearch.core.reindex.ReindexRequest; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * x * Note: the imperative version of these tests have more details and test methods, but they test that the request is diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ReactiveResourceUtilTest.java b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveResourceUtilTest.java index 250a43f63e..79c574b3d9 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ReactiveResourceUtilTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveResourceUtilTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/test/java/org/springframework/data/elasticsearch/core/ReactiveSearchTemplateELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveSearchTemplateELCIntegrationTests.java index f0b49c1eb5..d049cf2919 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ReactiveSearchTemplateELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveSearchTemplateELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/core/ReactiveSearchTemplateIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveSearchTemplateIntegrationTests.java index ed3beceed2..f9f143d7d0 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ReactiveSearchTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveSearchTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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. @@ -25,6 +25,7 @@ import java.util.Map; import org.json.JSONException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -40,7 +41,6 @@ import org.springframework.data.elasticsearch.core.script.Script; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * Integration tests for the point in time API. @@ -67,7 +67,7 @@ public abstract class ReactiveSearchTemplateIntegrationTests { "size": 100 } """; - private Script script = Script.builder() // + private final Script script = Script.builder() // .withId("testScript") // .withLanguage("mustache") // .withSource(SCRIPT) // diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ReindexELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ReindexELCIntegrationTests.java index 91d69ec2c2..3d5264ea8d 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ReindexELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ReindexELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/core/ReindexIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ReindexIntegrationTests.java index 92633654d3..2e77781087 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ReindexIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ReindexIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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.regex.Pattern; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -34,7 +35,6 @@ import org.springframework.data.elasticsearch.core.reindex.ReindexResponse; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch diff --git a/src/test/java/org/springframework/data/elasticsearch/core/SearchAsYouTypeELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/SearchAsYouTypeELCIntegrationTests.java index 3e3b31ce9a..af84914655 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/SearchAsYouTypeELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/SearchAsYouTypeELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/core/SearchAsYouTypeIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/SearchAsYouTypeIntegrationTests.java index 6591fd1632..b463ecaab9 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/SearchAsYouTypeIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/SearchAsYouTypeIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -22,6 +22,7 @@ import java.util.Objects; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -35,7 +36,6 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Aleksei Arsenev diff --git a/src/test/java/org/springframework/data/elasticsearch/core/SearchHitSupportTest.java b/src/test/java/org/springframework/data/elasticsearch/core/SearchHitSupportTest.java index fcbef6b9a3..df0034470b 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/SearchHitSupportTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/SearchHitSupportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; @@ -32,6 +33,8 @@ /** * @author Roman Puchkovskiy * @author Peter-Josef Meisch + * @author Haibo Liu + * @author Mohamed El Harrougui */ class SearchHitSupportTest { @@ -64,8 +67,8 @@ void shouldReturnTheSameListInstanceInSearchHitsAndGetContent() { hits.add(new SearchHit<>(null, null, null, 0, null, null, null, null, null, null, "four")); hits.add(new SearchHit<>(null, null, null, 0, null, null, null, null, null, null, "five")); - SearchHits originalSearchHits = new SearchHitsImpl<>(hits.size(), TotalHitsRelation.EQUAL_TO, 0, "scroll", - null, hits, null, null); + SearchHits originalSearchHits = new SearchHitsImpl<>(hits.size(), TotalHitsRelation.EQUAL_TO, 0, + Duration.ofMillis(1), "scroll", null, hits, null, null, null); SearchPage searchPage = SearchHitSupport.searchPageFor(originalSearchHits, PageRequest.of(0, 3)); SearchHits searchHits = searchPage.getSearchHits(); @@ -88,6 +91,11 @@ public float getMaxScore() { return 0; } + @Override + public Duration getExecutionDuration() { + return Duration.ofMillis(1); + } + @Override public long getTotalHits() { return 2; diff --git a/src/test/java/org/springframework/data/elasticsearch/core/SearchTemplateELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/SearchTemplateELCIntegrationTests.java index a0a980207c..0843de8adb 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/SearchTemplateELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/SearchTemplateELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/core/SearchTemplateIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/SearchTemplateIntegrationTests.java index 0284ee2a4f..e22c6e3b4f 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/SearchTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/SearchTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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. @@ -18,9 +18,11 @@ import static org.assertj.core.api.Assertions.*; import static org.skyscreamer.jsonassert.JSONAssert.*; +import java.util.List; import java.util.Map; import org.json.JSONException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -36,17 +38,17 @@ import org.springframework.data.elasticsearch.core.script.Script; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * Integration tests search template API. * * @author Peter-Josef Meisch + * @author Haibo Liu */ @SpringIntegrationTest public abstract class SearchTemplateIntegrationTests { - private static final String SCRIPT = """ + private static final String SEARCH_FIRSTNAME = """ { "query": { "bool": { @@ -63,21 +65,57 @@ public abstract class SearchTemplateIntegrationTests { "size": 100 } """; - private Script script = Script.builder() // - .withId("testScript") // + + private static final String SEARCH_LASTNAME = """ + { + "query": { + "bool": { + "must": [ + { + "match": { + "lastName": "{{lastName}}" + } + } + ] + } + }, + "from": 0, + "size": 100 + } + """; + + private static final Script SCRIPT_SEARCH_FIRSTNAME = Script.builder() // + .withId("searchFirstName") // + .withLanguage("mustache") // + .withSource(SEARCH_FIRSTNAME) // + .build(); + + private static final Script SCRIPT_SEARCH_LASTNAME = Script.builder() // + .withId("searchLastName") // .withLanguage("mustache") // - .withSource(SCRIPT) // + .withSource(SEARCH_LASTNAME) // .build(); @Autowired ElasticsearchOperations operations; @Autowired IndexNameProvider indexNameProvider; - @Nullable IndexOperations indexOperations; + IndexOperations personIndexOperations, studentIndexOperations; @BeforeEach void setUp() { indexNameProvider.increment(); - indexOperations = operations.indexOps(Person.class); - indexOperations.createWithMapping(); + personIndexOperations = operations.indexOps(Person.class); + personIndexOperations.createWithMapping(); + studentIndexOperations = operations.indexOps(Student.class); + studentIndexOperations.createWithMapping(); + + operations.save( // + new Person("1", "John", "Smith"), // + new Person("2", "Willy", "Smith"), // + new Person("3", "John", "Myers")); + + operations.save( + new Student("1", "Joey", "Dunlop"), // + new Student("2", "Michael", "Dunlop")); } @Test @@ -89,41 +127,35 @@ void cleanup() { @Test // #1891 @DisplayName("should store, retrieve and delete template script") void shouldStoreAndRetrieveAndDeleteTemplateScript() throws JSONException { - // we do all in this test because scripts aren't stored in an index but in the cluster and we need to clenaup. - var success = operations.putScript(script); + var success = operations.putScript(SCRIPT_SEARCH_FIRSTNAME); assertThat(success).isTrue(); - var savedScript = operations.getScript(script.id()); + var savedScript = operations.getScript(SCRIPT_SEARCH_FIRSTNAME.id()); assertThat(savedScript).isNotNull(); - assertThat(savedScript.id()).isEqualTo(script.id()); - assertThat(savedScript.language()).isEqualTo(script.language()); - assertEquals(savedScript.source(), script.source(), false); + assertThat(savedScript.id()).isEqualTo(SCRIPT_SEARCH_FIRSTNAME.id()); + assertThat(savedScript.language()).isEqualTo(SCRIPT_SEARCH_FIRSTNAME.language()); + assertEquals(savedScript.source(), SCRIPT_SEARCH_FIRSTNAME.source(), false); - success = operations.deleteScript(script.id()); + success = operations.deleteScript(SCRIPT_SEARCH_FIRSTNAME.id()); assertThat(success).isTrue(); - savedScript = operations.getScript(script.id()); + savedScript = operations.getScript(SCRIPT_SEARCH_FIRSTNAME.id()); assertThat(savedScript).isNull(); - assertThatThrownBy(() -> operations.deleteScript(script.id())) // + assertThatThrownBy(() -> operations.deleteScript(SCRIPT_SEARCH_FIRSTNAME.id())) // .isInstanceOf(ResourceNotFoundException.class); } @Test // #1891 @DisplayName("should search with template") void shouldSearchWithTemplate() { - - var success = operations.putScript(script); + var success = operations.putScript(SCRIPT_SEARCH_FIRSTNAME); assertThat(success).isTrue(); - operations.save( // - new Person("1", "John", "Smith"), // - new Person("2", "Willy", "Smith"), // - new Person("3", "John", "Myers")); var query = SearchTemplateQuery.builder() // - .withId(script.id()) // + .withId(SCRIPT_SEARCH_FIRSTNAME.id()) // .withParams(Map.of("firstName", "John")) // .build(); @@ -131,15 +163,169 @@ void shouldSearchWithTemplate() { assertThat(searchHits.getTotalHits()).isEqualTo(2); - success = operations.deleteScript(script.id()); + success = operations.deleteScript(SCRIPT_SEARCH_FIRSTNAME.id()); + assertThat(success).isTrue(); + } + + @Test // #2704 + @DisplayName("should search with template multisearch") + void shouldSearchWithTemplateMultiSearch() { + var success = operations.putScript(SCRIPT_SEARCH_FIRSTNAME); + assertThat(success).isTrue(); + + var q1 = SearchTemplateQuery.builder() // + .withId(SCRIPT_SEARCH_FIRSTNAME.id()) // + .withParams(Map.of("firstName", "John")) // + .build(); + var q2 = SearchTemplateQuery.builder() // + .withId(SCRIPT_SEARCH_FIRSTNAME.id()) // + .withParams(Map.of("firstName", "Willy")) // + .build(); + + var multiSearchHits = operations.multiSearch(List.of(q1, q2), Person.class); + + assertThat(multiSearchHits.size()).isEqualTo(2); + assertThat(multiSearchHits.get(0).getTotalHits()).isEqualTo(2); + assertThat(multiSearchHits.get(1).getTotalHits()).isEqualTo(1); + + assertThat(multiSearchHits.get(0).getSearchHits()) + .extracting(SearchHit::getContent) + .extracting(Person::lastName) + .contains("Smith", "Myers"); + assertThat(multiSearchHits.get(1).getSearchHits()) + .extracting(SearchHit::getContent) + .extracting(Person::lastName) + .containsExactly("Smith"); + + success = operations.deleteScript(SCRIPT_SEARCH_FIRSTNAME.id()); assertThat(success).isTrue(); } - @Document(indexName = "#{@indexNameProvider.indexName()}") + @Test // #2704 + @DisplayName("should search with template multisearch including different scripts") + void shouldSearchWithTemplateMultiSearchIncludingDifferentScripts() { + assertThat(operations.putScript(SCRIPT_SEARCH_FIRSTNAME)).isTrue(); + assertThat(operations.putScript(SCRIPT_SEARCH_LASTNAME)).isTrue(); + + var q1 = SearchTemplateQuery.builder() // + .withId(SCRIPT_SEARCH_FIRSTNAME.id()) // + .withParams(Map.of("firstName", "John")) // + .build(); + var q2 = SearchTemplateQuery.builder() // + .withId(SCRIPT_SEARCH_LASTNAME.id()) // + .withParams(Map.of("lastName", "smith")) // + .build(); + + var multiSearchHits = operations.multiSearch(List.of(q1, q2), Person.class); + + assertThat(multiSearchHits.size()).isEqualTo(2); + assertThat(multiSearchHits.get(0).getTotalHits()).isEqualTo(2); + assertThat(multiSearchHits.get(1).getTotalHits()).isEqualTo(2); + + assertThat(multiSearchHits.get(0).getSearchHits()) + .extracting(SearchHit::getContent) + .extracting(Person::lastName) + .contains("Smith", "Myers"); + assertThat(multiSearchHits.get(1).getSearchHits()) + .extracting(SearchHit::getContent) + .extracting(Person::firstName) + .contains("John", "Willy"); + + assertThat(operations.deleteScript(SCRIPT_SEARCH_FIRSTNAME.id())).isTrue(); + assertThat(operations.deleteScript(SCRIPT_SEARCH_LASTNAME.id())).isTrue(); + } + + @Test // #2704 + @DisplayName("should search with template multisearch with multiple classes") + void shouldSearchWithTemplateMultiSearchWithMultipleClasses() { + assertThat(operations.putScript(SCRIPT_SEARCH_FIRSTNAME)).isTrue(); + assertThat(operations.putScript(SCRIPT_SEARCH_LASTNAME)).isTrue(); + + var q1 = SearchTemplateQuery.builder() // + .withId(SCRIPT_SEARCH_FIRSTNAME.id()) // + .withParams(Map.of("firstName", "John")) // + .build(); + var q2 = SearchTemplateQuery.builder() // + .withId(SCRIPT_SEARCH_FIRSTNAME.id()) // + .withParams(Map.of("firstName", "Joey")) // + .build(); + + // search with multiple classes + var multiSearchHits = operations.multiSearch(List.of(q1, q2), List.of(Person.class, Student.class)); + + assertThat(multiSearchHits.size()).isEqualTo(2); + assertThat(multiSearchHits.get(0).getTotalHits()).isEqualTo(2); + assertThat(multiSearchHits.get(1).getTotalHits()).isEqualTo(1); + + assertThat(multiSearchHits.get(0).getSearchHits()) + // type casting is needed here + .extracting(hits -> (Person) hits.getContent()) + .extracting(Person::lastName) + .contains("Smith", "Myers"); + assertThat(multiSearchHits.get(1).getSearchHits()) + // type casting is needed here + .extracting(hits -> (Student) hits.getContent()) + .extracting(Student::lastName) + .containsExactly("Dunlop"); + + assertThat(operations.deleteScript(SCRIPT_SEARCH_FIRSTNAME.id())).isTrue(); + assertThat(operations.deleteScript(SCRIPT_SEARCH_LASTNAME.id())).isTrue(); + } + + @Test // #2704 + @DisplayName("should search with template multisearch with multiple index coordinates") + void shouldSearchWithTemplateMultiSearchWithMultipleIndexCoordinates() { + assertThat(operations.putScript(SCRIPT_SEARCH_FIRSTNAME)).isTrue(); + assertThat(operations.putScript(SCRIPT_SEARCH_LASTNAME)).isTrue(); + + var q1 = SearchTemplateQuery.builder() // + .withId(SCRIPT_SEARCH_FIRSTNAME.id()) // + .withParams(Map.of("firstName", "John")) // + .build(); + var q2 = SearchTemplateQuery.builder() // + .withId(SCRIPT_SEARCH_LASTNAME.id()) // + .withParams(Map.of("lastName", "Dunlop")) // + .build(); + + // search with multiple index coordinates + var multiSearchHits = operations.multiSearch( + List.of(q1, q2), + List.of(Person.class, Student.class), + List.of(IndexCoordinates.of(indexNameProvider.indexName() + "-person"), + IndexCoordinates.of(indexNameProvider.indexName() + "-student"))); + + assertThat(multiSearchHits.size()).isEqualTo(2); + assertThat(multiSearchHits.get(0).getTotalHits()).isEqualTo(2); + assertThat(multiSearchHits.get(1).getTotalHits()).isEqualTo(2); + + assertThat(multiSearchHits.get(0).getSearchHits()) + // type casting is needed here + .extracting(hits -> (Person) hits.getContent()) + .extracting(Person::lastName) + .contains("Smith", "Myers"); + assertThat(multiSearchHits.get(1).getSearchHits()) + // type casting is needed here + .extracting(hits -> (Student) hits.getContent()) + .extracting(Student::firstName) + .contains("Joey", "Michael"); + + assertThat(operations.deleteScript(SCRIPT_SEARCH_FIRSTNAME.id())).isTrue(); + assertThat(operations.deleteScript(SCRIPT_SEARCH_LASTNAME.id())).isTrue(); + } + + @Document(indexName = "#{@indexNameProvider.indexName()}-person") record Person( // @Nullable @Id String id, // @Field(type = FieldType.Text) String firstName, // @Field(type = FieldType.Text) String lastName // ) { } + + @Document(indexName = "#{@indexNameProvider.indexName()}-student") + record Student( // + @Nullable @Id String id, // + @Field(type = FieldType.Text) String firstName, // + @Field(type = FieldType.Text) String lastName // + ) { + } } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/SourceFilterELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/SourceFilterELCIntegrationTests.java index ec940509ab..1084b0d02a 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/SourceFilterELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/SourceFilterELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/core/SourceFilterIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/SourceFilterIntegrationTests.java index d0c405fe57..f08bc810a0 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/SourceFilterIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/SourceFilterIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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. @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -30,12 +31,12 @@ import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.data.elasticsearch.core.query.FetchSourceFilter; import org.springframework.data.elasticsearch.core.query.FetchSourceFilterBuilder; import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.SourceFilter; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch @@ -186,6 +187,38 @@ public String[] getExcludes() { assertThat(entity.getField3()).isNull(); } + @Test // #3009 + @DisplayName("should not return any fields when source is set to false") + void shouldNotReturnAnyFieldsWhenSourceIsSetToFalse() { + + Query query = Query.findAll(); + query.addSourceFilter(FetchSourceFilter.of(b -> b.withFetchSource(false))); + + SearchHits entities = operations.search(query, Entity.class); + + assertThat(entities).hasSize(1); + Entity entity = entities.getSearchHit(0).getContent(); + assertThat(entity.getField1()).isNull(); + assertThat(entity.getField2()).isNull(); + assertThat(entity.getField3()).isNull(); + } + + @Test // #3009 + @DisplayName("should return all fields when source is set to true") + void shouldReturnAllFieldsWhenSourceIsSetToTrue() { + + Query query = Query.findAll(); + query.addSourceFilter(FetchSourceFilter.of(b -> b.withFetchSource(true))); + + SearchHits entities = operations.search(query, Entity.class); + + assertThat(entities).hasSize(1); + Entity entity = entities.getSearchHit(0).getContent(); + assertThat(entity.getField1()).isNotNull(); + assertThat(entity.getField2()).isNotNull(); + assertThat(entity.getField3()).isNotNull(); + } + @Document(indexName = "#{@indexNameProvider.indexName()}") public static class Entity { @Nullable diff --git a/src/test/java/org/springframework/data/elasticsearch/core/StreamQueriesTest.java b/src/test/java/org/springframework/data/elasticsearch/core/StreamQueriesTest.java index 7bcbd93cf2..f543566116 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/StreamQueriesTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/StreamQueriesTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -17,6 +17,7 @@ import static org.assertj.core.api.Assertions.*; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -31,6 +32,8 @@ /** * @author Sascha Woo * @author Peter-Josef Meisch + * @author Haibo Liu + * @author Mohamed El Harrougui */ public class StreamQueriesTest { @@ -180,6 +183,7 @@ void shouldOnlyReturnRequestedCount() { } private SearchScrollHits newSearchScrollHits(List> hits, String scrollId) { - return new SearchHitsImpl<>(hits.size(), TotalHitsRelation.EQUAL_TO, 0, scrollId, null, hits, null, null); + return new SearchHitsImpl<>(hits.size(), TotalHitsRelation.EQUAL_TO, 0, Duration.ofMillis(1), scrollId, null, hits, + null, null, null); } } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/aggregation/AggregationELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/aggregation/AggregationELCIntegrationTests.java index 420d943cd1..84020c194e 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/aggregation/AggregationELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/aggregation/AggregationELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/test/java/org/springframework/data/elasticsearch/core/aggregation/AggregationIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/aggregation/AggregationIntegrationTests.java index 2d201192cd..106eb31233 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/aggregation/AggregationIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/aggregation/AggregationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -17,12 +17,11 @@ import static org.assertj.core.api.Assertions.*; import static org.springframework.data.elasticsearch.annotations.FieldType.*; -import static org.springframework.data.elasticsearch.annotations.FieldType.Integer; -import java.lang.Integer; import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -41,7 +40,6 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Rizwan Idrees diff --git a/src/test/java/org/springframework/data/elasticsearch/core/cluster/ClusterOperationsELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/cluster/ClusterOperationsELCIntegrationTests.java index 68734f62ad..3fdc0709fd 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/cluster/ClusterOperationsELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/cluster/ClusterOperationsELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/core/cluster/ClusterOperationsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/cluster/ClusterOperationsIntegrationTests.java index acc2c5ded1..ddf586108a 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/cluster/ClusterOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/cluster/ClusterOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/core/cluster/ClusterOperationsReactiveELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/cluster/ClusterOperationsReactiveELCIntegrationTests.java index f69f9eeb77..f4e839f021 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/cluster/ClusterOperationsReactiveELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/cluster/ClusterOperationsReactiveELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/core/cluster/ClusterOperationsReactiveIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/cluster/ClusterOperationsReactiveIntegrationTests.java index a189302e1c..2644dbfe78 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/cluster/ClusterOperationsReactiveIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/cluster/ClusterOperationsReactiveIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchCustomConversionsUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchCustomConversionsUnitTests.java index a21f1ef0da..7b584e661d 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchCustomConversionsUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchCustomConversionsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -26,8 +26,8 @@ */ class ElasticsearchCustomConversionsUnitTests { - private byte[] bytes = new byte[] { 0x01, 0x02, 0x03, 0x04 }; - private String base64 = "AQIDBA=="; + private final byte[] bytes = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + private final String base64 = "AQIDBA=="; @Test void shouldConvertFromByteArrayToBase64() { diff --git a/src/test/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverterUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverterUnitTests.java index 300d5e3fae..7880e619c0 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverterUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverterUnitTests.java @@ -27,7 +27,7 @@ */ class ElasticsearchDateConverterUnitTests { - private ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Europe/Berlin")); + private final ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Europe/Berlin")); @ParameterizedTest // DATAES-716 @EnumSource(DateFormat.class) diff --git a/src/test/java/org/springframework/data/elasticsearch/core/convert/GeoConvertersUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/convert/GeoConvertersUnitTests.java index ef5a193649..c251ed4cad 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/convert/GeoConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/convert/GeoConvertersUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/test/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverterUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverterUnitTests.java index 87dbbd15f8..659e461d9b 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverterUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverterUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -30,6 +30,7 @@ import org.intellij.lang.annotations.Language; import org.json.JSONException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -49,8 +50,10 @@ import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; import org.springframework.data.elasticsearch.annotations.GeoPointField; +import org.springframework.data.elasticsearch.annotations.ScriptedField; import org.springframework.data.elasticsearch.annotations.ValueConverter; import org.springframework.data.elasticsearch.core.document.Document; +import org.springframework.data.elasticsearch.core.document.SearchDocumentAdapter; import org.springframework.data.elasticsearch.core.geo.GeoJsonEntity; import org.springframework.data.elasticsearch.core.geo.GeoJsonGeometryCollection; import org.springframework.data.elasticsearch.core.geo.GeoJsonLineString; @@ -71,7 +74,6 @@ import org.springframework.data.geo.Circle; import org.springframework.data.geo.Point; import org.springframework.data.geo.Polygon; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -83,6 +85,7 @@ * @author Konrad Kurdej * @author Roman Puchkovskiy * @author Sascha Woo + * @author llosimura */ public class MappingElasticsearchConverterUnitTests { @@ -228,6 +231,13 @@ public void init() { "org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverterUnitTests$Notification"); } + private Map writeToMap(Object source) { + + Document sink = Document.create(); + mappingElasticsearchConverter.write(source, sink); + return sink; + } + @Test public void shouldFailToInitializeGivenMappingContextIsNull() { @@ -993,7 +1003,7 @@ class RangeTests { "nullRange": null, "integerRangeList": [ { - "gte": "2", + "gte": "2", "lte": "5" } ] @@ -1178,11 +1188,13 @@ public List> getIntegerRangeList() { public void setIntegerRangeList(List> integerRangeList) { this.integerRangeList = integerRangeList; } + } } @Nested class GeoJsonUnitTests { + private GeoJsonEntity entity; @BeforeEach @@ -1476,6 +1488,7 @@ void shouldReadGeoJsonProperties() { assertThat(entity).isEqualTo(mapped); } + } @Test // #1454 @@ -1697,7 +1710,6 @@ void shouldReadASingleObjectIntoAListProperty() { var entity = mappingElasticsearchConverter.read(EntityWithCollections.class, source); assertThat(entity.getChildrenList()).hasSize(1); - // noinspection ConstantConditions assertThat(entity.getChildrenList().get(0).getName()).isEqualTo("child"); } @@ -1717,13 +1729,12 @@ void shouldReadAnObjectArrayIntoAListProperty() { } ] } - """; + """; Document source = Document.parse(json); var entity = mappingElasticsearchConverter.read(EntityWithCollections.class, source); assertThat(entity.getChildrenList()).hasSize(2); - // noinspection ConstantConditions assertThat(entity.getChildrenList().get(0).getName()).isEqualTo("child1"); assertThat(entity.getChildrenList().get(1).getName()).isEqualTo("child2"); } @@ -1745,7 +1756,6 @@ void shouldReadASingleObjectIntoASetProperty() { var entity = mappingElasticsearchConverter.read(EntityWithCollections.class, source); assertThat(entity.getChildrenSet()).hasSize(1); - // noinspection ConstantConditions assertThat(entity.getChildrenSet().iterator().next().getName()).isEqualTo("child"); } @@ -1771,7 +1781,6 @@ void shouldReadAnObjectArrayIntoASetProperty() { var entity = mappingElasticsearchConverter.read(EntityWithCollections.class, source); assertThat(entity.getChildrenSet()).hasSize(2); - // noinspection ConstantConditions List names = entity.getChildrenSet().stream().map(EntityWithCollections.Child::getName) .collect(Collectors.toList()); assertThat(names).containsExactlyInAnyOrder("child1", "child2"); @@ -1794,6 +1803,66 @@ void shouldReadASingleStringIntoAListPropertyImmutable() { assertThat(entity.getStringList()).containsExactly("foo"); } + @Test + void shouldPopulateScriptedFields() { + SearchDocumentAdapter document = new SearchDocumentAdapter(Document.create(), + 0.0f, + new Object[] {}, + Map.of( + "scriptedField", List.of("scriptedField"), + "custom-name-scripted-field", List.of("custom-name-scripted-field")), + emptyMap(), + emptyMap(), + null, + null, + null, + null); + // Create a SearchDocument instance + var entity = mappingElasticsearchConverter.read(ScriptedEntity.class, document); + assertThat(entity.customScriptedField).isEqualTo("custom-name-scripted-field"); + assertThat(entity.scriptedField).isEqualTo("scriptedField"); + } + + static class ScriptedEntity { + @ScriptedField private String scriptedField; + @ScriptedField(name = "custom-name-scripted-field") String customScriptedField; + + ScriptedEntity() { + customScriptedField = ""; + scriptedField = ""; + } + + public String getScriptedField() { + return scriptedField; + } + + public void setScriptedField(String scriptedField) { + this.scriptedField = scriptedField; + } + + public String getCustomScriptedField() { + return customScriptedField; + } + + public void setCustomScriptedField(String customScriptedField) { + this.customScriptedField = customScriptedField; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) + return false; + ScriptedEntity that = (ScriptedEntity) o; + return Objects.equals(scriptedField, that.scriptedField) + && Objects.equals(customScriptedField, that.customScriptedField); + } + + @Override + public int hashCode() { + return Objects.hash(scriptedField, customScriptedField); + } + } + @Test // #2280 @DisplayName("should read a String array into a List property immutable") void shouldReadAStringArrayIntoAListPropertyImmutable() { @@ -1862,7 +1931,6 @@ void shouldReadASingleObjectIntoAListPropertyImmutable() { var entity = mappingElasticsearchConverter.read(ImmutableEntityWithCollections.class, source); assertThat(entity.getChildrenList()).hasSize(1); - // noinspection ConstantConditions assertThat(entity.getChildrenList().get(0).getName()).isEqualTo("child"); } @@ -1882,13 +1950,12 @@ void shouldReadAnObjectArrayIntoAListPropertyImmutable() { } ] } - """; + """; Document source = Document.parse(json); var entity = mappingElasticsearchConverter.read(ImmutableEntityWithCollections.class, source); assertThat(entity.getChildrenList()).hasSize(2); - // noinspection ConstantConditions assertThat(entity.getChildrenList().get(0).getName()).isEqualTo("child1"); assertThat(entity.getChildrenList().get(1).getName()).isEqualTo("child2"); } @@ -1910,7 +1977,6 @@ void shouldReadASingleObjectIntoASetPropertyImmutable() { var entity = mappingElasticsearchConverter.read(ImmutableEntityWithCollections.class, source); assertThat(entity.getChildrenSet()).hasSize(1); - // noinspection ConstantConditions assertThat(entity.getChildrenSet().iterator().next().getName()).isEqualTo("child"); } @@ -1936,19 +2002,11 @@ void shouldReadAnObjectArrayIntoASetPropertyImmutable() { var entity = mappingElasticsearchConverter.read(ImmutableEntityWithCollections.class, source); assertThat(entity.getChildrenSet()).hasSize(2); - // noinspection ConstantConditions List names = entity.getChildrenSet().stream().map(ImmutableEntityWithCollections.Child::getName) .collect(Collectors.toList()); assertThat(names).containsExactlyInAnyOrder("child1", "child2"); } - private Map writeToMap(Object source) { - - Document sink = Document.create(); - mappingElasticsearchConverter.write(source, sink); - return sink; - } - @Test // #2364 @DisplayName("should not write id property to document source if configured so") void shouldNotWriteIdPropertyToDocumentSourceIfConfiguredSo() throws JSONException { @@ -2078,6 +2136,25 @@ void shouldMapPropertyPathToFieldNames() { assertThat(mappedNames).isEqualTo("level-one.level-two.key-word"); } + @Test // #2879 + @DisplayName("should throw MappingConversionException with document id on reading error") + void shouldThrowMappingConversionExceptionWithDocumentIdOnReadingError() { + + @Language("JSON") + String json = """ + { + "birth-date": "this-is-not-a-local-date" + }"""; + + Document document = Document.parse(json); + document.setId("42"); + + assertThatThrownBy(() -> { + mappingElasticsearchConverter.read(Person.class, document); + }).isInstanceOf(MappingConversionException.class).hasFieldOrPropertyWithValue("documentId", "42") + .hasCauseInstanceOf(ConversionException.class); + } + // region entities public static class Sample { @Nullable public @ReadOnlyProperty String readOnly; @@ -2316,7 +2393,7 @@ enum Gender { MAN("1"), MACHINE("0"); - String theValue; + final String theValue; Gender(String theValue) { this.theValue = theValue; @@ -2951,16 +3028,16 @@ public void setName(String name) { private static final class ImmutableEntityWithCollections { @Field(type = FieldType.Keyword) - @Nullable private List stringList; + @Nullable private final List stringList; @Field(type = FieldType.Keyword) - @Nullable private Set stringSet; + @Nullable private final Set stringSet; @Field(type = FieldType.Object) - @Nullable private List childrenList; + @Nullable private final List childrenList; @Field(type = FieldType.Object) - @Nullable private Set childrenSet; + @Nullable private final Set childrenSet; public ImmutableEntityWithCollections(@Nullable List stringList, @Nullable Set stringSet, @Nullable List childrenList, @Nullable Set childrenSet) { @@ -2993,7 +3070,7 @@ public Set getChildrenSet() { public static class Child { @Field(type = FieldType.Keyword) - @Nullable private String name; + @Nullable private final String name; public Child(@Nullable String name) { this.name = name; @@ -3232,6 +3309,6 @@ private static String reverse(Object o) { Assert.notNull(o, "o must not be null"); - return new StringBuilder().append(o.toString()).reverse().toString(); + return new StringBuilder().append(o).reverse().toString(); } } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/convert/PropertyValueConvertersUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/convert/PropertyValueConvertersUnitTests.java index d1cc4f0d61..9465a1bd49 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/convert/PropertyValueConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/convert/PropertyValueConvertersUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,7 @@ import java.util.List; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Named; import org.junit.jupiter.params.ParameterizedTest; @@ -33,7 +34,6 @@ import org.springframework.data.elasticsearch.core.mapping.PropertyValueConverter; import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchPersistentEntity; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch diff --git a/src/test/java/org/springframework/data/elasticsearch/core/event/AuditingEntityCallbackTests.java b/src/test/java/org/springframework/data/elasticsearch/core/event/AuditingEntityCallbackTests.java index 151e9064f1..d95c342321 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/event/AuditingEntityCallbackTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/event/AuditingEntityCallbackTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -21,6 +21,7 @@ import java.time.LocalDateTime; import java.util.Objects; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -34,7 +35,6 @@ import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; import org.springframework.data.mapping.context.PersistentEntities; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch diff --git a/src/test/java/org/springframework/data/elasticsearch/core/event/CallbackELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/event/CallbackELCIntegrationTests.java index a164d6d6fb..614331306f 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/event/CallbackELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/event/CallbackELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/test/java/org/springframework/data/elasticsearch/core/event/CallbackIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/event/CallbackIntegrationTests.java index 34a48629ea..86ae92305e 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/event/CallbackIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/event/CallbackIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -41,7 +42,6 @@ import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; /** @@ -240,7 +240,7 @@ static class SampleEntity { @Id private String id; @Nullable private String text; -// @ReadOnlyProperty + // @ReadOnlyProperty @Nullable private String className; @Nullable diff --git a/src/test/java/org/springframework/data/elasticsearch/core/event/ReactiveAuditingEntityCallbackTests.java b/src/test/java/org/springframework/data/elasticsearch/core/event/ReactiveAuditingEntityCallbackTests.java index 11ab5d8f87..aafc41293f 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/event/ReactiveAuditingEntityCallbackTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/event/ReactiveAuditingEntityCallbackTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -24,6 +24,7 @@ import java.time.LocalDateTime; import java.util.Objects; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -37,7 +38,6 @@ import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; import org.springframework.data.mapping.context.PersistentEntities; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch diff --git a/src/test/java/org/springframework/data/elasticsearch/core/event/ReactiveCallbackELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/event/ReactiveCallbackELCIntegrationTests.java index 7ea5b1de80..3dddd26a58 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/event/ReactiveCallbackELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/event/ReactiveCallbackELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/core/event/ReactiveCallbackIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/event/ReactiveCallbackIntegrationTests.java index 199908bd78..de896111cb 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/event/ReactiveCallbackIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/event/ReactiveCallbackIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -21,6 +21,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -33,7 +34,6 @@ import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; /** diff --git a/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoELCIntegrationTests.java index 74221d9a99..38990853b3 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/test/java/org/springframework/data/elasticsearch/core/geo/GeoIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoIntegrationTests.java index 1135f1b8e4..786afe8e74 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -20,15 +20,14 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledIf; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.GeoPointField; -import org.springframework.data.elasticsearch.core.AbstractElasticsearchTemplate; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.SearchHit; import org.springframework.data.elasticsearch.core.SearchHits; @@ -41,7 +40,6 @@ import org.springframework.data.elasticsearch.utils.IndexNameProvider; import org.springframework.data.elasticsearch.utils.geohash.Geohash; import org.springframework.data.geo.Point; -import org.springframework.lang.Nullable; /** * @author Rizwan Idrees @@ -391,7 +389,7 @@ public void setLocation(@Nullable GeoPoint location) { static class AuthorMarkerEntityBuilder { - private AuthorMarkerEntity result; + private final AuthorMarkerEntity result; public AuthorMarkerEntityBuilder(String id) { result = new AuthorMarkerEntity(id); diff --git a/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonELCIntegrationTests.java index 0480efc363..e1476a43bd 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonEntity.java b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonEntity.java index 3cfc2aa8a9..b18e02ed63 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonEntity.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -17,10 +17,10 @@ import java.util.Objects; +import org.jspecify.annotations.Nullable; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.geo.Point; -import org.springframework.lang.Nullable; /** * this class contains each GeoJson type as explicit type and as GeoJson interface. Used by several test classes diff --git a/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonGeometryCollectionUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonGeometryCollectionUnitTests.java index 75f0893aa7..da2dcb3e44 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonGeometryCollectionUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonGeometryCollectionUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonIntegrationTests.java index 8385427ddf..633aa97aa9 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -19,6 +19,7 @@ import java.util.Arrays; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -35,7 +36,6 @@ import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; import org.springframework.data.geo.Point; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch diff --git a/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonLineStringUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonLineStringUnitTests.java index cee8394b4f..6eed5b7ced 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonLineStringUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonLineStringUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiLineStringUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiLineStringUnitTests.java index 9c0df0cd35..2f00301768 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiLineStringUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiLineStringUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiPointUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiPointUnitTests.java index e3ae508b30..2ea05ddbf7 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiPointUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiPointUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiPolygonUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiPolygonUnitTests.java index ec634d39d6..bfa7949e2f 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiPolygonUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonMultiPolygonUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonPointUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonPointUnitTests.java index 1712b3faa8..36d47c47c5 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonPointUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/geo/GeoJsonPointUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/test/java/org/springframework/data/elasticsearch/core/index/IndexOperationsELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/IndexOperationsELCIntegrationTests.java index 01d5ba2931..9f4f64319d 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/IndexOperationsELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/IndexOperationsELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/core/index/IndexOperationsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/IndexOperationsIntegrationTests.java index 0ef0358150..0431ec73a6 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/IndexOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/IndexOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,14 +15,18 @@ */ package org.springframework.data.elasticsearch.core.index; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.elasticsearch.annotations.FieldType.*; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import org.assertj.core.api.InstanceOfAssertFactories; import org.assertj.core.api.SoftAssertions; import org.json.JSONException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -30,16 +34,20 @@ import org.skyscreamer.jsonassert.JSONAssert; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Alias; import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.Filter; import org.springframework.data.elasticsearch.annotations.Mapping; import org.springframework.data.elasticsearch.annotations.Setting; +import org.springframework.data.elasticsearch.client.elc.Queries; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.IndexInformation; import org.springframework.data.elasticsearch.core.IndexOperations; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.data.elasticsearch.core.query.StringQuery; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch @@ -63,7 +71,7 @@ void setUp() { @Test @Order(java.lang.Integer.MAX_VALUE) void cleanup() { - operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete(); + operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + '*')).delete(); } @Test // #1646, #1718 @@ -171,6 +179,29 @@ void shouldReturnAliasDataWithGetAliasesForIndexMethod() { softly.assertAll(); } + @Test + void shouldCreateIndexWithAliases() { + // Given + indexNameProvider.increment(); + String indexName = indexNameProvider.indexName(); + indexOperations = operations.indexOps(EntityWithAliases.class); + indexOperations.createWithMapping(); + + // When + Map> aliases = indexOperations.getAliasesForIndex(indexName); + + // Then + AliasData result = aliases.values().stream().findFirst().orElse(new HashSet<>()).stream().findFirst().orElse(null); + assertThat(result).isNotNull(); + assertThat(result.getAlias()).isEqualTo("first_alias"); + assertThat(result.getFilterQuery()).asInstanceOf(InstanceOfAssertFactories.type(StringQuery.class)) + .extracting(StringQuery::getSource) + .asString() + .contains(Queries.wrapperQuery(""" + {"bool" : {"must" : {"term" : {"type" : "abc"}}}} + """).query()); + } + @Document(indexName = "#{@indexNameProvider.indexName()}") @Setting(settingPath = "settings/test-settings.json") @Mapping(mappingPath = "mappings/test-mappings.json") @@ -186,4 +217,33 @@ public void setId(@Nullable String id) { this.id = id; } } + + @Document(indexName = "#{@indexNameProvider.indexName()}", aliases = { + @Alias(value = "first_alias", filter = @Filter(""" + {"bool" : {"must" : {"term" : {"type" : "abc"}}}} + """)) + }) + private static class EntityWithAliases { + @Nullable private @Id String id; + @Nullable + @Field(type = Text) private String type; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + } } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/IndexTemplateELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/IndexTemplateELCIntegrationTests.java index 9ef028bc10..6476d5fc15 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/IndexTemplateELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/IndexTemplateELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/core/index/IndexTemplateIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/IndexTemplateIntegrationTests.java index fd9ad924d8..8d73d60790 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/IndexTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/IndexTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -23,6 +23,7 @@ import java.util.UUID; import org.json.JSONException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -38,7 +39,6 @@ import org.springframework.data.elasticsearch.core.query.CriteriaQuery; import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch @@ -151,7 +151,7 @@ void shouldGetComponentTemplate() throws JSONException { void shouldDeleteComponentTemplate() { IndexOperations indexOps = operations.indexOps(IndexCoordinates.of("dont-care")); - String templateName = "template" + UUID.randomUUID().toString(); + String templateName = "template" + UUID.randomUUID(); var putComponentTemplateRequest = PutComponentTemplateRequest.builder() // .withName(templateName) // .withTemplateData(ComponentTemplateRequestData.builder() // @@ -293,7 +293,7 @@ void shouldPutGetAndDeleteIndexTemplateOfComponents() { @Test // DATAES-612 void shouldReturnNullOnNonExistingGetTemplate() { - String templateName = "template" + UUID.randomUUID().toString(); + String templateName = "template" + UUID.randomUUID(); IndexOperations indexOps = operations.indexOps(IndexCoordinates.of("dont-care")); GetTemplateRequest getTemplateRequest = new GetTemplateRequest(templateName); @@ -349,7 +349,7 @@ void shouldGetTemplate() throws JSONException { void shouldCheckExists() { IndexOperations indexOps = operations.indexOps(IndexCoordinates.of("dont-care")); - String templateName = "template" + UUID.randomUUID().toString(); + String templateName = "template" + UUID.randomUUID(); ExistsTemplateRequest existsTemplateRequest = new ExistsTemplateRequest(templateName); boolean exists = indexOps.existsTemplate(existsTemplateRequest); @@ -372,7 +372,7 @@ void shouldDeleteTemplate() { IndexOperations indexOps = operations.indexOps(IndexCoordinates.of("dont-care")); - String templateName = "template" + UUID.randomUUID().toString(); + String templateName = "template" + UUID.randomUUID(); ExistsTemplateRequest existsTemplateRequest = new ExistsTemplateRequest(templateName); PutTemplateRequest putTemplateRequest = PutTemplateRequest.builder(templateName, "log-*") // diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderELCIntegrationTests.java index 5304d2c0ff..e717afb7ea 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderIntegrationTests.java index 080098ca07..c65867fe4d 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -18,10 +18,7 @@ import static org.assertj.core.api.Assertions.*; import static org.springframework.data.elasticsearch.annotations.FieldType.*; -import static org.springframework.data.elasticsearch.annotations.FieldType.Object; -import java.lang.Integer; -import java.lang.Object; import java.time.Instant; import java.time.LocalDate; import java.util.Collection; @@ -33,6 +30,7 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -46,7 +44,6 @@ import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Stuart Stevenson @@ -61,6 +58,8 @@ * @author Roman Puchkovskiy * @author Brian Kimmig * @author Morgan Lutz + * @author Haibo Liu + * @author Andriy Redko */ @SpringIntegrationTest public abstract class MappingBuilderIntegrationTests extends MappingContextBaseTests { @@ -79,6 +78,12 @@ void cleanup() { operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete(); } + @Test + public void shouldSupportAllTypes() { + IndexOperations indexOperations = operations.indexOps(EntityWithAllTypes.class); + indexOperations.createWithMapping(); + } + @Test public void shouldNotFailOnCircularReference() { @@ -269,6 +274,12 @@ void shouldWriteCorrectMappingForDenseVectorProperty() { operations.indexOps(SimilarityEntity.class).createWithMapping(); } + @Test // #2845 + @DisplayName("should write mapping with field aliases") + void shouldWriteMappingWithFieldAliases() { + operations.indexOps(FieldAliasEntity.class).createWithMapping(); + } + // region Entities @Document(indexName = "#{@indexNameProvider.indexName()}") static class Book { @@ -513,7 +524,7 @@ static class User { @Nullable @Id private String id; - @Field(type = FieldType.Nested, ignoreFields = { "users" }) private Set groups = new HashSet<>(); + @Field(type = FieldType.Nested, ignoreFields = { "users" }) private final Set groups = new HashSet<>(); } @Document(indexName = "#{@indexNameProvider.indexName()}") @@ -522,7 +533,7 @@ static class Group { @Nullable @Id String id; - @Field(type = FieldType.Nested, ignoreFields = { "groups" }) private Set users = new HashSet<>(); + @Field(type = FieldType.Nested, ignoreFields = { "groups" }) private final Set users = new HashSet<>(); } @Document(indexName = "#{@indexNameProvider.indexName()}") @@ -700,12 +711,11 @@ public void setText(@Nullable String text) { this.text = text; } - @Nullable - public java.lang.Object getObject() { + public java.lang.@Nullable Object getObject() { return object; } - public void setObject(@Nullable java.lang.Object object) { + public void setObject(java.lang.@Nullable Object object) { this.object = object; } } @@ -905,7 +915,21 @@ static class SimilarityEntity { @Nullable @Id private String id; - @Field(type = FieldType.Dense_Vector, dims = 42, similarity = "cosine") private double[] denseVector; + @Field(type = FieldType.Dense_Vector, dims = 42, knnSimilarity = KnnSimilarity.COSINE) private double[] denseVector; + } + + @Mapping(aliases = { + @MappingAlias(name = "someAlly", path = "someText"), + @MappingAlias(name = "otherAlly", path = "otherText") + }) + @Document(indexName = "#{@indexNameProvider.indexName()}") + private static class FieldAliasEntity { + @Id + @Nullable private String id; + @Nullable + @Field(type = Text) private String someText; + @Nullable + @Field(type = Text) private String otherText; } // endregion diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java index 6823fa2f43..d7a1e30a84 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -19,12 +19,7 @@ import static org.assertj.core.api.Assertions.*; import static org.skyscreamer.jsonassert.JSONAssert.*; import static org.springframework.data.elasticsearch.annotations.FieldType.*; -import static org.springframework.data.elasticsearch.annotations.FieldType.Object; -import java.lang.Boolean; -import java.lang.Double; -import java.lang.Integer; -import java.lang.Object; import java.math.BigDecimal; import java.time.Instant; import java.time.LocalDate; @@ -36,6 +31,7 @@ import java.util.Map; import org.json.JSONException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; @@ -52,7 +48,6 @@ import org.springframework.data.geo.Point; import org.springframework.data.geo.Polygon; import org.springframework.data.mapping.MappingException; -import org.springframework.lang.Nullable; /** * @author Stuart Stevenson @@ -67,6 +62,8 @@ * @author Roman Puchkovskiy * @author Brian Kimmig * @author Morgan Lutz + * @author Haibo Liu + * @author Andriy Redko */ public class MappingBuilderUnitTests extends MappingContextBaseTests { @@ -84,7 +81,7 @@ public void testInfiniteLoopAvoidance() throws JSONException { } } } - """; + """; String mapping = getMappingBuilder().buildPropertyMapping(SampleTransientEntity.class); @@ -127,7 +124,7 @@ public void shouldBuildMappingWithSuperclass() throws JSONException { } } } - """; + """; String mapping = getMappingBuilder().buildPropertyMapping(SampleInheritedEntity.class); @@ -262,7 +259,7 @@ public void shouldUseFieldNameOnMapping() throws JSONException { } } } - """; + """; String mapping = getMappingBuilder().buildPropertyMapping(FieldNameEntity.MappingEntity.class); @@ -700,6 +697,32 @@ void shouldWriteDenseVectorProperties() throws JSONException { assertEquals(expected, mapping, false); } + @Test + @DisplayName("should write dense_vector properties for knn search") + void shouldWriteDenseVectorPropertiesWithKnnSearch() throws JSONException { + String expected = """ + { + "properties":{ + "my_vector":{ + "type":"dense_vector", + "dims":16, + "element_type":"float", + "similarity":"dot_product", + "index_options":{ + "type":"hnsw", + "m":16, + "ef_construction":100 + } + } + } + } + """; + + String mapping = getMappingBuilder().buildPropertyMapping(DenseVectorEntityWithKnnSearch.class); + + assertEquals(expected, mapping, false); + } + @Test // #1370 @DisplayName("should not write mapping when enabled is false on entity") void shouldNotWriteMappingWhenEnabledIsFalseOnEntity() throws JSONException { @@ -746,6 +769,14 @@ void shouldOnlyAllowDisabledPropertiesOnTypeObject() { .isInstanceOf(MappingException.class); } + @Test + @DisplayName("should match confidence interval parameter for dense_vector type") + void shouldMatchConfidenceIntervalParameterForDenseVectorType() { + + assertThatThrownBy(() -> getMappingBuilder().buildPropertyMapping(DenseVectorMisMatchConfidenceIntervalClass.class)) + .isInstanceOf(IllegalArgumentException.class); + } + @Test // #1711 @DisplayName("should write typeHint entries") void shouldWriteTypeHintEntries() throws JSONException { @@ -1095,38 +1126,49 @@ void shouldAddFieldsThatAreExcludedFromSource() throws JSONException { String expected = """ { - "properties": { - "_class": { - "type": "keyword", - "index": false, - "doc_values": false - }, - "excluded-date": { - "type": "date", - "format": "date" - }, - "nestedEntity": { - "type": "nested", - "properties": { - "_class": { - "type": "keyword", - "index": false, - "doc_values": false - }, - "excluded-text": { - "type": "text" - } - } - } - }, - "_source": { - "excludes": [ - "excluded-date", - "nestedEntity.excluded-text" - ] - } - } - """; // + "properties": { + "_class": { + "type": "keyword", + "index": false, + "doc_values": false + }, + "excluded-date": { + "type": "date", + "format": "date" + }, + "nestedEntity": { + "type": "nested", + "properties": { + "_class": { + "type": "keyword", + "index": false, + "doc_values": false + }, + "excluded-text": { + "type": "text" + } + } + }, + "excluded-multifield": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + }, + "_source": { + "excludes": [ + "excluded-date", + "nestedEntity.excluded-text", + "excluded-multifield" + ] + } + + } + + """; // String mapping = getMappingBuilder().buildPropertyMapping(ExcludedFieldEntity.class); @@ -1179,6 +1221,124 @@ void shouldUseCustomNameWithDots() throws JSONException { assertEquals(expected, mapping, true); } + @Test // #2845 + @DisplayName("should write field aliases to the mapping") + void shouldWriteFieldAliasesToTheMapping() throws JSONException { + + var expected = """ + { + "properties": { + "_class": { + "type": "keyword", + "index": false, + "doc_values": false + }, + "someText": { + "type": "text" + }, + "otherText": { + "type": "text" + }, + "someAlly": { + "type": "alias", + "path": "someText" + }, + "otherAlly": { + "type": "alias", + "path": "otherText" + } + } + } + """; + String mapping = getMappingBuilder().buildPropertyMapping(FieldAliasEntity.class); + + assertEquals(expected, mapping, true); + } + + @Test // #2942 + @DisplayName("should use custom mapped name") + void shouldUseCustomMappedName() throws JSONException { + + var expected = """ + { + "properties": { + "_class": { + "type": "keyword", + "index": false, + "doc_values": false + }, + "someText": { + "type": "match_only_text" + } + } + } + """; + String mapping = getMappingBuilder().buildPropertyMapping(FieldMappedNameEntity.class); + + assertEquals(expected, mapping, true); + } + + @Test // #2942 + @DisplayName("should use custom mapped name for multifield") + void shouldUseCustomMappedNameMultiField() throws JSONException { + + var expected = """ + { + "properties": { + "_class": { + "type": "keyword", + "index": false, + "doc_values": false + }, + "description": { + "type": "match_only_text", + "fields": { + "lower_case": { + "type": "constant_keyword", + "normalizer": "lower_case_normalizer" + } + } + } + } + } + """; + String mapping = getMappingBuilder().buildPropertyMapping(MultiFieldMappedNameEntity.class); + + assertEquals(expected, mapping, true); + } + + @Test // #2952 + void shouldMapNullityParameters() throws JSONException { + // Given + String expected = """ + { + "properties": { + "_class": { + "type": "keyword", + "index": false, + "doc_values": false + }, + "empty-field": { + "type": "keyword", + "null_value": "EMPTY", + "fields": { + "suffix": { + "type": "keyword", + "null_value": "EMPTY_TEXT" + } + } + } + } + } + """; + + // When + String result = getMappingBuilder().buildPropertyMapping(MultiFieldWithNullEmptyParameters.class); + + // Then + assertEquals(expected, result, true); + } + // region entities @Document(indexName = "ignore-above-index") @@ -1298,8 +1458,8 @@ static class MultiFieldEntity { @Nullable @MultiField(mainField = @Field(name = "alternate-description", type = FieldType.Text, analyzer = "whitespace"), - otherFields = { - @InnerField(suffix = "suff-ix", type = FieldType.Text, analyzer = "stop", searchAnalyzer = "standard") }) // + otherFields = { + @InnerField(suffix = "suff-ix", type = FieldType.Text, analyzer = "stop", searchAnalyzer = "standard") }) // public String getAlternateDescription() { return alternateDescription; } @@ -1662,7 +1822,7 @@ static class GeoEntity { @GeoPointField private String pointC; @Nullable @GeoPointField private double[] pointD; - // geo shape, until e have the classes for this, us a strng + @Nullable @GeoShapeField private String shape1; @Nullable @@ -1981,21 +2141,19 @@ public void setId(@Nullable String id) { this.id = id; } - @Nullable - public java.lang.Integer getPageRank() { + public java.lang.@Nullable Integer getPageRank() { return pageRank; } - public void setPageRank(@Nullable java.lang.Integer pageRank) { + public void setPageRank(java.lang.@Nullable Integer pageRank) { this.pageRank = pageRank; } - @Nullable - public java.lang.Integer getUrlLength() { + public java.lang.@Nullable Integer getUrlLength() { return urlLength; } - public void setUrlLength(@Nullable java.lang.Integer urlLength) { + public void setUrlLength(java.lang.@Nullable Integer urlLength) { this.urlLength = urlLength; } @@ -2035,6 +2193,35 @@ public void setMy_vector(@Nullable float[] my_vector) { } } + @SuppressWarnings("unused") + static class DenseVectorEntityWithKnnSearch { + @Nullable + @Id private String id; + + @Nullable + @Field(type = FieldType.Dense_Vector, dims = 16, elementType = FieldElementType.FLOAT, + knnIndexOptions = @KnnIndexOptions(type = KnnAlgorithmType.HNSW, m = 16, efConstruction = 100), + knnSimilarity = KnnSimilarity.DOT_PRODUCT) private float[] my_vector; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public float[] getMy_vector() { + return my_vector; + } + + public void setMy_vector(@Nullable float[] my_vector) { + this.my_vector = my_vector; + } + } + @Mapping(enabled = false) static class DisabledMappingEntity { @Nullable @@ -2087,6 +2274,12 @@ public void setText(@Nullable String text) { } } + static class DenseVectorMisMatchConfidenceIntervalClass { + @Field(type = Dense_Vector, dims = 16, elementType = FieldElementType.FLOAT, + knnIndexOptions = @KnnIndexOptions(type = KnnAlgorithmType.HNSW, m = 16, confidenceInterval = 0.95F), + knnSimilarity = KnnSimilarity.DOT_PRODUCT) private float[] dense_vector; + } + static class DisabledMappingProperty { @Nullable @Id private String id; @@ -2114,12 +2307,11 @@ public void setText(@Nullable String text) { this.text = text; } - @Nullable - public java.lang.Object getObject() { + public java.lang.@Nullable Object getObject() { return object; } - public void setObject(@Nullable java.lang.Object object) { + public void setObject(java.lang.@Nullable Object object) { this.object = object; } } @@ -2367,6 +2559,10 @@ private static class ExcludedFieldEntity { excludeFromSource = true) private LocalDate excludedDate; @Nullable @Field(type = Nested) private NestedExcludedFieldEntity nestedEntity; + @Nullable + @MultiField(mainField = @Field(name = "excluded-multifield", type = Text, excludeFromSource = true), otherFields = { + @InnerField(suffix = "keyword", type = Keyword) + }) private String excludedMultifield; } @SuppressWarnings("unused") @@ -2390,5 +2586,42 @@ private static class FieldNameDotsEntity { @Nullable @Field(name = "dotted.field", type = Text) private String dottedField; } + + @Mapping(aliases = { + @MappingAlias(name = "someAlly", path = "someText"), + @MappingAlias(name = "otherAlly", path = "otherText") + }) + private static class FieldAliasEntity { + @Id + @Nullable private String id; + @Nullable + @Field(type = Text) private String someText; + @Nullable + @Field(type = Text) private String otherText; + } + + @SuppressWarnings("unused") + private static class FieldMappedNameEntity { + @Nullable + @Field(type = Text, mappedTypeName = "match_only_text") private String someText; + } + + @SuppressWarnings("unused") + private static class MultiFieldMappedNameEntity { + @Nullable + @MultiField(mainField = @Field(type = FieldType.Text, mappedTypeName = "match_only_text"), + otherFields = { @InnerField(suffix = "lower_case", + type = FieldType.Keyword, normalizer = "lower_case_normalizer", + mappedTypeName = "constant_keyword") }) private String description; + } + + @SuppressWarnings("unused") + private static class MultiFieldWithNullEmptyParameters { + @Nullable + @MultiField( + mainField = @Field(name = "empty-field", type = FieldType.Keyword, nullValue = "EMPTY", storeNullValue = true), + otherFields = { + @InnerField(suffix = "suffix", type = Keyword, nullValue = "EMPTY_TEXT") }) private List emptyField; + } // endregion } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingParametersTest.java b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingParametersTest.java index 29c329411c..1a41c64879 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingParametersTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingParametersTest.java @@ -2,10 +2,10 @@ import static org.assertj.core.api.Assertions.*; import static org.springframework.data.elasticsearch.annotations.FieldType.*; -import static org.springframework.data.elasticsearch.annotations.FieldType.Object; import java.lang.annotation.Annotation; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.data.elasticsearch.annotations.Field; @@ -14,7 +14,6 @@ import org.springframework.data.elasticsearch.annotations.MultiField; import org.springframework.data.elasticsearch.core.MappingContextBaseTests; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch @@ -23,7 +22,7 @@ */ public class MappingParametersTest extends MappingContextBaseTests { - private ElasticsearchPersistentEntity entity = elasticsearchConverter.get().getMappingContext() + private final ElasticsearchPersistentEntity entity = elasticsearchConverter.get().getMappingContext() .getRequiredPersistentEntity(AnnotatedClass.class); @Test // DATAES-621 @@ -70,8 +69,8 @@ void shouldAllowEnabledFalseOnlyOnObjectFields() { } @Test // #1700 - @DisplayName("should not allow dims length greater than 2048 for dense_vector type") - void shouldNotAllowDimsLengthGreaterThan2048ForDenseVectorType() { + @DisplayName("should not allow dims length greater than 4096 for dense_vector type") + void shouldNotAllowDimsLengthGreaterThan4096ForDenseVectorType() { ElasticsearchPersistentEntity failEntity = elasticsearchConverter.get().getMappingContext() .getRequiredPersistentEntity(DenseVectorInvalidDimsClass.class); Annotation annotation = failEntity.getRequiredPersistentProperty("dense_vector").findAnnotation(Field.class); @@ -90,21 +89,28 @@ void shouldRequireDimsParameterForDenseVectorType() { } static class AnnotatedClass { - @Nullable @Field private String field; - @Nullable @MultiField(mainField = @Field, + @Nullable + @Field private String field; + @Nullable + @MultiField(mainField = @Field, otherFields = { @InnerField(suffix = "test", type = FieldType.Text) }) private String mainField; - @Nullable @Field(type = FieldType.Text, docValues = false) private String docValuesText; - @Nullable @Field(type = FieldType.Nested, docValues = false) private String docValuesNested; - @Nullable @Field(type = Object, enabled = true) private String enabledObject; - @Nullable @Field(type = Object, enabled = false) private String disabledObject; + @Nullable + @Field(type = FieldType.Text, docValues = false) private String docValuesText; + @Nullable + @Field(type = FieldType.Nested, docValues = false) private String docValuesNested; + @Nullable + @Field(type = Object, enabled = true) private String enabledObject; + @Nullable + @Field(type = Object, enabled = false) private String disabledObject; } static class InvalidEnabledFieldClass { - @Nullable @Field(type = FieldType.Text, enabled = false) private String disabledObject; + @Nullable + @Field(type = FieldType.Text, enabled = false) private String disabledObject; } static class DenseVectorInvalidDimsClass { - @Field(type = Dense_Vector, dims = 2049) private float[] dense_vector; + @Field(type = Dense_Vector, dims = 4097) private float[] dense_vector; } static class DenseVectorMissingDimsClass { diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexOperationsELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexOperationsELCIntegrationTests.java index 7666f91535..b991e644a9 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexOperationsELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexOperationsELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 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/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexOperationsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexOperationsIntegrationTests.java index 7b3e0f6d32..ba83137035 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 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. @@ -17,33 +17,40 @@ import static org.assertj.core.api.Assertions.*; import static org.skyscreamer.jsonassert.JSONAssert.*; +import static org.springframework.data.elasticsearch.annotations.FieldType.*; import static org.springframework.data.elasticsearch.core.IndexOperationsAdapter.*; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.time.LocalDate; +import java.util.HashSet; import java.util.Set; +import org.assertj.core.api.InstanceOfAssertFactories; import org.json.JSONException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Alias; 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.FieldType; +import org.springframework.data.elasticsearch.annotations.Filter; import org.springframework.data.elasticsearch.annotations.Mapping; import org.springframework.data.elasticsearch.annotations.Setting; +import org.springframework.data.elasticsearch.client.elc.Queries; import org.springframework.data.elasticsearch.core.IndexOperations; import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; import org.springframework.data.elasticsearch.core.ReactiveIndexOperations; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.data.elasticsearch.core.query.StringQuery; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch @@ -346,6 +353,34 @@ void shouldGetAliasData() { .verifyComplete(); } + @Test + void shouldCreateIndexWithAliases() { + // Given + indexNameProvider.increment(); + String indexName = indexNameProvider.indexName(); + indexOperations = operations.indexOps(EntityWithAliases.class); + blocking(indexOperations).createWithMapping(); + + // When + + // Then + indexOperations.getAliasesForIndex(indexName) + .as(StepVerifier::create) + .assertNext(aliases -> { + AliasData result = aliases.values().stream().findFirst().orElse(new HashSet<>()).stream().findFirst() + .orElse(null); + + assertThat(result).isNotNull(); + assertThat(result.getAlias()).isEqualTo("first_alias"); + assertThat(result.getFilterQuery()).asInstanceOf(InstanceOfAssertFactories.type(StringQuery.class)) + .extracting(StringQuery::getSource) + .asString() + .contains(Queries.wrapperQuery(""" + {"bool" : {"must" : {"term" : {"type" : "abc"}}}} + """).query()); + }).verifyComplete(); + } + @Document(indexName = "#{@indexNameProvider.indexName()}") @Setting(shards = 3, replicas = 2, refreshInterval = "4s") static class Entity { @@ -401,4 +436,31 @@ public void setId(@Nullable String id) { this.id = id; } } + + @Document(indexName = "#{@indexNameProvider.indexName()}", aliases = { + @Alias(value = "first_alias", filter = @Filter(""" + {"bool" : {"must" : {"term" : {"type" : "abc"}}}} + """)) + }) + private static class EntityWithAliases { + @Nullable private @Id String id; + @Field(type = Text) private String type; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + } } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexTemplateELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexTemplateELCIntegrationTests.java index a608aad631..07388144ef 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexTemplateELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexTemplateELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexTemplateIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexTemplateIntegrationTests.java index 14fcce4387..4cb0383248 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 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. @@ -27,6 +27,7 @@ import org.assertj.core.api.SoftAssertions; import org.json.JSONException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -46,7 +47,6 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch @@ -177,7 +177,7 @@ void shouldGetComponentTemplate() throws JSONException { void shouldDeleteComponentTemplate() { var blockingIndexOps = blocking(operations.indexOps(IndexCoordinates.of("dont-care"))); - String templateName = "template" + UUID.randomUUID().toString(); + String templateName = "template" + UUID.randomUUID(); var putComponentTemplateRequest = PutComponentTemplateRequest.builder() // .withName(templateName) // .withTemplateData(ComponentTemplateRequestData.builder() // @@ -321,7 +321,7 @@ void shouldPutGetAndDeleteIndexTemplateOfComponents() { @Test // DATAES-612 void shouldReturnNullOnNonExistingGetTemplate() { - String templateName = "template" + UUID.randomUUID().toString(); + String templateName = "template" + UUID.randomUUID(); GetTemplateRequest getTemplateRequest = new GetTemplateRequest(templateName); indexOperations.getTemplate(getTemplateRequest) // @@ -373,7 +373,7 @@ void shouldGetTemplate() throws JSONException { @Test // DATAES-612 void shouldCheckTemplateExists() { - String templateName = "template" + UUID.randomUUID().toString(); + String templateName = "template" + UUID.randomUUID(); ExistsTemplateRequest existsTemplateRequest = new ExistsTemplateRequest(templateName); boolean exists = blockingIndexOperations.existsTemplate(existsTemplateRequest); @@ -394,7 +394,7 @@ void shouldCheckTemplateExists() { @Test // DATAES-612 void shouldDeleteTemplate() { - String templateName = "template" + UUID.randomUUID().toString(); + String templateName = "template" + UUID.randomUUID(); ExistsTemplateRequest existsTemplateRequest = new ExistsTemplateRequest(templateName); PutTemplateRequest putTemplateRequest = PutTemplateRequest.builder(templateName, "log-*") // diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveMappingBuilderUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveMappingBuilderUnitTests.java index b54a9be05c..d2b171d283 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveMappingBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveMappingBuilderUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,17 +22,21 @@ import reactor.core.scheduler.Schedulers; import java.time.Instant; +import java.util.List; import org.json.JSONException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; 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.FieldType; +import org.springframework.data.elasticsearch.annotations.InnerField; import org.springframework.data.elasticsearch.annotations.Mapping; +import org.springframework.data.elasticsearch.annotations.MultiField; import org.springframework.data.elasticsearch.core.MappingContextBaseTests; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch @@ -79,6 +83,41 @@ void shouldWriteRuntimeFields() throws JSONException { assertEquals(expected, mapping, true); } + @Test // #2952 + void shouldMapNullityParameters() throws JSONException { + // Given + ReactiveMappingBuilder mappingBuilder = getReactiveMappingBuilder(); + String expected = """ + { + "properties": { + "_class": { + "type": "keyword", + "index": false, + "doc_values": false + }, + "empty-field": { + "type": "keyword", + "null_value": "EMPTY", + "fields": { + "suffix": { + "type": "keyword", + "null_value": "EMPTY_TEXT" + } + } + } + } + } + """; + + // When + String result = Mono + .defer(() -> mappingBuilder.buildReactivePropertyMapping(MultiFieldWithNullEmptyParameters.class)) + .subscribeOn(Schedulers.parallel()).block(); + + // Then + assertEquals(expected, result, true); + } + // region entities @Document(indexName = "runtime-fields") @Mapping(runtimeFieldsPath = "/mappings/runtime-fields.json") @@ -88,5 +127,14 @@ private static class RuntimeFieldEntity { @Field(type = Date, format = DateFormat.epoch_millis, name = "@timestamp") @Nullable private Instant timestamp; } + + @SuppressWarnings("unused") + private static class MultiFieldWithNullEmptyParameters { + @Nullable + @MultiField( + mainField = @Field(name = "empty-field", type = FieldType.Keyword, nullValue = "EMPTY", storeNullValue = true), + otherFields = { + @InnerField(suffix = "suffix", type = Keyword, nullValue = "EMPTY_TEXT") }) private List emptyField; + } // endregion } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/SettingsUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/SettingsUnitTests.java index 7029b00d3e..e745f67b69 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/SettingsUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/SettingsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/core/index/SimpleDynamicTemplatesMappingTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/SimpleDynamicTemplatesMappingTests.java index 2277fe29a8..c827bef665 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/SimpleDynamicTemplatesMappingTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/SimpleDynamicTemplatesMappingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 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. @@ -21,6 +21,7 @@ import java.util.Map; import org.json.JSONException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Document; @@ -29,7 +30,6 @@ import org.springframework.data.elasticsearch.annotations.FieldType; import org.springframework.data.elasticsearch.annotations.Setting; import org.springframework.data.elasticsearch.core.MappingContextBaseTests; -import org.springframework.lang.Nullable; /** * Dynamic templates tests @@ -76,7 +76,7 @@ static class SampleDynamicTemplatesEntity { @Id private String id; @Nullable - @Field(type = FieldType.Object) private Map names = new HashMap<>(); + @Field(type = FieldType.Object) private final Map names = new HashMap<>(); } /** @@ -91,7 +91,7 @@ static class SampleDynamicTemplatesEntityTwo { @Id private String id; @Nullable - @Field(type = FieldType.Object) private Map names = new HashMap<>(); + @Field(type = FieldType.Object) private final Map names = new HashMap<>(); } } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/SimpleElasticsearchDateMappingTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/SimpleElasticsearchDateMappingTests.java index bd0fe7f8d2..a4a963c711 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/SimpleElasticsearchDateMappingTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/SimpleElasticsearchDateMappingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -21,6 +21,7 @@ import java.time.LocalDateTime; import org.json.JSONException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.DateFormat; @@ -28,7 +29,6 @@ import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; import org.springframework.data.elasticsearch.core.MappingContextBaseTests; -import org.springframework.lang.Nullable; /** * @author Jakub Vavrik diff --git a/src/test/java/org/springframework/data/elasticsearch/core/mapping/EntityCustomConversionELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/mapping/EntityCustomConversionELCIntegrationTests.java index 8bc9920957..57f57d51ae 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/mapping/EntityCustomConversionELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/mapping/EntityCustomConversionELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/core/mapping/EntityCustomConversionIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/mapping/EntityCustomConversionIntegrationTests.java index a3e64921dd..19da497cd9 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/mapping/EntityCustomConversionIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/mapping/EntityCustomConversionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,6 +21,7 @@ import java.util.Map; import java.util.Objects; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -39,7 +40,6 @@ import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * Test that a whole entity can be converted using custom conversions diff --git a/src/test/java/org/springframework/data/elasticsearch/core/mapping/FieldNamingStrategyELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/mapping/FieldNamingStrategyELCIntegrationTests.java index 86bb11f28a..b9acc4a9f9 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/mapping/FieldNamingStrategyELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/mapping/FieldNamingStrategyELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/core/mapping/FieldNamingStrategyIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/mapping/FieldNamingStrategyIntegrationTests.java index 161acde163..35964e683f 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/mapping/FieldNamingStrategyIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/mapping/FieldNamingStrategyIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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. @@ -17,6 +17,7 @@ import static org.assertj.core.api.Assertions.*; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -32,7 +33,6 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch diff --git a/src/test/java/org/springframework/data/elasticsearch/core/mapping/ReactiveFieldNamingStrategyELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/mapping/ReactiveFieldNamingStrategyELCIntegrationTests.java index cda3043647..abf40388c9 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/mapping/ReactiveFieldNamingStrategyELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/mapping/ReactiveFieldNamingStrategyELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/core/mapping/ReactiveFieldNamingStrategyIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/mapping/ReactiveFieldNamingStrategyIntegrationTests.java index f8f64b372e..374a610e1d 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/mapping/ReactiveFieldNamingStrategyIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/mapping/ReactiveFieldNamingStrategyIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,6 +19,7 @@ import reactor.test.StepVerifier; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -32,7 +33,6 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch diff --git a/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntityTests.java b/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntityTests.java index 9051aad29c..7c59723d3a 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntityTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntityTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -19,6 +19,7 @@ import static org.skyscreamer.jsonassert.JSONAssert.*; import org.json.JSONException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -37,7 +38,6 @@ import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; /** @@ -172,7 +172,7 @@ void shouldErrorIfIndexSortingParametersDoNotHaveTheSameNumberOfArguments() { assertThatThrownBy(() -> elasticsearchConverter.get().getMappingContext() .getRequiredPersistentEntity(SettingsInvalidSortParameterSizes.class).getDefaultSettings()) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(IllegalArgumentException.class); } @Test // #1719, #2158 diff --git a/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentPropertyUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentPropertyUnitTests.java index 114a8340d0..ac5fab3bd6 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentPropertyUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentPropertyUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 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,6 +25,7 @@ import java.util.GregorianCalendar; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; @@ -44,7 +45,6 @@ import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.mapping.model.SnakeCaseFieldNamingStrategy; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; /** diff --git a/src/test/java/org/springframework/data/elasticsearch/core/paginating/ReactiveSearchAfterELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/paginating/ReactiveSearchAfterELCIntegrationTests.java index 52298999d6..24c6df1a9f 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/paginating/ReactiveSearchAfterELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/paginating/ReactiveSearchAfterELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/core/paginating/ReactiveSearchAfterIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/paginating/ReactiveSearchAfterIntegrationTests.java index 1ca4029124..fc75eacc93 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/paginating/ReactiveSearchAfterIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/paginating/ReactiveSearchAfterIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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. @@ -26,6 +26,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -43,7 +44,6 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch @@ -87,7 +87,7 @@ void shouldReadPagesWithSearchAfter() { query.setSearchAfter(searchAfter); List> searchHits = operations.search(query, Entity.class).collectList().block(); - if (searchHits.size() == 0) { + if (searchHits.isEmpty()) { break; } foundEntities.addAll(searchHits.stream().map(SearchHit::getContent).collect(Collectors.toList())); diff --git a/src/test/java/org/springframework/data/elasticsearch/core/paginating/SearchAfterELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/paginating/SearchAfterELCIntegrationTests.java index 52cec4990f..f5a9ce8ecf 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/paginating/SearchAfterELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/paginating/SearchAfterELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/core/paginating/SearchAfterIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/paginating/SearchAfterIntegrationTests.java index 29a8f46c4e..89d3503856 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/paginating/SearchAfterIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/paginating/SearchAfterIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -41,7 +42,6 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch diff --git a/src/test/java/org/springframework/data/elasticsearch/core/paginating/package-info.java b/src/test/java/org/springframework/data/elasticsearch/core/paginating/package-info.java index d242ad3372..d82a51c47d 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/paginating/package-info.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/paginating/package-info.java @@ -1,6 +1,5 @@ /** * Test for paginating support with search_after and point_in_time API */ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.core.paginating; diff --git a/src/test/java/org/springframework/data/elasticsearch/core/query/CriteriaQueryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/query/CriteriaQueryELCIntegrationTests.java index adb3e9792b..63101772f0 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/query/CriteriaQueryELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/query/CriteriaQueryELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/test/java/org/springframework/data/elasticsearch/core/query/CriteriaQueryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/query/CriteriaQueryIntegrationTests.java index 245adaf01d..24acedc9db 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/query/CriteriaQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/query/CriteriaQueryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -20,10 +20,10 @@ import static org.springframework.data.elasticsearch.utils.IdGenerator.*; import static org.springframework.data.elasticsearch.utils.IndexBuilder.*; -import java.lang.Long; import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -38,7 +38,6 @@ import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Rizwan Idrees @@ -762,12 +761,11 @@ public void setRate(int rate) { this.rate = rate; } - @Nullable - public java.lang.Long getVersion() { + public java.lang.@Nullable Long getVersion() { return version; } - public void setVersion(@Nullable java.lang.Long version) { + public void setVersion(java.lang.@Nullable Long version) { this.version = version; } } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/query/NativeQueryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/query/NativeQueryELCIntegrationTests.java index eecab3f898..6bda9b7d50 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/query/NativeQueryELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/query/NativeQueryELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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/test/java/org/springframework/data/elasticsearch/core/query/NativeQueryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/query/NativeQueryIntegrationTests.java index cfe2acbc2e..21f613f16b 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/query/NativeQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/query/NativeQueryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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. @@ -17,6 +17,12 @@ import static org.assertj.core.api.Assertions.*; +import co.elastic.clients.elasticsearch._types.GeoHashPrecision; +import co.elastic.clients.elasticsearch._types.aggregations.Aggregation; + +import java.util.List; + +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -26,12 +32,13 @@ import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; +import org.springframework.data.elasticsearch.client.elc.ElasticsearchAggregation; import org.springframework.data.elasticsearch.client.elc.NativeQuery; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.geo.GeoPoint; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch @@ -50,7 +57,7 @@ public void before() { @Test @Order(java.lang.Integer.MAX_VALUE) void cleanup() { - operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete(); + operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + '*')).delete(); } @Test // #2391 @@ -75,6 +82,62 @@ void shouldBeAbleToUseCriteriaQueryInANativeQuery() { assertThat(searchHits.getSearchHit(0).getId()).isEqualTo(entity.getId()); } + @Test // #2840 + @DisplayName("should be able to use CriteriaQuery with filter arguments in a NativeQuery") + void shouldBeAbleToUseCriteriaQueryWithFilterArgumentsInANativeQuery() { + var entity1 = new SampleEntity(); + entity1.setId("60"); + var location1 = new GeoPoint(60.0, 60.0); + entity1.setLocation(location1); + var entity2 = new SampleEntity(); + entity2.setId("70"); + var location70 = new GeoPoint(70.0, 70.0); + entity2.setLocation(location70); + operations.save(entity1, entity2); + + var criteriaQuery = new CriteriaQuery(Criteria.where("location").within(location1, "10km")); + var nativeQuery = NativeQuery.builder().withQuery(criteriaQuery).build(); + + var searchHits = operations.search(nativeQuery, SampleEntity.class); + + assertThat(searchHits.getTotalHits()).isEqualTo(1); + assertThat(searchHits.getSearchHit(0).getId()).isEqualTo(entity1.getId()); + } + + @Test // #2857 + @DisplayName("should apply CriteriaQuery with filter arguments in a NativeQuery to aggregations") + void shouldBeAbleToUseCriteriaQueryWithFilterArgumentsInANativeQueryToAggregations() { + var entity1 = new SampleEntity(); + entity1.setId("60"); + var location1 = new GeoPoint(60.0, 60.0); + entity1.setLocation(location1); + entity1.setText("60"); + var entity2 = new SampleEntity(); + entity2.setId("70"); + entity2.setText("70"); + var location70 = new GeoPoint(70.0, 70.0); + entity2.setLocation(location70); + operations.save(entity1, entity2); + + var criteriaQuery = new CriteriaQuery(Criteria.where("location").within(location1, "10km")); + var nativeQuery = NativeQuery.builder() + .withQuery(criteriaQuery) + .withAggregation("geohashgrid", Aggregation.of(ab -> ab + .geohashGrid(ghg -> ghg + .field("location") + .precision(GeoHashPrecision.of(ghp -> ghp.distance("10000km")))))) + .build(); + + var searchHits = operations.search(nativeQuery, SampleEntity.class); + + assertThat(searchHits.getTotalHits()).isEqualTo(1); + assertThat(searchHits.getSearchHit(0).getId()).isEqualTo(entity1.getId()); + assertThat(searchHits.getAggregations()).isNotNull(); + // noinspection unchecked + var aggregations = (List) searchHits.getAggregations().aggregations(); + assertThat(aggregations).hasSize(1); + } + @Test // #2391 @DisplayName("should be able to use StringQuery in a NativeQuery") void shouldBeAbleToUseStringQueryInANativeQuery() { @@ -100,7 +163,7 @@ void shouldBeAbleToUseStringQueryInANativeQuery() { ] } } - """).build(); + """).build(); var nativeQuery = NativeQuery.builder().withQuery(stringQuery).build(); var searchHits = operations.search(nativeQuery, SampleEntity.class); @@ -112,6 +175,14 @@ void shouldBeAbleToUseStringQueryInANativeQuery() { @Document(indexName = "#{@indexNameProvider.indexName()}") static class SampleEntity { + @Nullable + @Id private String id; + + @Nullable + @Field(type = FieldType.Text) private String text; + + @Nullable private GeoPoint location; + @Nullable public String getId() { return id; @@ -121,6 +192,7 @@ public void setId(@Nullable String id) { this.id = id; } + @Nullable public String getText() { return text; } @@ -130,8 +202,12 @@ public void setText(String text) { } @Nullable - @Id private String id; + public GeoPoint getLocation() { + return location; + } - @Field(type = FieldType.Text) private String text; + public void setLocation(GeoPoint location) { + this.location = location; + } } } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/query/ElasticsearchPartQueryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/query/RepositoryPartQueryIntegrationTests.java similarity index 84% rename from src/test/java/org/springframework/data/elasticsearch/core/query/ElasticsearchPartQueryIntegrationTests.java rename to src/test/java/org/springframework/data/elasticsearch/core/query/RepositoryPartQueryIntegrationTests.java index 944c533c16..617ea0ef35 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/query/ElasticsearchPartQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/query/RepositoryPartQueryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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,6 +15,7 @@ */ package org.springframework.data.elasticsearch.core.query; +import static org.assertj.core.api.Assertions.*; import static org.skyscreamer.jsonassert.JSONAssert.*; import java.lang.reflect.Method; @@ -23,39 +24,40 @@ import java.util.List; import org.json.JSONException; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.annotation.Id; +import org.springframework.data.domain.Sort; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; -import org.springframework.data.elasticsearch.repository.query.ElasticsearchPartQuery; import org.springframework.data.elasticsearch.repository.query.ElasticsearchQueryMethod; +import org.springframework.data.elasticsearch.repository.query.RepositoryPartQuery; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; -import org.springframework.lang.Nullable; +import org.springframework.data.repository.query.ValueExpressionDelegate; /** - * Tests for {@link ElasticsearchPartQuery}. The tests make sure that queries are built according to the method naming. + * Tests for {@link RepositoryPartQuery}. The tests make sure that queries are built according to the method naming. * Classes implementing this abstract class are in the packages of their request factories and converters as these are * kept package private. * * @author Peter-Josef Meisch + * @author Haibo Liu */ +@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") @SpringIntegrationTest -public abstract class ElasticsearchPartQueryIntegrationTests { +public abstract class RepositoryPartQueryIntegrationTests { public static final String BOOK_TITLE = "Title"; public static final int BOOK_PRICE = 42; @Autowired protected ElasticsearchOperations operations; - @BeforeEach - public void setUp() {} - @Test void findByName() throws NoSuchMethodException, JSONException { String methodName = "findByName"; @@ -640,6 +642,84 @@ void findByAvailableTrueOrderByNameDesc() throws NoSuchMethodException, JSONExce assertEquals(expected, query, false); } + @Test // #3072 + @DisplayName("should build sort object with correct field names") + void shouldBuildSortObjectWithCorrectFieldNames() throws NoSuchMethodException, JSONException { + + String methodName = "findByNameOrderBySortAuthor_SortName"; + Class[] parameterClasses = new Class[] { String.class }; + Object[] parameters = new Object[] { BOOK_TITLE }; + + String query = getQueryString(methodName, parameterClasses, parameters); + + String expected = """ + + { + "query": { + "bool": { + "must": [ + { + "query_string": { + "query": "Title", + "fields": [ + "name" + ] + } + } + ] + } + }, + "sort": [ + { + "sort_author.sort_name": { + "order": "asc" + } + } + ] + }"""; + + assertEquals(expected, query, false); + } + + @Test // #3081 + @DisplayName("should build sort object with unknown field names") + void shouldBuildSortObjectWithUnknownFieldNames() throws NoSuchMethodException, JSONException { + + String methodName = "findByName"; + Class[] parameterClasses = new Class[] { String.class, Sort.class }; + Object[] parameters = new Object[] { BOOK_TITLE, Sort.by("sortAuthor.sortName.raw") }; + + String query = getQueryString(methodName, parameterClasses, parameters); + + String expected = """ + + { + "query": { + "bool": { + "must": [ + { + "query_string": { + "query": "Title", + "fields": [ + "name" + ] + } + } + ] + } + }, + "sort": [ + { + "sort_author.sort_name.raw": { + "order": "asc" + } + } + ] + }"""; + + assertEquals(expected, query, false); + } + private String getQueryString(String methodName, Class[] parameterClasses, Object[] parameters) throws NoSuchMethodException { @@ -647,7 +727,8 @@ private String getQueryString(String methodName, Class[] parameterClasses, Ob ElasticsearchQueryMethod queryMethod = new ElasticsearchQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class), new SpelAwareProxyProjectionFactory(), operations.getElasticsearchConverter().getMappingContext()); - ElasticsearchPartQuery partQuery = new ElasticsearchPartQuery(queryMethod, operations); + RepositoryPartQuery partQuery = new RepositoryPartQuery(queryMethod, operations, + ValueExpressionDelegate.create()); Query query = partQuery.createQuery(parameters); return buildQueryString(query, Book.class); } @@ -726,6 +807,9 @@ private interface SampleRepository extends ElasticsearchRepository List findByAvailableTrueOrderByNameDesc(); + List findByNameOrderBySortAuthor_SortName(String name); + + List findByName(String name, Sort sort); } public static class Book { @@ -735,6 +819,10 @@ public static class Book { @Nullable private Integer price; @Field(type = FieldType.Boolean) private boolean available; + // this is needed for the #3072 test + @Nullable + @Field(name = "sort_author", type = FieldType.Object) private Author sortAuthor; + @Nullable public String getId() { return id; @@ -766,8 +854,32 @@ public Boolean getAvailable() { return available; } + @Nullable + public Author getSortAuthor() { + return sortAuthor; + } + + public void setSortAuthor(@Nullable Author sortAuthor) { + this.sortAuthor = sortAuthor; + } + public void setAvailable(Boolean available) { this.available = available; + + } + } + + public static class Author { + @Nullable + @Field(name = "sort_name", type = FieldType.Keyword) private String sortName; + + @Nullable + public String getSortName() { + return sortName; + } + + public void setSortName(@Nullable String sortName) { + this.sortName = sortName; } } } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/query/RuntimeFieldTest.java b/src/test/java/org/springframework/data/elasticsearch/core/query/RuntimeFieldTest.java index 1f05517967..7b34c21d88 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/query/RuntimeFieldTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/query/RuntimeFieldTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/core/query/SeqNoPrimaryTermTests.java b/src/test/java/org/springframework/data/elasticsearch/core/query/SeqNoPrimaryTermTests.java index 9e5c2347fc..4bc264b637 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/query/SeqNoPrimaryTermTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/query/SeqNoPrimaryTermTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ReactiveScriptedAndRuntimeFieldsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ReactiveScriptedAndRuntimeFieldsIntegrationTests.java index 284f3db2f4..c17b4a8dfe 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ReactiveScriptedAndRuntimeFieldsIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ReactiveScriptedAndRuntimeFieldsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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,6 +25,7 @@ import java.util.Map; import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -47,11 +48,9 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.RuntimeField; import org.springframework.data.elasticsearch.core.query.ScriptData; -import org.springframework.data.elasticsearch.core.query.ScriptType; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch @@ -165,7 +164,6 @@ private static org.springframework.data.elasticsearch.core.query.ScriptedField g return org.springframework.data.elasticsearch.core.query.ScriptedField.of( fieldName, ScriptData.of(b -> b - .withType(ScriptType.INLINE) .withScript("doc['value'].size() > 0 ? doc['value'].value * params['factor'] : 0") .withParams(Map.of("factor", factor)))); } @@ -395,7 +393,7 @@ Flux> findByValue(Integer value, } } } - """) + """) Flux> findWithScriptedFields(Integer value, org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1, org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2); @@ -410,7 +408,7 @@ Flux> findWithScriptedFields(Integer value, } } } - """) + """) Flux> findWithRuntimeFields(Integer value, RuntimeField runtimeField1, RuntimeField runtimeField2); } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ScriptedAndRuntimeFieldsELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ScriptedAndRuntimeFieldsELCIntegrationTests.java index b4ebd42416..b9a2ad56e1 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ScriptedAndRuntimeFieldsELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ScriptedAndRuntimeFieldsELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ScriptedAndRuntimeFieldsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ScriptedAndRuntimeFieldsIntegrationTests.java index 9493ea26c9..d4b147d469 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ScriptedAndRuntimeFieldsIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/query/scriptedandruntimefields/ScriptedAndRuntimeFieldsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,6 +22,7 @@ import java.util.Map; import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -44,11 +45,9 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.RuntimeField; import org.springframework.data.elasticsearch.core.query.ScriptData; -import org.springframework.data.elasticsearch.core.query.ScriptType; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch @@ -99,6 +98,8 @@ void shouldUseRuntimeFieldFromQueryInSearch() { @DisplayName("should use runtime-field without script") void shouldUseRuntimeFieldWithoutScript() { + // a runtime field without a script can be used to redefine the type of a field for the search, + // here we change the type from text to double insert("1", "11", 10); Query query = new CriteriaQuery(new Criteria("description").matches(11.0)); RuntimeField runtimeField = new RuntimeField("description", "double"); @@ -133,6 +134,25 @@ void shouldReturnValueFromRuntimeFieldDefinedInMapping() { assertThat(foundPerson.getBirthDate()).isEqualTo(birthDate); } + @Test // #3076 + @DisplayName("should return scripted fields that are lists") + void shouldReturnScriptedFieldsThatAreLists() { + var person = new Person(); + person.setFirstName("John"); + person.setLastName("Doe"); + operations.save(person); + var query = Query.findAll(); + query.addFields("allNames"); + query.addSourceFilter(new FetchSourceFilterBuilder().withIncludes("*").build()); + + var searchHits = operations.search(query, Person.class); + + assertThat(searchHits.getTotalHits()).isEqualTo(1); + var foundPerson = searchHits.getSearchHit(0).getContent(); + // the painless script seems to return the data sorted no matter in which order the values are emitted + assertThat(foundPerson.getAllNames()).containsExactlyInAnyOrderElementsOf(List.of("John", "Doe")); + } + @Test // #2035 @DisplayName("should use repository method with ScriptedField parameters") void shouldUseRepositoryMethodWithScriptedFieldParameters() { @@ -143,9 +163,11 @@ void shouldUseRepositoryMethodWithScriptedFieldParameters() { repository.save(entity); - org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1 = getScriptedField("scriptedValue1", + org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1 = buildScriptedField( + "scriptedValue1", 2); - org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2 = getScriptedField("scriptedValue2", + org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2 = buildScriptedField( + "scriptedValue2", 3); var searchHits = repository.findByValue(3, scriptedField1, scriptedField2); @@ -157,17 +179,6 @@ void shouldUseRepositoryMethodWithScriptedFieldParameters() { assertThat(foundEntity.getScriptedValue2()).isEqualTo(9); } - @NotNull - private static org.springframework.data.elasticsearch.core.query.ScriptedField getScriptedField(String fieldName, - int factor) { - return org.springframework.data.elasticsearch.core.query.ScriptedField.of( - fieldName, - ScriptData.of(b -> b - .withType(ScriptType.INLINE) - .withScript("doc['value'].size() > 0 ? doc['value'].value * params['factor'] : 0") - .withParams(Map.of("factor", factor)))); - } - @Test // #2035 @DisplayName("should use repository string query method with ScriptedField parameters") void shouldUseRepositoryStringQueryMethodWithScriptedFieldParameters() { @@ -178,9 +189,11 @@ void shouldUseRepositoryStringQueryMethodWithScriptedFieldParameters() { repository.save(entity); - org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1 = getScriptedField("scriptedValue1", + org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1 = buildScriptedField( + "scriptedValue1", 2); - org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2 = getScriptedField("scriptedValue2", + org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2 = buildScriptedField( + "scriptedValue2", 3); var searchHits = repository.findWithScriptedFields(3, scriptedField1, scriptedField2); @@ -202,8 +215,8 @@ void shouldUseRepositoryMethodWithRuntimeFieldParameters() { repository.save(entity); - var runtimeField1 = getRuntimeField("scriptedValue1", 3); - var runtimeField2 = getRuntimeField("scriptedValue2", 4); + var runtimeField1 = buildRuntimeField("scriptedValue1", 3); + var runtimeField2 = buildRuntimeField("scriptedValue2", 4); var searchHits = repository.findByValue(3, runtimeField1, runtimeField2); @@ -214,14 +227,6 @@ void shouldUseRepositoryMethodWithRuntimeFieldParameters() { assertThat(foundEntity.getScriptedValue2()).isEqualTo(12); } - @NotNull - private static RuntimeField getRuntimeField(String fieldName, int factor) { - return new RuntimeField( - fieldName, - "long", - String.format("emit(doc['value'].size() > 0 ? doc['value'].value * %d : 0)", factor)); - } - @Test // #2035 @DisplayName("should use repository string query method with RuntimeField parameters") void shouldUseRepositoryStringQueryMethodWithRuntimeFieldParameters() { @@ -232,8 +237,8 @@ void shouldUseRepositoryStringQueryMethodWithRuntimeFieldParameters() { repository.save(entity); - var runtimeField1 = getRuntimeField("scriptedValue1", 3); - var runtimeField2 = getRuntimeField("scriptedValue2", 4); + var runtimeField1 = buildRuntimeField("scriptedValue1", 3); + var runtimeField2 = buildRuntimeField("scriptedValue2", 4); var searchHits = repository.findWithRuntimeFields(3, runtimeField1, runtimeField2); @@ -263,8 +268,7 @@ void shouldUseParametersForRuntimeFieldsInSearchQueries() { "priceWithTax", "double", "emit(doc['price'].value * params.tax)", - Map.of("tax", 1.19) - ); + Map.of("tax", 1.19)); var query = CriteriaQuery.builder( Criteria.where("priceWithTax").greaterThan(100.0)) .withRuntimeFields(List.of(runtimeField)) @@ -275,6 +279,55 @@ void shouldUseParametersForRuntimeFieldsInSearchQueries() { assertThat(searchHits).hasSize(1); } + @Test // #3076 + @DisplayName("should use runtime fields in queries returning lists") + void shouldUseRuntimeFieldsInQueriesReturningLists() { + + insert("1", "item 1", 80.0); + + var runtimeField = new RuntimeField( + "someStrings", + "keyword", + "emit('foo'); emit('bar');", + null); + + var query = Query.findAll(); + query.addRuntimeField(runtimeField); + query.addFields("someStrings"); + query.addSourceFilter(new FetchSourceFilterBuilder().withIncludes("*").build()); + + var searchHits = operations.search(query, SomethingToBuy.class); + + assertThat(searchHits).hasSize(1); + var somethingToBuy = searchHits.getSearchHit(0).getContent(); + assertThat(somethingToBuy.someStrings).containsExactlyInAnyOrder("foo", "bar"); + } + + /** + * build a {@link org.springframework.data.elasticsearch.core.query.ScriptedField} to return the product of the + * document's value property and the given factor + */ + @NotNull + private static org.springframework.data.elasticsearch.core.query.ScriptedField buildScriptedField(String fieldName, + int factor) { + return org.springframework.data.elasticsearch.core.query.ScriptedField.of( + fieldName, + ScriptData.of(b -> b + .withScript("doc['value'].size() > 0 ? doc['value'].value * params['factor'] : 0") + .withParams(Map.of("factor", factor)))); + } + + /** + * build a {@link RuntimeField} to return the product of the document's value property and the given factor + */ + @NotNull + private static RuntimeField buildRuntimeField(String fieldName, int factor) { + return new RuntimeField( + fieldName, + "long", + String.format("emit(doc['value'].size() > 0 ? doc['value'].value * %d : 0)", factor)); + } + @SuppressWarnings("unused") @Document(indexName = "#{@indexNameProvider.indexName()}-something-to-by") private static class SomethingToBuy { @@ -286,6 +339,9 @@ private static class SomethingToBuy { @Nullable @Field(type = FieldType.Double) private Double price; + @Nullable + @ScriptedField private List someStrings; + @Nullable public String getId() { return id; @@ -312,6 +368,15 @@ public Double getPrice() { public void setPrice(@Nullable Double price) { this.price = price; } + + @Nullable + public List getSomeStrings() { + return someStrings; + } + + public void setSomeStrings(@Nullable List someStrings) { + this.someStrings = someStrings; + } } @SuppressWarnings("unused") @@ -320,6 +385,13 @@ public void setPrice(@Nullable Double price) { public static class Person { @Nullable private String id; + // need keywords as we are using them in the script + @Nullable + @Field(type = FieldType.Keyword) private String firstName; + @Nullable + @Field(type = FieldType.Keyword) private String lastName; + @ScriptedField private List allNames = List.of(); + @Field(type = FieldType.Date, format = DateFormat.basic_date) @Nullable private LocalDate birthDate; @@ -335,6 +407,24 @@ public void setId(@Nullable String id) { this.id = id; } + @Nullable + public String getFirstName() { + return firstName; + } + + public void setFirstName(@Nullable String firstName) { + this.firstName = firstName; + } + + @Nullable + public String getLastName() { + return lastName; + } + + public void setLastName(@Nullable String lastName) { + this.lastName = lastName; + } + @Nullable public LocalDate getBirthDate() { return birthDate; @@ -352,6 +442,14 @@ public Integer getAge() { public void setAge(@Nullable Integer age) { this.age = age; } + + public List getAllNames() { + return allNames; + } + + public void setAllNames(List allNames) { + this.allNames = allNames; + } } @SuppressWarnings("unused") diff --git a/src/test/java/org/springframework/data/elasticsearch/core/query/sort/NestedSortIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/query/sort/NestedSortIntegrationTests.java index dbb90fc596..f6ee3aaa4f 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/query/sort/NestedSortIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/query/sort/NestedSortIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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. @@ -20,6 +20,7 @@ import java.util.List; import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -38,7 +39,6 @@ import org.springframework.data.elasticsearch.core.query.StringQuery; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * Integration tests for nested sorts. @@ -103,27 +103,27 @@ void shouldSortDirectorsByYearOfBirthOfActorInTheirMoviesAscending() { assertThat(searchHits.getSearchHit(0).getContent().id).isEqualTo(francisFordCoppola.id); var sortValues = searchHits.getSearchHit(0).getSortValues(); assertThat(sortValues).hasSize(1); - assertThat(sortValues.get(0)).isEqualTo("1924"); + assertThat(sortValues.get(0)).isEqualTo(1924L); assertThat(searchHits.getSearchHit(1).getContent().id).isEqualTo(stanleyKubrik.id); sortValues = searchHits.getSearchHit(1).getSortValues(); assertThat(sortValues).hasSize(1); - assertThat(sortValues.get(0)).isEqualTo("1937"); + assertThat(sortValues.get(0)).isEqualTo(1937L); } @Test // #1784 @DisplayName("should sort directors by year of birth of actor in their movies descending") void shouldSortDirectorsByYearOfBirthOfActorInTheirMoviesDescending() { -var order = new org.springframework.data.elasticsearch.core.query.Order(Sort.Direction.DESC, - "movies.actors.yearOfBirth") // - .withNested( // - Nested.builder("movies") // - .withNested(Nested.builder("movies.actors") // - .build()) // - .build()); + var order = new org.springframework.data.elasticsearch.core.query.Order(Sort.Direction.DESC, + "movies.actors.yearOfBirth") // + .withNested( // + Nested.builder("movies") // + .withNested(Nested.builder("movies.actors") // + .build()) // + .build()); -var query = Query.findAll().addSort(Sort.by(order)); + var query = Query.findAll().addSort(Sort.by(order)); var searchHits = operations.search(query, Director.class); @@ -132,32 +132,32 @@ void shouldSortDirectorsByYearOfBirthOfActorInTheirMoviesDescending() { assertThat(searchHits.getSearchHit(0).getContent().id).isEqualTo(stanleyKubrik.id); var sortValues = searchHits.getSearchHit(0).getSortValues(); assertThat(sortValues).hasSize(1); - assertThat(sortValues.get(0)).isEqualTo("1959"); + assertThat(sortValues.get(0)).isEqualTo(1959L); assertThat(searchHits.getSearchHit(1).getContent().id).isEqualTo(francisFordCoppola.id); sortValues = searchHits.getSearchHit(1).getSortValues(); assertThat(sortValues).hasSize(1); - assertThat(sortValues.get(0)).isEqualTo("1946"); + assertThat(sortValues.get(0)).isEqualTo(1946L); } @Test // #1784 @DisplayName("should sort directors by year of birth of male actor in their movies descending") void shouldSortDirectorsByYearOfBirthOfMaleActorInTheirMoviesDescending() { -var filter = StringQuery.builder(""" - { "term": {"movies.actors.sex": "m"} } - """).build(); -var order = new org.springframework.data.elasticsearch.core.query.Order(Sort.Direction.DESC, - "movies.actors.yearOfBirth") // - .withNested( // - Nested.builder("movies") // - .withNested( // - Nested.builder("movies.actors") // - .withFilter(filter) // - .build()) // - .build()); + var filter = StringQuery.builder(""" + { "term": {"movies.actors.sex": "m"} } + """).build(); + var order = new org.springframework.data.elasticsearch.core.query.Order(Sort.Direction.DESC, + "movies.actors.yearOfBirth") // + .withNested( // + Nested.builder("movies") // + .withNested( // + Nested.builder("movies.actors") // + .withFilter(filter) // + .build()) // + .build()); -var query = Query.findAll().addSort(Sort.by(order)); + var query = Query.findAll().addSort(Sort.by(order)); var searchHits = operations.search(query, Director.class); @@ -166,12 +166,12 @@ void shouldSortDirectorsByYearOfBirthOfMaleActorInTheirMoviesDescending() { assertThat(searchHits.getSearchHit(0).getContent().id).isEqualTo(stanleyKubrik.id); var sortValues = searchHits.getSearchHit(0).getSortValues(); assertThat(sortValues).hasSize(1); - assertThat(sortValues.get(0)).isEqualTo("1959"); + assertThat(sortValues.get(0)).isEqualTo(1959L); assertThat(searchHits.getSearchHit(1).getContent().id).isEqualTo(francisFordCoppola.id); sortValues = searchHits.getSearchHit(1).getSortValues(); assertThat(sortValues).hasSize(1); - assertThat(sortValues.get(0)).isEqualTo("1940"); + assertThat(sortValues.get(0)).isEqualTo(1940L); } @Document(indexName = "#{@indexNameProvider.indexName()}") diff --git a/src/test/java/org/springframework/data/elasticsearch/core/routing/DefaultRoutingResolverUnitTest.java b/src/test/java/org/springframework/data/elasticsearch/core/routing/DefaultRoutingResolverUnitTest.java index 707da7b3d1..349f8a3f45 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/routing/DefaultRoutingResolverUnitTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/routing/DefaultRoutingResolverUnitTest.java @@ -17,6 +17,7 @@ import static org.assertj.core.api.Assertions.*; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -29,7 +30,6 @@ import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Routing; import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; -import org.springframework.lang.Nullable; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** @@ -39,7 +39,6 @@ class DefaultRoutingResolverUnitTest { @Autowired private ApplicationContext applicationContext; - private SimpleElasticsearchMappingContext mappingContext; @Nullable private RoutingResolver routingResolver; @@ -53,7 +52,7 @@ SpelRouting spelRouting() { @BeforeEach void setUp() { - mappingContext = new SimpleElasticsearchMappingContext(); + SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext(); mappingContext.setApplicationContext(applicationContext); routingResolver = new DefaultRoutingResolver(mappingContext); @@ -93,7 +92,8 @@ void shouldReturnRoutingFromSpElExpression() { @Document(indexName = "routing-resolver-test") @Routing("theRouting") static class ValidRoutingEntity { - @Nullable @Id private String id; + @Nullable + @Id private String id; @Nullable private String theRouting; public ValidRoutingEntity(@Nullable String id, @Nullable String theRouting) { @@ -123,7 +123,8 @@ public void setTheRouting(@Nullable String theRouting) { @Document(indexName = "routing-resolver-test") @Routing(value = "@spelRouting.getRouting(#entity)") static class ValidSpelRoutingEntity { - @Nullable @Id private String id; + @Nullable + @Id private String id; @Nullable private String theRouting; public ValidSpelRoutingEntity(@Nullable String id, @Nullable String theRouting) { @@ -153,7 +154,8 @@ public void setTheRouting(@Nullable String theRouting) { @Document(indexName = "routing-resolver-test") @Routing("unknownProperty") static class InvalidRoutingEntity { - @Nullable @Id private String id; + @Nullable + @Id private String id; @Nullable private String theRouting; public InvalidRoutingEntity(@Nullable String id, @Nullable String theRouting) { diff --git a/src/test/java/org/springframework/data/elasticsearch/core/routing/ReactiveRoutingELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/routing/ReactiveRoutingELCIntegrationTests.java index 21d39bfb4a..ba154127e8 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/routing/ReactiveRoutingELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/routing/ReactiveRoutingELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/core/routing/ReactiveRoutingTests.java b/src/test/java/org/springframework/data/elasticsearch/core/routing/ReactiveRoutingTests.java index 95345858a8..f80111f8db 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/routing/ReactiveRoutingTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/routing/ReactiveRoutingTests.java @@ -21,6 +21,7 @@ import java.util.Objects; import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -37,7 +38,6 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch diff --git a/src/test/java/org/springframework/data/elasticsearch/core/routing/RoutingIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/routing/RoutingIntegrationTests.java index ddbc4949a2..901af03ca7 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/routing/RoutingIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/routing/RoutingIntegrationTests.java @@ -25,6 +25,7 @@ import org.apache.commons.codec.digest.MurmurHash3; import org.assertj.core.api.SoftAssertions; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -45,7 +46,6 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch diff --git a/src/test/java/org/springframework/data/elasticsearch/core/sql/ReactiveSqlOperationsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/sql/ReactiveSqlOperationsIntegrationTests.java new file mode 100644 index 0000000000..a47eba4da0 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/sql/ReactiveSqlOperationsIntegrationTests.java @@ -0,0 +1,124 @@ +/* + * 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.core.sql; + +import static org.springframework.data.elasticsearch.core.IndexOperationsAdapter.*; + +import reactor.test.StepVerifier; + +import java.util.List; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; +import org.springframework.data.elasticsearch.core.query.SqlQuery; +import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchTemplateConfiguration; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.test.context.ContextConfiguration; + +/** + * Testing the reactive querying using SQL syntax. + * + * @author Youssef Aouichaoui + */ +@SpringIntegrationTest +@ContextConfiguration(classes = { ReactiveSqlOperationsIntegrationTests.Config.class }) +@DisplayName("Using Elasticsearch SQL Reactive Client") +public class ReactiveSqlOperationsIntegrationTests { + @Autowired ReactiveElasticsearchOperations operations; + + @BeforeEach + void setUp() { + // create index + blocking(operations.indexOps(EntityForSQL.class)).createWithMapping(); + + // add data + operations + .saveAll(List.of(EntityForSQL.builder().withViews(3).build(), EntityForSQL.builder().withViews(0).build()), + EntityForSQL.class) + .blockLast(); + } + + @AfterEach + void tearDown() { + // delete index + blocking(operations.indexOps(EntityForSQL.class)).delete(); + } + + // begin configuration region + @Configuration + @Import({ ReactiveElasticsearchTemplateConfiguration.class }) + static class Config {} + // end region + + @Test // #2683 + void when_search_with_an_sql_query() { + // Given + SqlQuery query = SqlQuery.builder("SELECT * FROM entity_for_sql WHERE views = 0").build(); + + // When + + // Then + operations.search(query).as(StepVerifier::create).expectNextCount(1).verifyComplete(); + } + + // begin region + @Document(indexName = "entity_for_sql") + static class EntityForSQL { + @Id private String id; + private final Integer views; + + public EntityForSQL(EntityForSQL.Builder builder) { + this.views = builder.views; + } + + @Nullable + public String getId() { + return id; + } + + public Integer getViews() { + return views; + } + + public static EntityForSQL.Builder builder() { + return new EntityForSQL.Builder(); + } + + static class Builder { + private Integer views = 0; + + public EntityForSQL.Builder withViews(Integer views) { + this.views = views; + + return this; + } + + public EntityForSQL build() { + return new EntityForSQL(this); + } + } + } + // end region +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/sql/SqlOperationsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/sql/SqlOperationsIntegrationTests.java new file mode 100644 index 0000000000..b62cc7ee4b --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/sql/SqlOperationsIntegrationTests.java @@ -0,0 +1,141 @@ +/* + * 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.core.sql; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.IndexOperations; +import org.springframework.data.elasticsearch.core.query.SqlQuery; +import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.test.context.ContextConfiguration; + +/** + * Testing the querying using SQL syntax. + * + * @author Youssef Aouichaoui + */ +@SpringIntegrationTest +@ContextConfiguration(classes = { SqlOperationsIntegrationTests.Config.class }) +@DisplayName("Using Elasticsearch SQL Client") +class SqlOperationsIntegrationTests { + @Autowired ElasticsearchOperations operations; + @Nullable IndexOperations indexOps; + + @BeforeEach + void setUp() { + // create index + indexOps = operations.indexOps(EntityForSQL.class); + indexOps.createWithMapping(); + + // add data + operations.save(EntityForSQL.builder().withViews(3).build(), EntityForSQL.builder().withViews(0).build()); + } + + @AfterEach + void tearDown() { + // delete index + if (indexOps != null) { + indexOps.delete(); + } + } + + // begin configuration region + @Configuration + @Import({ ElasticsearchTemplateConfiguration.class }) + static class Config {} + // end region + + @Test // #2683 + void when_search_with_an_sql_query() { + // Given + SqlQuery query = SqlQuery.builder("SELECT * FROM entity_for_sql WHERE views = 0").build(); + + // When + + // Then + SqlResponse response = operations.search(query); + assertNotNull(response); + assertFalse(response.getRows().isEmpty()); + assertEquals(1, response.getRows().size()); + } + + @Test // #2683 + void when_search_with_an_sql_query_that_has_aggregated_column() { + // Given + SqlQuery query = SqlQuery.builder("SELECT SUM(views) AS TOTAL FROM entity_for_sql").build(); + + // When + + // Then + SqlResponse response = operations.search(query); + assertThat(response.getColumns()).first().extracting(SqlResponse.Column::name).isEqualTo("TOTAL"); + assertThat(response.getRows()).hasSize(1).first().extracting(row -> row.get(response.getColumns().get(0))) + .hasToString("3"); + } + + // begin region + @Document(indexName = "entity_for_sql") + static class EntityForSQL { + @Id private String id; + private final Integer views; + + public EntityForSQL(Builder builder) { + this.views = builder.views; + } + + @Nullable + public String getId() { + return id; + } + + public Integer getViews() { + return views; + } + + public static Builder builder() { + return new Builder(); + } + + static class Builder { + private Integer views = 0; + + public Builder withViews(Integer views) { + this.views = views; + + return this; + } + + public EntityForSQL build() { + return new EntityForSQL(this); + } + } + } + // end region + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/suggest/CompletionELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/suggest/CompletionELCIntegrationTests.java index 7d1e968a4b..041b9a093d 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/suggest/CompletionELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/suggest/CompletionELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/test/java/org/springframework/data/elasticsearch/core/suggest/CompletionIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/suggest/CompletionIntegrationTests.java index da4a81cd4d..e167a8314e 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/suggest/CompletionIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/suggest/CompletionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -36,7 +37,6 @@ import org.springframework.data.elasticsearch.core.suggest.response.Suggest; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Rizwan Idrees @@ -269,7 +269,7 @@ public void setSuggest(Completion suggest) { static class CompletionEntityBuilder { - private CompletionEntity result; + private final CompletionEntity result; public CompletionEntityBuilder(String id) { result = new CompletionEntity(id); @@ -354,7 +354,7 @@ public void setSuggest(Completion suggest) { */ static class AnnotatedCompletionEntityBuilder { - private AnnotatedCompletionEntity result; + private final AnnotatedCompletionEntity result; public AnnotatedCompletionEntityBuilder(String id) { result = new AnnotatedCompletionEntity(id); diff --git a/src/test/java/org/springframework/data/elasticsearch/core/suggest/CompletionWithContextsELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/suggest/CompletionWithContextsELCIntegrationTests.java index 381274e5d8..e3e130f0cf 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/suggest/CompletionWithContextsELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/suggest/CompletionWithContextsELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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/test/java/org/springframework/data/elasticsearch/core/suggest/CompletionWithContextsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/suggest/CompletionWithContextsIntegrationTests.java index 41fbdd4e54..24e495af43 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/suggest/CompletionWithContextsIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/suggest/CompletionWithContextsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -15,10 +15,15 @@ */ package org.springframework.data.elasticsearch.core.suggest; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -34,7 +39,6 @@ import org.springframework.data.elasticsearch.core.suggest.response.Suggest; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Robert Gruendler @@ -243,7 +247,7 @@ public void setSuggest(Completion suggest) { */ static class ContextCompletionEntityBuilder { - private ContextCompletionEntity result; + private final ContextCompletionEntity result; public ContextCompletionEntityBuilder(String id) { result = new ContextCompletionEntity(id); diff --git a/src/test/java/org/springframework/data/elasticsearch/core/suggest/ReactiveSuggestELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/suggest/ReactiveSuggestELCIntegrationTests.java index ac39ab785f..e46dee4f8b 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/suggest/ReactiveSuggestELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/suggest/ReactiveSuggestELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/core/suggest/ReactiveSuggestIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/suggest/ReactiveSuggestIntegrationTests.java index 2f2f247958..69f4fc8a4e 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/suggest/ReactiveSuggestIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/suggest/ReactiveSuggestIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,6 +24,7 @@ import java.util.Arrays; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -40,7 +41,6 @@ import org.springframework.data.elasticsearch.core.suggest.response.Suggest; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch @@ -159,7 +159,7 @@ public void setSuggest(Completion suggest) { static class CompletionEntityBuilder { - private CompletionEntity result; + private final CompletionEntity result; public CompletionEntityBuilder(String id) { result = new CompletionEntity(id); diff --git a/src/test/java/org/springframework/data/elasticsearch/immutable/ImmutableRepositoryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/immutable/ImmutableRepositoryELCIntegrationTests.java index 46ebbe5192..bd9e93ab51 100644 --- a/src/test/java/org/springframework/data/elasticsearch/immutable/ImmutableRepositoryELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/immutable/ImmutableRepositoryELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/immutable/ImmutableRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/immutable/ImmutableRepositoryIntegrationTests.java index acb6074396..1236b88971 100644 --- a/src/test/java/org/springframework/data/elasticsearch/immutable/ImmutableRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/immutable/ImmutableRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-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.Optional; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -30,7 +31,6 @@ import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; import org.springframework.data.repository.CrudRepository; -import org.springframework.lang.Nullable; /** * @author Young Gu diff --git a/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ClusterConnection.java b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ClusterConnection.java index a92939f838..30112fa260 100644 --- a/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ClusterConnection.java +++ b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ClusterConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -26,10 +26,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.extension.ExtensionContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.lang.Nullable; import org.testcontainers.elasticsearch.ElasticsearchContainer; import org.testcontainers.utility.DockerImageName; @@ -129,10 +129,13 @@ private ClusterConnectionInfo startElasticsearchContainer() { Map testcontainersProperties = testcontainersProperties( "testcontainers-" + testcontainersConfiguration + ".properties"); + var testcontainersPropertiesLocal = testcontainersProperties("testcontainers-local.properties"); + testcontainersProperties.putAll(testcontainersPropertiesLocal); + DockerImageName dockerImageName = getDockerImageName(testcontainersProperties); ElasticsearchContainer elasticsearchContainer = new SpringDataElasticsearchContainer(dockerImageName) - .withEnv(testcontainersProperties).withStartupTimeout(Duration.ofMinutes(2)); + .withEnv(testcontainersProperties).withStartupTimeout(Duration.ofMinutes(2)).withReuse(true); elasticsearchContainer.start(); return ClusterConnectionInfo.builder() // @@ -192,16 +195,6 @@ private Map testcontainersProperties(String propertiesFile) { @Override public void close() { - if (clusterConnectionInfo != null && clusterConnectionInfo.getElasticsearchContainer() != null) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Stopping container"); - } - clusterConnectionInfo.getElasticsearchContainer().stop(); - } - - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("closed"); - } } private static class SpringDataElasticsearchContainer extends ElasticsearchContainer { diff --git a/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ClusterConnectionException.java b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ClusterConnectionException.java index 611df2ba0c..8b17c85093 100644 --- a/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ClusterConnectionException.java +++ b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ClusterConnectionException.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/test/java/org/springframework/data/elasticsearch/junit/jupiter/ClusterConnectionInfo.java b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ClusterConnectionInfo.java index b5a1c83dc0..1a3266a9f9 100644 --- a/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ClusterConnectionInfo.java +++ b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ClusterConnectionInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -15,7 +15,7 @@ */ package org.springframework.data.elasticsearch.junit.jupiter; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.testcontainers.elasticsearch.ElasticsearchContainer; diff --git a/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ElasticsearchTemplateConfiguration.java b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ElasticsearchTemplateConfiguration.java index a2e620eb7c..1de266c482 100644 --- a/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ElasticsearchTemplateConfiguration.java +++ b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ElasticsearchTemplateConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/junit/jupiter/IntegrationTest.java b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/IntegrationTest.java index 81078ae6f0..4e79298127 100644 --- a/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/IntegrationTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/IntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/test/java/org/springframework/data/elasticsearch/junit/jupiter/IntegrationtestEnvironment.java b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/IntegrationtestEnvironment.java index 89b5129c6a..505aecc740 100644 --- a/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/IntegrationtestEnvironment.java +++ b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/IntegrationtestEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/junit/jupiter/ReactiveElasticsearchTemplateConfiguration.java b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ReactiveElasticsearchTemplateConfiguration.java index c020fe7e79..135ef1f980 100644 --- a/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ReactiveElasticsearchTemplateConfiguration.java +++ b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ReactiveElasticsearchTemplateConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/junit/jupiter/SpringDataElasticsearchExtension.java b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/SpringDataElasticsearchExtension.java index 834c806fc3..ec9abeebdb 100644 --- a/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/SpringDataElasticsearchExtension.java +++ b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/SpringDataElasticsearchExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/test/java/org/springframework/data/elasticsearch/junit/jupiter/SpringIntegrationTest.java b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/SpringIntegrationTest.java index 6f3b759575..7bad52bdb1 100644 --- a/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/SpringIntegrationTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/SpringIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/test/java/org/springframework/data/elasticsearch/junit/jupiter/Tags.java b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/Tags.java index 6bd763b4ab..efeca7ccb5 100644 --- a/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/Tags.java +++ b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/Tags.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/test/java/org/springframework/data/elasticsearch/junit/jupiter/package-info.java b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/package-info.java index e6e7542679..bf18bbf1eb 100644 --- a/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/package-info.java +++ b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/package-info.java @@ -1,5 +1,5 @@ /** * interfaces, annotations and classes related to JUnit 5 test handling. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.junit.jupiter; diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/CdiProductRepository.java b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/CdiProductRepository.java index f59d97ba67..35e8f45374 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/CdiProductRepository.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/CdiProductRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 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/test/java/org/springframework/data/elasticsearch/repositories/cdi/CdiRepositoryClient.java b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/CdiRepositoryClient.java index 1700727982..ad7f72ec1a 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/CdiRepositoryClient.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/CdiRepositoryClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 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. @@ -17,7 +17,7 @@ import jakarta.inject.Inject; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * @author Mohsin Husen diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/CdiRepositoryTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/CdiRepositoryTests.java index 280d5522f0..f0236249f9 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/CdiRepositoryTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/CdiRepositoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 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. @@ -17,6 +17,9 @@ import static org.assertj.core.api.Assertions.*; +import jakarta.enterprise.inject.se.SeContainer; +import jakarta.enterprise.inject.se.SeContainerInitializer; + import java.util.Collection; import java.util.Date; import java.util.HashMap; @@ -24,9 +27,7 @@ import java.util.Map; import java.util.Optional; -import jakarta.enterprise.inject.se.SeContainer; -import jakarta.enterprise.inject.se.SeContainerInitializer; - +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -40,7 +41,6 @@ import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.junit.jupiter.IntegrationTest; -import org.springframework.lang.Nullable; /** * @author Mohsin Husen diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/ElasticsearchOperationsProducer.java b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/ElasticsearchOperationsProducer.java index d15e7547ea..74371fd688 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/ElasticsearchOperationsProducer.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/ElasticsearchOperationsProducer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 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. @@ -48,6 +48,7 @@ public ElasticsearchOperations createQualifiedElasticsearchTemplate(Elasticsearc return new ElasticsearchTemplate(elasticsearchClient); } + @SuppressWarnings("EmptyMethod") @PreDestroy public void shutdown() { // remove everything to avoid conflicts with other tests in case server not shut down properly diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/OtherQualifier.java b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/OtherQualifier.java index 4bce75577c..7bcb96403d 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/OtherQualifier.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/OtherQualifier.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-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/test/java/org/springframework/data/elasticsearch/repositories/cdi/PersonDB.java b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/PersonDB.java index a775ec118d..bd56b8fc54 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/PersonDB.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/PersonDB.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-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/test/java/org/springframework/data/elasticsearch/repositories/cdi/QualifiedProductRepository.java b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/QualifiedProductRepository.java index 16436ecd38..689b96c6f1 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/QualifiedProductRepository.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/QualifiedProductRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-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/test/java/org/springframework/data/elasticsearch/repositories/cdi/SamplePersonRepository.java b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/SamplePersonRepository.java index 21d584bcd8..52b45564b3 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/SamplePersonRepository.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/SamplePersonRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 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/test/java/org/springframework/data/elasticsearch/repositories/cdi/SamplePersonRepositoryCustom.java b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/SamplePersonRepositoryCustom.java index 7be2a18b3d..7c3845c394 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/SamplePersonRepositoryCustom.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/SamplePersonRepositoryCustom.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 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/test/java/org/springframework/data/elasticsearch/repositories/cdi/SamplePersonRepositoryImpl.java b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/SamplePersonRepositoryImpl.java index c096d3b537..d2b345f8f1 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/SamplePersonRepositoryImpl.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/SamplePersonRepositoryImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 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/test/java/org/springframework/data/elasticsearch/repositories/cdi/package-info.java b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/package-info.java index 00c6f2bb9a..065343171d 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/package-info.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/cdi/package-info.java @@ -1,3 +1,2 @@ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.data.elasticsearch.repositories.cdi; diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/autowiring/ComplexCustomMethodRepositoryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/autowiring/ComplexCustomMethodRepositoryELCIntegrationTests.java index 937f2668c0..d488fd4276 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/autowiring/ComplexCustomMethodRepositoryELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/autowiring/ComplexCustomMethodRepositoryELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/autowiring/ComplexCustomMethodRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/autowiring/ComplexCustomMethodRepositoryIntegrationTests.java index 610bcf53d2..435c018c83 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/autowiring/ComplexCustomMethodRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/autowiring/ComplexCustomMethodRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.*; import static org.springframework.data.elasticsearch.annotations.FieldType.*; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -29,7 +30,6 @@ import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Artur Konczak diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/autowiring/ComplexElasticsearchRepository.java b/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/autowiring/ComplexElasticsearchRepository.java index 0fc9b8e4e0..062231b55a 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/autowiring/ComplexElasticsearchRepository.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/autowiring/ComplexElasticsearchRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/autowiring/ComplexElasticsearchRepositoryCustom.java b/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/autowiring/ComplexElasticsearchRepositoryCustom.java index 580b4ddc33..ae81cf4b22 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/autowiring/ComplexElasticsearchRepositoryCustom.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/autowiring/ComplexElasticsearchRepositoryCustom.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 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/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/manualwiring/ComplexCustomMethodRepositoryManualWiringELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/manualwiring/ComplexCustomMethodRepositoryManualWiringELCIntegrationTests.java index 4b08a22a7d..6013306350 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/manualwiring/ComplexCustomMethodRepositoryManualWiringELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/manualwiring/ComplexCustomMethodRepositoryManualWiringELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/manualwiring/ComplexCustomMethodRepositoryManualWiringIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/manualwiring/ComplexCustomMethodRepositoryManualWiringIntegrationTests.java index 02dbc729fe..4f74e728a4 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/manualwiring/ComplexCustomMethodRepositoryManualWiringIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/manualwiring/ComplexCustomMethodRepositoryManualWiringIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.*; import static org.springframework.data.elasticsearch.annotations.FieldType.*; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -29,7 +30,6 @@ import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Artur Konczak diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/manualwiring/ComplexElasticsearchRepositoryManualWiring.java b/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/manualwiring/ComplexElasticsearchRepositoryManualWiring.java index 4bf569f9d3..686a572d65 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/manualwiring/ComplexElasticsearchRepositoryManualWiring.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/manualwiring/ComplexElasticsearchRepositoryManualWiring.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/manualwiring/ComplexElasticsearchRepositoryManualWiringImpl.java b/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/manualwiring/ComplexElasticsearchRepositoryManualWiringImpl.java index 13771f6dd9..b71e6e919f 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/manualwiring/ComplexElasticsearchRepositoryManualWiringImpl.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/complex/custommethod/manualwiring/ComplexElasticsearchRepositoryManualWiringImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 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. @@ -27,7 +27,7 @@ */ public class ComplexElasticsearchRepositoryManualWiringImpl implements ComplexElasticsearchRepositoryCustom { - private ElasticsearchOperations operations; + private final ElasticsearchOperations operations; public ComplexElasticsearchRepositoryManualWiringImpl(ElasticsearchOperations operations) { this.operations = operations; diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryELCIntegrationTests.java index dfd2441bcd..d57d469d34 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 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. @@ -15,9 +15,12 @@ */ package org.springframework.data.elasticsearch.repositories.custommethod; +import java.util.List; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.data.elasticsearch.core.convert.ElasticsearchCustomConversions; import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration; import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; import org.springframework.data.elasticsearch.utils.IndexNameProvider; @@ -25,6 +28,7 @@ /** * @author Peter-Josef Meisch + * @author Haibo Liu * @since 4.4 */ @ContextConfiguration(classes = { CustomMethodRepositoryELCIntegrationTests.Config.class }) @@ -40,5 +44,19 @@ static class Config { IndexNameProvider indexNameProvider() { return new IndexNameProvider("custom-method-repository"); } + + /** + * a normal bean referenced by SpEL in query + */ + @Bean + QueryParameter queryParameter() { + return new QueryParameter("abc"); + } + + @Bean + public ElasticsearchCustomConversions elasticsearchCustomConversions() { + return new ElasticsearchCustomConversions(List.of(SamplePropertyToStringConverter.INSTANCE, + StringToSamplePropertyConverter.INSTANCE)); + } } } diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryIntegrationTests.java index c593a42a14..bdbc8b7750 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/CustomMethodRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -16,10 +16,10 @@ package org.springframework.data.elasticsearch.repositories.custommethod; import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; import static org.springframework.data.elasticsearch.annotations.FieldType.*; import static org.springframework.data.elasticsearch.utils.IdGenerator.*; -import java.lang.Long; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -29,11 +29,12 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledIf; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Version; import org.springframework.data.domain.Page; @@ -46,13 +47,14 @@ import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.Highlight; import org.springframework.data.elasticsearch.annotations.HighlightField; +import org.springframework.data.elasticsearch.annotations.HighlightParameters; import org.springframework.data.elasticsearch.annotations.Query; import org.springframework.data.elasticsearch.annotations.SourceFilters; -import org.springframework.data.elasticsearch.core.AbstractElasticsearchTemplate; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.SearchHit; import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.data.elasticsearch.core.SearchPage; +import org.springframework.data.elasticsearch.core.convert.ConversionException; import org.springframework.data.elasticsearch.core.geo.GeoBox; import org.springframework.data.elasticsearch.core.geo.GeoPoint; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; @@ -64,7 +66,8 @@ import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; -import org.springframework.lang.Nullable; +import org.springframework.data.repository.query.Param; +import org.springframework.util.Assert; /** * @author Rizwan Idrees @@ -76,6 +79,7 @@ * @author Peter-Josef Meisch * @author Rasmus Faber-Espensen * @author James Mudd + * @author Haibo Liu */ @SpringIntegrationTest public abstract class CustomMethodRepositoryIntegrationTests { @@ -1522,6 +1526,174 @@ void shouldReturnSearchHitsForStringQuery() { assertThat(searchHits.getTotalHits()).isEqualTo(20); } + @Test + void shouldRaiseExceptionForNullStringQuery() { + List entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + // when + ConversionException thrown = assertThrows(ConversionException.class, () -> repository.queryByString(null)); + + assertThat(thrown.getMessage()) + .contains("Parameter value can't be null for placeholder at index '0' in query", + "when querying elasticsearch"); + } + + @Test + void shouldReturnSearchHitsForStringQuerySpEL() { + List entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + // when + SearchHits searchHits = repository.queryByStringSpEL("abc"); + + assertThat(searchHits.getTotalHits()).isEqualTo(20); + } + + @Test + void shouldRaiseExceptionForNullStringQuerySpEL() { + List entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + ConversionException thrown = assertThrows(ConversionException.class, () -> repository.queryByStringSpEL(null)); + + assertThat(thrown.getMessage()) + .isEqualTo("Parameter value can't be null for SpEL expression '#type' in method 'queryByStringSpEL'" + + " when querying elasticsearch"); + } + + @Test + void shouldReturnSearchHitsForParameterPropertyQuerySpEL() { + List entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + QueryParameter param = new QueryParameter("abc"); + // when + SearchHits searchHits = repository.queryByParameterPropertySpEL(param); + + assertThat(searchHits.getTotalHits()).isEqualTo(20); + } + + @Test + void shouldReturnSearchHitsForBeanQuerySpEL() { + List entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + // when + SearchHits searchHits = repository.queryByBeanPropertySpEL(); + + assertThat(searchHits.getTotalHits()).isEqualTo(20); + } + + @Test + void shouldReturnSearchHitsForCollectionQuerySpEL() { + List entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + // when + SearchHits searchHits = repository.queryByCollectionSpEL(List.of("abc")); + + assertThat(searchHits.getTotalHits()).isEqualTo(20); + } + + @Test + void shouldRaiseExceptionForNullCollectionQuerySpEL() { + List entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + ConversionException thrown = assertThrows(ConversionException.class, () -> repository.queryByCollectionSpEL(null)); + + assertThat(thrown.getMessage()) + .isEqualTo("Parameter value can't be null for SpEL expression '#types' in method 'queryByCollectionSpEL'" + + " when querying elasticsearch"); + } + + @Test + void shouldNotReturnSearchHitsForEmptyCollectionQuerySpEL() { + List entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + // when + SearchHits searchHits = repository.queryByCollectionSpEL(List.of()); + + assertThat(searchHits.getTotalHits()).isEqualTo(0); + } + + @Test + void shouldNotReturnSearchHitsForCollectionQueryWithOnlyNullValuesSpEL() { + List entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + List params = new ArrayList<>(); + params.add(null); + // when + SearchHits searchHits = repository.queryByCollectionSpEL(params); + + assertThat(searchHits.getTotalHits()).isEqualTo(0); + } + + @Test + void shouldIgnoreNullValuesInCollectionQuerySpEL() { + List entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + // when + SearchHits searchHits = repository.queryByCollectionSpEL(Arrays.asList("abc", null)); + + assertThat(searchHits.getTotalHits()).isEqualTo(20); + } + + @Test + void shouldReturnSearchHitsForParameterPropertyCollectionQuerySpEL() { + List entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + QueryParameter param = new QueryParameter("abc"); + // when + SearchHits searchHits = repository.queryByParameterPropertyCollectionSpEL(List.of(param)); + + assertThat(searchHits.getTotalHits()).isEqualTo(20); + } + + @Test + void shouldReturnSearchHitsForParameterPropertyCollectionQuerySpELWithParamAnnotation() { + List entities = createSampleEntities("abc", 20); + repository.saveAll(entities); + + QueryParameter param = new QueryParameter("abc"); + // when + SearchHits searchHits = repository.queryByParameterPropertyCollectionSpELWithParamAnnotation( + List.of(param)); + + assertThat(searchHits.getTotalHits()).isEqualTo(20); + } + + @Test + void shouldReturnSearchHitsBySampleProperty() { + List entities = createSampleEntities("abc", 20); + entities.forEach(e -> e.setSampleProperty(new SampleProperty("hello", "world"))); + repository.saveAll(entities); + + SampleProperty sampleProperty = new SampleProperty("hello", "world"); + // when + SearchHits searchHits = repository.queryBySampleProperty(sampleProperty); + + assertThat(searchHits.getTotalHits()).isEqualTo(20); + } + + @Test + void shouldReturnSearchHitsWithCustomConversionSpEL() { + List entities = createSampleEntities("abc", 20); + entities.forEach(e -> e.setSampleProperty(new SampleProperty("hello", "world"))); + repository.saveAll(entities); + + SampleProperty sampleProperty = new SampleProperty("hello", "world"); + // when + SearchHits searchHits = repository.queryBySamplePropertySpEL(sampleProperty); + + assertThat(searchHits.getTotalHits()).isEqualTo(20); + } + @Test // DATAES-372 void shouldReturnHighlightsOnAnnotatedMethod() { List entities = createSampleEntities("abc", 2); @@ -1548,6 +1720,46 @@ void shouldReturnHighlightsOnAnnotatedStringQueryMethod() { assertThat(searchHit.getHighlightField("type")).hasSize(1).contains("abc"); } + @Test + void shouldReturnDifferentHighlightsOnAnnotatedStringQueryMethod() { + List entities = createSampleEntities("abc xyz", 2); + repository.saveAll(entities); + + // when + SearchHits highlightAbcHits = repository.queryByStringWithSeparateHighlight("abc", "abc"); + + assertThat(highlightAbcHits.getTotalHits()).isEqualTo(2); + SearchHit highlightAbcHit = highlightAbcHits.getSearchHit(0); + assertThat(highlightAbcHit.getHighlightField("type")).hasSize(1).contains("abc xyz"); + + // when + SearchHits highlightXyzHits = repository.queryByStringWithSeparateHighlight("abc", "xyz"); + + assertThat(highlightXyzHits.getTotalHits()).isEqualTo(2); + SearchHit highlightXyzHit = highlightXyzHits.getSearchHit(0); + assertThat(highlightXyzHit.getHighlightField("type")).hasSize(1).contains("abc xyz"); + } + + @Test + void shouldReturnDifferentHighlightsOnAnnotatedStringQueryMethodSpEL() { + List entities = createSampleEntities("abc xyz", 2); + repository.saveAll(entities); + + // when + SearchHits highlightAbcHits = repository.queryByStringWithSeparateHighlightSpEL("abc", "abc"); + + assertThat(highlightAbcHits.getTotalHits()).isEqualTo(2); + SearchHit highlightAbcHit = highlightAbcHits.getSearchHit(0); + assertThat(highlightAbcHit.getHighlightField("type")).hasSize(1).contains("abc xyz"); + + // when + SearchHits highlightXyzHits = repository.queryByStringWithSeparateHighlightSpEL("abc", "xyz"); + + assertThat(highlightXyzHits.getTotalHits()).isEqualTo(2); + SearchHit highlightXyzHit = highlightXyzHits.getSearchHit(0); + assertThat(highlightXyzHit.getHighlightField("type")).hasSize(1).contains("abc xyz"); + } + @Test // DATAES-734 void shouldUseGeoSortParameter() { GeoPoint munich = new GeoPoint(48.137154, 11.5761247); @@ -1746,6 +1958,28 @@ void shouldUseSourceIncludesFromParameter() { assertThat(foundEntity.getKeyword()).isNull(); } + @Test + @DisplayName("should use sourceIncludes from parameter SpEL") + void shouldUseSourceIncludesFromParameterSpEL() { + + SampleEntity entity = new SampleEntity(); + entity.setId("42"); + entity.setMessage("message"); + entity.setCustomFieldNameMessage("customFieldNameMessage"); + entity.setType("type"); + entity.setKeyword("keyword"); + repository.save(entity); + + var searchHits = repository.queryBy(List.of("message", "customFieldNameMessage")); + + assertThat(searchHits.hasSearchHits()).isTrue(); + var foundEntity = searchHits.getSearchHit(0).getContent(); + assertThat(foundEntity.getMessage()).isEqualTo("message"); + assertThat(foundEntity.getCustomFieldNameMessage()).isEqualTo("customFieldNameMessage"); + assertThat(foundEntity.getType()).isNull(); + assertThat(foundEntity.getKeyword()).isNull(); + } + @Test // #2146 @DisplayName("should use sourceExcludes from annotation") void shouldUseSourceExcludesFromAnnotation() { @@ -1790,6 +2024,28 @@ void shouldUseSourceExcludesFromParameter() { assertThat(foundEntity.getKeyword()).isNull(); } + @Test + @DisplayName("should use source excludes from parameter SpEL") + void shouldUseSourceExcludesFromParameterSpEL() { + + SampleEntity entity = new SampleEntity(); + entity.setId("42"); + entity.setMessage("message"); + entity.setCustomFieldNameMessage("customFieldNameMessage"); + entity.setType("type"); + entity.setKeyword("keyword"); + repository.save(entity); + + var searchHits = repository.getBy(List.of("type", "keyword")); + + assertThat(searchHits.hasSearchHits()).isTrue(); + var foundEntity = searchHits.getSearchHit(0).getContent(); + assertThat(foundEntity.getMessage()).isEqualTo("message"); + assertThat(foundEntity.getCustomFieldNameMessage()).isEqualTo("customFieldNameMessage"); + assertThat(foundEntity.getType()).isNull(); + assertThat(foundEntity.getKeyword()).isNull(); + } + private List createSampleEntities(String type, int numberOfEntities) { List entities = new ArrayList<>(); @@ -1916,9 +2172,204 @@ public interface SampleCustomMethodRepository extends ElasticsearchRepository queryByType(String type); + /** + * The parameter is annotated with {@link Nullable} deliberately to test that our placeholder parameter will not + * accept a null parameter as query value. + */ @Query("{\"bool\": {\"must\": [{\"term\": {\"type\": \"?0\"}}]}}") @Highlight(fields = { @HighlightField(name = "type") }) - SearchHits queryByString(String type); + SearchHits queryByString(@Nullable String type); + + /** + * The parameter is annotated with {@link Nullable} deliberately to test that our elasticsearch SpEL converters will + * not accept a null parameter as query value. + */ + @Query(""" + { + "bool":{ + "must":[ + { + "term":{ + "type": "#{#type}" + } + } + ] + } + } + """) + SearchHits queryByStringSpEL(@Nullable String type); + + @Query(""" + { + "bool":{ + "must":[ + { + "term":{ + "type": "#{#parameter.value}" + } + } + ] + } + } + """) + SearchHits queryByParameterPropertySpEL(QueryParameter parameter); + + @Query(""" + { + "bool":{ + "must":[ + { + "term":{ + "type": "#{@queryParameter.value}" + } + } + ] + } + } + """) + SearchHits queryByBeanPropertySpEL(); + + /** + * The parameter is annotated with {@link Nullable} deliberately to test that our elasticsearch SpEL converters will + * not accept a null parameter as query value. + */ + @Query(""" + { + "bool":{ + "must":[ + { + "terms":{ + "type": #{#types} + } + } + ] + } + } + """) + SearchHits queryByCollectionSpEL(@Nullable Collection types); + + @Query(""" + { + "bool":{ + "must":[ + { + "terms":{ + "type": #{#parameters.![value]} + } + } + ] + } + } + """) + SearchHits queryByParameterPropertyCollectionSpEL(Collection parameters); + + @Query(""" + { + "bool":{ + "must":[ + { + "terms":{ + "type": #{#e.![value]} + } + } + ] + } + } + """) + SearchHits queryByParameterPropertyCollectionSpELWithParamAnnotation( + @Param("e") Collection parameters); + + @Query(""" + { + "bool":{ + "must":[ + { + "term":{ + "sample_property": "?0" + } + } + ] + } + } + """) + SearchHits queryBySampleProperty(SampleProperty sampleProperty); + + @Query(""" + { + "bool":{ + "must":[ + { + "term":{ + "sample_property": "#{#sampleProperty}" + } + } + ] + } + } + """) + SearchHits queryBySamplePropertySpEL(SampleProperty sampleProperty); + + @Query(""" + { + "bool":{ + "must":[ + { + "match":{ + "type":"?0" + } + } + ] + } + } + """) + @Highlight( + fields = { @HighlightField(name = "type") }, + parameters = @HighlightParameters( + highlightQuery = @Query(""" + { + "bool":{ + "must":[ + { + "match":{ + "type":"?1" + } + } + ] + } + } + """))) + SearchHits queryByStringWithSeparateHighlight(String type, String highlight); + + @Query(""" + { + "bool":{ + "must":[ + { + "match":{ + "type":"#{#type}" + } + } + ] + } + } + """) + @Highlight( + fields = { @HighlightField(name = "type") }, + parameters = @HighlightParameters( + highlightQuery = @Query(""" + { + "bool":{ + "must":[ + { + "match":{ + "type":"#{#highlight}" + } + } + ] + } + } + """))) + SearchHits queryByStringWithSeparateHighlightSpEL(String type, String highlight); List> queryByMessage(String message); @@ -1951,6 +2402,9 @@ public interface SampleCustomMethodRepository extends ElasticsearchRepository searchBy(Collection sourceIncludes); + @SourceFilters(includes = "#{#sourceIncludes}") + SearchHits queryBy(Collection sourceIncludes); + @Query(""" { "match_all": {} @@ -1961,6 +2415,9 @@ public interface SampleCustomMethodRepository extends ElasticsearchRepository findBy(Collection sourceExcludes); + + @SourceFilters(excludes = "#{#sourceExcludes}") + SearchHits getBy(Collection sourceExcludes); } public interface SampleStreamingCustomMethodRepository extends ElasticsearchRepository { @@ -1995,6 +2452,9 @@ static class SampleEntity { @Field(name = "custom_field_name", type = Text) @Nullable private String customFieldNameMessage; + @Field(name = "sample_property", type = Keyword) + @Nullable private SampleProperty sampleProperty; + @Nullable public String getId() { return id; @@ -2031,6 +2491,15 @@ public void setCustomFieldNameMessage(@Nullable String customFieldNameMessage) { this.customFieldNameMessage = customFieldNameMessage; } + @Nullable + public SampleProperty getSampleProperty() { + return sampleProperty; + } + + public void setSampleProperty(@Nullable SampleProperty sampleProperty) { + this.sampleProperty = sampleProperty; + } + @Nullable public String getKeyword() { return keyword; @@ -2065,13 +2534,60 @@ public void setLocation(@Nullable GeoPoint location) { this.location = location; } - @Nullable - public java.lang.Long getVersion() { + public java.lang.@Nullable Long getVersion() { return version; } - public void setVersion(@Nullable java.lang.Long version) { + public void setVersion(java.lang.@Nullable Long version) { this.version = version; } } + + static class SampleProperty { + @Nullable private String first; + @Nullable private String last; + + SampleProperty(@Nullable String first, @Nullable String last) { + this.first = first; + this.last = last; + } + + @Nullable + public String getFirst() { + return first; + } + + public void setFirst(@Nullable String first) { + this.first = first; + } + + @Nullable + public String getLast() { + return last; + } + + public void setLast(@Nullable String last) { + this.last = last; + } + } + + enum SamplePropertyToStringConverter implements Converter { + INSTANCE; + + @Override + public String convert(SampleProperty sampleProperty) { + return sampleProperty.getFirst() + '-' + sampleProperty.getLast(); + } + } + + enum StringToSamplePropertyConverter implements Converter { + INSTANCE; + + @Override + public SampleProperty convert(String string) { + String[] splits = string.split("-"); + Assert.isTrue(splits.length == 2, "only 1 '-' should be in sample property value"); + return new SampleProperty(splits[0], splits[1]); + } + } } diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/QueryParameter.java b/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/QueryParameter.java new file mode 100644 index 0000000000..7cac5dd15a --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/custommethod/QueryParameter.java @@ -0,0 +1,25 @@ +/* + * 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.repositories.custommethod; + +/** + * Used as a parameter referenced by SpEL in query method tests. + * + * @param value content + * @author Haibo Liu + */ +public record QueryParameter(String value) { +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/doubleid/DoubleIDRepositoryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/doubleid/DoubleIDRepositoryELCIntegrationTests.java index 3e0a738ef1..692a973f79 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/doubleid/DoubleIDRepositoryELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/doubleid/DoubleIDRepositoryELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/repositories/doubleid/DoubleIDRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/doubleid/DoubleIDRepositoryIntegrationTests.java index 688159f770..a6a191f777 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/doubleid/DoubleIDRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/doubleid/DoubleIDRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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/test/java/org/springframework/data/elasticsearch/repositories/dynamicindex/DynamicIndexEntityELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/dynamicindex/DynamicIndexEntityELCIntegrationTests.java index 7f4131ef41..2fabf32ed5 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/dynamicindex/DynamicIndexEntityELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/dynamicindex/DynamicIndexEntityELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/repositories/dynamicindex/DynamicIndexEntityIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/dynamicindex/DynamicIndexEntityIntegrationTests.java index 4d6002a5f3..5f5160262f 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/dynamicindex/DynamicIndexEntityIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/dynamicindex/DynamicIndexEntityIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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/test/java/org/springframework/data/elasticsearch/repositories/geo/GeoRepositoryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/geo/GeoRepositoryELCIntegrationTests.java index 4e154b5e16..06de58ecc8 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/geo/GeoRepositoryELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/geo/GeoRepositoryELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/repositories/geo/GeoRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/geo/GeoRepositoryIntegrationTests.java index 214876eb88..c876044d8a 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/geo/GeoRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/geo/GeoRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-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.Locale; import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -38,7 +39,6 @@ import org.springframework.data.geo.Circle; import org.springframework.data.geo.Point; import org.springframework.data.geo.Polygon; -import org.springframework.lang.Nullable; /** * @author Mark Paluch diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/integer/IntegerIDRepositoryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/integer/IntegerIDRepositoryELCIntegrationTests.java index 5c49356ca8..ea6f41f336 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/integer/IntegerIDRepositoryELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/integer/IntegerIDRepositoryELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/repositories/integer/IntegerIDRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/integer/IntegerIDRepositoryIntegrationTests.java index 54f7e8cb6d..12712e9b40 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/integer/IntegerIDRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/integer/IntegerIDRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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/test/java/org/springframework/data/elasticsearch/repositories/knn/KnnSearchELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/knn/KnnSearchELCIntegrationTests.java new file mode 100644 index 0000000000..3cbee4163d --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/knn/KnnSearchELCIntegrationTests.java @@ -0,0 +1,44 @@ +/* + * 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.repositories.knn; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration; +import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Haibo Liu + * @since 5.4 + */ +@ContextConfiguration(classes = { KnnSearchELCIntegrationTests.Config.class }) +public class KnnSearchELCIntegrationTests extends KnnSearchIntegrationTests { + + @Configuration + @Import({ ElasticsearchTemplateConfiguration.class }) + @EnableElasticsearchRepositories( + basePackages = { "org.springframework.data.elasticsearch.repositories.knn" }, + considerNestedRepositories = true) + static class Config { + @Bean + IndexNameProvider indexNameProvider() { + return new IndexNameProvider("knn-repository"); + } + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/knn/KnnSearchIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/knn/KnnSearchIntegrationTests.java new file mode 100644 index 0000000000..12af3eccee --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/knn/KnnSearchIntegrationTests.java @@ -0,0 +1,177 @@ +/* + * 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.repositories.knn; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.elasticsearch.annotations.FieldType.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.annotation.Id; +import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldElementType; +import org.springframework.data.elasticsearch.annotations.FieldType; +import org.springframework.data.elasticsearch.annotations.KnnAlgorithmType; +import org.springframework.data.elasticsearch.annotations.KnnIndexOptions; +import org.springframework.data.elasticsearch.annotations.KnnSimilarity; +import org.springframework.data.elasticsearch.client.elc.NativeQuery; +import org.springframework.data.elasticsearch.client.elc.NativeQueryBuilder; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHit; +import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; + +/** + * @author Haibo Liu + * @since 5.4 + */ +@SpringIntegrationTest +public abstract class KnnSearchIntegrationTests { + + @Autowired ElasticsearchOperations operations; + @Autowired private IndexNameProvider indexNameProvider; + @Autowired private VectorEntityRepository vectorEntityRepository; + + @BeforeEach + public void before() { + indexNameProvider.increment(); + operations.indexOps(VectorEntity.class).createWithMapping(); + } + + @Test + @org.junit.jupiter.api.Order(java.lang.Integer.MAX_VALUE) + void cleanup() { + operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete(); + } + + private List createVectorEntities(int n) { + List entities = new ArrayList<>(); + float increment = 1.0f / n; + for (int i = 0; i < n; i++) { + VectorEntity entity = new VectorEntity(); + entity.setId(UUID.randomUUID().toString()); + entity.setMessage("top" + (i + 1)); + + // The generated vector is always in the first quadrant, from the x-axis direction to the y-axis direction + float[] vector = new float[] { 1.0f - i * increment, increment }; + entity.setVector(vector); + entities.add(entity); + } + + return entities; + } + + @Test + public void shouldReturnXAxisVector() { + + // given + List entities = createVectorEntities(5); + vectorEntityRepository.saveAll(entities); + List xAxisVector = List.of(100f, 0f); + + // when + NativeQuery query = new NativeQueryBuilder() + .withKnnSearches(ksb -> ksb.queryVector(xAxisVector).k(3).field("vector")) + .withPageable(Pageable.ofSize(2)) + .build(); + SearchHits result = operations.search(query, VectorEntity.class); + + List vectorEntities = result.getSearchHits().stream().map(SearchHit::getContent).toList(); + + // then + assertThat(result).isNotNull(); + assertThat(result.getTotalHits()).isEqualTo(3L); + // should return the first vector, because it's near x-axis + assertThat(vectorEntities.get(0).getMessage()).isEqualTo("top1"); + } + + @Test + public void shouldReturnYAxisVector() { + + // given + List entities = createVectorEntities(10); + vectorEntityRepository.saveAll(entities); + List yAxisVector = List.of(0f, 100f); + + // when + NativeQuery query = new NativeQueryBuilder() + .withKnnSearches(ksb -> ksb.queryVector(yAxisVector).k(3).field("vector")) + .withPageable(Pageable.ofSize(2)) + .build(); + SearchHits result = operations.search(query, VectorEntity.class); + + List vectorEntities = result.getSearchHits().stream().map(SearchHit::getContent).toList(); + + // then + assertThat(result).isNotNull(); + assertThat(result.getTotalHits()).isEqualTo(3L); + // should return the last vector, because it's near y-axis + assertThat(vectorEntities.get(0).getMessage()).isEqualTo("top10"); + } + + public interface VectorEntityRepository extends ElasticsearchRepository {} + + @Document(indexName = "#{@indexNameProvider.indexName()}") + static class VectorEntity { + @Nullable + @Id private String id; + + @Nullable + @Field(type = Keyword) private String message; + + @Field(type = FieldType.Dense_Vector, dims = 2, elementType = FieldElementType.FLOAT, + knnIndexOptions = @KnnIndexOptions(type = KnnAlgorithmType.HNSW, m = 16, efConstruction = 100), + knnSimilarity = KnnSimilarity.COSINE) private float[] vector; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public String getMessage() { + return message; + } + + public void setMessage(@Nullable String message) { + this.message = message; + } + + @Nullable + public float[] getVector() { + return vector; + } + + public void setVector(@Nullable float[] vector) { + this.vector = vector; + } + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/nestedobject/InnerObjectIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/nestedobject/InnerObjectIntegrationTests.java index 2881089f5a..8c744e28fe 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/nestedobject/InnerObjectIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/nestedobject/InnerObjectIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 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. @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -38,7 +39,6 @@ import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Mohsin Husen diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/setting/dynamic/DynamicSettingAndMappingEntityRepositoryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/setting/dynamic/DynamicSettingAndMappingEntityRepositoryELCIntegrationTests.java index df924e640c..1d808a83b7 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/setting/dynamic/DynamicSettingAndMappingEntityRepositoryELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/setting/dynamic/DynamicSettingAndMappingEntityRepositoryELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/repositories/setting/dynamic/DynamicSettingAndMappingEntityRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/setting/dynamic/DynamicSettingAndMappingEntityRepositoryIntegrationTests.java index f73dc6a734..c02318a21c 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/setting/dynamic/DynamicSettingAndMappingEntityRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/setting/dynamic/DynamicSettingAndMappingEntityRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 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/test/java/org/springframework/data/elasticsearch/repositories/setting/fielddynamic/FieldDynamicMappingEntityRepositoryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/setting/fielddynamic/FieldDynamicMappingEntityRepositoryELCIntegrationTests.java index 96c5b44f62..72eb6819ce 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/setting/fielddynamic/FieldDynamicMappingEntityRepositoryELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/setting/fielddynamic/FieldDynamicMappingEntityRepositoryELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/repositories/setting/fielddynamic/FieldDynamicMappingEntityRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/setting/fielddynamic/FieldDynamicMappingEntityRepositoryIntegrationTests.java index 2af4337de7..ac327d26be 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/setting/fielddynamic/FieldDynamicMappingEntityRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/setting/fielddynamic/FieldDynamicMappingEntityRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 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/test/java/org/springframework/data/elasticsearch/repositories/spel/SpELEntityELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/spel/SpELEntityELCIntegrationTests.java index a7bc1d0477..4939f21760 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/spel/SpELEntityELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/spel/SpELEntityELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/repositories/spel/SpELEntityIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/spel/SpELEntityIntegrationTests.java index 1c81ec80e0..a5c403991a 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/spel/SpELEntityIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/spel/SpELEntityIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 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/test/java/org/springframework/data/elasticsearch/repositories/synonym/SynonymRepositoryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/synonym/SynonymRepositoryELCIntegrationTests.java index e4ef24e91f..683a31cbd3 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/synonym/SynonymRepositoryELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/synonym/SynonymRepositoryELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/repositories/synonym/SynonymRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/synonym/SynonymRepositoryIntegrationTests.java index ec727bc37c..d28685a162 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/synonym/SynonymRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/synonym/SynonymRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 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. @@ -17,6 +17,7 @@ import static org.assertj.core.api.Assertions.*; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -34,7 +35,6 @@ import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * SynonymRepositoryIntegrationTests diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/uuidkeyed/UUIDElasticsearchRepositoryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/uuidkeyed/UUIDElasticsearchRepositoryELCIntegrationTests.java index c0d328de6d..c507c22544 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/uuidkeyed/UUIDElasticsearchRepositoryELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/uuidkeyed/UUIDElasticsearchRepositoryELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/repositories/uuidkeyed/UUIDElasticsearchRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repositories/uuidkeyed/UUIDElasticsearchRepositoryIntegrationTests.java index c9a3b2522f..cb279b7ee3 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/uuidkeyed/UUIDElasticsearchRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/uuidkeyed/UUIDElasticsearchRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -24,6 +24,7 @@ import java.util.Optional; import java.util.UUID; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -38,13 +39,11 @@ import org.springframework.data.elasticsearch.annotations.FieldType; import org.springframework.data.elasticsearch.annotations.ScriptedField; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; -import org.springframework.data.elasticsearch.core.IndexOperations; import org.springframework.data.elasticsearch.core.geo.GeoPoint; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Gad Akuka @@ -61,20 +60,18 @@ public abstract class UUIDElasticsearchRepositoryIntegrationTests { @Autowired private SampleUUIDKeyedElasticsearchRepository repository; @Autowired ElasticsearchOperations operations; - private IndexOperations indexOperations; @Autowired IndexNameProvider indexNameProvider; @BeforeEach public void before() { indexNameProvider.increment(); - indexOperations = operations.indexOps(SampleEntityUUIDKeyed.class); - indexOperations.createWithMapping(); + operations.indexOps(SampleEntityUUIDKeyed.class).createWithMapping(); } @Test @org.junit.jupiter.api.Order(Integer.MAX_VALUE) void cleanup() { - operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete(); + operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + '*')).delete(); } @Test diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoriesRegistrarTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoriesRegistrarTests.java index fad4bd83a3..67066c2204 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoriesRegistrarTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoriesRegistrarTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -18,6 +18,7 @@ import static org.springframework.data.elasticsearch.annotations.FieldType.*; import org.assertj.core.api.Assertions; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -30,7 +31,6 @@ import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchTemplateConfiguration; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository; -import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfiguration; /** @@ -58,7 +58,7 @@ public void testConfiguration() { Assertions.assertThat(repository).isNotNull(); // there is an index to delete after this test - operations.indexOps(ReactiveElasticsearchRepositoryConfigurationExtensionUnitTests.SwCharacter.class).delete() + operations.indexOps(SampleEntity.class).delete() .block(); } diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoryConfigurationExtensionUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoryConfigurationExtensionUnitTests.java index ad59df818c..2dc981dc67 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoryConfigurationExtensionUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/config/ReactiveElasticsearchRepositoryConfigurationExtensionUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -26,7 +26,7 @@ import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; -import org.springframework.core.type.StandardAnnotationMetadata; +import org.springframework.core.type.AnnotationMetadata; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository; import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; @@ -40,13 +40,13 @@ */ public class ReactiveElasticsearchRepositoryConfigurationExtensionUnitTests { - StandardAnnotationMetadata metadata = new StandardAnnotationMetadata(Config.class, true); + AnnotationMetadata metadata = AnnotationMetadata.introspect(Config.class); ResourceLoader loader = new PathMatchingResourcePatternResolver(); Environment environment = new StandardEnvironment(); BeanDefinitionRegistry registry = new DefaultListableBeanFactory(); RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata, - EnableReactiveElasticsearchRepositories.class, loader, environment, registry); + EnableReactiveElasticsearchRepositories.class, loader, environment, registry, null); @Test // DATAES-519 public void isStrictMatchIfDomainTypeIsAnnotatedWithDocument() { diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethodUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethodUnitTests.java index 4c46626963..00ec7a663c 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethodUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethodUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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. @@ -20,6 +20,7 @@ import java.lang.reflect.Method; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -32,7 +33,6 @@ import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQueryUnitTestBase.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQueryUnitTestBase.java deleted file mode 100644 index 454700a0d2..0000000000 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQueryUnitTestBase.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2021-2023 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.repository.query; - -import java.util.ArrayList; -import java.util.Collection; - -import org.springframework.core.convert.converter.Converter; -import org.springframework.data.convert.CustomConversions; -import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; -import org.springframework.data.elasticsearch.core.convert.ElasticsearchCustomConversions; -import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter; -import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; -import org.springframework.lang.Nullable; - -/** - * @author Peter-Josef Meisch - */ -public class ElasticsearchStringQueryUnitTestBase { - - protected ElasticsearchConverter setupConverter() { - MappingElasticsearchConverter converter = new MappingElasticsearchConverter( - new SimpleElasticsearchMappingContext()); - Collection> converters = new ArrayList<>(); - converters.add(ElasticsearchStringQueryUnitTests.CarConverter.INSTANCE); - CustomConversions customConversions = new ElasticsearchCustomConversions(converters); - converter.setConversions(customConversions); - converter.afterPropertiesSet(); - return converter; - } - - static class Car { - @Nullable private String name; - @Nullable private String model; - - @Nullable - public String getName() { - return name; - } - - public void setName(@Nullable String name) { - this.name = name; - } - - @Nullable - public String getModel() { - return model; - } - - public void setModel(@Nullable String model) { - this.model = model; - } - } - - enum CarConverter implements Converter { - INSTANCE; - - @Override - public String convert(ElasticsearchStringQueryUnitTests.Car car) { - return (car.getName() != null ? car.getName() : "null") + '-' - + (car.getModel() != null ? car.getModel() : "null"); - } - } - -} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethodUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethodUnitTests.java index af5c911a53..460ede8666 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethodUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethodUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -26,6 +26,7 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -45,7 +46,6 @@ import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; -import org.springframework.lang.Nullable; /** * @author Christoph Strobl @@ -77,7 +77,7 @@ public void rejectsNullMappingContext() throws Exception { assertThatThrownBy(() -> new ReactiveElasticsearchQueryMethod(method, new DefaultRepositoryMetadata(PersonRepository.class), new SpelAwareProxyProjectionFactory(), null)) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(IllegalArgumentException.class); } @Test // DATAES-519 diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositoryQueryUnitTestsBase.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositoryQueryUnitTestsBase.java new file mode 100644 index 0000000000..4ab8196324 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositoryQueryUnitTestsBase.java @@ -0,0 +1,73 @@ +/* + * 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.repository.query; + +import static org.mockito.Mockito.*; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; +import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; +import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter; +import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; + +@ExtendWith(MockitoExtension.class) +public class ReactiveRepositoryQueryUnitTestsBase { + + @Mock ReactiveElasticsearchOperations operations; + + /** + * set up the {operations} mock to return the {@link ElasticsearchConverter} from setupConverter(). + */ + @BeforeEach + public void setUp() { + when(operations.getElasticsearchConverter()).thenReturn(setupConverter()); + } + + /** + * @return a simple {@link MappingElasticsearchConverter} with no special setup. + */ + protected MappingElasticsearchConverter setupConverter() { + return new MappingElasticsearchConverter( + new SimpleElasticsearchMappingContext()); + } + + /** + * Creates a {@link ReactiveElasticsearchQueryMethod} for the given method + * + * @param repositoryClass + * @param name + * @param parameters + * @return + * @throws NoSuchMethodException + */ + + protected ReactiveElasticsearchQueryMethod getQueryMethod(Class repositoryClass, String name, + Class... parameters) + throws NoSuchMethodException { + + Method method = repositoryClass.getMethod(name, parameters); + return new ReactiveElasticsearchQueryMethod(method, + new DefaultRepositoryMetadata(repositoryClass), + new SpelAwareProxyProjectionFactory(), operations.getElasticsearchConverter().getMappingContext()); + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositorySearchTemplateQueryUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositorySearchTemplateQueryUnitTests.java new file mode 100644 index 0000000000..9e6e89c19e --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositorySearchTemplateQueryUnitTests.java @@ -0,0 +1,113 @@ +/* + * 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.repository.query; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Arrays; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.domain.Sort; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.SearchTemplateQuery; +import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.query.Query; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; +import org.springframework.data.repository.query.ValueExpressionDelegate; + +public class ReactiveRepositorySearchTemplateQueryUnitTests extends ReactiveRepositoryQueryUnitTestsBase { + + @Test // #2997 + @DisplayName("should set searchtemplate id") + void shouldSetSearchTemplateId() throws NoSuchMethodException { + + var query = createQuery("searchWithArgs", "answer", 42); + + assertThat(query).isInstanceOf(org.springframework.data.elasticsearch.core.query.SearchTemplateQuery.class); + var searchTemplateQuery = (org.springframework.data.elasticsearch.core.query.SearchTemplateQuery) query; + + assertThat(searchTemplateQuery.getId()).isEqualTo("searchtemplate-42"); + } + + @Test // #2997 + @DisplayName("should set searchtemplate parameters") + void shouldSetSearchTemplateParameters() throws NoSuchMethodException { + + var query = createQuery("searchWithArgs", "answer", 42); + + assertThat(query).isInstanceOf(org.springframework.data.elasticsearch.core.query.SearchTemplateQuery.class); + var searchTemplateQuery = (org.springframework.data.elasticsearch.core.query.SearchTemplateQuery) query; + + var params = searchTemplateQuery.getParams(); + assertThat(params).isNotNull().hasSize(2); + assertThat(params.get("stringArg")).isEqualTo("answer"); + assertThat(params.get("intArg")).isEqualTo(42); + } + + // region helper methods + private Query createQuery(String methodName, Object... args) throws NoSuchMethodException { + Class[] argTypes = Arrays.stream(args).map(Object::getClass).toArray(Class[]::new); + ReactiveElasticsearchQueryMethod queryMethod = getQueryMethod(SampleRepository.class, methodName, argTypes); + + ReactiveRepositorySearchTemplateQuery repositorySearchTemplateQuery = queryForMethod(queryMethod); + + return repositorySearchTemplateQuery.createQuery(new ElasticsearchParametersParameterAccessor(queryMethod, args)); + } + + private ReactiveRepositorySearchTemplateQuery queryForMethod(ReactiveElasticsearchQueryMethod queryMethod) { + return new ReactiveRepositorySearchTemplateQuery(queryMethod, operations, ValueExpressionDelegate.create(), + queryMethod.getAnnotatedSearchTemplateQuery().id()); + } + // endregion + + // region test data + private interface SampleRepository extends ElasticsearchRepository { + @SearchTemplateQuery(id = "searchtemplate-42") + SearchHits searchWithArgs(String stringArg, Integer intArg); + + @SearchTemplateQuery(id = "searchtemplate-42") + SearchHits searchWithArgsAndSort(String stringArg, Integer intArg, Sort sort); + } + + @Document(indexName = "not-relevant") + static class SampleEntity { + @Nullable + @Id String id; + @Nullable String data; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public String getData() { + return data; + } + + public void setData(@Nullable String data) { + this.data = data; + } + } + // endregion +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQueryUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositoryStringQueryUnitTests.java similarity index 53% rename from src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQueryUnitTests.java rename to src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositoryStringQueryUnitTests.java index 20747b986c..c6b93802b7 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQueryUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositoryStringQueryUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -16,65 +16,101 @@ package org.springframework.data.elasticsearch.repository.query; import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; +import org.springframework.data.convert.CustomConversions; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; import org.springframework.data.elasticsearch.annotations.InnerField; import org.springframework.data.elasticsearch.annotations.MultiField; import org.springframework.data.elasticsearch.annotations.Query; -import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; import org.springframework.data.elasticsearch.core.SearchHit; +import org.springframework.data.elasticsearch.core.convert.ElasticsearchCustomConversions; +import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter; import org.springframework.data.elasticsearch.core.query.StringQuery; -import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.elasticsearch.repositories.custommethod.QueryParameter; import org.springframework.data.repository.Repository; -import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; +import org.springframework.data.repository.query.ValueExpressionDelegate; /** * @author Christoph Strobl * @author Peter-Josef Meisch + * @author Haibo Liu */ @ExtendWith(MockitoExtension.class) -public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryUnitTestBase { +public class ReactiveRepositoryStringQueryUnitTests extends ReactiveRepositoryQueryUnitTestsBase { - SpelExpressionParser PARSER = new SpelExpressionParser(); + /** + * Adds some data class and custom conversion to the base class implementation. + */ + protected MappingElasticsearchConverter setupConverter() { + + Collection> converters = new ArrayList<>(); + converters.add(CarConverter.INSTANCE); + CustomConversions customConversions = new ElasticsearchCustomConversions(converters); + + MappingElasticsearchConverter converter = super.setupConverter(); + converter.setConversions(customConversions); + converter.afterPropertiesSet(); + return converter; + } + + static class Car { + @Nullable private String name; + @Nullable private String model; + + @Nullable + public String getName() { + return name; + } + + public void setName(@Nullable String name) { + this.name = name; + } + + @Nullable + public String getModel() { + return model; + } + + public void setModel(@Nullable String model) { + this.model = model; + } + } - @Mock ReactiveElasticsearchOperations operations; + enum CarConverter implements Converter { + INSTANCE; - @BeforeEach - public void setUp() { - when(operations.getElasticsearchConverter()).thenReturn(setupConverter()); + @Override + public String convert(Car car) { + return (car.getName() != null ? car.getName() : "null") + '-' + + (car.getModel() != null ? car.getModel() : "null"); + } } @Test // DATAES-519 public void bindsSimplePropertyCorrectly() throws Exception { - ReactiveElasticsearchStringQuery elasticsearchStringQuery = createQueryForMethod("findByName", String.class); - StubParameterAccessor accessor = new StubParameterAccessor("Luke"); - - org.springframework.data.elasticsearch.core.query.Query query = elasticsearchStringQuery.createQuery(accessor); + org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByName", "Luke"); StringQuery reference = new StringQuery("{ 'bool' : { 'must' : { 'term' : { 'name' : 'Luke' } } } }"); assertThat(query).isInstanceOf(StringQuery.class); @@ -82,20 +118,214 @@ public void bindsSimplePropertyCorrectly() throws Exception { } @Test // DATAES-519 - @Disabled("TODO: fix spel query integration") public void bindsExpressionPropertyCorrectly() throws Exception { - ReactiveElasticsearchStringQuery elasticsearchStringQuery = createQueryForMethod("findByNameWithExpression", - String.class); - StubParameterAccessor accessor = new StubParameterAccessor("Luke"); - - org.springframework.data.elasticsearch.core.query.Query query = elasticsearchStringQuery.createQuery(accessor); + org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNameWithExpression", "Luke"); StringQuery reference = new StringQuery("{ 'bool' : { 'must' : { 'term' : { 'name' : 'Luke' } } } }"); assertThat(query).isInstanceOf(StringQuery.class); assertThat(((StringQuery) query).getSource()).isEqualTo(reference.getSource()); } + @Test + public void shouldReplaceParametersSpEL() throws Exception { + + org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNameSpEL", "Luke"); + String expected = """ + { + "bool":{ + "must":{ + "term":{ + "name": "Luke" + } + } + } + } + """; + + assertThat(query).isInstanceOf(StringQuery.class); + JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void shouldReplaceParametersSpELWithQuotes() throws Exception { + + org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNameSpEL", + "hello \"world\""); + String expected = """ + { + "bool":{ + "must":{ + "term":{ + "name": "hello \\"world\\"" + } + } + } + } + """; + + assertThat(query).isInstanceOf(StringQuery.class); + JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void shouldUseParameterPropertySpEL() throws Exception { + + org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByParameterPropertySpEL", + new QueryParameter("Luke")); + String expected = """ + { + "bool":{ + "must":{ + "term":{ + "name": "Luke" + } + } + } + } + """; + + assertThat(query).isInstanceOf(StringQuery.class); + JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void shouldReplaceCollectionSpEL() throws Exception { + + final List anotherString = List.of("hello \"Stranger\"", "Another string"); + List params = new ArrayList<>(anotherString); + org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params); + String expected = """ + { + "bool":{ + "must":{ + "terms":{ + "name": ["hello \\"Stranger\\"", "Another string"] + } + } + } + } + """; + + assertThat(query).isInstanceOf(StringQuery.class); + JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void shouldReplaceNonStringCollectionSpEL() throws Exception { + + final List ages = List.of(1, 2, 3); + List params = new ArrayList<>(ages); + org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByAgesSpEL", params); + String expected = """ + { + "bool":{ + "must":{ + "terms":{ + "age": [1, 2, 3] + } + } + } + } + """; + + assertThat(query).isInstanceOf(StringQuery.class); + JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void shouldReplaceEmptyCollectionSpEL() throws Exception { + + final List anotherString = List.of(); + List params = new ArrayList<>(anotherString); + org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params); + String expected = """ + { + "bool":{ + "must":{ + "terms":{ + "name": [] + } + } + } + } + """; + + assertThat(query).isInstanceOf(StringQuery.class); + JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void shouldBeEmptyWithNullValuesInCollectionSpEL() throws Exception { + + final List anotherString = List.of(); + List params = new ArrayList<>(anotherString); + // add a null value + params.add(null); + org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params); + String expected = """ + { + "bool":{ + "must":{ + "terms":{ + "name": [] + } + } + } + } + """; + + assertThat(query).isInstanceOf(StringQuery.class); + JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void shouldIgnoreNullValuesInCollectionSpEL() throws Exception { + + final List anotherString = List.of("abc"); + List params = new ArrayList<>(anotherString); + // add a null value + params.add(null); + org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params); + String expected = """ + { + "bool":{ + "must":{ + "terms":{ + "name": ["abc"] + } + } + } + } + """; + + assertThat(query).isInstanceOf(StringQuery.class); + JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void shouldReplaceCollectionParametersSpEL() throws Exception { + + final List anotherString = List.of(new QueryParameter("hello \"Stranger\""), + new QueryParameter("Another string")); + List params = new ArrayList<>(anotherString); + org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesParameterSpEL", params); + String expected = """ + { + "bool":{ + "must":{ + "terms":{ + "name": ["hello \\"Stranger\\"", "Another string"] + } + } + } + } + """; + + assertThat(query).isInstanceOf(StringQuery.class); + JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE); + } + @Test // DATAES-552 public void shouldReplaceLotsOfParametersCorrectly() throws Exception { @@ -175,29 +405,21 @@ private org.springframework.data.elasticsearch.core.query.Query createQuery(Stri Class[] argTypes = Arrays.stream(args).map(Object::getClass) .map(clazz -> Collection.class.isAssignableFrom(clazz) ? List.class : clazz).toArray(Class[]::new); - ReactiveElasticsearchQueryMethod queryMethod = getQueryMethod(methodName, argTypes); - ReactiveElasticsearchStringQuery elasticsearchStringQuery = queryForMethod(queryMethod); + ReactiveElasticsearchQueryMethod queryMethod = getQueryMethod(SampleRepository.class, methodName, argTypes); + ReactiveRepositoryStringQuery elasticsearchStringQuery = queryForMethod(queryMethod); return elasticsearchStringQuery.createQuery(new ElasticsearchParametersParameterAccessor(queryMethod, args)); } - private ReactiveElasticsearchStringQuery queryForMethod(ReactiveElasticsearchQueryMethod queryMethod) { - return new ReactiveElasticsearchStringQuery(queryMethod, operations, PARSER, - QueryMethodEvaluationContextProvider.DEFAULT); - } - - private ReactiveElasticsearchQueryMethod getQueryMethod(String name, Class... parameters) - throws NoSuchMethodException { + private ReactiveRepositoryStringQuery createQueryForMethod(String name, Class... parameters) throws Exception { - Method method = SampleRepository.class.getMethod(name, parameters); - return new ReactiveElasticsearchQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class), - new SpelAwareProxyProjectionFactory(), operations.getElasticsearchConverter().getMappingContext()); + ReactiveElasticsearchQueryMethod queryMethod = getQueryMethod(SampleRepository.class, name, parameters); + return queryForMethod(queryMethod); } - private ReactiveElasticsearchStringQuery createQueryForMethod(String name, Class... parameters) throws Exception { - - ReactiveElasticsearchQueryMethod queryMethod = getQueryMethod(name, parameters); - return queryForMethod(queryMethod); + private ReactiveRepositoryStringQuery queryForMethod(ReactiveElasticsearchQueryMethod queryMethod) { + return new ReactiveRepositoryStringQuery(queryMethod, operations, + ValueExpressionDelegate.create()); } private interface SampleRepository extends Repository { @@ -205,7 +427,59 @@ private interface SampleRepository extends Repository { @Query("{ 'bool' : { 'must' : { 'term' : { 'name' : '?0' } } } }") Mono findByName(String name); - @Query("{ 'bool' : { 'must' : { 'term' : { 'name' : '?#{[0]}' } } } }") + @Query(""" + { + "bool":{ + "must":{ + "term":{ + "name": "#{#name}" + } + } + } + } + """) + Mono findByNameSpEL(String name); + + @Query(""" + { + "bool":{ + "must":{ + "terms":{ + "name": #{#names} + } + } + } + } + """) + Flux findByNamesSpEL(List names); + + @Query(""" + { + "bool":{ + "must":{ + "term":{ + "name": "#{#param.value}" + } + } + } + } + """) + Flux findByParameterPropertySpEL(QueryParameter param); + + @Query(""" + { + "bool":{ + "must":{ + "terms":{ + "name": #{#names.![value]} + } + } + } + } + """) + Flux findByNamesParameterSpEL(List names); + + @Query("{ 'bool' : { 'must' : { 'term' : { 'name' : '#{[0]}' } } } }") Flux findByNameWithExpression(String param0); @Query(value = "name:(?0, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)") @@ -228,6 +502,18 @@ Person findWithRepeatedPlaceholder(String arg0, String arg1, String arg2, String @Query("{ 'bool' : { 'must' : { 'terms' : { 'ages' : ?0 } } } }") Flux findByAges(List ages); + @Query(""" + { + "bool":{ + "must":{ + "terms":{ + "age": #{#ages} + } + } + } + } + """) + Flux findByAgesSpEL(List ages); } /** diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositoryQueryUnitTestsBase.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositoryQueryUnitTestsBase.java new file mode 100644 index 0000000000..3d80c995af --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositoryQueryUnitTestsBase.java @@ -0,0 +1,71 @@ +/* + * 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.repository.query; + +import static org.mockito.Mockito.*; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; +import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter; +import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; + +@ExtendWith(MockitoExtension.class) +public class RepositoryQueryUnitTestsBase { + + @Mock ElasticsearchOperations operations; + + /** + * set up the {operations} mock to return the {@link ElasticsearchConverter} from setupConverter(). + */ + @BeforeEach + public void setUp() { + when(operations.getElasticsearchConverter()).thenReturn(setupConverter()); + } + + /** + * @return a simple {@link MappingElasticsearchConverter} with no special setup. + */ + protected MappingElasticsearchConverter setupConverter() { + return new MappingElasticsearchConverter( + new SimpleElasticsearchMappingContext()); + } + + /** + * Creates a {@link ElasticsearchQueryMethod} for the given method + * + * @param repositoryClass + * @param name + * @param parameters + * @return + * @throws NoSuchMethodException + */ + protected ElasticsearchQueryMethod getQueryMethod(Class repositoryClass, String name, Class... parameters) + throws NoSuchMethodException { + + Method method = repositoryClass.getMethod(name, parameters); + return new ElasticsearchQueryMethod(method, new DefaultRepositoryMetadata(repositoryClass), + new SpelAwareProxyProjectionFactory(), operations.getElasticsearchConverter().getMappingContext()); + } + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositorySearchTemplateQueryUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositorySearchTemplateQueryUnitTests.java new file mode 100644 index 0000000000..6f0d996cf2 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositorySearchTemplateQueryUnitTests.java @@ -0,0 +1,113 @@ +/* + * 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.repository.query; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Arrays; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.domain.Sort; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.SearchTemplateQuery; +import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.query.Query; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; +import org.springframework.data.repository.query.ValueExpressionDelegate; + +public class RepositorySearchTemplateQueryUnitTests extends RepositoryQueryUnitTestsBase { + + @Test // #2997 + @DisplayName("should set searchtemplate id") + void shouldSetSearchTemplateId() throws NoSuchMethodException { + + var query = createQuery("searchWithArgs", "answer", 42); + + assertThat(query).isInstanceOf(org.springframework.data.elasticsearch.core.query.SearchTemplateQuery.class); + var searchTemplateQuery = (org.springframework.data.elasticsearch.core.query.SearchTemplateQuery) query; + + assertThat(searchTemplateQuery.getId()).isEqualTo("searchtemplate-42"); + } + + @Test // #2997 + @DisplayName("should set searchtemplate parameters") + void shouldSetSearchTemplateParameters() throws NoSuchMethodException { + + var query = createQuery("searchWithArgs", "answer", 42); + + assertThat(query).isInstanceOf(org.springframework.data.elasticsearch.core.query.SearchTemplateQuery.class); + var searchTemplateQuery = (org.springframework.data.elasticsearch.core.query.SearchTemplateQuery) query; + + var params = searchTemplateQuery.getParams(); + assertThat(params).isNotNull().hasSize(2); + assertThat(params.get("stringArg")).isEqualTo("answer"); + assertThat(params.get("intArg")).isEqualTo(42); + } + + // region helper methods + private Query createQuery(String methodName, Object... args) throws NoSuchMethodException { + Class[] argTypes = Arrays.stream(args).map(Object::getClass).toArray(Class[]::new); + ElasticsearchQueryMethod queryMethod = getQueryMethod(SampleRepository.class, methodName, argTypes); + + RepositorySearchTemplateQuery repositorySearchTemplateQuery = queryForMethod(queryMethod); + + return repositorySearchTemplateQuery.createQuery(new ElasticsearchParametersParameterAccessor(queryMethod, args)); + } + + private RepositorySearchTemplateQuery queryForMethod(ElasticsearchQueryMethod queryMethod) { + return new RepositorySearchTemplateQuery(queryMethod, operations, ValueExpressionDelegate.create(), + queryMethod.getAnnotatedSearchTemplateQuery().id()); + } + // endregion + + // region test data + private interface SampleRepository extends ElasticsearchRepository { + @SearchTemplateQuery(id = "searchtemplate-42") + SearchHits searchWithArgs(String stringArg, Integer intArg); + + @SearchTemplateQuery(id = "searchtemplate-42") + SearchHits searchWithArgsAndSort(String stringArg, Integer intArg, Sort sort); + } + + @Document(indexName = "not-relevant") + static class SampleEntity { + @Nullable + @Id String id; + @Nullable String data; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public String getData() { + return data; + } + + public void setData(@Nullable String data) { + this.data = data; + } + } + // endregion +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQueryUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositoryStringQueryUnitTests.java similarity index 52% rename from src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQueryUnitTests.java rename to src/test/java/org/springframework/data/elasticsearch/repository/query/RepositoryStringQueryUnitTests.java index 14931f3249..bb4595e478 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQueryUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositoryStringQueryUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -16,9 +16,7 @@ package org.springframework.data.elasticsearch.repository.query; import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -26,40 +24,81 @@ import java.util.List; import java.util.Map; -import org.junit.jupiter.api.BeforeEach; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; +import org.springframework.data.convert.CustomConversions; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; import org.springframework.data.elasticsearch.annotations.InnerField; import org.springframework.data.elasticsearch.annotations.MultiField; import org.springframework.data.elasticsearch.annotations.Query; -import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.convert.ElasticsearchCustomConversions; +import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter; import org.springframework.data.elasticsearch.core.query.StringQuery; -import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.elasticsearch.repositories.custommethod.QueryParameter; import org.springframework.data.repository.Repository; -import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; -import org.springframework.lang.Nullable; +import org.springframework.data.repository.query.ValueExpressionDelegate; /** * @author Christoph Strobl * @author Peter-Josef Meisch * @author Niklas Herder + * @author Haibo Liu */ -@ExtendWith(MockitoExtension.class) -public class ElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryUnitTestBase { +public class RepositoryStringQueryUnitTests extends RepositoryStringQueryUnitTestsBase { + /** + * Adds some data class and custom conversion to the base class implementation. + */ + protected MappingElasticsearchConverter setupConverter() { - @Mock ElasticsearchOperations operations; + Collection> converters = new ArrayList<>(); + converters.add(RepositoryStringQueryUnitTests.CarConverter.INSTANCE); + CustomConversions customConversions = new ElasticsearchCustomConversions(converters); - @BeforeEach - public void setUp() { - when(operations.getElasticsearchConverter()).thenReturn(setupConverter()); + MappingElasticsearchConverter converter = super.setupConverter(); + converter.setConversions(customConversions); + converter.afterPropertiesSet(); + return converter; + } + + static class Car { + @Nullable private String name; + @Nullable private String model; + + @Nullable + public String getName() { + return name; + } + + public void setName(@Nullable String name) { + this.name = name; + } + + @Nullable + public String getModel() { + return model; + } + + public void setModel(@Nullable String model) { + this.model = model; + } + } + + enum CarConverter implements Converter { + INSTANCE; + + @Override + public String convert(Car car) { + return (car.getName() != null ? car.getName() : "null") + '-' + + (car.getModel() != null ? car.getModel() : "null"); + } } @Test // DATAES-552 @@ -83,6 +122,205 @@ public void shouldReplaceRepeatedParametersCorrectly() throws Exception { .isEqualTo("name:(zero, eleven, one, two, three, four, five, six, seven, eight, nine, ten, eleven, zero, one)"); } + @Test + public void shouldReplaceParametersSpEL() throws Exception { + + org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNameSpEL", "Luke"); + String expected = """ + { + "bool":{ + "must":{ + "term":{ + "name": "Luke" + } + } + } + } + """; + + assertThat(query).isInstanceOf(StringQuery.class); + JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void shouldReplaceParametersSpELWithQuotes() throws Exception { + + org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNameSpEL", + "hello \"world\""); + String expected = """ + { + "bool":{ + "must":{ + "term":{ + "name": "hello \\"world\\"" + } + } + } + } + """; + + assertThat(query).isInstanceOf(StringQuery.class); + JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void shouldUseParameterPropertySpEL() throws Exception { + + org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByParameterPropertySpEL", + new QueryParameter("Luke")); + String expected = """ + { + "bool":{ + "must":{ + "term":{ + "name": "Luke" + } + } + } + } + """; + + assertThat(query).isInstanceOf(StringQuery.class); + JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void shouldReplaceCollectionSpEL() throws Exception { + + final List anotherString = List.of("hello \"Stranger\"", "Another string"); + List params = new ArrayList<>(anotherString); + org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params); + String expected = """ + { + "bool":{ + "must":{ + "terms":{ + "name": ["hello \\"Stranger\\"", "Another string"] + } + } + } + } + """; + + assertThat(query).isInstanceOf(StringQuery.class); + JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void shouldReplaceNonStringCollectionSpEL() throws Exception { + + final List ages = List.of(1, 2, 3); + List params = new ArrayList<>(ages); + org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByAgesSpEL", params); + String expected = """ + { + "bool":{ + "must":{ + "terms":{ + "age": [1, 2, 3] + } + } + } + } + """; + + assertThat(query).isInstanceOf(StringQuery.class); + JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void shouldReplaceEmptyCollectionSpEL() throws Exception { + + final List anotherString = List.of(); + List params = new ArrayList<>(anotherString); + org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params); + String expected = """ + { + "bool":{ + "must":{ + "terms":{ + "name": [] + } + } + } + } + """; + + assertThat(query).isInstanceOf(StringQuery.class); + JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void shouldBeEmptyWithNullValuesInCollectionSpEL() throws Exception { + + final List anotherString = List.of(); + List params = new ArrayList<>(anotherString); + // add a null value + params.add(null); + org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params); + String expected = """ + { + "bool":{ + "must":{ + "terms":{ + "name": [] + } + } + } + } + """; + + assertThat(query).isInstanceOf(StringQuery.class); + JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void shouldIgnoreNullValuesInCollectionSpEL() throws Exception { + + final List anotherString = List.of("abc"); + List params = new ArrayList<>(anotherString); + // add a null value + params.add(null); + org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesSpEL", params); + String expected = """ + { + "bool":{ + "must":{ + "terms":{ + "name": ["abc"] + } + } + } + } + """; + + assertThat(query).isInstanceOf(StringQuery.class); + JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void shouldReplaceCollectionParametersSpEL() throws Exception { + + final List anotherString = List.of(new QueryParameter("hello \"Stranger\""), + new QueryParameter("Another string")); + List params = new ArrayList<>(anotherString); + org.springframework.data.elasticsearch.core.query.Query query = createQuery("findByNamesParameterSpEL", params); + String expected = """ + { + "bool":{ + "must":{ + "terms":{ + "name": ["hello \\"Stranger\\"", "Another string"] + } + } + } + } + """; + + assertThat(query).isInstanceOf(StringQuery.class); + JSONAssert.assertEquals(((StringQuery) query).getSource(), expected, JSONCompareMode.NON_EXTENSIBLE); + } + @Test // #1790 @DisplayName("should escape Strings in query parameters") void shouldEscapeStringsInQueryParameters() throws Exception { @@ -139,14 +377,16 @@ void shouldEscapeBackslashesInCollectionQueryParameters() throws NoSuchMethodExc assertThat(query).isInstanceOf(StringQuery.class); assertThat(((StringQuery) query).getSource()).isEqualTo( - "{ 'bool' : { 'must' : { 'terms' : { 'name' : [\"param\\\\1\",\"param\\\\2\"] } } } }"); + "{ 'bool' : { 'must' : { 'terms' : { 'name' : [\"param\\\\1\",\"param\\\\2\"] } } } }"); } + private org.springframework.data.elasticsearch.core.query.Query createQuery(String methodName, Object... args) throws NoSuchMethodException { Class[] argTypes = Arrays.stream(args).map(Object::getClass).toArray(Class[]::new); - ElasticsearchQueryMethod queryMethod = getQueryMethod(methodName, argTypes); - ElasticsearchStringQuery elasticsearchStringQuery = queryForMethod(queryMethod); + ElasticsearchQueryMethod queryMethod = getQueryMethod(RepositoryStringQueryUnitTests.SampleRepository.class, + methodName, argTypes); + RepositoryStringQuery elasticsearchStringQuery = queryForMethod(queryMethod); return elasticsearchStringQuery.createQuery(new ElasticsearchParametersParameterAccessor(queryMethod, args)); } @@ -165,15 +405,9 @@ void shouldUseConverterOnParameters() throws NoSuchMethodException { .isEqualTo("{ 'bool' : { 'must' : { 'term' : { 'car' : 'Toyota-Prius' } } } }"); } - private ElasticsearchStringQuery queryForMethod(ElasticsearchQueryMethod queryMethod) { - return new ElasticsearchStringQuery(queryMethod, operations, queryMethod.getAnnotatedQuery()); - } - - private ElasticsearchQueryMethod getQueryMethod(String name, Class... parameters) throws NoSuchMethodException { - - Method method = SampleRepository.class.getMethod(name, parameters); - return new ElasticsearchQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class), - new SpelAwareProxyProjectionFactory(), operations.getElasticsearchConverter().getMappingContext()); + private RepositoryStringQuery queryForMethod(ElasticsearchQueryMethod queryMethod) { + return new RepositoryStringQuery(queryMethod, operations, queryMethod.getAnnotatedQuery(), + ValueExpressionDelegate.create()); } private interface SampleRepository extends Repository { @@ -187,9 +421,74 @@ private interface SampleRepository extends Repository { @Query("{ 'bool' : { 'must' : { 'term' : { 'name' : '?0' } } } }") Person findByName(String name); + @Query(""" + { + "bool":{ + "must":{ + "term":{ + "name": "#{#name}" + } + } + } + } + """) + Person findByNameSpEL(String name); + + @Query(""" + { + "bool":{ + "must":{ + "term":{ + "name": "#{#param.value}" + } + } + } + } + """) + Person findByParameterPropertySpEL(QueryParameter param); + @Query("{ 'bool' : { 'must' : { 'terms' : { 'name' : ?0 } } } }") Person findByNameIn(ArrayList names); + @Query(""" + { + "bool":{ + "must":{ + "terms":{ + "name": #{#names} + } + } + } + } + """) + Person findByNamesSpEL(ArrayList names); + + @Query(""" + { + "bool":{ + "must":{ + "terms":{ + "age": #{#ages} + } + } + } + } + """) + Person findByAgesSpEL(ArrayList ages); + + @Query(""" + { + "bool":{ + "must":{ + "terms":{ + "name": #{#names.![value]} + } + } + } + } + """) + Person findByNamesParameterSpEL(ArrayList names); + @Query(value = "name:(?0, ?11, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?0, ?1)") Person findWithRepeatedPlaceholder(String arg0, String arg1, String arg2, String arg3, String arg4, String arg5, String arg6, String arg7, String arg8, String arg9, String arg10, String arg11); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptType.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositoryStringQueryUnitTestsBase.java similarity index 70% rename from src/main/java/org/springframework/data/elasticsearch/core/query/ScriptType.java rename to src/test/java/org/springframework/data/elasticsearch/repository/query/RepositoryStringQueryUnitTestsBase.java index f2b54c923f..c6ae397433 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptType.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositoryStringQueryUnitTestsBase.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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. @@ -13,15 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.elasticsearch.core.query; +package org.springframework.data.elasticsearch.repository.query; /** - * Define script types for update queries. - * - * @author Farid Faoudi - * @since 4.2 + * @author Peter-Josef Meisch */ +public class RepositoryStringQueryUnitTestsBase extends RepositoryQueryUnitTestsBase { -public enum ScriptType { - INLINE, STORED } diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/StubParameterAccessor.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/StubParameterAccessor.java deleted file mode 100644 index 69c35f0c14..0000000000 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/StubParameterAccessor.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2019-2023 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.repository.query; - -import java.util.Arrays; -import java.util.Iterator; -import java.util.Optional; - -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.ScrollPosition; -import org.springframework.data.domain.Sort; -import org.springframework.data.repository.query.ParameterAccessor; - -/** - * Simple {@link ParameterAccessor} that returns the given parameters unfiltered. - * - * @author Christoph Strobl - * @author Peter-Josef Meisch - */ -class StubParameterAccessor implements ElasticsearchParameterAccessor { - - private final Object[] values; - - StubParameterAccessor(Object... values) { - this.values = values; - } - - @Override - public ScrollPosition getScrollPosition() { - return null; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.query.ParameterAccessor#getPageable() - */ - @Override - public Pageable getPageable() { - return Pageable.unpaged(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.query.ParameterAccessor#getBindableValue(int) - */ - @Override - public Object getBindableValue(int index) { - return values[index]; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.query.ParameterAccessor#hasBindableNullValue() - */ - @Override - public boolean hasBindableNullValue() { - return false; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.query.ParameterAccessor#getSort() - */ - @Override - public Sort getSort() { - return Sort.unsorted(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.query.ParameterAccessor#iterator() - */ - @Override - public Iterator iterator() { - return Arrays.asList(values).iterator(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.elasticsearch.repository.query.ElasticsearchParameterAccessor#getValues() - */ - @Override - public Object[] getValues() { - return this.values; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.query.ParameterAccessor#findDynamicProjection() - */ - @Override - public Class findDynamicProjection() { - return null; - } -} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/keywords/QueryKeywordsELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/keywords/QueryKeywordsELCIntegrationTests.java index 9f7d2ec7ec..8371384bdb 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/keywords/QueryKeywordsELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/keywords/QueryKeywordsELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/repository/query/keywords/QueryKeywordsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/keywords/QueryKeywordsIntegrationTests.java index 321d9f4407..32b92dc691 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/keywords/QueryKeywordsIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/keywords/QueryKeywordsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-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.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -38,7 +38,6 @@ import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * base class for query keyword tests. Implemented by subclasses using ElasticsearchClient and ElasticsearchRestClient @@ -80,11 +79,6 @@ void cleanup() { @Test public void shouldSupportAND() { - // given - - // when - - // then assertThat(repository.findByNameAndText("Sugar", "Cane sugar")).hasSize(2); assertThat(repository.findByNameAndPrice("Sugar", 1.1f)).hasSize(1); } @@ -92,11 +86,6 @@ public void shouldSupportAND() { @Test public void shouldSupportOR() { - // given - - // when - - // then assertThat(repository.findByNameOrPrice("Sugar", 1.9f)).hasSize(4); assertThat(repository.findByNameOrText("Salt", "Beet sugar")).hasSize(3); } @@ -104,11 +93,6 @@ public void shouldSupportOR() { @Test public void shouldSupportTrueAndFalse() { - // given - - // when - - // then assertThat(repository.findByAvailableTrue()).hasSize(3); assertThat(repository.findByAvailableFalse()).hasSize(4); } @@ -116,11 +100,6 @@ public void shouldSupportTrueAndFalse() { @Test public void shouldSupportInAndNotInAndNot() { - // given - - // when - - // then assertThat(repository.findByPriceIn(Arrays.asList(1.2f, 1.1f))).hasSize(2); assertThat(repository.findByPriceNotIn(Arrays.asList(1.2f, 1.1f))).hasSize(5); assertThat(repository.findByPriceNot(1.2f)).hasSize(6); @@ -129,33 +108,18 @@ public void shouldSupportInAndNotInAndNot() { @Test // DATAES-171 public void shouldWorkWithNotIn() { - // given - - // when - - // then assertThat(repository.findByIdNotIn(Arrays.asList("2", "3"))).hasSize(5); } @Test public void shouldSupportBetween() { - // given - - // when - - // then assertThat(repository.findByPriceBetween(1.0f, 2.0f)).hasSize(4); } @Test public void shouldSupportLessThanAndGreaterThan() { - // given - - // when - - // then assertThat(repository.findByPriceLessThan(1.1f)).hasSize(1); assertThat(repository.findByPriceLessThanEqual(1.1f)).hasSize(2); @@ -400,7 +364,6 @@ public void setSortName(@Nullable String sortName) { } } - @SuppressWarnings({ "SpringDataRepositoryMethodParametersInspection", "SpringDataMethodInconsistencyInspection" }) interface ProductRepository extends ElasticsearchRepository { List findByName(@Nullable String name); diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/keywords/ReactiveQueryKeywordsELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/keywords/ReactiveQueryKeywordsELCIntegrationTests.java index d108368bae..7471d64719 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/keywords/ReactiveQueryKeywordsELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/keywords/ReactiveQueryKeywordsELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/repository/query/keywords/ReactiveQueryKeywordsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/keywords/ReactiveQueryKeywordsIntegrationTests.java index 8a754a4a1d..e5d05446d6 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/keywords/ReactiveQueryKeywordsIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/keywords/ReactiveQueryKeywordsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import java.lang.Boolean; - +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -38,7 +37,6 @@ import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * @author Peter-Josef Meisch @@ -139,7 +137,6 @@ void shouldRunExistsQuery() { .verifyComplete(); } - @SuppressWarnings("SpringDataMethodInconsistencyInspection") interface SampleRepository extends ReactiveElasticsearchRepository { Flux> findByMessageExists(); diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/valueconverter/ReactiveValueConverterELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/valueconverter/ReactiveValueConverterELCIntegrationTests.java index 6cc35425a8..482dea83f9 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/valueconverter/ReactiveValueConverterELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/valueconverter/ReactiveValueConverterELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/repository/query/valueconverter/ReactiveValueConverterIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/valueconverter/ReactiveValueConverterIntegrationTests.java index 4c48cadbac..ad36038f7f 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/valueconverter/ReactiveValueConverterIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/valueconverter/ReactiveValueConverterIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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,21 +15,10 @@ */ package org.springframework.data.elasticsearch.repository.query.valueconverter; -import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.elasticsearch.annotations.FieldType.*; - -import org.springframework.data.elasticsearch.annotations.FieldType; -import org.springframework.data.elasticsearch.annotations.Query; -import org.springframework.data.elasticsearch.annotations.ValueConverter; -import org.springframework.data.elasticsearch.core.SearchHits; -import org.springframework.data.elasticsearch.core.mapping.PropertyValueConverter; -import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import java.lang.Boolean; - +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -38,13 +27,16 @@ import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; +import org.springframework.data.elasticsearch.annotations.Query; +import org.springframework.data.elasticsearch.annotations.ValueConverter; import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; import org.springframework.data.elasticsearch.core.SearchHit; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.data.elasticsearch.core.mapping.PropertyValueConverter; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * Integration tests to check that {@link org.springframework.data.elasticsearch.annotations.ValueConverter} annotated @@ -73,7 +65,6 @@ void cleanup() { operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + '*')).delete().block(); } - @Test // #2338 @DisplayName("should apply ValueConverter") void shouldApplyValueConverter() { @@ -84,14 +75,14 @@ void shouldApplyValueConverter() { operations.save(entity).block(); repository.queryByText("text-answer") // - .as(StepVerifier::create) // - .expectNextCount(1) // - .verifyComplete(); + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); repository.findByText("answer") // - .as(StepVerifier::create) // - .expectNextCount(1) // - .verifyComplete(); + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); } interface EntityRepository extends ReactiveElasticsearchRepository { diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/valueconverter/ValueConverterELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/valueconverter/ValueConverterELCIntegrationTests.java index 838c060a52..6018bfaecf 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/valueconverter/ValueConverterELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/valueconverter/ValueConverterELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/repository/query/valueconverter/ValueConverterIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/valueconverter/ValueConverterIntegrationTests.java index 01109e8d7a..f4054676db 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/valueconverter/ValueConverterIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/valueconverter/ValueConverterIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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. @@ -17,6 +17,7 @@ import static org.assertj.core.api.Assertions.*; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; @@ -35,7 +36,6 @@ import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.data.elasticsearch.utils.IndexNameProvider; -import org.springframework.lang.Nullable; /** * Integration tests to check that {@link org.springframework.data.elasticsearch.annotations.ValueConverter} annotated diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformationCreatorImplTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformationCreatorImplTests.java index 76f5d0d216..ff97fe7488 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformationCreatorImplTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchEntityInformationCreatorImplTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 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/test/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryELCIntegrationTests.java index 3b65ea37b3..51b05be02b 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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/test/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryIntegrationTests.java index f87f70816d..d4576de5f8 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -28,6 +28,7 @@ import java.util.stream.Collectors; import org.assertj.core.api.Assertions; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -47,7 +48,6 @@ import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.data.elasticsearch.utils.IndexNameProvider; import org.springframework.data.util.StreamUtils; -import org.springframework.lang.Nullable; /** * @author Rizwan Idrees @@ -712,12 +712,11 @@ public void setAvailable(boolean available) { this.available = available; } - @Nullable - public java.lang.Long getVersion() { + public java.lang.@Nullable Long getVersion() { return version; } - public void setVersion(@Nullable java.lang.Long version) { + public void setVersion(java.lang.@Nullable Long version) { this.version = version; } diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/ReactiveRepositoryQueryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/ReactiveRepositoryQueryELCIntegrationTests.java new file mode 100644 index 0000000000..da01834385 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/ReactiveRepositoryQueryELCIntegrationTests.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.repository.support; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchTemplateConfiguration; +import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; +import org.springframework.test.context.ContextConfiguration; + +/** + * @since 5.5 + */ +@ContextConfiguration(classes = ReactiveRepositoryQueryELCIntegrationTests.Config.class) +public class ReactiveRepositoryQueryELCIntegrationTests + extends ReactiveRepositoryQueryIntegrationTests { + + @Configuration + @Import({ ReactiveElasticsearchTemplateConfiguration.class }) + @EnableReactiveElasticsearchRepositories(considerNestedRepositories = true) + static class Config { + @Bean + IndexNameProvider indexNameProvider() { + return new IndexNameProvider("reactive-repository-query"); + } + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/ReactiveRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/ReactiveRepositoryQueryIntegrationTests.java new file mode 100644 index 0000000000..f5729f3474 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/ReactiveRepositoryQueryIntegrationTests.java @@ -0,0 +1,159 @@ +/* + * 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.repository.support; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.elasticsearch.core.IndexOperationsAdapter.*; + +import reactor.core.publisher.Flux; + +import java.util.List; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; +import org.springframework.data.elasticsearch.annotations.SearchTemplateQuery; +import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHit; +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.data.elasticsearch.core.script.Script; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; + +/** + * @since 5.5 + */ +@SpringIntegrationTest +abstract class ReactiveRepositoryQueryIntegrationTests { + @Autowired private SampleElasticsearchRepository repository; + @Autowired private ReactiveElasticsearchOperations operations; + @Autowired private IndexNameProvider indexNameProvider; + + @BeforeEach + void before() { + indexNameProvider.increment(); + blocking(operations.indexOps(LOTRCharacter.class)).createWithMapping(); + } + + @Test + @org.junit.jupiter.api.Order(Integer.MAX_VALUE) + public void cleanup() { + blocking(operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*"))).delete(); + } + + @Test // #2997 + @DisplayName("should use searchtemplate query") + void shouldUseSearchtemplateQuery() { + // store some data + repository.saveAll(List.of( + new LOTRCharacter("1", "Frodo is a hobbit"), + new LOTRCharacter("2", "Legolas is an elf"), + new LOTRCharacter("3", "Gandalf is a wizard"), + new LOTRCharacter("4", "Bilbo is a hobbit"), + new LOTRCharacter("5", "Gimli is a dwarf"))) + .blockLast(); + + // store a searchtemplate + String searchInCharacter = """ + { + "query": { + "bool": { + "must": [ + { + "match": { + "lotrCharacter": "{{word}}" + } + } + ] + } + }, + "from": 0, + "size": 100, + "sort": { + "id": { + "order": "desc" + } + } + } + """; + + Script scriptSearchInCharacter = Script.builder() // + .withId("searchInCharacter") // + .withLanguage("mustache") // + .withSource(searchInCharacter) // + .build(); + + var success = operations.putScript(scriptSearchInCharacter).block(); + assertThat(success).isTrue(); + + // search with repository for hobbits order by id descending + var searchHits = repository.searchInCharacter("hobbit") + .collectList().block(); + + // check result (bilbo, frodo) + assertThat(searchHits).isNotNull(); + assertThat(searchHits.size()).isEqualTo(2); + assertThat(searchHits.get(0).getId()).isEqualTo("4"); + assertThat(searchHits.get(1).getId()).isEqualTo("1"); + } + + @Document(indexName = "#{@indexNameProvider.indexName()}") + static class LOTRCharacter { + @Nullable + @Id + @Field(fielddata = true) // needed for the sort to work + private String id; + + @Field(type = FieldType.Text) + @Nullable private String lotrCharacter; + + public LOTRCharacter(@Nullable String id, @Nullable String lotrCharacter) { + this.id = id; + this.lotrCharacter = lotrCharacter; + } + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public String getLotrCharacter() { + return lotrCharacter; + } + + public void setLotrCharacter(@Nullable String lotrCharacter) { + this.lotrCharacter = lotrCharacter; + } + } + + interface SampleElasticsearchRepository + extends ReactiveElasticsearchRepository { + @SearchTemplateQuery(id = "searchInCharacter") + Flux> searchInCharacter(String word); + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/RepositoryQueryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/RepositoryQueryELCIntegrationTests.java new file mode 100644 index 0000000000..bc079feb78 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/RepositoryQueryELCIntegrationTests.java @@ -0,0 +1,38 @@ +/* + * 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.repository.support; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration; +import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; +import org.springframework.test.context.ContextConfiguration; + +@ContextConfiguration(classes = {RepositoryQueryELCIntegrationTests.Config.class })public class RepositoryQueryELCIntegrationTests extends RepositoryQueryIntegrationTests { + @Configuration + @Import({ElasticsearchTemplateConfiguration.class }) + @EnableElasticsearchRepositories(basePackages = {"org.springframework.data.elasticsearch.repository.support" }, + considerNestedRepositories = true) + static class Config { + @Bean + IndexNameProvider indexNameProvider() { + return new IndexNameProvider("repository-query"); + } + + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/RepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/RepositoryQueryIntegrationTests.java new file mode 100644 index 0000000000..39db45c18b --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/RepositoryQueryIntegrationTests.java @@ -0,0 +1,151 @@ +/* + * 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.repository.support; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; +import org.springframework.data.elasticsearch.annotations.SearchTemplateQuery; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.data.elasticsearch.core.script.Script; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; + +@SpringIntegrationTest +abstract class RepositoryQueryIntegrationTests { + @Autowired private SampleElasticsearchRepository repository; + @Autowired private ElasticsearchOperations operations; + @Autowired private IndexNameProvider indexNameProvider; + + @BeforeEach + void before() { + indexNameProvider.increment(); + operations.indexOps(LOTRCharacter.class).createWithMapping(); + } + + @Test + @org.junit.jupiter.api.Order(Integer.MAX_VALUE) + public void cleanup() { + operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete(); + } + + @Test // #2997 + @DisplayName("should use searchtemplate query") + void shouldUseSearchtemplateQuery() { + // store some data + repository.saveAll(List.of( + new LOTRCharacter("1", "Frodo is a hobbit"), + new LOTRCharacter("2", "Legolas is an elf"), + new LOTRCharacter("3", "Gandalf is a wizard"), + new LOTRCharacter("4", "Bilbo is a hobbit"), + new LOTRCharacter("5", "Gimli is a dwarf"))); + + // store a searchtemplate + String searchInCharacter = """ + { + "query": { + "bool": { + "must": [ + { + "match": { + "lotrCharacter": "{{word}}" + } + } + ] + } + }, + "from": 0, + "size": 100, + "sort": { + "id": { + "order": "desc" + } + } + } + """; + + Script scriptSearchInCharacter = Script.builder() // + .withId("searchInCharacter") // + .withLanguage("mustache") // + .withSource(searchInCharacter) // + .build(); + + var success = operations.putScript(scriptSearchInCharacter); + assertThat(success).isTrue(); + + // search with repository for hobbits order by id descending + var searchHits = repository.searchInCharacter("hobbit"); + + // check result (bilbo, frodo) + assertThat(searchHits).isNotNull(); + assertThat(searchHits.getTotalHits()).isEqualTo(2); + assertThat(searchHits.getSearchHit(0).getId()).isEqualTo("4"); + assertThat(searchHits.getSearchHit(1).getId()).isEqualTo("1"); + } + + @Document(indexName = "#{@indexNameProvider.indexName()}") + static class LOTRCharacter { + @Nullable + @Id + @Field(fielddata = true) // needed for the sort to work + private String id; + + @Field(type = FieldType.Text) + @Nullable private String lotrCharacter; + + public LOTRCharacter(@Nullable String id, @Nullable String lotrCharacter) { + this.id = id; + this.lotrCharacter = lotrCharacter; + } + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public String getLotrCharacter() { + return lotrCharacter; + } + + public void setLotrCharacter(@Nullable String lotrCharacter) { + this.lotrCharacter = lotrCharacter; + } + } + + interface SampleElasticsearchRepository + extends ElasticsearchRepository { + @SearchTemplateQuery(id = "searchInCharacter") + SearchHits searchInCharacter(String word); + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryELCIntegrationTests.java index ff21a21769..20a0ddff3c 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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,16 +15,26 @@ */ package org.springframework.data.elasticsearch.repository.support; +import co.elastic.clients.elasticsearch.core.search.FieldCollapse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.elasticsearch.client.elc.NativeQuery; +import org.springframework.data.elasticsearch.client.elc.Queries; import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchTemplateConfiguration; +import org.springframework.data.elasticsearch.repositories.custommethod.QueryParameter; import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories; import org.springframework.data.elasticsearch.utils.IndexNameProvider; import org.springframework.test.context.ContextConfiguration; +import reactor.test.StepVerifier; /** * @author Peter-Josef Meisch + * @author Haibo Liu * @since 4.4 */ @ContextConfiguration(classes = { SimpleReactiveElasticsearchRepositoryELCIntegrationTests.Config.class }) @@ -39,6 +49,43 @@ static class Config { IndexNameProvider indexNameProvider() { return new IndexNameProvider("simple-reactive-repository"); } + + /** + * a normal bean referenced by SpEL in query + */ + @Bean + QueryParameter queryParameter() { + return new QueryParameter("message"); + } + } + + /** + * search_after is used by the reactive search operation, it normally always adds _shard_doc as a tiebreaker sort + * parameter. This must not be done when a collapse field is used as sort field, as in that case the collapse field + * must be the only sort field. + */ + @Test // #2935 + @DisplayName("should use collapse_field for search_after in pit search") + void shouldUseCollapseFieldForSearchAfterI() { + var entity = new SampleEntity(); + entity.setId("42"); + entity.setMessage("m"); + entity.setKeyword("kw"); + repository.save(entity).block(); + + var query = NativeQuery.builder() + .withQuery(Queries.matchAllQueryAsQuery()) + .withPageable(Pageable.unpaged()) + .withFieldCollapse(FieldCollapse.of(fcb -> fcb + .field("keyword"))) + .withSort(Sort.by("keyword")) + .build(); + + operations.search(query, SampleEntity.class) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); } + } diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryIntegrationTests.java index 40c674e580..6141870f62 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/SimpleReactiveElasticsearchRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -16,6 +16,7 @@ package org.springframework.data.elasticsearch.repository.support; import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; import static org.springframework.data.elasticsearch.core.IndexOperationsAdapter.*; import static org.springframework.data.elasticsearch.core.query.Query.*; import static org.springframework.data.elasticsearch.utils.IdGenerator.*; @@ -32,6 +33,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -50,20 +52,24 @@ import org.springframework.data.elasticsearch.annotations.FieldType; import org.springframework.data.elasticsearch.annotations.Highlight; import org.springframework.data.elasticsearch.annotations.HighlightField; +import org.springframework.data.elasticsearch.annotations.HighlightParameters; import org.springframework.data.elasticsearch.annotations.Query; import org.springframework.data.elasticsearch.annotations.SourceFilters; import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; import org.springframework.data.elasticsearch.core.SearchHit; +import org.springframework.data.elasticsearch.core.convert.ConversionException; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.data.elasticsearch.repositories.custommethod.QueryParameter; import org.springframework.data.elasticsearch.utils.IndexNameProvider; +import org.springframework.data.repository.query.Param; import org.springframework.data.repository.reactive.ReactiveCrudRepository; -import org.springframework.lang.Nullable; /** * @author Christoph Strobl * @author Peter-Josef Meisch * @author Jens Schauder + * @author Haibo Liu */ @SpringIntegrationTest abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests { @@ -99,6 +105,26 @@ private Mono documentWithIdExistsInIndex(String id) { return operations.exists(id, IndexCoordinates.of(indexNameProvider.indexName())); } + @Test // #3093 + @DisplayName("should save all from empty collection") + void shouldSaveAllFromEmptyCollection() { + + repository.saveAll(Collections.emptyList()) + .as(StepVerifier::create) + .expectNextCount(0) + .verifyComplete(); + } + + @Test // #3093 + @DisplayName("should save all from empty flux") + void shouldSaveAllFromEmptyFlux() { + + repository.saveAll(Flux.empty()) + .as(StepVerifier::create) + .expectNextCount(0) + .verifyComplete(); + } + @Test // DATAES-519 void saveShouldComputeMultipleEntities() { @@ -234,6 +260,167 @@ void shouldReturnFluxOfSearchHitForStringQuery() { .verifyComplete(); } + @Test + void shouldReturnSearchHitsForStringQuerySpEL() { + bulkIndex(new SampleEntity("id-one", "message"), // + new SampleEntity("id-two", "message"), // + new SampleEntity("id-three", "message")) // + .block(); + + repository.queryByStringSpEL("message") + .as(StepVerifier::create) // + .expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))// + .expectNextCount(2) // + .verifyComplete(); + } + + @Test + void shouldRaiseExceptionForNullStringQuerySpEL() { + bulkIndex(new SampleEntity("id-one", "message"), // + new SampleEntity("id-two", "message"), // + new SampleEntity("id-three", "message")) // + .block(); + + ConversionException thrown = assertThrows(ConversionException.class, () -> repository.queryByStringSpEL(null)); + + assertThat(thrown.getMessage()) + .isEqualTo("Parameter value can't be null for SpEL expression '#message' in method 'queryByStringSpEL'" + + " when querying elasticsearch"); + } + + @Test + void shouldReturnSearchHitsForParameterPropertyQuerySpEL() { + bulkIndex(new SampleEntity("id-one", "message"), // + new SampleEntity("id-two", "message"), // + new SampleEntity("id-three", "message")) // + .block(); + + QueryParameter param = new QueryParameter("message"); + + repository.queryByParameterPropertySpEL(param) + .as(StepVerifier::create) // + .expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))// + .expectNextCount(2) // + .verifyComplete(); + } + + @Test + void shouldReturnSearchHitsForBeanQuerySpEL() { + bulkIndex(new SampleEntity("id-one", "message"), // + new SampleEntity("id-two", "message"), // + new SampleEntity("id-three", "message")) // + .block(); + + repository.queryByBeanPropertySpEL() + .as(StepVerifier::create) // + .expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))// + .expectNextCount(2) // + .verifyComplete(); + } + + @Test + void shouldReturnSearchHitsForCollectionQuerySpEL() { + bulkIndex(new SampleEntity("id-one", "message"), // + new SampleEntity("id-two", "message"), // + new SampleEntity("id-three", "message")) // + .block(); + + repository.queryByCollectionSpEL(List.of("message")) + .as(StepVerifier::create) // + .expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))// + .expectNextCount(2) // + .verifyComplete(); + } + + @Test + void shouldRaiseExceptionForNullCollectionQuerySpEL() { + bulkIndex(new SampleEntity("id-one", "message"), // + new SampleEntity("id-two", "message"), // + new SampleEntity("id-three", "message")) // + .block(); + + ConversionException thrown = assertThrows(ConversionException.class, () -> repository.queryByCollectionSpEL(null)); + + assertThat(thrown.getMessage()) + .isEqualTo("Parameter value can't be null for SpEL expression '#messages' in method 'queryByCollectionSpEL'" + + " when querying elasticsearch"); + } + + @Test + void shouldNotReturnSearchHitsForEmptyCollectionQuerySpEL() { + bulkIndex(new SampleEntity("id-one", "message"), // + new SampleEntity("id-two", "message"), // + new SampleEntity("id-three", "message")) // + .block(); + + repository.queryByCollectionSpEL(List.of()) + .as(StepVerifier::create) // + .expectNextCount(0) // + .verifyComplete(); + } + + @Test + void shouldNotReturnSearchHitsForCollectionQueryWithOnlyNullValuesSpEL() { + bulkIndex(new SampleEntity("id-one", "message"), // + new SampleEntity("id-two", "message"), // + new SampleEntity("id-three", "message")) // + .block(); + + List params = new ArrayList<>(); + params.add(null); + + repository.queryByCollectionSpEL(params) + .as(StepVerifier::create) // + .expectNextCount(0) // + .verifyComplete(); + } + + @Test + void shouldIgnoreNullValuesInCollectionQuerySpEL() { + bulkIndex(new SampleEntity("id-one", "message"), // + new SampleEntity("id-two", "message"), // + new SampleEntity("id-three", "message")) // + .block(); + + repository.queryByCollectionSpEL(Arrays.asList("message", null)) + .as(StepVerifier::create) // + .expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))// + .expectNextCount(2) // + .verifyComplete(); + } + + @Test + void shouldReturnSearchHitsForParameterPropertyCollectionQuerySpEL() { + bulkIndex(new SampleEntity("id-one", "message"), // + new SampleEntity("id-two", "message"), // + new SampleEntity("id-three", "message")) // + .block(); + + QueryParameter param = new QueryParameter("message"); + + repository.queryByParameterPropertyCollectionSpEL(List.of(param)) + .as(StepVerifier::create) // + .expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))// + .expectNextCount(2) // + .verifyComplete(); + } + + @Test + void shouldReturnSearchHitsForParameterPropertyCollectionQuerySpELWithParamAnnotation() { + bulkIndex(new SampleEntity("id-one", "message"), // + new SampleEntity("id-two", "message"), // + new SampleEntity("id-three", "message")) // + .block(); + + QueryParameter param = new QueryParameter("message"); + + repository.queryByParameterPropertyCollectionSpELWithParamAnnotation(List.of(param)) + .as(StepVerifier::create) // + .expectNextMatches(searchHit -> SearchHit.class.isAssignableFrom(searchHit.getClass()))// + .expectNextCount(2) // + .verifyComplete(); + } + @Test // DATAES-372 void shouldReturnHighlightsOnAnnotatedMethod() { @@ -270,6 +457,60 @@ void shouldReturnHighlightsOnAnnotatedStringQueryMethod() { .verifyComplete(); } + @Test + void shouldReturnDifferentHighlightsOnAnnotatedStringQueryMethod() { + + bulkIndex(new SampleEntity("id-one", "abc xyz"), // + new SampleEntity("id-two", "abc xyz"), // + new SampleEntity("id-three", "abc xyz")) // + .block(); + + repository.queryByMessageWithSeparateHighlight("abc", "abc") // + .as(StepVerifier::create) // + .expectNextMatches(searchHit -> { + List hitHighlightField = searchHit.getHighlightField("message"); + return hitHighlightField.size() == 1 && hitHighlightField.get(0).equals("abc xyz"); + }) // + .expectNextCount(2) // + .verifyComplete(); + + repository.queryByMessageWithSeparateHighlight("abc", "xyz") // + .as(StepVerifier::create) // + .expectNextMatches(searchHit -> { + List hitHighlightField = searchHit.getHighlightField("message"); + return hitHighlightField.size() == 1 && hitHighlightField.get(0).equals("abc xyz"); + }) // + .expectNextCount(2) // + .verifyComplete(); + } + + @Test + void shouldReturnDifferentHighlightsOnAnnotatedStringQueryMethodSpEL() { + + bulkIndex(new SampleEntity("id-one", "abc xyz"), // + new SampleEntity("id-two", "abc xyz"), // + new SampleEntity("id-three", "abc xyz")) // + .block(); + + repository.queryByMessageWithSeparateHighlightSpEL("abc", "abc") // + .as(StepVerifier::create) // + .expectNextMatches(searchHit -> { + List hitHighlightField = searchHit.getHighlightField("message"); + return hitHighlightField.size() == 1 && hitHighlightField.get(0).equals("abc xyz"); + }) // + .expectNextCount(2) // + .verifyComplete(); + + repository.queryByMessageWithSeparateHighlightSpEL("abc", "xyz") // + .as(StepVerifier::create) // + .expectNextMatches(searchHit -> { + List hitHighlightField = searchHit.getHighlightField("message"); + return hitHighlightField.size() == 1 && hitHighlightField.get(0).equals("abc xyz"); + }) // + .expectNextCount(2) // + .verifyComplete(); + } + @Test // DATAES-519, DATAES-767, DATAES-822 void countShouldErrorWhenIndexDoesNotExist() { @@ -694,6 +935,29 @@ void shouldUseSourceIncludesFromParameter() { .verifyComplete(); } + @Test + @DisplayName("should use sourceIncludes from parameter SpEL") + void shouldUseSourceIncludesFromParameterSpEL() { + + var entity = new SampleEntity(); + entity.setId("42"); + entity.setMessage("message"); + entity.setCustomFieldNameMessage("customFieldNameMessage"); + entity.setType("type"); + entity.setKeyword("keyword"); + repository.save(entity).block(); + + repository.queryBy(List.of("message", "customFieldNameMessage")) // + .as(StepVerifier::create) // + .consumeNextWith(foundEntity -> { // + assertThat(foundEntity.getMessage()).isEqualTo("message"); // + assertThat(foundEntity.getCustomFieldNameMessage()).isEqualTo("customFieldNameMessage"); // + assertThat(foundEntity.getType()).isNull(); // + assertThat(foundEntity.getKeyword()).isNull(); // + }) // + .verifyComplete(); + } + @Test // #2146 @DisplayName("should use sourceExcludes from annotation") void shouldUseSourceExcludesFromAnnotation() { @@ -740,6 +1004,29 @@ void shouldUseSourceExcludesFromParameter() { .verifyComplete(); } + @Test + @DisplayName("should use source excludes from parameter SpEL") + void shouldUseSourceExcludesFromParameterSpEL() { + + var entity = new SampleEntity(); + entity.setId("42"); + entity.setMessage("message"); + entity.setCustomFieldNameMessage("customFieldNameMessage"); + entity.setType("type"); + entity.setKeyword("keyword"); + repository.save(entity).block(); + + repository.getBy(List.of("type", "keyword")) // + .as(StepVerifier::create) // + .consumeNextWith(foundEntity -> { // + assertThat(foundEntity.getMessage()).isEqualTo("message"); // + assertThat(foundEntity.getCustomFieldNameMessage()).isEqualTo("customFieldNameMessage"); // + assertThat(foundEntity.getType()).isNull(); // + assertThat(foundEntity.getKeyword()).isNull(); // + }) // + .verifyComplete(); + } + @Test // #2496 @DisplayName("should save data from Flux and return saved data in a flux") void shouldSaveDataFromFluxAndReturnSavedDataInAFlux() { @@ -781,12 +1068,173 @@ interface ReactiveSampleEntityRepository extends ReactiveCrudRepository> queryByMessageWithString(String message); + @Query(""" + { + "bool":{ + "must":[ + { + "match":{ + "message":"?0" + } + } + ] + } + } + """) + @Highlight( + fields = { @HighlightField(name = "message") }, + parameters = @HighlightParameters( + highlightQuery = @Query(""" + { + "bool":{ + "must":[ + { + "match":{ + "message":"?1" + } + } + ] + } + } + """))) + Flux> queryByMessageWithSeparateHighlight(String message, String highlight); + + @Query(""" + { + "bool":{ + "must":[ + { + "match":{ + "message":"#{#message}" + } + } + ] + } + } + """) + @Highlight( + fields = { @HighlightField(name = "message") }, + parameters = @HighlightParameters( + highlightQuery = @Query(""" + { + "bool":{ + "must":[ + { + "match":{ + "message":"#{#highlight}" + } + } + ] + } + } + """))) + Flux> queryByMessageWithSeparateHighlightSpEL(String message, String highlight); + @Query("{ \"bool\" : { \"must\" : { \"term\" : { \"message\" : \"?0\" } } } }") Flux findAllViaAnnotatedQueryByMessageLike(String message); @Query("{ \"bool\" : { \"must\" : { \"term\" : { \"message\" : \"?0\" } } } }") Flux findAllViaAnnotatedQueryByMessageLikePaged(String message, Pageable pageable); + /** + * The parameter is annotated with {@link Nullable} deliberately to test that our elasticsearch SpEL converters will + * not accept a null parameter as query value. + */ + @Query(""" + { + "bool":{ + "must":[ + { + "term":{ + "message": "#{#message}" + } + } + ] + } + } + """) + Flux> queryByStringSpEL(@Nullable String message); + + @Query(""" + { + "bool":{ + "must":[ + { + "term":{ + "message": "#{#parameter.value}" + } + } + ] + } + } + """) + Flux> queryByParameterPropertySpEL(QueryParameter parameter); + + @Query(""" + { + "bool":{ + "must":[ + { + "term":{ + "message": "#{@queryParameter.value}" + } + } + ] + } + } + """) + Flux> queryByBeanPropertySpEL(); + + /** + * The parameter is annotated with {@link Nullable} deliberately to test that our elasticsearch SpEL converters will + * not accept a null parameter as query value. + */ + @Query(""" + { + "bool":{ + "must":[ + { + "terms":{ + "message": #{#messages} + } + } + ] + } + } + """) + Flux> queryByCollectionSpEL(@Nullable Collection messages); + + @Query(""" + { + "bool":{ + "must":[ + { + "terms":{ + "message": #{#parameters.![value]} + } + } + ] + } + } + """) + Flux> queryByParameterPropertyCollectionSpEL(Collection parameters); + + @Query(""" + { + "bool":{ + "must":[ + { + "terms":{ + "message": #{#e.![value]} + } + } + ] + } + } + """) + Flux> queryByParameterPropertyCollectionSpELWithParamAnnotation( + @Param("e") Collection parameters); + Mono findFirstByMessageLike(String message); Mono countAllByMessage(String message); @@ -818,6 +1266,9 @@ interface ReactiveSampleEntityRepository extends ReactiveCrudRepository searchBy(Collection sourceIncludes); + @SourceFilters(includes = "#{#sourceIncludes}") + Flux queryBy(Collection sourceIncludes); + @Query(""" { "match_all": {} @@ -828,6 +1279,9 @@ interface ReactiveSampleEntityRepository extends ReactiveCrudRepository findBy(Collection sourceExcludes); + + @SourceFilters(excludes = "#{#sourceExcludes}") + Flux getBy(Collection sourceExcludes); } @Document(indexName = "#{@indexNameProvider.indexName()}") @@ -924,12 +1378,11 @@ public void setAvailable(boolean available) { this.available = available; } - @Nullable - public java.lang.Long getVersion() { + public java.lang.@Nullable Long getVersion() { return version; } - public void setVersion(@Nullable java.lang.Long version) { + public void setVersion(java.lang.@Nullable Long version) { this.version = version; } diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorELCIntegrationTests.java index e9358052d8..1238849711 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorIntegrationTests.java index d56c78ec49..e8c89e35c0 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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. @@ -23,6 +23,7 @@ import java.util.Optional; import org.assertj.core.api.AbstractThrowableAssert; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -43,7 +44,6 @@ import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.data.elasticsearch.utils.IndexNameProvider; import org.springframework.data.repository.query.QueryByExampleExecutor; -import org.springframework.lang.Nullable; /** * @author Ezequiel Antúnez Camacho @@ -526,12 +526,11 @@ public void setSampleNestedEntity(SampleNestedEntity sampleNestedEntity) { this.sampleNestedEntity = sampleNestedEntity; } - @Nullable - public java.lang.Long getVersion() { + public java.lang.@Nullable Long getVersion() { return version; } - public void setVersion(@Nullable java.lang.Long version) { + public void setVersion(java.lang.@Nullable Long version) { this.version = version; } diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests.java index 68227cca42..7482bb4884 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorIntegrationTests.java index 710dbc75e0..3a09482245 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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. @@ -24,6 +24,7 @@ import java.util.List; import java.util.Objects; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -43,7 +44,6 @@ import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository; import org.springframework.data.elasticsearch.utils.IndexNameProvider; import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; -import org.springframework.lang.Nullable; /** * @author Ezequiel Antúnez Camacho @@ -483,9 +483,9 @@ static class SampleEntity { @Field(type = FieldType.Keyword, store = true) private String message; @Nullable private Integer rate; @Nullable private Boolean available; - @Nullable + @Field(type = FieldType.Nested, store = true, - fielddata = true) private SampleEntity.SampleNestedEntity sampleNestedEntity; + fielddata = true) private SampleEntity.@Nullable SampleNestedEntity sampleNestedEntity; @Nullable @Version private Long version; @@ -534,8 +534,7 @@ public void setAvailable(Boolean available) { this.available = available; } - @Nullable - public SampleEntity.SampleNestedEntity getSampleNestedEntity() { + public SampleEntity.@Nullable SampleNestedEntity getSampleNestedEntity() { return sampleNestedEntity; } @@ -543,12 +542,11 @@ public void setSampleNestedEntity(SampleEntity.SampleNestedEntity sampleNestedEn this.sampleNestedEntity = sampleNestedEntity; } - @Nullable - public java.lang.Long getVersion() { + public java.lang.@Nullable Long getVersion() { return version; } - public void setVersion(@Nullable java.lang.Long version) { + public void setVersion(java.lang.@Nullable Long version) { this.version = version; } diff --git a/src/test/java/org/springframework/data/elasticsearch/support/DefaultStringObjectMapUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/support/DefaultStringObjectMapUnitTests.java index 85f6899ea0..a77682d364 100644 --- a/src/test/java/org/springframework/data/elasticsearch/support/DefaultStringObjectMapUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/support/DefaultStringObjectMapUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/support/HttpHeadersTest.java b/src/test/java/org/springframework/data/elasticsearch/support/HttpHeadersTest.java index a236c98d15..c72fb1b3f2 100644 --- a/src/test/java/org/springframework/data/elasticsearch/support/HttpHeadersTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/support/HttpHeadersTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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. @@ -28,6 +28,7 @@ * @author Peter-Josef Meisch * @since 5.0 */ +@SuppressWarnings("UastIncorrectHttpHeaderInspection") class HttpHeadersTest { public static final String X_TEST_HEADER = "X-Test-Header"; @@ -77,7 +78,8 @@ void shouldInitializeFromSpringHttpHeaders() { springHttpHeaders.add(headerName, "true"); var httpHeaders = new HttpHeaders(); - httpHeaders.addAll(springHttpHeaders); + + springHttpHeaders.forEach(httpHeaders::addAll); assertThat(httpHeaders.get(X_TEST_HEADER)).containsExactly("foo", "bar"); assertThat(httpHeaders.get(headerName)).containsExactly("true"); diff --git a/src/test/java/org/springframework/data/elasticsearch/support/VersionInfoTest.java b/src/test/java/org/springframework/data/elasticsearch/support/VersionInfoTest.java index 09c6ba69e7..2571a41ca0 100644 --- a/src/test/java/org/springframework/data/elasticsearch/support/VersionInfoTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/support/VersionInfoTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/support/VersionUnitTest.java b/src/test/java/org/springframework/data/elasticsearch/support/VersionUnitTest.java index 781a75a801..6e643d34e3 100644 --- a/src/test/java/org/springframework/data/elasticsearch/support/VersionUnitTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/support/VersionUnitTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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/test/java/org/springframework/data/elasticsearch/utils/IdGenerator.java b/src/test/java/org/springframework/data/elasticsearch/utils/IdGenerator.java index ef450e1698..adffb8be68 100644 --- a/src/test/java/org/springframework/data/elasticsearch/utils/IdGenerator.java +++ b/src/test/java/org/springframework/data/elasticsearch/utils/IdGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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/test/java/org/springframework/data/elasticsearch/utils/IndexBuilder.java b/src/test/java/org/springframework/data/elasticsearch/utils/IndexBuilder.java index 2fabc53b29..d6acdda21b 100644 --- a/src/test/java/org/springframework/data/elasticsearch/utils/IndexBuilder.java +++ b/src/test/java/org/springframework/data/elasticsearch/utils/IndexBuilder.java @@ -24,7 +24,7 @@ public static IndexQuery buildIndex(Object object) { indexQuery.setObject(object); return indexQuery; } catch (IllegalAccessException e) { - e.printStackTrace(); + throw new RuntimeException(e); } } } diff --git a/src/test/java/org/springframework/data/elasticsearch/utils/IndexNameProvider.java b/src/test/java/org/springframework/data/elasticsearch/utils/IndexNameProvider.java index 966e1665bc..dcc28c9457 100644 --- a/src/test/java/org/springframework/data/elasticsearch/utils/IndexNameProvider.java +++ b/src/test/java/org/springframework/data/elasticsearch/utils/IndexNameProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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,7 @@ package org.springframework.data.elasticsearch.utils; /** - * Class providing an index name with a prefix and a index number + * Class providing an index name with a prefix, an index number and a random 6-digit number. * * @author Peter-Josef Meisch */ @@ -35,7 +35,7 @@ public IndexNameProvider(String prefix) { } public void increment() { - indexName = prefix + '-' + ++idx; + indexName = prefix + '-' + ++idx + '-' + sixDigits(); } public String indexName() { @@ -48,4 +48,8 @@ public String indexName() { public String getPrefix() { return prefix; } + + private String sixDigits() { + return String.valueOf((int) (100000 + Math.random() * 900000)); + } } diff --git a/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/CoroutineRepositoryELCIntegrationTests.kt b/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/CoroutineRepositoryELCIntegrationTests.kt index 57c5f6356e..2bd6d5a0da 100644 --- a/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/CoroutineRepositoryELCIntegrationTests.kt +++ b/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/CoroutineRepositoryELCIntegrationTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 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. @@ -29,19 +29,19 @@ import org.springframework.test.context.ContextConfiguration @ContextConfiguration(classes = [CoroutineRepositoryELCIntegrationTests.Config::class]) class CoroutineRepositoryELCIntegrationTests : CoroutineRepositoryIntegrationTests() { - @Configuration - @Import(ReactiveElasticsearchTemplateConfiguration::class) - @EnableReactiveElasticsearchRepositories( - considerNestedRepositories = true, - includeFilters = [ComponentScan.Filter( - type = FilterType.ASSIGNABLE_TYPE, - classes = [CoroutineElasticsearchRepository::class] - )] - ) - open class Config { - @Bean - open fun indexNameProvider(): IndexNameProvider { - return IndexNameProvider("coroutine-repository") - } - } + @Configuration + @Import(ReactiveElasticsearchTemplateConfiguration::class) + @EnableReactiveElasticsearchRepositories( + considerNestedRepositories = true, + includeFilters = [ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + classes = [CoroutineElasticsearchRepository::class] + )] + ) + open class Config { + @Bean + open fun indexNameProvider(): IndexNameProvider { + return IndexNameProvider("coroutine-repository") + } + } } diff --git a/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/CoroutineRepositoryIntegrationTests.kt b/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/CoroutineRepositoryIntegrationTests.kt index 99ebe2a519..2438aabb81 100644 --- a/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/CoroutineRepositoryIntegrationTests.kt +++ b/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/CoroutineRepositoryIntegrationTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 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. @@ -21,13 +21,16 @@ import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.data.annotation.Id import org.springframework.data.elasticsearch.annotations.Document import org.springframework.data.elasticsearch.annotations.Field import org.springframework.data.elasticsearch.annotations.FieldType +import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations import org.springframework.data.elasticsearch.core.SearchHit +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest import org.springframework.data.elasticsearch.repository.CoroutineElasticsearchRepository import org.springframework.data.elasticsearch.utils.IndexNameProvider @@ -37,65 +40,73 @@ import org.springframework.data.elasticsearch.utils.IndexNameProvider * @author Peter-Josef Meisch * @since 5.2 */ -@Suppress("SpringJavaInjectionPointsAutowiringInspection") @SpringIntegrationTest abstract class CoroutineRepositoryIntegrationTests { - @Autowired - lateinit var indexNameProvider: IndexNameProvider + @Autowired + lateinit var indexNameProvider: IndexNameProvider - @Autowired - lateinit var repository: CoroutineEntityRepository + @Autowired + lateinit var operations: ReactiveElasticsearchOperations - val entities = listOf( - Entity("1", "test"), - Entity("2", "test"), - ) + @Autowired + lateinit var repository: CoroutineEntityRepository - @BeforeEach - fun setUp() = runTest { - repository.saveAll(entities).last() - } + val entities = listOf( + Entity("1", "test"), + Entity("2", "test"), + ) - @Test - fun `should instantiate repository`() = runTest { - assertThat(repository).isNotNull() - } + @BeforeEach + fun setUp() = runTest { + repository.saveAll(entities).last() + } - @Test - fun `should run with method returning a list of entities`() = runTest { + @Test + @Order(Int.MAX_VALUE) + fun cleanup() { + operations.indexOps(IndexCoordinates.of(indexNameProvider.prefix + "*")).delete().block() + } - val result = repository.searchByText("test") + @Test + fun `should instantiate repository`() = runTest { + assertThat(repository).isNotNull() + } - assertThat(result).containsExactlyInAnyOrderElementsOf(entities) - } + @Test + fun `should run with method returning a list of entities`() = runTest { - @Test - fun `should run with method returning a flow of entities`() = runTest { + val result = repository.searchByText("test") - val result = repository.findByText("test").toList(mutableListOf()) + assertThat(result).containsExactlyInAnyOrderElementsOf(entities) + } - assertThat(result).containsExactlyInAnyOrderElementsOf(entities) - } + @Test + fun `should run with method returning a flow of entities`() = runTest { - @Test - fun `should run with method returning a flow of SearchHit`() = runTest { + val result = repository.findByText("test").toList(mutableListOf()) - val result = repository.queryByText("test").toList(mutableListOf()) + assertThat(result).containsExactlyInAnyOrderElementsOf(entities) + } - assertThat(result.map { it.content }).containsExactlyInAnyOrderElementsOf(entities) - } + @Test + fun `should run with method returning a flow of SearchHit`() = runTest { - @Document(indexName = "#{@indexNameProvider.indexName()}") - data class Entity( - @Id val id: String?, - @Field(type = FieldType.Text) val text: String?, - ) + val result = repository.queryByText("test").toList(mutableListOf()) - interface CoroutineEntityRepository : CoroutineElasticsearchRepository { + assertThat(result.map { it.content }).containsExactlyInAnyOrderElementsOf(entities) + } - suspend fun searchByText(text: String): List - suspend fun findByText(text: String): Flow - suspend fun queryByText(text: String): Flow> - } + @Document(indexName = "#{@indexNameProvider.indexName()}") + data class Entity( + @Id val id: String?, + @Field(type = FieldType.Text) val text: String?, + ) + + interface CoroutineEntityRepository : CoroutineElasticsearchRepository { + + suspend fun searchByText(text: String): List + suspend fun findByText(text: String): Flow + suspend fun queryByText(text: String): Flow> + } } diff --git a/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethodCoroutineUnitTests.kt b/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethodCoroutineUnitTests.kt index 1b89d7c433..9c9c746b22 100644 --- a/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethodCoroutineUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethodCoroutineUnitTests.kt @@ -17,46 +17,61 @@ import kotlin.coroutines.Continuation */ class ReactiveElasticsearchQueryMethodCoroutineUnitTests { - val projectionFactory = SpelAwareProxyProjectionFactory() + private val projectionFactory = SpelAwareProxyProjectionFactory() - interface PersonRepository : CoroutineElasticsearchRepository { + interface PersonRepository : CoroutineElasticsearchRepository { - suspend fun findSuspendAllByName(): Flow + suspend fun findSuspendAllByName(): Flow - fun findAllByName(): Flow + fun findAllByName(): Flow - suspend fun findSuspendByName(): List - } + suspend fun findSuspendByName(): List + } - @Test // #2545 - internal fun `should consider methods returning Flow as collection queries`() { + @Test // #2545 + internal fun `should consider methods returning Flow as collection queries`() { - val method = PersonRepository::class.java.getMethod("findAllByName") - val queryMethod = ReactiveElasticsearchQueryMethod(method, DefaultRepositoryMetadata(PersonRepository::class.java), projectionFactory, SimpleElasticsearchMappingContext()) + val method = PersonRepository::class.java.getMethod("findAllByName") + val queryMethod = ReactiveElasticsearchQueryMethod( + method, + DefaultRepositoryMetadata(PersonRepository::class.java), + projectionFactory, + SimpleElasticsearchMappingContext() + ) - assertThat(queryMethod.isCollectionQuery).isTrue() - } + assertThat(queryMethod.isCollectionQuery).isTrue() + } - @Test // #2545 - internal fun `should consider suspended methods returning Flow as collection queries`() { + @Test // #2545 + internal fun `should consider suspended methods returning Flow as collection queries`() { - val method = PersonRepository::class.java.getMethod("findSuspendAllByName", Continuation::class.java) - val queryMethod = ReactiveElasticsearchQueryMethod(method, DefaultRepositoryMetadata(PersonRepository::class.java), projectionFactory, SimpleElasticsearchMappingContext()) + val method = PersonRepository::class.java.getMethod("findSuspendAllByName", Continuation::class.java) + val queryMethod = ReactiveElasticsearchQueryMethod( + method, + DefaultRepositoryMetadata(PersonRepository::class.java), + projectionFactory, + SimpleElasticsearchMappingContext() + ) - assertThat(queryMethod.isCollectionQuery).isTrue() - } + assertThat(queryMethod.isCollectionQuery).isTrue() + } - @Test // #2545 - internal fun `should consider suspended methods returning List as collection queries`() { + @Test // #2545 + internal fun `should consider suspended methods returning List as collection queries`() { - val method = PersonRepository::class.java.getMethod("findSuspendByName", Continuation::class.java) - val queryMethod = ReactiveElasticsearchQueryMethod(method, DefaultRepositoryMetadata(PersonRepository::class.java), projectionFactory, SimpleElasticsearchMappingContext()) + val method = PersonRepository::class.java.getMethod("findSuspendByName", Continuation::class.java) + val queryMethod = ReactiveElasticsearchQueryMethod( + method, + DefaultRepositoryMetadata(PersonRepository::class.java), + projectionFactory, + SimpleElasticsearchMappingContext() + ) - assertThat(queryMethod.isCollectionQuery).isTrue() - } + assertThat(queryMethod.isCollectionQuery).isTrue() + } - data class Person( - @Id val id: String?, - @Field val name: String? - ) + data class Person( + @Id val id: String?, + @Field val name: String? + ) } diff --git a/src/test/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration b/src/test/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration deleted file mode 100644 index 1cd5f8f92f..0000000000 --- a/src/test/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration +++ /dev/null @@ -1 +0,0 @@ -org.springframework.data.elasticsearch.blockhound.BlockHoundIntegrationCustomizer diff --git a/src/test/resources/runtime-fields-person.json b/src/test/resources/runtime-fields-person.json index 85f24d9649..f225e02a09 100644 --- a/src/test/resources/runtime-fields-person.json +++ b/src/test/resources/runtime-fields-person.json @@ -5,5 +5,12 @@ "lang": "painless", "source": "Instant currentDate = Instant.ofEpochMilli(new Date().getTime()); Instant startDate = doc['birthDate'].value.toInstant(); emit(ChronoUnit.DAYS.between(startDate, currentDate) / 365);" } + }, + "allNames": { + "type": "keyword", + "script": { + "lang": "painless", + "source": "emit(doc['firstName'].value);emit(doc['lastName'].value);" + } } } diff --git a/src/test/resources/testcontainers-elasticsearch.properties b/src/test/resources/testcontainers-elasticsearch.properties index 4ed32f6004..8784f82c32 100644 --- a/src/test/resources/testcontainers-elasticsearch.properties +++ b/src/test/resources/testcontainers-elasticsearch.properties @@ -15,7 +15,7 @@ # # sde.testcontainers.image-name=docker.elastic.co/elasticsearch/elasticsearch -sde.testcontainers.image-version=8.11.1 +sde.testcontainers.image-version=9.0.2 # # # needed as we do a DELETE /* at the end of the tests, will be required from 8.0 on, produces a warning since 7.13