1
0
mirror of synced 2026-05-24 05:03:17 +00:00

Compare commits

..

116 Commits

Author SHA1 Message Date
Mark Paluch 5bbccbf9d0 Release version 5.5.3 (2025.0.3).
See #3134
2025-08-15 10:01:35 +02:00
Mark Paluch 19af94f87e Prepare 5.5.3 (2025.0.3).
See #3134
2025-08-15 10:01:14 +02:00
Mark Paluch 71a3e24096 Polishing.
Use documentation variables for references, reorder antora keys.

See #3135
2025-08-14 17:28:01 +02:00
Peter-Josef Meisch 23eacd4d4b Upgrade to Elasticsearch 8.18.5.
Original Pull Request #3151
Closes #3149
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-08-12 18:39:04 +02:00
Mark Paluch 56bda34666 After release cleanups.
See #3120
2025-07-18 10:30:34 +02:00
Mark Paluch 76ba2324a2 Prepare next development iteration.
See #3120
2025-07-18 10:30:33 +02:00
Mark Paluch 4c217fa9c4 Release version 5.5.2 (2025.0.2).
See #3120
2025-07-18 10:27:59 +02:00
Mark Paluch 003213d4b0 Prepare 5.5.2 (2025.0.2).
See #3120
2025-07-18 10:27:38 +02:00
Mark Paluch 85d52014dc Upgrade to Maven Wrapper 3.9.11.
See #3131
2025-07-17 14:00:55 +02:00
Peter-Josef Meisch 786e0928ed Fix the calculation of the requested number of documents.
Original Pull Request #3128
Closes #3127

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
(cherry picked from commit 12ddb74fae)
2025-07-15 18:41:17 +02:00
Mark Paluch ef6f091d6b After release cleanups.
See #3114
2025-06-13 13:42:19 +02:00
Mark Paluch 9ff829a829 Prepare next development iteration.
See #3114
2025-06-13 13:42:18 +02:00
Mark Paluch 30f32b6bbe Release version 5.5.1 (2025.0.1).
See #3114
2025-06-13 13:39:35 +02:00
Mark Paluch 8ce113a083 Prepare 5.5.1 (2025.0.1).
See #3114
2025-06-13 13:39:14 +02:00
Mark Paluch 262781c0a0 After release cleanups.
See #3096
2025-05-16 11:31:36 +02:00
Mark Paluch ffe8293365 Prepare next development iteration.
See #3096
2025-05-16 11:31:35 +02:00
Mark Paluch 62a34cf09c Release version 5.5 GA (2025.0.0).
See #3096
2025-05-16 11:28:31 +02:00
Mark Paluch cc5f149c5a Prepare 5.5 GA (2025.0.0).
See #3096
2025-05-16 11:28:10 +02:00
Peter-Josef Meisch 0728c8e4aa Update versions doc for the next release
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-05-15 17:39:17 +02:00
Peter-Josef Meisch ebbe242a72 Fix missing return value in ByQueryResponse.
Original Pull Request #3109
Closes #3108

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-05-14 13:53:58 +02:00
Mark Paluch 0ce9a1c400 Update CI Properties.
See #3096
2025-05-12 09:33:06 +02:00
Mark Paluch 22763d17a7 Update CI Properties.
See #3096
2025-05-12 09:00:23 +02:00
Mark Paluch 9870de1e77 Update CI Properties.
See #3096
2025-05-12 08:56:22 +02:00
Mark Paluch 8c9d9ae1e7 Update CI Properties.
See #3096
2025-05-12 08:55:54 +02:00
Peter-Josef Meisch 945179e4eb Fix handling of page size and max results in search request preparation.
Original Pull Request #3106
Closes #3089

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-05-10 21:21:17 +02:00
Peter-Josef Meisch ea38ef1d41 Upgrade Elasticsearch libraries to 8.18.1.
Original Pull Request #3105
Closes #3103

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

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

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-05-02 10:21:24 +02:00
Peter-Josef Meisch a07ac3c93d Fix code not terminating on repository saving an empty flux.
Original Pull Request #3099
Closes: #3039

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-04-26 10:40:31 +02:00
Mark Paluch 9d025dd469 After release cleanups.
See #3079
2025-04-22 11:41:58 +02:00
Mark Paluch 925921f174 Prepare next development iteration.
See #3079
2025-04-22 11:41:58 +02:00
Mark Paluch 2f0a259045 Release version 5.5 RC1 (2025.0.0).
See #3079
2025-04-22 11:41:58 +02:00
Mark Paluch 9ffcb092db Prepare 5.5 RC1 (2025.0.0).
See #3079
2025-04-22 11:41:58 +02:00
Peter-Josef Meisch 0e5af90581 Fix implementation of equlas/hashcode for Criteria class.
Original Pull Request: #3088
Closes: #3083

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-04-05 20:44:01 +02:00
Peter-Josef Meisch 95059b3282 Upgrade to Elasticsearch 8.17.4.
Original Pull Request: #3085
Closes #3084

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-03-31 16:36:42 +02:00
Peter-Josef Meisch 1ae6301c2f Fix cutting of unknown properties in property paths for search.
Original Pull Request #3082
Closes #3081

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-03-24 21:44:53 +01:00
Peter-Josef Meisch 2366f67bba Enable scripted fields and runtime fields of collection type.
Original Pull Request #3080
Closes #3076

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-03-18 20:24:02 +01:00
Mark Paluch 6f424318ec After release cleanups.
See #3058
2025-03-14 09:33:43 +01:00
Mark Paluch 300fe2ac8b Prepare next development iteration.
See #3058
2025-03-14 09:33:42 +01:00
Mark Paluch 1fdee7399f Release version 5.5 M2 (2025.0.0).
See #3058
2025-03-14 09:31:05 +01:00
Mark Paluch ace17b9751 Prepare 5.5 M2 (2025.0.0).
See #3058
2025-03-14 09:30:46 +01:00
Peter-Josef Meisch 42383624ea Fix mapping of property names in sort parameters.
Original Pull Request #3074
Closes #3072

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-03-09 12:31:59 +01:00
Hope Kim 35e7b45f1a Fix syntax errors in link formatting in adoc files.
Original Pull Request: #3070
Closes #3071

Signed-off-by: esperar <s22043@gsm.hs.kr>
2025-03-04 19:25:25 +01:00
Volodymyr 89f60f2356 Fix typo.
Original Pull Request #3069
Closes: #3068

Signed-off-by: Dgray16 <vova235@gmail.com>
2025-03-02 09:45:59 +01:00
Peter-Josef Meisch fa979249fc Remove deprecated methods.
Original Pull Request #3067
Closes #3066

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-02-23 12:37:26 +01:00
정보교 (Bogus Jung) 8b43af2d33 optimize capacity & add assert messages in GeoJson.
Original Pull Request #3064
Closes #3063

Signed-off-by: 정보교 (Bogus Jung) <bogusjung0317@gmail.com>
2025-02-21 14:24:58 +01:00
Peter-Josef Meisch 64f88ae9ac Add testcontainers-local.properties handling.
Original Pull Request #3062
Closes #3061

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-02-19 20:14:16 +01:00
Mark Paluch 15f086359d After release cleanups.
See #3005
2025-02-14 12:25:09 +01:00
Mark Paluch 78ea67b6a6 Prepare next development iteration.
See #3005
2025-02-14 12:25:08 +01:00
Mark Paluch 6d0825b121 Release version 5.5 M1 (2025.0.0).
See #3005
2025-02-14 12:22:37 +01:00
Mark Paluch 846344891d Prepare 5.5 M1 (2025.0.0).
See #3005
2025-02-14 12:22:19 +01:00
Peter-Josef Meisch f9f64e6b39 Upgrade to Elasticsearch 8.17.2.
Closes #3054

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-02-12 07:54:06 +01:00
Mark Paluch 7fe4d8e1a4 Update CI Properties.
See #3005
2025-02-11 15:23:14 +01:00
Peter-Josef Meisch bd87dae1a3 Upgrade to Elasticsearch 8.17.1.
Original Pull Request #3053
Closes #3052 

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-02-11 12:47:00 +01:00
Peter-Josef Meisch ea62cf0abd Adopt to deprecation removals in Commons.
Original Pull Request #3051
Closes #3050 

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-02-09 20:20:01 +01:00
Peter-Josef Meisch cb77b328ae Add repository method support for search templates.
Original Pull Request #3049
Closes #2997 

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-02-08 12:17:42 +01:00
Peter-Josef Meisch 5568c7bbc4 Add IndexQuery.builder() method.
Original Pull Request: #3041
Closes #3030

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-01-11 13:21:55 +01:00
Peter-Josef Meisch 03591326d7 Update copyright comment to 2025.
Original Pull Request #3031
Closes #3030
2025-01-02 19:12:45 +01:00
Peter-Josef Meisch a94b74c877 Upgrade to Elasticsearch 8.17.0. (#3027)
Closes #3026
2024-12-15 19:16:38 +01:00
Alfonso 944e7e81dd fix: use scripted field name to populate entity.
Original Pull Request: #3023
Closes: #3022
2024-12-14 18:04:06 +01:00
Peter-Josef Meisch 5f297f1dc3 Upgrade to Elasticsearch 8.16.1.
Original Pull Request #3019
Closes #3017
2024-12-01 13:49:43 +01:00
Peter-Josef Meisch 028239fbdb Add optional fetchSource flag to the SourceFilter.
Original Pull Request #3014
Closes #3009
2024-11-28 15:37:19 +01:00
Peter-Josef Meisch 01d2d24916 Update versions documentation.
Original Pull Request #3013
Closes #3012
2024-11-24 10:43:11 +01:00
Mark Paluch 4f159d5de5 After release cleanups.
See #2990
2024-11-15 14:13:09 +01:00
Mark Paluch 00f13ac3e9 Prepare next development iteration.
See #2990
2024-11-15 14:13:08 +01:00
Mark Paluch 6f3941b43b Release version 5.4 GA (2024.1.0).
See #2990
2024-11-15 14:10:52 +01:00
Mark Paluch bfd3c35d93 Prepare 5.4 GA (2024.1.0).
See #2990
2024-11-15 14:10:37 +01:00
Mark Paluch f5b29cb524 Update CI Properties.
See #2990
2024-11-15 10:39:50 +01:00
Peter-Josef Meisch 7f5bfffc34 fix geohash conversion
Original Pull Request #3002
Closes #3001
2024-11-08 18:55:15 +01:00
Mark Paluch 61176940cb Upgrade to Maven Wrapper 3.9.9.
See #2998
2024-11-07 09:47:28 +01:00
Peter-Josef Meisch 24618ecfbe Upgrade dependency to elasticsearch 8.15.3.
Original Pull Request #2994 
Closes #2993
2024-10-29 11:37:38 +01:00
Peter-Josef Meisch 3e2c67a39f Update elasticsearch-new.adoc 2024-10-22 06:13:44 +02:00
El-Harrougui MOHAMED d2ab03e6a4 Add support for retrieving request executionDuration.
Original Pull Request #2991
Closes #2986
2024-10-22 06:11:57 +02:00
Mark Paluch 172933af8e After release cleanups.
See #2982
2024-10-18 12:49:41 +02:00
Mark Paluch 378dcabe19 Prepare next development iteration.
See #2982
2024-10-18 12:49:40 +02:00
Mark Paluch 893c9cbf92 Release version 5.4 RC1 (2024.1.0).
See #2982
2024-10-18 12:47:01 +02:00
Mark Paluch 3157c62198 Prepare 5.4 RC1 (2024.1.0).
See #2982
2024-10-18 12:46:44 +02:00
Maryanto fe8c2b13b0 Add count methods to ELC's ReactiveElasticsearchClient.
Original Pull Request #2985
Closes #2749
2024-10-17 17:13:28 +02:00
Mark Paluch 98716a871b Consistently run all CI steps with the same user.
See #2982
2024-10-09 09:27:13 +02:00
Jens Schauder d55947b81e After release cleanups.
See #2914
2024-09-13 12:42:28 +02:00
Jens Schauder 6cb5f92928 Prepare next development iteration.
See #2914
2024-09-13 12:42:27 +02:00
Jens Schauder b4ab1f28cd Release version 5.4 M1 (2024.1.0).
See #2914
2024-09-13 12:39:33 +02:00
Jens Schauder aab66c9595 Prepare 5.4 M1 (2024.1.0).
See #2914
2024-09-13 12:39:15 +02:00
Peter-Josef Meisch d06c122fd5 Remove Blockhound
Original Pull Request #2978
Closes #2977
2024-09-04 18:10:20 +02:00
HAN SEUNGWOO b1b232d354 Set refresh on DeleteByQueryRequest by DeleteQuery.
Original Pull Request #2976
Closes #2973
2024-09-03 20:09:26 +02:00
Peter-Josef Meisch 555b570246 Add excludeFromSource handling to multifield.
Original Pull Request #2975
Closes #2971
2024-08-31 21:18:35 +02:00
Peter-Josef Meisch 81eb167981 Upgrade to Elasticsearch 8.15.0.
Ortiginal Pull Request #2974
Closes #2963
2024-08-30 21:31:07 +02:00
Peter-Josef Meisch 6ad98bf500 Polishing 2024-08-19 20:14:13 +02:00
Aouichaoui Youssef 9149c1bc2e Allow for null and empty parameters in the MultiField annotation.
Original Pull Request #2960
Closes #2952
2024-08-19 20:06:56 +02:00
Mark Paluch d079a59cb4 Upgrade to Maven Wrapper 3.9.8.
See #2958
2024-08-08 10:22:09 +02:00
Mark Paluch 7a7145e5b1 Update CI properties.
See #2914
2024-08-08 10:20:12 +02:00
Peter-Josef Meisch dbf932cb20 Polishing 2024-08-06 20:39:02 +02:00
Aouichaoui Youssef 738ee54a25 Support for SQL.
Original Pull Request: #2949
Closes: #2683
2024-08-06 20:32:26 +02:00
Peter-Josef Meisch 03992ba722 Polishing 2024-08-06 18:15:57 +02:00
Andriy Redko 06de217ceb Allow to customize the mapped type name for @InnerField and @Field annotations.
Original Pull request: #2950
Closes #2942
2024-08-06 18:04:37 +02:00
Peter-Josef Meisch eba8eec6c3 Upgrade to Elasticsearch 8.14.3.
Original Pull Request #2953
Closes #2947
2024-08-04 11:46:04 +02:00
Eric Haag 3fc19bbe8c Migrate build to Spring Develocity Conventions extension.
* Migrate build to Spring Develocity Conventions extension.

* Adopt Develocity environment variables.

Closes #2944
2024-08-01 14:53:14 +02:00
Mark Paluch 8f8600727c Bundle Javadoc with Antora documentation site.
Closes #2948.
2024-07-31 14:53:15 +02:00
Peter-Josef Meisch 95e028a1e9 Dependency updates and cleanup.
Original Pull Request #2946
Closes #2945
2024-07-30 06:56:32 +02:00
Peter-Josef Meisch dd156b9e29 Enable use of search_after with field_collapse.
Original Pull Request #2937
Closes #2935
2024-07-06 09:08:27 +02:00
Peter-Josef Meisch 8d0ecf2aa3 Update migration-guide-5.2-5.3.adoc 2024-07-04 21:02:41 +02:00
Mark Paluch 4cc80abcd8 Switch to Broadcom docker proxy.
Closes #2934
2024-06-20 11:20:58 +02:00
Peter-Josef Meisch eca6a7ec77 Upgrade to Elasticsearch-8.14.1.
Original Pull Request #2930
Closes #2929
2024-06-13 23:34:55 +02:00
puppylpg d9d1b73dad Fix missing element_type when using elasticsearch-java 8.14.x
Original Pull Request #2928
Closes #2927
2024-06-12 18:27:43 +02:00
Peter-Josef Meisch d101eebc6d Upgrade to Elasticsearch-8.14.0.
Original Pull Request #2926
Closes #2924
2024-06-11 21:24:57 +02:00
Peter-Josef Meisch 4ef5af1f2d Update dependencies.
Original Pull Request #2923
Closes #2922
2024-06-01 15:26:45 +02:00
Peter-Josef Meisch fade919be6 Polishing. 2024-05-28 20:57:27 +02:00
puppylpg 687b014e70 Add knn search parameter and remove knn query.
Original Pull Rrequest #2920
Closes #2919
2024-05-28 20:52:47 +02:00
Peter-Josef Meisch 9d139299b2 Add documentation for migration 5.3 to 5.4 2024-05-27 19:38:00 +02:00
Peter-Josef Meisch 161439ae22 Polishing 2024-05-26 20:31:42 +02:00
Aouichaoui Youssef fbe54e485b Add support for index aliases.
Original Pull Request #2905
Closes #2599
2024-05-26 20:16:21 +02:00
Peter-Josef Meisch 86e0e660be Upgrade to Elasticsearch 8.13.4.
Original Pull Request #2918
Closes #2915
2024-05-19 11:48:34 +02:00
Peter-Josef Meisch 5ebe9f4492 Update version document. 2024-05-18 19:15:02 +02:00
Peter-Josef Meisch e997b39f68 Fix max dim value for dense vector.
Closes #2911
2024-05-18 18:11:43 +02:00
Mark Paluch 41e32576e3 After release cleanups.
See #2896
2024-05-17 11:51:34 +02:00
Mark Paluch 82c4ea1391 Prepare next development iteration.
See #2896
2024-05-17 11:51:33 +02:00
151 changed files with 5567 additions and 998 deletions
+1
View File
@@ -33,3 +33,4 @@ node
package-lock.json
.mvn/.develocity
/src/test/resources/testcontainers-local.properties
+4
View File
@@ -8,3 +8,7 @@
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
--add-opens=java.base/java.util=ALL-UNNAMED
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED
--add-opens=java.base/java.text=ALL-UNNAMED
--add-opens=java.desktop/java.awt.font=ALL-UNNAMED
+2 -2
View File
@@ -1,3 +1,3 @@
#Thu Nov 07 09:49:32 CET 2024
#Thu Jul 17 14:00:55 CEST 2025
wrapperUrl=https\://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
Vendored
+1 -1
View File
@@ -9,7 +9,7 @@ pipeline {
triggers {
pollSCM 'H/10 * * * *'
upstream(upstreamProjects: "spring-data-commons/3.3.x", threshold: hudson.model.Result.SUCCESS)
upstream(upstreamProjects: "spring-data-commons/3.5.x", threshold: hudson.model.Result.SUCCESS)
}
options {
+2 -2
View File
@@ -62,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
@@ -168,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
+6 -9
View File
@@ -1,23 +1,20 @@
# Java versions
java.main.tag=17.0.13_11-jdk-focal
java.next.tag=22.0.2_9-jdk-jammy
java.main.tag=17.0.15_6-jdk-focal
java.next.tag=24.0.1_9-jdk-noble
# Docker container images - standard
docker.java.main.image=library/eclipse-temurin:${java.main.tag}
docker.java.next.image=library/eclipse-temurin:${java.next.tag}
# Supported versions of MongoDB
docker.mongodb.4.4.version=4.4.25
docker.mongodb.5.0.version=5.0.21
docker.mongodb.6.0.version=6.0.10
docker.mongodb.7.0.version=7.0.2
docker.mongodb.6.0.version=6.0.23
docker.mongodb.7.0.version=7.0.20
docker.mongodb.8.0.version=8.0.9
# Supported versions of Redis
docker.redis.6.version=6.2.13
docker.redis.7.version=7.2.4
# Supported versions of Cassandra
docker.cassandra.3.version=3.11.16
docker.valkey.8.version=8.1.1
# Docker environment settings
docker.java.inside.basic=-v $HOME:/tmp/jenkins-home
+14 -13
View File
@@ -1,16 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>5.3.9</version>
<version>5.5.3</version>
<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<version>3.3.9</version>
<version>3.5.3</version>
</parent>
<name>Spring Data Elasticsearch</name>
@@ -18,16 +18,16 @@
<url>https://github.com/spring-projects/spring-data-elasticsearch</url>
<properties>
<springdata.commons>3.3.9</springdata.commons>
<springdata.commons>3.5.3</springdata.commons>
<!-- version of the ElasticsearchClient -->
<elasticsearch-java>8.13.4</elasticsearch-java>
<elasticsearch-java>8.18.5</elasticsearch-java>
<hoverfly>0.14.4</hoverfly>
<log4j>2.18.0</log4j>
<jsonassert>1.5.1</jsonassert>
<testcontainers>1.18.0</testcontainers>
<wiremock>2.35.1</wiremock>
<hoverfly>0.19.0</hoverfly>
<log4j>2.23.1</log4j>
<jsonassert>1.5.3</jsonassert>
<testcontainers>1.20.0</testcontainers>
<wiremock>3.9.1</wiremock>
<java-module-name>spring.data.elasticsearch</java-module-name>
@@ -131,9 +131,10 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId> <!-- is Apache 2-->
<artifactId>elasticsearch-rest-client</artifactId>
<version>${elasticsearch-java}</version>
<exclusions>
<exclusion>
@@ -255,8 +256,8 @@
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<version>${wiremock}</version>
<scope>test</scope>
<exclusions>
+1 -1
View File
@@ -17,7 +17,7 @@ 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:
+3
View File
@@ -10,6 +10,9 @@
*** 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:elasticsearch.adoc[]
** xref:elasticsearch/clients.adoc[]
@@ -1,10 +1,20 @@
[[new-features]]
= What's new
[[new-features.5-3-1]]
== New in Spring Data Elasticsearch 5.3.1
[[new-features.5-5-0]]
== New in Spring Data Elasticsearch 5.5
* Upgrade to Elasticsearch 8.13.4.
* Upgrade to Elasticsearch 8.18.1.
* Add support for the `@SearchTemplateQuery` annotation on repository methods.
* Scripted field properties of type collection can be populated from scripts returning arrays.
[[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
@@ -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.
@@ -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<T>`
[[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<Book, String> {
@@ -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<SampleEntity> getByIds(Collection<String> 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]
----
{
@@ -367,8 +376,7 @@ would make an https://www.elastic.co/guide/en/elasticsearch/reference/current/qu
.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`.
{spring-framework-docs}/core/expressions.html[SpEL expression] is also supported when defining query in `@Query`.
[source,java]
----
@@ -411,6 +419,7 @@ If for example the function is called with the parameter _John_, it would produc
.accessing parameter property.
====
Supposing that we have the following class as query parameter type:
[source,java]
----
public record QueryParameter(String value) {
@@ -444,7 +453,9 @@ We can pass `new QueryParameter("John")` as the parameter now, and it will produ
.accessing bean property.
====
https://docs.spring.io/spring-framework/reference/core/expressions/language-ref/bean-references.html[Bean property] is also supported to access. Given that there is a bean named `queryParameter` of type `QueryParameter`, we can access the bean with symbol `@` rather than `#`, and there is no need to declare a parameter of type `QueryParameter` in the query method:
{spring-framework-docs}/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<Book, String> {
@@ -493,6 +504,7 @@ interface BookRepository extends ElasticsearchRepository<Book, String> {
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]
----
{
@@ -511,7 +523,7 @@ A collection of `names` like `List.of("name1", "name2")` will produce the follow
.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`:
{spring-framework-docs}/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]
----
@@ -532,6 +544,7 @@ interface BookRepository extends ElasticsearchRepository<Book, String> {
Page<Book> findByName(Collection<QueryParameter> parameters, Pageable pageable);
}
----
This will extract all the `value` property values as a new `Collection` from `QueryParameter` collection, thus takes the same effect as above.
====
@@ -560,3 +573,20 @@ interface BookRepository extends ElasticsearchRepository<Book, String> {
----
====
[[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<Book, String> {
@SearchTemplateQuery(id = "book-by-title")
SearchHits<Book> 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.
@@ -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<T>
@@ -6,9 +6,11 @@ The following table shows the Elasticsearch and Spring versions that are used by
[cols="^,^,^,^",options="header"]
|===
| Spring Data Release Train | Spring Data Elasticsearch | Elasticsearch | Spring Framework
| 2024.0 | 5.3.3 | 8.13.4 | 6.1.x
| 2023.1 (Vaughan) | 5.2.x | 8.11.1 | 6.1.x
| 2023.0 (Ullmann) | 5.1.xfootnote:oom[Out of maintenance] | 8.7.1 | 6.0.x
| 2025.0 | 5.5.x | 8.18.5 | 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
@@ -5,14 +5,17 @@ This section describes breaking changes from version 5.2.x to 5.3.x and how remo
[[elasticsearch-migration-guide-5.2-5.3.breaking-changes]]
== Breaking Changes
During the parameter replacement in `@Query` annotated repository methods previous versions wrote the String _"null"_ into the query that was sent to Elasticsearch
when the actual parameter value was `null`. As Elasticsearch does not store `null` values, this behaviour could lead to problems, for example whent the fields to be
searched contains the string `"null"`. In Version 5.3 a `null` value in a parameter will cause a `ConversionException` to be thrown. If you are using `"null"` as the
During the parameter replacement in `@Query` annotated repository methods previous versions wrote the String `"null"` into the query that was sent to Elasticsearch when the actual parameter value was `null`.
As Elasticsearch does not store `null` values, this behaviour could lead to problems, for example whent the fields to be searched contains the string `"null"`.
In Version 5.3 a `null` value in a parameter will cause a `ConversionException` to be thrown.
If you are using `"null"` as the
`null_value` defined in a field mapping, then pass that string into the query instead of a Java `null`.
[[elasticsearch-migration-guide-5.2-5.3.deprecations]]
== Deprecations
=== Removals
The deprecated classes `org.springframework.data.elasticsearch.ELCQueries`
and `org.springframework.data.elasticsearch.client.elc.QueryBuilders` have been removed, use `org.springframework.data.elasticsearch.client.elc.Queries` instead.
and `org.springframework.data.elasticsearch.client.elc.QueryBuilders` have been removed, use `org.springframework.data.elasticsearch.client.elc.Queries` instead.
@@ -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
@@ -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)
```
@@ -3,18 +3,19 @@ prerelease: ${antora-component.prerelease}
asciidoc:
attributes:
copyright-year: ${current.year}
attribute-missing: 'warn'
chomp: 'all'
version: ${project.version}
copyright-year: ${current.year}
springversionshort: ${spring.short}
springversion: ${spring}
attribute-missing: 'warn'
commons: ${springdata.commons.docs}
include-xml-namespaces: false
spring-data-commons-docs-url: https://docs.spring.io/spring-data/commons/reference
spring-data-commons-javadoc-base: https://docs.spring.io/spring-data/commons/docs/${springdata.commons}/api/
spring-data-commons-docs-url: https://docs.spring.io/spring-data/commons/reference/{commons}
spring-data-commons-javadoc-base: '{spring-data-commons-docs-url}/api/java'
springdocsurl: https://docs.spring.io/spring-framework/reference/{springversionshort}
springjavadocurl: https://docs.spring.io/spring-framework/docs/${spring}/javadoc-api
spring-framework-docs: '{springdocsurl}'
springjavadocurl: https://docs.spring.io/spring-framework/docs/${spring}/javadoc-api
spring-framework-javadoc: '{springjavadocurl}'
springhateoasversion: ${spring-hateoas}
releasetrainversion: ${releasetrain}
@@ -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;
}
@@ -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();
}
@@ -100,6 +100,13 @@ public @interface Document {
*/
boolean storeVersionInSource() default true;
/**
* Aliases for the index.
*
* @since 5.4
*/
Alias[] aliases() default {};
/**
* @since 4.3
*/
@@ -37,6 +37,8 @@ import org.springframework.core.annotation.AliasFor;
* @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 @@ public @interface Field {
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 @@ public @interface Field {
*/
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.<br>
* To be used in combination with {@link FieldType#Object} or {@link FieldType#Nested}
@@ -218,4 +245,11 @@ public @interface Field {
* @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 "";
}
@@ -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";
}
@@ -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 "";
}
@@ -29,6 +29,8 @@ import java.lang.annotation.Target;
* @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 @@ public @interface InnerField {
* @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 "";
}
@@ -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;
}
}
@@ -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;
}
@@ -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;
}
}
@@ -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();
}
@@ -18,6 +18,8 @@ package org.springframework.data.elasticsearch.client.elc;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.transport.ElasticsearchTransport;
import java.io.IOException;
import org.elasticsearch.client.RestClient;
import org.springframework.util.Assert;
@@ -36,7 +38,10 @@ public class AutoCloseableElasticsearchClient extends ElasticsearchClient implem
}
@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();
}
}
@@ -127,7 +127,7 @@ class CriteriaQueryProcessor extends AbstractQueryProcessor {
mustQueries.add(Query.of(qb -> qb.matchAll(m -> m)));
}
return new Query.Builder().bool(boolQueryBuilder -> {
return new Query.Builder().bool(boolQueryBuilder -> {
if (!shouldQueries.isEmpty()) {
boolQueryBuilder.should(shouldQueries);
@@ -249,49 +249,54 @@ class CriteriaQueryProcessor extends AbstractQueryProcessor {
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;
Assert.notNull(value, "value for a between condition must not be null");
queryBuilder //
.range(rb -> {
rb.field(fieldName);
if (ranges[0] != null) {
rb.gte(JsonData.of(ranges[0]));
}
queryBuilder
.range(rb -> rb
.untyped(ut -> {
ut.field(fieldName);
if (ranges[0] != null) {
ut.gte(JsonData.of(ranges[0]));
}
if (ranges[1] != null) {
rb.lte(JsonData.of(ranges[1]));
}
rb.boost(boost); //
return rb;
}); //
if (ranges[1] != null) {
ut.lte(JsonData.of(ranges[1]));
}
ut.boost(boost); //
return ut;
}));
break;
case FUZZY:
@@ -50,6 +50,7 @@ import org.springframework.util.Assert;
*
* @author Peter-Josef Meisch
* @author Haibo Liu
* @author Mohamed El Harrougui
* @since 4.4
*/
final class DocumentAdapters {
@@ -74,7 +75,7 @@ final class DocumentAdapters {
Map<String, SearchDocumentResponse> innerHits = new LinkedHashMap<>();
hit.innerHits().forEach((name, innerHitsResult) -> {
// noinspection ReturnOfNull
innerHits.put(name, SearchDocumentResponseBuilder.from(innerHitsResult.hits(), null, null, null, null, null,
innerHits.put(name, SearchDocumentResponseBuilder.from(innerHitsResult.hits(), null, null, null, 0, null, null,
searchDocument -> null, jsonpMapper));
});
@@ -329,7 +329,7 @@ public final class ElasticsearchClients {
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);
@@ -135,6 +135,6 @@ public abstract class ElasticsearchConfiguration extends ElasticsearchConfigurat
* @return the options that should be added to every request. Must not be {@literal null}
*/
public TransportOptions transportOptions() {
return new RestClientOptions(RequestOptions.DEFAULT);
return new RestClientOptions(RequestOptions.DEFAULT, false);
}
}
@@ -23,6 +23,8 @@ import co.elastic.clients.elasticsearch.core.*;
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;
@@ -56,6 +58,7 @@ 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.data.elasticsearch.core.sql.SqlResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -74,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;
@@ -85,6 +89,7 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
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);
@@ -97,6 +102,7 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
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);
@@ -175,19 +181,6 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
return delete(query, clazz, getIndexCoordinatesFor(clazz));
}
@Override
public ByQueryResponse delete(Query query, Class<?> clazz, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
DeleteByQueryRequest request = requestConverter.documentDeleteByQueryRequest(query, routingResolver.getRouting(),
clazz, index, getRefreshPolicy());
DeleteByQueryResponse response = execute(client -> client.deleteByQuery(request));
return responseConverter.byQueryResponse(response);
}
@Override
public ByQueryResponse delete(DeleteQuery query, Class<?> clazz, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
@@ -656,6 +649,19 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
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
@@ -21,13 +21,12 @@ import co.elastic.clients.elasticsearch.indices.*;
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.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.elasticsearch.UncategorizedElasticsearchException;
@@ -46,6 +45,8 @@ import org.springframework.data.elasticsearch.core.index.GetIndexTemplateRequest
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;
@@ -60,8 +61,6 @@ import org.springframework.util.Assert;
public class IndicesTemplate extends ChildTemplate<ElasticsearchTransport, ElasticsearchIndicesClient>
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 class IndicesTemplate extends ChildTemplate<ElasticsearchTransport, Elast
}
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 class IndicesTemplate extends ChildTemplate<ElasticsearchTransport, Elast
protected boolean doCreate(IndexCoordinates indexCoordinates, Map<String, Object> settings,
@Nullable Document mapping) {
Set<Alias> aliases = (boundClass != null) ? getAliasesFor(boundClass) : new HashSet<>();
CreateIndexSettings indexSettings = CreateIndexSettings.builder(indexCoordinates)
.withAliases(aliases)
.withSettings(settings)
.withMapping(mapping)
.build();
Assert.notNull(indexCoordinates, "indexCoordinates must not be null");
Assert.notNull(settings, "settings must not be null");
CreateIndexRequest createIndexRequest = requestConverter.indicesCreateRequest(indexCoordinates, settings, mapping);
CreateIndexRequest createIndexRequest = requestConverter.indicesCreateRequest(indexSettings);
CreateIndexResponse createIndexResponse = execute(client -> client.create(createIndexRequest));
return Boolean.TRUE.equals(createIndexResponse.acknowledged());
}
@@ -236,8 +238,7 @@ public class IndicesTemplate extends ChildTemplate<ElasticsearchTransport, Elast
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 class IndicesTemplate extends ChildTemplate<ElasticsearchTransport, Elast
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<Alias> getAliasesFor(Class<?> clazz) {
return getRequiredPersistentEntity(clazz).getAliases();
}
// endregion
}
@@ -15,7 +15,6 @@
*/
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;
@@ -30,7 +29,6 @@ import java.util.List;
import java.util.Map;
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;
@@ -40,6 +38,7 @@ import org.springframework.util.Assert;
*
* @author Peter-Josef Meisch
* @author Sascha Woo
* @author Haibo Liu
* @since 4.4
*/
public class NativeQuery extends BaseQuery {
@@ -54,7 +53,6 @@ public class NativeQuery extends BaseQuery {
private List<SortOptions> sortOptions = Collections.emptyList();
private Map<String, JsonData> searchExtensions = Collections.emptyMap();
@Nullable private KnnQuery knnQuery;
@Nullable private List<KnnSearch> knnSearches = Collections.emptyList();
public NativeQuery(NativeQueryBuilder builder) {
@@ -72,7 +70,6 @@ public class NativeQuery extends BaseQuery {
"Cannot add an NativeQuery in a NativeQuery");
}
this.springDataQuery = builder.getSpringDataQuery();
this.knnQuery = builder.getKnnQuery();
this.knnSearches = builder.getKnnSearches();
}
@@ -124,14 +121,6 @@ public class NativeQuery extends BaseQuery {
this.springDataQuery = springDataQuery;
}
/**
* @since 5.1
*/
@Nullable
public KnnQuery getKnnQuery() {
return knnQuery;
}
/**
* @since 5.3.1
*/
@@ -40,6 +40,7 @@ import org.springframework.util.Assert;
/**
* @author Peter-Josef Meisch
* @author Sascha Woo
* @author Haibo Liu
* @since 4.4
*/
public class NativeQueryBuilder extends BaseQueryBuilder<NativeQuery, NativeQueryBuilder> {
@@ -213,13 +214,30 @@ public class NativeQueryBuilder extends BaseQueryBuilder<NativeQuery, NativeQuer
}
/**
* @since 5.1
* @since 5.4
*/
public NativeQueryBuilder withKnnQuery(KnnQuery knnQuery) {
this.knnQuery = knnQuery;
public NativeQueryBuilder withKnnSearches(List<KnnSearch> knnSearches) {
this.knnSearches = knnSearches;
return this;
}
/**
* @since 5.4
*/
public NativeQueryBuilder withKnnSearches(Function<KnnSearch.Builder, ObjectBuilder<KnnSearch>> 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);
@@ -27,6 +27,7 @@ import co.elastic.clients.transport.endpoints.EndpointWithResponseMapperAttr;
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;
@@ -36,6 +37,7 @@ 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<ElasticsearchTransport, ReactiveElasticsearchClient>
@@ -55,8 +57,11 @@ public class ReactiveElasticsearchClient extends ApiClient<ElasticsearchTranspor
}
@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 class ReactiveElasticsearchClient extends ApiClient<ElasticsearchTranspor
return new ReactiveElasticsearchIndicesClient(transport, transportOptions);
}
public ReactiveElasticsearchSqlClient sql() {
return new ReactiveElasticsearchSqlClient(transport, transportOptions);
}
// endregion
// region info
@@ -122,7 +131,8 @@ public class ReactiveElasticsearchClient extends ApiClient<ElasticsearchTranspor
// java.lang.Class<TDocument>)
// noinspection unchecked
JsonEndpoint<GetRequest, GetResponse<T>, ErrorResponse> endpoint = (JsonEndpoint<GetRequest, GetResponse<T>, ErrorResponse>) GetRequest._ENDPOINT;
endpoint = new EndpointWithResponseMapperAttr<>(endpoint, "co.elastic.clients:Deserializer:_global.get.TDocument",
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 class ReactiveElasticsearchClient extends ApiClient<ElasticsearchTranspor
// noinspection unchecked
JsonEndpoint<UpdateRequest<?, ?>, UpdateResponse<T>, 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 class ReactiveElasticsearchClient extends ApiClient<ElasticsearchTranspor
// noinspection unchecked
JsonEndpoint<MgetRequest, MgetResponse<T>, ErrorResponse> endpoint = (JsonEndpoint<MgetRequest, MgetResponse<T>, 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 class ReactiveElasticsearchClient extends ApiClient<ElasticsearchTranspor
return deleteByQuery(fn.apply(new DeleteByQueryRequest.Builder()).build());
}
/**
* @since 5.4
*/
public Mono<CountResponse> 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<CountResponse> count(Function<CountRequest.Builder, ObjectBuilder<CountRequest>> 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 class ReactiveElasticsearchClient extends ApiClient<ElasticsearchTranspor
// noinspection unchecked
JsonEndpoint<ScrollRequest, ScrollResponse<T>, ErrorResponse> endpoint = (JsonEndpoint<ScrollRequest, ScrollResponse<T>, ErrorResponse>) ScrollRequest._ENDPOINT;
endpoint = new EndpointWithResponseMapperAttr<>(endpoint,
"co.elastic.clients:Deserializer:_global.scroll.TDocument", getDeserializer(tDocumentClass));
"co.elastic.clients:Deserializer:_global.scroll.Response.TDocument", getDeserializer(tDocumentClass));
return Mono.fromFuture(transport.performRequestAsync(request, endpoint, transportOptions));
}
@@ -125,6 +125,6 @@ public abstract class ReactiveElasticsearchConfiguration extends ElasticsearchCo
* @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();
}
}
@@ -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<ElasticsearchTransport, ReactiveElasticsearchSqlClient> {
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<QueryResponse> query(Function<QueryRequest.Builder, ObjectBuilder<QueryRequest>> fn)
throws IOException, ElasticsearchException {
return query(fn.apply(new QueryRequest.Builder()).build());
}
/**
* Executes a SQL request.
*/
public Mono<QueryResponse> query(QueryRequest query) {
return Mono.fromFuture(transport.performRequestAsync(query, QueryRequest._ENDPOINT, transportOptions));
}
/**
* Executes a SQL request.
*/
public Mono<QueryResponse> query() {
return Mono.fromFuture(
transport.performRequestAsync(new QueryRequest.Builder().build(), QueryRequest._ENDPOINT, transportOptions));
}
}
@@ -57,18 +57,12 @@ import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.document.SearchDocument;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
import org.springframework.data.elasticsearch.core.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.DeleteQuery;
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.data.elasticsearch.core.sql.SqlResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
@@ -88,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;
@@ -99,6 +94,7 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
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);
@@ -171,16 +167,6 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
.onErrorReturn(NoSuchIndexException.class, false);
}
@Override
public Mono<ByQueryResponse> delete(Query query, Class<?> entityType, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
DeleteByQueryRequest request = requestConverter.documentDeleteByQueryRequest(query, routingResolver.getRouting(),
entityType, index, getRefreshPolicy());
return Mono.from(execute(client -> client.deleteByQuery(request))).map(responseConverter::byQueryResponse);
}
@Override
public Mono<ByQueryResponse> delete(DeleteQuery query, Class<?> entityType, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
@@ -646,6 +632,14 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
return NativeQuery.builder().withIds(ids);
}
@Override
public Mono<SqlResponse> 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}.
@@ -15,7 +15,7 @@
*/
package org.springframework.data.elasticsearch.client.elc;
import static org.springframework.util.StringUtils.hasText;
import static org.springframework.util.StringUtils.*;
import co.elastic.clients.elasticsearch._types.AcknowledgedResponseBase;
import co.elastic.clients.elasticsearch.indices.*;
@@ -24,6 +24,7 @@ import co.elastic.clients.transport.endpoints.BooleanResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@@ -46,6 +47,8 @@ import org.springframework.data.elasticsearch.core.index.GetIndexTemplateRequest
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;
@@ -130,8 +133,14 @@ public class ReactiveIndicesTemplate
private Mono<Boolean> doCreate(IndexCoordinates indexCoordinates, Map<String, Object> settings,
@Nullable Document mapping) {
Set<Alias> aliases = (boundClass != null) ? getAliasesFor(boundClass) : new HashSet<>();
CreateIndexSettings indexSettings = CreateIndexSettings.builder(indexCoordinates)
.withAliases(aliases)
.withSettings(settings)
.withMapping(mapping)
.build();
CreateIndexRequest createIndexRequest = requestConverter.indicesCreateRequest(indexCoordinates, settings, mapping);
CreateIndexRequest createIndexRequest = requestConverter.indicesCreateRequest(indexSettings);
Mono<CreateIndexResponse> createIndexResponse = Mono.from(execute(client -> client.create(createIndexRequest)));
return createIndexResponse.map(CreateIndexResponse::acknowledged);
}
@@ -435,6 +444,15 @@ public class ReactiveIndicesTemplate
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<Alias> getAliasesFor(Class<?> clazz) {
return elasticsearchConverter.getMappingContext().getRequiredPersistentEntity(clazz).getAliases();
}
private Class<?> checkForBoundClass() {
if (boundClass == null) {
throw new InvalidDataAccessApiUsageException("IndexOperations are not bound");
@@ -20,7 +20,6 @@ import static org.springframework.util.CollectionUtils.*;
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;
@@ -52,9 +51,11 @@ 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;
@@ -68,9 +69,11 @@ import java.util.Collections;
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;
@@ -88,6 +91,8 @@ import org.springframework.data.elasticsearch.core.index.GetIndexTemplateRequest
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;
@@ -111,14 +116,10 @@ import org.springframework.util.StringUtils;
* @author Haibo Liu
* @since 4.4
*/
@SuppressWarnings("ClassCanBeRecord")
class RequestConverter extends AbstractQueryProcessor {
private static final Log LOGGER = LogFactory.getLog(RequestConverter.class);
// the default max result window size of Elasticsearch
public static final Integer INDEX_MAX_RESULT_WINDOW = 10_000;
protected final JsonpMapper jsonpMapper;
protected final ElasticsearchConverter elasticsearchConverter;
@@ -170,7 +171,8 @@ class RequestConverter extends AbstractQueryProcessor {
}));
}
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) {
if (parameters.getRouting() != null) {
aliasBuilder.routing(parameters.getRouting());
@@ -234,17 +236,25 @@ class RequestConverter extends AbstractQueryProcessor {
return new ExistsRequest.Builder().index(Arrays.asList(indexCoordinates.getIndexNames())).build();
}
public CreateIndexRequest indicesCreateRequest(IndexCoordinates indexCoordinates, Map<String, Object> settings,
@Nullable Document mapping) {
Assert.notNull(indexCoordinates, "indexCoordinates must not be null");
Assert.notNull(settings, "settings must not be null");
public CreateIndexRequest indicesCreateRequest(CreateIndexSettings indexSettings) {
Map<String, co.elastic.clients.elasticsearch.indices.Alias> 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();
}
@@ -399,7 +409,7 @@ class RequestConverter extends AbstractQueryProcessor {
if (putTemplateRequest.getSettings() != null) {
Map<String, JsonData> settings = getTemplateParams(putTemplateRequest.getSettings().entrySet());
builder.settings(settings);
builder.settings(sb -> sb.otherSettings(settings));
}
if (putTemplateRequest.getMappings() != null) {
@@ -520,6 +530,27 @@ class RequestConverter extends AbstractQueryProcessor {
.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
@@ -717,16 +748,12 @@ class RequestConverter extends AbstractQueryProcessor {
scriptData.params().forEach((key, value) -> params.put(key, JsonData.of(value, jsonpMapper)));
}
return co.elastic.clients.elasticsearch._types.Script.of(sb -> {
sb.lang(scriptData.language())
.params(params);
if (scriptData.type() == ScriptType.INLINE) {
sb.inline(is -> is //
.lang(scriptData.language()) //
.source(scriptData.script()) //
.params(params)); //
sb.source(scriptData.script());
} else if (scriptData.type() == ScriptType.STORED) {
sb.stored(ss -> ss //
.id(scriptData.script()) //
.params(params) //
);
sb.id(scriptData.script());
}
return sb;
});
@@ -898,7 +925,9 @@ class RequestConverter extends AbstractQueryProcessor {
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 -> sb
.lang(script.getLang())
.source(script.getSource()));
}
builder.timeout(time(reindexRequest.getTimeout())) //
@@ -1016,7 +1045,7 @@ class RequestConverter extends AbstractQueryProcessor {
order = sortField.order().jsonValue();
}
return sortField.field() + ":" + order;
return sortField.field() + ':' + order;
})
.collect(Collectors.toList()));
}
@@ -1054,21 +1083,15 @@ class RequestConverter extends AbstractQueryProcessor {
}
uqb.script(sb -> {
sb.lang(query.getLang()).params(params);
if (query.getScriptType() == ScriptType.INLINE) {
sb.inline(is -> is //
.lang(query.getLang()) //
.source(query.getScript()) //
.params(params)); //
sb.source(query.getScript()); //
} else if (query.getScriptType() == ScriptType.STORED) {
sb.stored(ss -> ss //
.id(query.getScript()) //
.params(params) //
);
sb.id(query.getScript());
}
return sb;
}
);
});
}
uqb //
@@ -1269,11 +1292,8 @@ class RequestConverter extends AbstractQueryProcessor {
.timeout(timeStringMs(query.getTimeout())) //
;
if (query.getPageable().isPaged()) {
bb //
.from((int) query.getPageable().getOffset()) //
.size(query.getPageable().getPageSize());
}
bb.from((int) (query.getPageable().isPaged() ? query.getPageable().getOffset() : 0))
.size(query.getRequestSize());
if (!isEmpty(query.getFields())) {
bb.fields(fb -> {
@@ -1286,10 +1306,6 @@ class RequestConverter extends AbstractQueryProcessor {
bb.storedFields(query.getStoredFields());
}
if (query.isLimiting()) {
bb.size(query.getMaxResults());
}
if (query.getMinScore() > 0) {
bb.minScore((double) query.getMinScore());
}
@@ -1323,17 +1339,16 @@ class RequestConverter extends AbstractQueryProcessor {
String script = runtimeField.getScript();
if (script != null) {
rfb
.script(s -> s
.inline(is -> {
is.source(script);
rfb.script(s -> {
s.source(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;
});
runtimeMappings.put(runtimeField.getName(), esRuntimeField);
@@ -1342,9 +1357,14 @@ class RequestConverter extends AbstractQueryProcessor {
}
if (!isEmpty(query.getIndicesBoost())) {
bb.indicesBoost(query.getIndicesBoost().stream()
.map(indexBoost -> Map.of(indexBoost.getIndexName(), (double) indexBoost.getBoost()))
.collect(Collectors.toList()));
Stream<NamedValue<Double>> namedValueStream = query.getIndicesBoost().stream()
.map(indexBoost -> {
var namedValue = new NamedValue(indexBoost.getIndexName(),
Float.valueOf(indexBoost.getBoost()).doubleValue());
return namedValue;
});
List<NamedValue<Double>> namedValueList = namedValueStream.collect(Collectors.toList());
bb.indicesBoost(namedValueList);
}
query.getScriptedFields().forEach(scriptedField -> bb.scriptFields(scriptedField.getFieldName(),
@@ -1370,7 +1390,7 @@ class RequestConverter extends AbstractQueryProcessor {
private Function<MultisearchHeader.Builder, ObjectBuilder<MultisearchHeader>> msearchHeaderBuilder(Query query,
IndexCoordinates index, @Nullable String routing) {
return h -> {
var searchType = (query instanceof NativeQuery nativeQuery && nativeQuery.getKnnQuery() != null) ? null
var searchType = (query instanceof NativeQuery nativeQuery && !isEmpty(nativeQuery.getKnnSearches())) ? null
: searchType(query.getSearchType());
h //
@@ -1402,7 +1422,7 @@ class RequestConverter extends AbstractQueryProcessor {
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 //
@@ -1443,13 +1463,8 @@ class RequestConverter extends AbstractQueryProcessor {
builder.seqNoPrimaryTerm(true);
}
if (query.getPageable().isPaged()) {
builder //
.from((int) query.getPageable().getOffset()) //
.size(query.getPageable().getPageSize());
} else {
builder.from(0).size(INDEX_MAX_RESULT_WINDOW);
}
builder.from((int) (query.getPageable().isPaged() ? query.getPageable().getOffset() : 0))
.size(query.getRequestSize());
if (!isEmpty(query.getFields())) {
var fieldAndFormats = query.getFields().stream().map(field -> FieldAndFormat.of(b -> b.field(field))).toList();
@@ -1464,10 +1479,6 @@ class RequestConverter extends AbstractQueryProcessor {
addIndicesOptions(builder, query.getIndicesOptions());
}
if (query.isLimiting()) {
builder.size(query.getMaxResults());
}
if (query.getMinScore() > 0) {
builder.minScore((double) query.getMinScore());
}
@@ -1524,16 +1535,14 @@ class RequestConverter extends AbstractQueryProcessor {
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 -> {
s.source(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;
@@ -1555,9 +1564,14 @@ class RequestConverter extends AbstractQueryProcessor {
}
if (!isEmpty(query.getIndicesBoost())) {
builder.indicesBoost(query.getIndicesBoost().stream()
.map(indexBoost -> Map.of(indexBoost.getIndexName(), (double) indexBoost.getBoost()))
.collect(Collectors.toList()));
Stream<NamedValue<Double>> namedValueStream = query.getIndicesBoost().stream()
.map(indexBoost -> {
var namedValue = new NamedValue(indexBoost.getIndexName(),
Float.valueOf(indexBoost.getBoost()).doubleValue());
return namedValue;
});
List<NamedValue<Double>> namedValueList = namedValueStream.collect(Collectors.toList());
builder.indicesBoost(namedValueList);
}
if (!isEmpty(query.getDocValueFields())) {
@@ -1729,17 +1743,6 @@ class RequestConverter extends AbstractQueryProcessor {
.sort(query.getSortOptions()) //
;
if (query.getKnnQuery() != null) {
var kq = query.getKnnQuery();
builder.knn(ksb -> ksb
.field(kq.field())
.queryVector(kq.queryVector())
.numCandidates(kq.numCandidates())
.filter(kq.filter())
.similarity(kq.similarity()));
}
if (!isEmpty(query.getKnnSearches())) {
builder.knn(query.getKnnSearches());
}
@@ -1761,17 +1764,6 @@ class RequestConverter extends AbstractQueryProcessor {
.collapse(query.getFieldCollapse()) //
.sort(query.getSortOptions());
if (query.getKnnQuery() != null) {
var kq = query.getKnnQuery();
builder.knn(ksb -> ksb
.field(kq.field())
.queryVector(kq.queryVector())
.numCandidates(kq.numCandidates())
.filter(kq.filter())
.similarity(kq.similarity()));
}
if (!isEmpty(query.getKnnSearches())) {
builder.knn(query.getKnnSearches());
}
@@ -1791,7 +1783,6 @@ class RequestConverter extends AbstractQueryProcessor {
return getEsQuery(query, (q) -> elasticsearchConverter.updateQuery(q, clazz));
}
@SuppressWarnings("StatementWithEmptyBody")
private void addPostFilter(Query query, SearchRequest.Builder builder) {
// we only need to handle NativeQuery here. filter from a CriteriaQuery are added into the query and not as post
@@ -2022,9 +2013,12 @@ class RequestConverter extends AbstractQueryProcessor {
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();
@@ -2037,7 +2031,10 @@ class RequestConverter extends AbstractQueryProcessor {
}
return sfb;
}));
});
}
return s;
});
} else {
return null;
}
@@ -33,6 +33,8 @@ import co.elastic.clients.elasticsearch.core.mget.MultiGetResponseItem;
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.ArrayList;
@@ -61,6 +63,7 @@ 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;
@@ -89,7 +92,7 @@ class ResponseConverter {
return ClusterHealth.builder() //
.withActivePrimaryShards(healthResponse.activePrimaryShards()) //
.withActiveShards(healthResponse.activeShards()) //
.withActiveShardsPercent(Double.parseDouble(healthResponse.activeShardsPercentAsNumber()))//
.withActiveShardsPercent(healthResponse.activeShardsPercentAsNumber())//
.withClusterName(healthResponse.clusterName()) //
.withDelayedUnassignedShards(healthResponse.delayedUnassignedShards()) //
.withInitializingShards(healthResponse.initializingShards()) //
@@ -397,7 +400,6 @@ class ResponseConverter {
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())) //
@@ -408,7 +410,6 @@ class ResponseConverter {
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();
@@ -497,6 +498,10 @@ class ResponseConverter {
builder.withDeleted(response.deleted());
}
if(response.updated() != null) {
builder.withUpdated(response.updated());
}
if (response.batches() != null) {
builder.withBatches(Math.toIntExact(response.batches()));
}
@@ -536,6 +541,29 @@ class ResponseConverter {
}
// 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<SqlResponse.Column> columns = response.columns().stream()
.map(column -> new SqlResponse.Column(column.name(), column.type())).toList();
builder.withColumns(columns);
for (List<JsonData> 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) {
@@ -29,6 +29,7 @@ import co.elastic.clients.elasticsearch.core.search.Suggestion;
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;
@@ -56,6 +57,7 @@ import org.springframework.util.CollectionUtils;
*
* @author Peter-Josef Meisch
* @author Haibo Liu
* @author Mohamed El Harrougui
* @since 4.4
*/
class SearchDocumentResponseBuilder {
@@ -83,8 +85,10 @@ class SearchDocumentResponseBuilder {
Map<String, List<Suggestion<EntityAsMap>>> suggest = responseBody.suggest();
var pointInTimeId = responseBody.pitId();
var shards = responseBody.shards();
var executionDurationInMillis = responseBody.took();
return from(hitsMetadata, shards, scrollId, pointInTimeId, aggregations, suggest, entityCreator, jsonpMapper);
return from(hitsMetadata, shards, scrollId, pointInTimeId, executionDurationInMillis, aggregations, suggest,
entityCreator, jsonpMapper);
}
/**
@@ -109,8 +113,10 @@ class SearchDocumentResponseBuilder {
var aggregations = response.aggregations();
var suggest = response.suggest();
var pointInTimeId = response.pitId();
var executionDurationInMillis = response.took();
return from(hitsMetadata, shards, scrollId, pointInTimeId, aggregations, suggest, entityCreator, jsonpMapper);
return from(hitsMetadata, shards, scrollId, pointInTimeId, executionDurationInMillis, aggregations, suggest,
entityCreator, jsonpMapper);
}
/**
@@ -127,7 +133,7 @@ class SearchDocumentResponseBuilder {
* @return the {@link SearchDocumentResponse}
*/
public static <T> SearchDocumentResponse from(HitsMetadata<?> hitsMetadata, @Nullable ShardStatistics shards,
@Nullable String scrollId, @Nullable String pointInTimeId, @Nullable Map<String, Aggregate> aggregations,
@Nullable String scrollId, @Nullable String pointInTimeId, long executionDurationInMillis, @Nullable Map<String, Aggregate> aggregations,
Map<String, List<Suggestion<EntityAsMap>>> suggestES, SearchDocumentResponse.EntityCreator<T> entityCreator,
JsonpMapper jsonpMapper) {
@@ -151,6 +157,8 @@ class SearchDocumentResponseBuilder {
float maxScore = hitsMetadata.maxScore() != null ? hitsMetadata.maxScore().floatValue() : Float.NaN;
Duration executionDuration = Duration.ofMillis(executionDurationInMillis);
List<SearchDocument> searchDocuments = new ArrayList<>();
for (Hit<?> hit : hitsMetadata.hits()) {
searchDocuments.add(DocumentAdapters.from(hit, jsonpMapper));
@@ -163,7 +171,7 @@ class SearchDocumentResponseBuilder {
SearchShardStatistics shardStatistics = shards != null ? shardsFrom(shards) : null;
return new SearchDocumentResponse(totalHits, totalHitsRelation, maxScore, scrollId, pointInTimeId, searchDocuments,
return new SearchDocumentResponse(totalHits, totalHitsRelation, maxScore, executionDuration, scrollId, pointInTimeId, searchDocuments,
aggregationsContainer, suggest, shardStatistics);
}
@@ -298,12 +298,6 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
return this.delete(id, getIndexCoordinatesFor(entityType));
}
@Override
@Deprecated
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()));
@@ -233,6 +233,7 @@ abstract public class AbstractReactiveElasticsearchTemplate
.subscribe(new Subscriber<>() {
@Nullable private Subscription subscription = null;
private final AtomicBoolean upstreamComplete = new AtomicBoolean(false);
private final AtomicBoolean onNextHasBeenCalled = new AtomicBoolean(false);
@Override
public void onSubscribe(Subscription subscription) {
@@ -242,6 +243,7 @@ abstract public class AbstractReactiveElasticsearchTemplate
@Override
public void onNext(List<T> entityList) {
onNextHasBeenCalled.set(true);
saveAll(entityList, index)
.map(sink::tryEmitNext)
.doOnComplete(() -> {
@@ -267,6 +269,10 @@ abstract public class AbstractReactiveElasticsearchTemplate
@Override
public void onComplete() {
upstreamComplete.set(true);
if (!onNextHasBeenCalled.get()) {
// this happens when an empty flux is saved
sink.tryEmitComplete();
}
}
});
return sink.asFlux();
@@ -408,12 +414,6 @@ abstract public class AbstractReactiveElasticsearchTemplate
abstract protected Mono<String> doDeleteById(String id, @Nullable String routing, IndexCoordinates index);
@Override
@Deprecated
public Mono<ByQueryResponse> delete(Query query, Class<?> entityType) {
return delete(query, entityType, getIndexCoordinatesFor(entityType));
}
@Override
public Mono<ByQueryResponse> delete(DeleteQuery query, Class<?> entityType) {
return delete(query, entityType, getIndexCoordinatesFor(entityType));
@@ -272,19 +272,6 @@ public interface DocumentOperations {
*/
String delete(Object entity, IndexCoordinates index);
/**
* Delete all records matching the query.
*
* @param query query defining the objects
* @param clazz The entity class, must be annotated with
* {@link org.springframework.data.elasticsearch.annotations.Document}
* @return response with detailed information
* @since 4.1
* @deprecated since 5.3.0, use {@link #delete(DeleteQuery, Class)}
*/
@Deprecated
ByQueryResponse delete(Query query, Class<?> clazz);
/**
* Delete all records matching the query.
*
@@ -296,19 +283,6 @@ public interface DocumentOperations {
*/
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
* {@link org.springframework.data.elasticsearch.annotations.Document}
* @param index the index from which to delete
* @return response with detailed information
* @deprecated since 5.3.0, use {@link #delete(DeleteQuery, Class, IndexCoordinates)}
*/
@Deprecated
ByQueryResponse delete(Query query, Class<?> clazz, IndexCoordinates index);
/**
* Delete all records matching the query.
*
@@ -20,6 +20,7 @@ import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverte
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.data.elasticsearch.core.sql.SqlOperations;
import org.springframework.lang.Nullable;
/**
@@ -35,7 +36,7 @@ import org.springframework.lang.Nullable;
* @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
@@ -326,17 +326,6 @@ public interface ReactiveDocumentOperations {
*/
Mono<String> delete(String id, Class<?> entityType);
/**
* Delete the documents matching the given {@link Query} extracting index from entity metadata.
*
* @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.
* @deprecated since 5.3.0, use {@link #delete(DeleteQuery, Class)}
*/
@Deprecated
Mono<ByQueryResponse> delete(Query query, Class<?> entityType);
/**
* Delete the documents matching the given {@link Query} extracting index from entity metadata.
*
@@ -347,18 +336,6 @@ public interface ReactiveDocumentOperations {
*/
Mono<ByQueryResponse> delete(DeleteQuery query, Class<?> entityType);
/**
* Delete the documents matching the given {@link Query} extracting index from entity metadata.
*
* @param query must not be {@literal null}.
* @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.
* @deprecated since 5.3.0, use {@link #delete(DeleteQuery, Class, IndexCoordinates)}
*/
@Deprecated
Mono<ByQueryResponse> delete(Query query, Class<?> entityType, IndexCoordinates index);
/**
* Delete the documents matching the given {@link Query} extracting index from entity metadata.
*
@@ -21,6 +21,7 @@ import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersiste
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.data.elasticsearch.core.sql.ReactiveSqlOperations;
import org.springframework.lang.Nullable;
/**
@@ -31,7 +32,7 @@ import org.springframework.lang.Nullable;
* @since 3.2
*/
public interface ReactiveElasticsearchOperations
extends ReactiveDocumentOperations, ReactiveSearchOperations, ReactiveScriptOperations {
extends ReactiveDocumentOperations, ReactiveSearchOperations, ReactiveScriptOperations, ReactiveSqlOperations {
/**
* Get the {@link ElasticsearchConverter} used.
@@ -17,6 +17,8 @@ package org.springframework.data.elasticsearch.core;
import reactor.core.publisher.Flux;
import java.time.Duration;
import org.springframework.data.elasticsearch.core.suggest.response.Suggest;
import org.springframework.lang.Nullable;
@@ -25,6 +27,7 @@ import org.springframework.lang.Nullable;
*
* @param <T> the result data class.
* @author Peter-Josef Meisch
* @author Mohamed El Harrougui
* @since 4.4
*/
public interface ReactiveSearchHits<T> {
@@ -37,6 +40,11 @@ public interface ReactiveSearchHits<T> {
float getMaxScore();
/**
* @return the execution duration it took to complete the request
*/
Duration getExecutionDuration();
/**
* @return the {@link SearchHit}s from the search result.
*/
@@ -17,11 +17,14 @@ package org.springframework.data.elasticsearch.core;
import reactor.core.publisher.Flux;
import java.time.Duration;
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<T> implements ReactiveSearchHits<T> {
@@ -58,6 +61,11 @@ public class ReactiveSearchHitsImpl<T> implements ReactiveSearchHits<T> {
return delegate.getMaxScore();
}
@Override
public Duration getExecutionDuration() {
return delegate.getExecutionDuration();
}
@Override
public boolean hasSearchHits() {
return delegate.hasSearchHits();
@@ -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;
@@ -47,6 +48,7 @@ import org.springframework.util.Assert;
* @author Sascha Woo
* @author Jakob Hoeper
* @author Haibo Liu
* @author Mohamed El Harrougui
* @since 4.0
*/
public class SearchHitMapping<T> {
@@ -87,6 +89,7 @@ public class SearchHitMapping<T> {
long totalHits = searchDocumentResponse.getTotalHits();
SearchShardStatistics shardStatistics = searchDocumentResponse.getSearchShardStatistics();
float maxScore = searchDocumentResponse.getMaxScore();
Duration executionDuration = searchDocumentResponse.getExecutionDuration();
String scrollId = searchDocumentResponse.getScrollId();
String pointInTimeId = searchDocumentResponse.getPointInTimeId();
@@ -104,8 +107,8 @@ public class SearchHitMapping<T> {
Suggest suggest = searchDocumentResponse.getSuggest();
mapHitsInCompletionSuggestion(suggest);
return new SearchHitsImpl<>(totalHits, totalHitsRelation, maxScore, scrollId, pointInTimeId, searchHits,
aggregations, suggest, shardStatistics);
return new SearchHitsImpl<>(totalHits, totalHitsRelation, maxScore, executionDuration, scrollId, pointInTimeId,
searchHits, aggregations, suggest, shardStatistics);
}
@SuppressWarnings("unchecked")
@@ -238,6 +241,7 @@ public class SearchHitMapping<T> {
return new SearchHitsImpl<>(searchHits.getTotalHits(),
searchHits.getTotalHitsRelation(),
searchHits.getMaxScore(),
searchHits.getExecutionDuration(),
scrollId,
searchHits.getPointInTimeId(),
convertedSearchHits,
@@ -15,6 +15,7 @@
*/
package org.springframework.data.elasticsearch.core;
import java.time.Duration;
import java.util.Iterator;
import java.util.List;
@@ -28,6 +29,7 @@ import org.springframework.lang.Nullable;
* @param <T> the result data class.
* @author Sascha Woo
* @author Haibo Liu
* @author Mohamed El Harrougui
* @since 4.0
*/
public interface SearchHits<T> extends Streamable<SearchHit<T>> {
@@ -43,6 +45,11 @@ public interface SearchHits<T> extends Streamable<SearchHit<T>> {
*/
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}
@@ -15,6 +15,7 @@
*/
package org.springframework.data.elasticsearch.core;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
@@ -30,6 +31,7 @@ import org.springframework.util.Assert;
* @author Peter-Josef Meisch
* @author Sascha Woo
* @author Haibo Liu
* @author Mohamed El Harrougui
* @since 4.0
*/
public class SearchHitsImpl<T> implements SearchScrollHits<T> {
@@ -37,6 +39,7 @@ public class SearchHitsImpl<T> implements SearchScrollHits<T> {
private final long totalHits;
private final TotalHitsRelation totalHitsRelation;
private final float maxScore;
private final Duration executionDuration;
@Nullable private final String scrollId;
private final List<? extends SearchHit<T>> searchHits;
private final Lazy<List<SearchHit<T>>> unmodifiableSearchHits;
@@ -49,12 +52,13 @@ public class SearchHitsImpl<T> implements SearchScrollHits<T> {
* @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<? extends SearchHit<T>> searchHits,
public SearchHitsImpl(long totalHits, TotalHitsRelation totalHitsRelation, float maxScore, Duration executionDuration,
@Nullable String scrollId, @Nullable String pointInTimeId, List<? extends SearchHit<T>> searchHits,
@Nullable AggregationsContainer<?> aggregations, @Nullable Suggest suggest,
@Nullable SearchShardStatistics searchShardStatistics) {
@@ -63,6 +67,7 @@ public class SearchHitsImpl<T> implements SearchScrollHits<T> {
this.totalHits = totalHits;
this.totalHitsRelation = totalHitsRelation;
this.maxScore = maxScore;
this.executionDuration = executionDuration;
this.scrollId = scrollId;
this.pointInTimeId = pointInTimeId;
this.searchHits = searchHits;
@@ -88,6 +93,11 @@ public class SearchHitsImpl<T> implements SearchScrollHits<T> {
return maxScore;
}
@Override
public Duration getExecutionDuration() {
return executionDuration;
}
@Override
@Nullable
public String getScrollId() {
@@ -133,6 +143,7 @@ public class SearchHitsImpl<T> implements SearchScrollHits<T> {
"totalHits=" + totalHits + //
", totalHitsRelation=" + totalHitsRelation + //
", maxScore=" + maxScore + //
", executionDuration=" + executionDuration + //
", scrollId='" + scrollId + '\'' + //
", pointInTimeId='" + pointInTimeId + '\'' + //
", searchHits={" + searchHits.size() + " elements}" + //
@@ -15,6 +15,8 @@
*/
package org.springframework.data.elasticsearch.core;
import java.time.Duration;
import org.springframework.data.util.CloseableIterator;
import org.springframework.lang.Nullable;
@@ -23,6 +25,7 @@ import org.springframework.lang.Nullable;
* {@link java.util.stream.Stream}.
*
* @author Sascha Woo
* @author Mohamed El Harrougui
* @param <T>
* @since 4.0
*/
@@ -39,6 +42,11 @@ public interface SearchHitsIterator<T> extends CloseableIterator<SearchHit<T>> {
*/
float getMaxScore();
/**
* @return the execution duration it took to complete the request
*/
Duration getExecutionDuration();
/**
* @return the number of total hits.
*/
@@ -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;
@@ -31,6 +32,7 @@ import org.springframework.util.Assert;
*
* @author Mark Paluch
* @author Sascha Woo
* @author Mohamed El Harrougui
* @since 3.2
*/
abstract class StreamQueries {
@@ -56,6 +58,7 @@ abstract class StreamQueries {
AggregationsContainer<?> aggregations = searchHits.getAggregations();
float maxScore = searchHits.getMaxScore();
Duration executionDuration = searchHits.getExecutionDuration();
long totalHits = searchHits.getTotalHits();
TotalHitsRelation totalHitsRelation = searchHits.getTotalHitsRelation();
@@ -86,6 +89,11 @@ abstract class StreamQueries {
return maxScore;
}
@Override
public Duration getExecutionDuration() {
return executionDuration;
}
@Override
public long getTotalHits() {
return totalHits;
@@ -38,6 +38,7 @@ 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;
@@ -50,6 +51,7 @@ import org.springframework.data.elasticsearch.core.query.Criteria;
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;
@@ -392,7 +394,7 @@ public class MappingElasticsearchConverter
}
if (source instanceof SearchDocument searchDocument) {
populateScriptFields(targetEntity, result, searchDocument);
populateScriptedFields(targetEntity, result, searchDocument);
}
return result;
} catch (ConversionException e) {
@@ -650,7 +652,16 @@ public class MappingElasticsearchConverter
return conversionService.convert(value, target);
}
private <T> 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 <T> the result type
*/
private <T> void populateScriptedFields(ElasticsearchPersistentEntity<?> entity, T result,
SearchDocument searchDocument) {
Map<String, List<Object>> fields = searchDocument.getFields();
entity.doWithProperties((SimplePropertyHandler) property -> {
@@ -659,8 +670,13 @@ public class MappingElasticsearchConverter
// noinspection ConstantConditions
String name = scriptedField.name().isEmpty() ? property.getName() : scriptedField.name();
if (fields.containsKey(name)) {
Object value = searchDocument.getFieldValue(name);
entity.getPropertyAccessor(result).setProperty(property, value);
if (property.isCollectionLike()) {
List<Object> values = searchDocument.getFieldValues(name);
entity.getPropertyAccessor(result).setProperty(property, values);
} else {
Object value = searchDocument.getFieldValue(name);
entity.getPropertyAccessor(result).setProperty(property, value);
}
}
}
});
@@ -1231,7 +1247,7 @@ public class MappingElasticsearchConverter
return;
}
updatePropertiesInFieldsAndSourceFilter(query, domainClass);
updatePropertiesInFieldsSortAndSourceFilter(query, domainClass);
if (query instanceof CriteriaQuery criteriaQuery) {
updatePropertiesInCriteriaQuery(criteriaQuery, domainClass);
@@ -1242,7 +1258,14 @@ public class MappingElasticsearchConverter
}
}
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);
@@ -1250,12 +1273,12 @@ public class MappingElasticsearchConverter
List<String> fields = query.getFields();
if (!fields.isEmpty()) {
query.setFields(updateFieldNames(fields, persistentEntity));
query.setFields(propertyToFieldNames(fields, persistentEntity));
}
List<String> storedFields = query.getStoredFields();
if (!CollectionUtils.isEmpty(storedFields)) {
query.setStoredFields(updateFieldNames(storedFields, persistentEntity));
query.setStoredFields(propertyToFieldNames(storedFields, persistentEntity));
}
SourceFilter sourceFilter = query.getSourceFilter();
@@ -1266,37 +1289,60 @@ public class MappingElasticsearchConverter
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<String> updateFieldNames(List<String> fieldNames, ElasticsearchPersistentEntity<?> persistentEntity) {
return fieldNames.stream().map(fieldName -> updateFieldName(persistentEntity, fieldName))
private List<String> propertyToFieldNames(List<String> 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) {
@@ -1326,15 +1372,85 @@ public class MappingElasticsearchConverter
return;
}
String[] fieldNames = field.getName().split("\\.");
var propertyNamesUpdate = updatePropertyNames(persistentEntity, field.getName());
var fieldNames = propertyNamesUpdate.names();
field.setName(String.join(".", fieldNames));
if (propertyNamesUpdate.propertyCount() > 1 && propertyNamesUpdate.nestedProperty()) {
List<String> propertyNames = Arrays.asList(fieldNames);
field.setPath(String.join(".", propertyNames.subList(0, propertyNamesUpdate.propertyCount - 1)));
}
if (propertyNamesUpdate.persistentProperty != null) {
if (propertyNamesUpdate.persistentProperty.hasPropertyValueConverter()) {
PropertyValueConverter propertyValueConverter = Objects
.requireNonNull(propertyNamesUpdate.persistentProperty.getPropertyValueConverter());
criteria.getQueryCriteriaEntries().forEach(criteriaEntry -> {
if (criteriaEntry.getKey().hasValue()) {
Object value = criteriaEntry.getValue();
if (value.getClass().isArray()) {
Object[] objects = (Object[]) value;
for (int i = 0; i < objects.length; i++) {
objects[i] = propertyValueConverter.write(objects[i]);
}
} else {
criteriaEntry.setValue(propertyValueConverter.write(value));
}
}
});
}
org.springframework.data.elasticsearch.annotations.Field fieldAnnotation = propertyNamesUpdate.persistentProperty
.findAnnotation(org.springframework.data.elasticsearch.annotations.Field.class);
if (fieldAnnotation != null) {
field.setFieldType(fieldAnnotation.type());
}
}
}
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 propertyNamesUpdate = updatePropertyNames(persistentEntity, propertyPath);
return String.join(".", propertyNamesUpdate.names());
}
/**
* 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) {
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 < fieldNames.length; i++) {
persistentProperty = currentEntity.getPersistentProperty(fieldNames[i]);
for (int i = 0; i < propertyNames.length; i++) {
persistentProperty = currentEntity.getPersistentProperty(propertyNames[i]);
if (persistentProperty != null) {
propertyCount++;
@@ -1361,77 +1477,8 @@ public class MappingElasticsearchConverter
}
}
field.setName(String.join(".", fieldNames));
if (propertyCount > 1 && isNested) {
List<String> propertyNames = Arrays.asList(fieldNames);
field.setPath(String.join(".", propertyNames.subList(0, propertyCount - 1)));
}
if (persistentProperty != null) {
if (persistentProperty.hasPropertyValueConverter()) {
PropertyValueConverter propertyValueConverter = Objects
.requireNonNull(persistentProperty.getPropertyValueConverter());
criteria.getQueryCriteriaEntries().forEach(criteriaEntry -> {
if (criteriaEntry.getKey().hasValue()) {
Object value = criteriaEntry.getValue();
if (value.getClass().isArray()) {
Object[] objects = (Object[]) value;
for (int i = 0; i < objects.length; i++) {
objects[i] = propertyValueConverter.write(objects[i]);
}
} else {
criteriaEntry.setValue(propertyValueConverter.write(value));
}
}
});
}
org.springframework.data.elasticsearch.annotations.Field fieldAnnotation = persistentProperty
.findAnnotation(org.springframework.data.elasticsearch.annotations.Field.class);
if (fieldAnnotation != null) {
field.setFieldType(fieldAnnotation.type());
}
}
return new PropertyNamesUpdate(fieldNames, isNested, propertyCount, 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);
if (properties.length > 0) {
var propertyName = properties[0];
var fieldName = updateFieldName(persistentEntity, propertyName);
if (properties.length > 1) {
var persistentProperty = persistentEntity.getPersistentProperty(propertyName);
if (persistentProperty != null) {
ElasticsearchPersistentEntity<?> nestedPersistentEntity = mappingContext
.getPersistentEntity(persistentProperty);
if (nestedPersistentEntity != null) {
return fieldName + '.' + updateFieldNames(properties[1], nestedPersistentEntity);
} else {
return fieldName;
}
}
}
return fieldName;
} else {
return propertyPath;
}
}
// endregion
@SuppressWarnings("ClassCanBeRecord")
@@ -57,6 +57,20 @@ public interface SearchDocument extends Document {
return (V) values.get(0);
}
/**
* @param name the field name
* @param <V> the type of elements
* @return the values of the given field.
*/
@Nullable
default <V> List<V> getFieldValues(final String name) {
List<Object> values = getFields().get(name);
if (values == null) {
return null;
}
return (List<V>) values;
}
/**
* @return the sort values for the search hit
*/
@@ -15,6 +15,7 @@
*/
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;
@@ -29,6 +30,7 @@ import org.springframework.lang.Nullable;
*
* @author Peter-Josef Meisch
* @author Haibo Liu
* @author Mohamed El Harrougui
* @since 4.0
*/
public class SearchDocumentResponse {
@@ -36,6 +38,7 @@ 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<SearchDocument> searchDocuments;
@Nullable private final AggregationsContainer<?> aggregations;
@@ -44,13 +47,14 @@ public class SearchDocumentResponse {
@Nullable String pointInTimeId;
@Nullable private final SearchShardStatistics searchShardStatistics;
public SearchDocumentResponse(long totalHits, String totalHitsRelation, float maxScore, @Nullable String scrollId,
@Nullable String pointInTimeId, List<SearchDocument> searchDocuments,
public SearchDocumentResponse(long totalHits, String totalHitsRelation, float maxScore, Duration executionDuration,
@Nullable String scrollId, @Nullable String pointInTimeId, List<SearchDocument> 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;
@@ -71,6 +75,10 @@ public class SearchDocumentResponse {
return maxScore;
}
public Duration getExecutionDuration() {
return executionDuration;
}
@Nullable
public String getScrollId() {
return scrollId;
@@ -21,7 +21,7 @@ import org.springframework.data.elasticsearch.core.document.Document;
/**
* Interface definition for structures defined in <a href="https://geojson.org">GeoJSON</a>
* format. copied from Spring Data Mongodb
* format. copied from Spring Data Mongodb
*
* @author Christoph Strobl
* @since 1.7
@@ -69,7 +69,7 @@ public class GeoJsonLineString implements GeoJson<Iterable<Point>> {
Assert.notNull(second, "Second point must not be null!");
Assert.notNull(others, "Additional points must not be null!");
List<Point> points = new ArrayList<>();
List<Point> points = new ArrayList<>(2 + others.length);
points.add(first);
points.add(second);
points.addAll(Arrays.asList(others));
@@ -103,7 +103,7 @@ public class GeoJsonLineString implements GeoJson<Iterable<Point>> {
Assert.notNull(second, "Second point must not be null!");
Assert.notNull(others, "Additional points must not be null!");
List<Point> points = new ArrayList<>();
List<Point> 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()));
@@ -69,7 +69,7 @@ public class GeoJsonMultiPoint implements GeoJson<Iterable<Point>> {
Assert.notNull(second, "Second point must not be null!");
Assert.notNull(others, "Additional points must not be null!");
List<Point> points = new ArrayList<>();
List<Point> points = new ArrayList<>(2 + others.length);
points.add(first);
points.add(second);
points.addAll(Arrays.asList(others));
@@ -103,7 +103,7 @@ public class GeoJsonMultiPoint implements GeoJson<Iterable<Point>> {
Assert.notNull(second, "Second point must not be null!");
Assert.notNull(others, "Additional points must not be null!");
List<Point> points = new ArrayList<>();
List<Point> 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()));
@@ -189,7 +189,13 @@ public class GeoJsonPolygon implements GeoJson<Iterable<GeoJsonLineString>> {
@SafeVarargs
private static <T> List<T> asList(T first, T second, T third, T fourth, T... others) {
ArrayList<T> 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<T> result = new ArrayList<>(4 + others.length);
result.add(first);
result.add(second);
@@ -69,6 +69,7 @@ import com.fasterxml.jackson.databind.util.RawValue;
* @author Peter-Josef Meisch
* @author Xiao Yu
* @author Subhobrata Dey
* @author Andriy Redko
*/
public class MappingBuilder {
@@ -175,7 +176,9 @@ public class MappingBuilder {
.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,6 +214,7 @@ public class MappingBuilder {
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 {
@@ -244,7 +248,7 @@ public class MappingBuilder {
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);
@@ -372,7 +376,7 @@ public class MappingBuilder {
nestedPropertyPrefix = nestedPropertyPath;
mapEntity(propertiesNode, persistentEntity, false, property.getFieldName(), true, fieldAnnotation.type(),
fieldAnnotation, dynamicMapping, null);
getMappedTypeName(fieldAnnotation), fieldAnnotation, dynamicMapping, null);
nestedPropertyPrefix = currentNestedPropertyPrefix;
return;
@@ -473,7 +477,7 @@ public class MappingBuilder {
}
propertiesNode.set(property.getFieldName(), objectMapper.createObjectNode() //
.put(FIELD_PARAM_TYPE, field.type().getMappedName()) //
.put(FIELD_PARAM_TYPE, getMappedTypeName(field)) //
.put(MAPPING_ENABLED, false) //
);
@@ -482,6 +486,16 @@ public class MappingBuilder {
}
}
/**
* 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
*
@@ -23,15 +23,7 @@ import java.util.Collections;
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.data.elasticsearch.annotations.*;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -49,6 +41,7 @@ import com.fasterxml.jackson.databind.node.TextNode;
* @author Brian Kimmig
* @author Morgan Lutz
* @author Sascha Woo
* @author Haibo Liu
* @since 4.0
*/
public final class MappingParameters {
@@ -78,6 +71,10 @@ 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";
@@ -110,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.
@@ -141,6 +142,7 @@ public final class MappingParameters {
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();
@@ -174,6 +176,9 @@ public final class MappingParameters {
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();
@@ -184,6 +189,7 @@ public final class MappingParameters {
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();
@@ -217,6 +223,9 @@ public final class MappingParameters {
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();
}
@@ -239,7 +248,7 @@ public final class MappingParameters {
}
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<String> formats = new ArrayList<>();
@@ -356,6 +365,48 @@ public final class MappingParameters {
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) {
@@ -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.springframework.data.elasticsearch.core.query.Query;
import org.springframework.lang.Nullable;
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);
}
}
}
@@ -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.springframework.data.elasticsearch.core.document.Document;
import org.springframework.lang.Nullable;
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<Alias> aliases;
@Nullable private final Map<String, Object> 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<String, Object> getSettings() {
return settings;
}
@Nullable
public Document getMapping() {
return mapping;
}
public static class Builder {
private final IndexCoordinates indexCoordinates;
private final Set<Alias> aliases = new HashSet<>();
@Nullable private Map<String, Object> 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<Alias> aliases) {
Assert.notNull(aliases, "aliases must not be null");
this.aliases.addAll(aliases);
return this;
}
public Builder withSettings(Map<String, Object> 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);
}
}
}
@@ -15,6 +15,8 @@
*/
package org.springframework.data.elasticsearch.core.mapping;
import java.util.Set;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Dynamic;
import org.springframework.data.elasticsearch.annotations.Field;
@@ -42,6 +44,14 @@ public interface ElasticsearchPersistentEntity<T> extends PersistentEntity<T, El
IndexCoordinates getIndexCoordinates();
/**
* Retrieves the aliases associated with the current entity.
*
* @return Returns a set of aliases of the {@link PersistentEntity}.
* @since 5.4
*/
Set<Alias> getAliases();
short getShards();
short getReplicas();
@@ -66,7 +76,7 @@ public interface ElasticsearchPersistentEntity<T> extends PersistentEntity<T, El
boolean isCreateIndexAndMapping();
/**
* returns the {@link ElasticsearchPersistentProperty} with the given fieldName (may be set by the {@link Field}
* returns the {@link ElasticsearchPersistentProperty} with the given fieldName (can be set by the {@link Field})
* annotation.
*
* @param fieldName to field name for the search, must not be {@literal null}
@@ -189,7 +199,7 @@ public interface ElasticsearchPersistentEntity<T> extends PersistentEntity<T, El
boolean storeVersionInSource();
/**
* @return if the mapping should be written to the index on repositry bootstrap even if the index already exists.
* @return if the mapping should be written to the index on repository bootstrap even if the index already exists.
* @since 5.2
*/
boolean isAlwaysWriteMapping();
@@ -34,12 +34,12 @@ public class IndexCoordinates {
private final String[] indexNames;
public static IndexCoordinates of(String... indexNames) {
Assert.notNull(indexNames, "indexNames must not be null");
return new IndexCoordinates(indexNames);
}
private IndexCoordinates(String... indexNames) {
Assert.notEmpty(indexNames, "indexNames may not be null or empty");
Assert.noNullElements(indexNames, "indexNames may not contain null elements");
this.indexNames = indexNames;
}
@@ -15,7 +15,9 @@
*/
package org.springframework.data.elasticsearch.core.mapping;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
@@ -31,6 +33,8 @@ import org.springframework.data.elasticsearch.annotations.Routing;
import org.springframework.data.elasticsearch.annotations.Setting;
import org.springframework.data.elasticsearch.core.index.Settings;
import org.springframework.data.elasticsearch.core.join.JoinField;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.model.BasicPersistentEntity;
@@ -80,6 +84,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
private final ConcurrentHashMap<String, Expression> routingExpressions = new ConcurrentHashMap<>();
private @Nullable String routing;
private final ContextConfiguration contextConfiguration;
private final Set<Alias> aliases = new HashSet<>();
private final ConcurrentHashMap<String, Expression> indexNameExpressions = new ConcurrentHashMap<>();
private final Lazy<EvaluationContext> indexNameEvaluationContext = Lazy.of(this::getIndexNameEvaluationContext);
@@ -112,6 +117,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
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 class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
return resolve(IndexCoordinates.of(getIndexName()));
}
@Override
public Set<Alias> getAliases() {
return aliases;
}
@Nullable
@Override
public String getIndexStoreType() {
@@ -247,12 +258,12 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
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;
@@ -615,4 +626,35 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
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());
}
}
}
}
@@ -98,6 +98,7 @@ public class SimpleElasticsearchPersistentProperty extends
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 class SimpleElasticsearchPersistentProperty extends
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
@@ -27,6 +27,7 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
@@ -47,10 +48,15 @@ import org.springframework.util.Assert;
*/
public class BaseQuery implements Query {
public static final int INDEX_MAX_RESULT_WINDOW = 10_000;
private static final int DEFAULT_REACTIVE_BATCH_SIZE = 500;
// the instance to mark the query pageable initial status, needed to distinguish between the initial
// value and a user-set unpaged value; values don't matter, the RequestConverter compares to the isntance.
private static final Pageable UNSET_PAGE = PageRequest.of(0, 1);
@Nullable protected Sort sort;
protected Pageable pageable = DEFAULT_PAGE;
protected Pageable pageable = UNSET_PAGE;
protected List<String> fields = new ArrayList<>();
@Nullable protected List<String> storedFields;
@Nullable protected SourceFilter sourceFilter;
@@ -78,7 +84,7 @@ public class BaseQuery implements Query {
private boolean queryIsUpdatedByConverter = false;
@Nullable private Integer reactiveBatchSize = null;
@Nullable private Boolean allowNoIndices = null;
private EnumSet<IndicesOptions.WildcardStates> expandWildcards;
private EnumSet<IndicesOptions.WildcardStates> expandWildcards = EnumSet.noneOf(IndicesOptions.WildcardStates.class);
private List<DocValueField> docValueFields = new ArrayList<>();
private List<ScriptedField> scriptedFields = new ArrayList<>();
@@ -87,7 +93,7 @@ public class BaseQuery implements Query {
public <Q extends BaseQuery, B extends BaseQueryBuilder<Q, B>> BaseQuery(BaseQueryBuilder<Q, B> builder) {
this.sort = builder.getSort();
// do a setPageable after setting the sort, because the pageable may contain an additional sort
this.setPageable(builder.getPageable() != null ? builder.getPageable() : DEFAULT_PAGE);
this.setPageable(builder.getPageable() != null ? builder.getPageable() : UNSET_PAGE);
this.fields = builder.getFields();
this.storedFields = builder.getStoredFields();
this.sourceFilter = builder.getSourceFilter();
@@ -203,7 +209,7 @@ public class BaseQuery implements Query {
@Override
@SuppressWarnings("unchecked")
public final <T extends Query> T addSort(@Nullable Sort sort) {
if (sort == null) {
if (sort == null || sort.isUnsorted()) {
return (T) this;
}
@@ -561,4 +567,52 @@ public class BaseQuery implements Query {
public List<ScriptedField> getScriptedFields() {
return scriptedFields;
}
@Override
public Integer getRequestSize() {
var pageable = getPageable();
Integer requestSize = null;
if (pageable.isPaged() && pageable != UNSET_PAGE) {
// pagesize defined by the user
if (!isLimiting()) {
// no maxResults
requestSize = pageable.getPageSize();
} else {
// if we have both a page size and a max results, we take the min, this is necessary for
// searchForStream to work correctly (#3098) as there the page size defines what is
// returned in a single request, and the max result determines the total number of
// documents returned.
requestSize = Math.min(pageable.getPageSize(), getMaxResults());
}
} else if (pageable == UNSET_PAGE) {
// no user defined pageable
if (isLimiting()) {
// maxResults
requestSize = getMaxResults();
} else {
requestSize = DEFAULT_PAGE_SIZE;
}
} else {
// explicitly set unpaged
if (!isLimiting()) {
// no maxResults
requestSize = INDEX_MAX_RESULT_WINDOW;
} else {
// if we have both a implicit page size and a max results, we take the min, this is necessary for
// searchForStream to work correctly (#3098) as there the page size defines what is
// returned in a single request, and the max result determines the total number of
// documents returned.
requestSize = Math.min(INDEX_MAX_RESULT_WINDOW, getMaxResults());
}
}
if (requestSize == null) {
// this should not happen
requestSize = DEFAULT_PAGE_SIZE;
}
return requestSize;
}
}
@@ -167,7 +167,6 @@ public class ByQueryResponse {
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 class ByQueryResponse {
@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 class ByQueryResponse {
return index;
}
@Nullable
public String getType() {
return type;
}
@Nullable
public String getId() {
return id;
@@ -250,7 +243,6 @@ public class ByQueryResponse {
*/
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;
@@ -266,11 +258,6 @@ public class ByQueryResponse {
return this;
}
public FailureBuilder withType(String type) {
this.type = type;
return this;
}
public FailureBuilder withId(String id) {
this.id = id;
return this;
@@ -307,7 +294,7 @@ public class ByQueryResponse {
}
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);
}
}
}
@@ -22,6 +22,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.elasticsearch.core.geo.GeoBox;
@@ -859,6 +860,7 @@ public class Criteria {
// endregion
// region equals/hashcode
@Override
public boolean equals(Object o) {
if (this == o)
@@ -874,6 +876,8 @@ public class Criteria {
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))
@@ -886,11 +890,16 @@ public class Criteria {
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() {
@@ -938,7 +947,17 @@ public class Criteria {
*
* @since 4.1
*/
public static class CriteriaChain extends LinkedList<Criteria> {}
public static class CriteriaChain extends LinkedList<Criteria> {
/**
* return a copy of this list with the given element filtered out.
*
* @param criteria the element to filter
* @return the filtered list
*/
List<Criteria> filter(Criteria criteria) {
return this.stream().filter(c -> c != criteria).collect(Collectors.toList());
}
}
/**
* Operator to join the entries of the criteria chain
@@ -28,14 +28,16 @@ import org.springframework.util.Assert;
*/
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 class FetchSourceFilter implements SourceFilter {
return builderFunction.apply(new FetchSourceFilterBuilder()).build();
}
public FetchSourceFilter(@Nullable final String[] includes, @Nullable final String[] excludes) {
public FetchSourceFilter(@Nullable Boolean fetchSource, @Nullable final String[] includes,
@Nullable final String[] excludes) {
this.fetchSource = fetchSource;
this.includes = includes;
this.excludes = excludes;
}
@Override
public Boolean fetchSource() {
return fetchSource;
}
@Override
public String[] getIncludes() {
return includes;
@@ -25,6 +25,7 @@ import org.springframework.lang.Nullable;
*/
public class FetchSourceFilterBuilder {
@Nullable private Boolean fetchSource;
@Nullable private String[] includes;
@Nullable private String[] excludes;
@@ -38,12 +39,17 @@ public class FetchSourceFilterBuilder {
return this;
}
public FetchSourceFilterBuilder withFetchSource(Boolean fetchSource) {
this.fetchSource = fetchSource;
return this;
}
public SourceFilter build() {
if (includes == null)
includes = new String[0];
if (excludes == null)
excludes = new String[0];
return new FetchSourceFilter(includes, excludes);
return new FetchSourceFilter(fetchSource, includes, excludes);
}
}
@@ -54,6 +54,13 @@ public class IndexQuery {
this.indexName = indexName;
}
/**
* @since 5.5
*/
public static IndexQueryBuilder builder() {
return new IndexQueryBuilder();
}
@Nullable
public String getId() {
return id;
@@ -484,6 +484,13 @@ public interface Query {
*/
List<ScriptedField> getScriptedFields();
/**
* @return the number of documents that should be requested from Elasticsearch in this query. Depends wether a
* Pageable and/or maxResult size is set on the query.
* @since 5.4.8 5.5.2
*/
public Integer getRequestSize();
/**
* @since 4.3
*/
@@ -15,22 +15,22 @@
*/
package org.springframework.data.elasticsearch.core.query;
import org.springframework.lang.Nullable;
import java.util.Map;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
/**
* @author Peter-Josef Meisch
* @since 5.1
*/
public class SearchTemplateQueryBuilder extends BaseQueryBuilder<SearchTemplateQuery, SearchTemplateQueryBuilder> {
@Nullable
private String id;
@Nullable private String id;
@Nullable String source;
@Nullable
Map<String, Object> params;
@Nullable Map<String, Object> params;
@Nullable
public String getId() {
@@ -62,6 +62,18 @@ public class SearchTemplateQueryBuilder extends BaseQueryBuilder<SearchTemplateQ
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);
@@ -20,7 +20,7 @@ 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;
}
}
@@ -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.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Defines an SQL request.
*
* @author Aouichaoui Youssef
* @see <a href= "https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-search-api.html">docs</a>
* @since 5.4
*/
public class SqlQuery {
/**
* If true, returns partial results if there are shard request timeouts or shard failures.
* <p>
* 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.
* <p>
* 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.
* <p>
* Default, this is set to {@code 1000}.
*/
@Nullable private final Integer fetchSize;
/**
* If false, the API returns an error for fields containing array values.
* <p>
* 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.
* <p>
* Default, this is set to {@code false}.
*/
@Nullable private final Boolean indexIncludeFrozen;
/**
* Retention period for an async or saved synchronous search.
* <p>
* 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.
* <p>
* Default, this is set to {@code 45 seconds}.
*/
@Nullable private final Duration pageTimeout;
/**
* Timeout before the request fails.
* <p>
* Default, this is set to {@code 90 seconds}.
*/
@Nullable private final Duration requestTimeout;
/**
* Values for parameters in the query.
*/
@Nullable private final List<Object> params;
/**
* SQL query to run.
*/
private final String query;
/**
* Time zone ID for the search.
* <p>
* Default, this is set to {@code UTC}.
*/
@Nullable private final TimeZone timeZone;
/**
* Period to wait for complete results.
* <p>
* 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<Object> 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<Object> 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<Object> 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);
}
}
}
@@ -80,6 +80,10 @@ public abstract class HighlightCommonParameters {
return boundaryScannerLocale;
}
/**
* @deprecated the underlying functionality is deprecated since Elasticsearch 8.8.
*/
@Deprecated(since = "5.5")
public boolean getForceSource() {
return forceSource;
}
@@ -173,6 +177,10 @@ public abstract class HighlightCommonParameters {
return (SELF) this;
}
/**
* @deprecated the underlying functionality is deprecated since Elasticsearch 8.8.
*/
@Deprecated(since = "5.5")
public SELF withForceSource(boolean forceSource) {
this.forceSource = forceSource;
return (SELF) this;
@@ -187,7 +187,6 @@ public class ReindexResponse {
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;
@@ -196,11 +195,10 @@ public class ReindexResponse {
@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;
@@ -215,11 +213,6 @@ public class ReindexResponse {
return index;
}
@Nullable
public String getType() {
return type;
}
@Nullable
public String getId() {
return id;
@@ -269,7 +262,6 @@ public class ReindexResponse {
*/
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;
@@ -285,11 +277,6 @@ public class ReindexResponse {
return this;
}
public Failure.FailureBuilder withType(String type) {
this.type = type;
return this;
}
public Failure.FailureBuilder withId(String id) {
this.id = id;
return this;
@@ -326,7 +313,7 @@ public class ReindexResponse {
}
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);
}
}
}
@@ -0,0 +1,37 @@
/*
* 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;
import reactor.core.publisher.Mono;
/**
* The reactive version of operations for the
* <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-search-api.html">SQL search API</a>.
*
* @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<SqlResponse> search(SqlQuery query);
}
@@ -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
* <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-search-api.html">SQL search API</a>.
*
* @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);
}
@@ -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.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* Defines an SQL response.
*
* @author Aouichaoui Youssef
* @see <a href= "https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-search-api.html">docs</a>
* @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<Column> columns;
/**
* Values for the search results.
*/
private final List<Row> 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<Column> getColumns() {
return columns;
}
public List<Row> getRows() {
return rows;
}
public static Builder builder() {
return new Builder();
}
public record Column(String name, String type) {
}
public static class Row implements Iterable<Map.Entry<Column, JsonValue>> {
private final Map<Column, JsonValue> row;
private Row(Builder builder) {
this.row = builder.row;
}
public static Builder builder() {
return new Builder();
}
@NonNull
@Override
public Iterator<Map.Entry<Column, JsonValue>> iterator() {
return row.entrySet().iterator();
}
@Nullable
public JsonValue get(Column column) {
return row.get(column);
}
public static class Builder {
private final Map<Column, JsonValue> 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<Column> columns = new ArrayList<>();
private final List<Row> 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<Column> 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<Row> 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);
}
}
}
@@ -0,0 +1,6 @@
/**
* Classes and interfaces to access to SQL API of Elasticsearch.
*/
@org.springframework.lang.NonNullApi
@org.springframework.lang.NonNullFields
package org.springframework.data.elasticsearch.core.sql;
@@ -24,9 +24,10 @@ 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.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.util.StreamUtils;
@@ -49,11 +50,11 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository
protected ElasticsearchQueryMethod queryMethod;
protected final ElasticsearchOperations elasticsearchOperations;
protected final ElasticsearchConverter elasticsearchConverter;
protected final QueryMethodEvaluationContextProvider evaluationContextProvider;
protected final ValueEvaluationContextProvider evaluationContextProvider;
public AbstractElasticsearchRepositoryQuery(ElasticsearchQueryMethod queryMethod,
ElasticsearchOperations elasticsearchOperations,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
ValueEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(queryMethod, "queryMethod must not be null");
Assert.notNull(elasticsearchOperations, "elasticsearchOperations must not be null");
@@ -114,11 +115,15 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository
: 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);
query.setPageable(PageRequest.of(0, Math.max(1, itemCount)));
if (query instanceof SearchTemplateQuery) {
// we cannot get a count here, from and size would be in the template
} else {
query.setPageable(parameterAccessor.getPageable());
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());
}
}
result = elasticsearchOperations.search(query, clazz, index);
} else {
@@ -137,7 +142,8 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository
var query = createQuery(parameterAccessor);
Assert.notNull(query, "unsupported query");
queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter(),
queryMethod.addSpecialMethodParameters(query, parameterAccessor,
elasticsearchOperations.getElasticsearchConverter(),
evaluationContextProvider);
return query;
@@ -15,6 +15,7 @@
*/
package org.springframework.data.elasticsearch.repository.query;
import org.springframework.data.expression.ValueEvaluationContextProvider;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -52,11 +53,11 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor
protected final ReactiveElasticsearchQueryMethod queryMethod;
private final ReactiveElasticsearchOperations elasticsearchOperations;
protected final QueryMethodEvaluationContextProvider evaluationContextProvider;
protected final ValueEvaluationContextProvider evaluationContextProvider;
AbstractReactiveElasticsearchRepositoryQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations elasticsearchOperations,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
ValueEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(queryMethod, "queryMethod must not be null");
Assert.notNull(elasticsearchOperations, "elasticsearchOperations must not be null");
@@ -105,7 +106,7 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor
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();
@@ -16,12 +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.repository.query.parser.ElasticsearchQueryCreator;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.data.repository.query.ValueExpressionDelegate;
/**
* ElasticsearchPartQuery
@@ -33,42 +28,12 @@ import org.springframework.data.repository.query.parser.PartTree;
* @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<?, ElasticsearchPersistentProperty> mappingContext;
@Deprecated(forRemoval = true)
public class ElasticsearchPartQuery extends RepositoryPartQuery {
public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, elasticsearchOperations, evaluationContextProvider);
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;
ValueExpressionDelegate valueExpressionDelegate) {
super(method, elasticsearchOperations, valueExpressionDelegate);
}
}
@@ -28,6 +28,7 @@ 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;
@@ -43,6 +44,7 @@ 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.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;
@@ -84,6 +86,7 @@ public class ElasticsearchQueryMethod extends QueryMethod {
@Nullable private final Query queryAnnotation;
@Nullable private final Highlight highlightAnnotation;
@Nullable private final SourceFilters sourceFilters;
@Nullable private final SearchTemplateQuery searchTemplateQueryAnnotation;
public ElasticsearchQueryMethod(Method method, RepositoryMetadata repositoryMetadata, ProjectionFactory factory,
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext) {
@@ -98,17 +101,11 @@ public class ElasticsearchQueryMethod extends QueryMethod {
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();
}
@SuppressWarnings("removal")
@Override
@Deprecated
protected Parameters<?, ?> createParameters(Method method, TypeInformation<?> domainType) {
return new ElasticsearchParameters(ParametersSource.of(method));
}
@Override
protected Parameters<?, ?> createParameters(ParametersSource parametersSource) {
return new ElasticsearchParameters(parametersSource);
@@ -125,12 +122,16 @@ public class ElasticsearchQueryMethod extends QueryMethod {
}
}
/**
* @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() {
@@ -158,6 +159,27 @@ public class ElasticsearchQueryMethod extends QueryMethod {
return new HighlightQuery(highlightConverter.convert(highlightAnnotation), getDomainClass());
}
/**
* @return if the method is annotated with the {@link SearchTemplateQuery} annotation.
* @since 5.5
*/
public boolean hasAnnotatedSearchTemplateQuery() {
return this.searchTemplateQueryAnnotation != 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 searchTemplateQueryAnnotation;
}
/**
* @return the {@link ElasticsearchEntityMetadata} for the query methods {@link #getReturnedObjectType() return type}.
* @since 3.2
@@ -281,7 +303,7 @@ public class ElasticsearchQueryMethod extends QueryMethod {
/**
* @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() {
@@ -303,7 +325,7 @@ public class ElasticsearchQueryMethod extends QueryMethod {
@Nullable
SourceFilter getSourceFilter(ElasticsearchParametersParameterAccessor parameterAccessor,
ElasticsearchConverter converter,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
ValueEvaluationContextProvider evaluationContextProvider) {
if (sourceFilters == null || (sourceFilters.includes().length == 0 && sourceFilters.excludes().length == 0)) {
return null;
@@ -326,7 +348,7 @@ public class ElasticsearchQueryMethod extends QueryMethod {
}
private String[] mapParameters(String[] source, ElasticsearchParametersParameterAccessor parameterAccessor,
ConversionService conversionService, QueryMethodEvaluationContextProvider evaluationContextProvider) {
ConversionService conversionService, ValueEvaluationContextProvider evaluationContextProvider) {
List<String> fieldNames = new ArrayList<>();
@@ -377,9 +399,9 @@ public class ElasticsearchQueryMethod extends QueryMethod {
}
}
void addMethodParameter(BaseQuery query, ElasticsearchParametersParameterAccessor parameterAccessor,
ElasticsearchConverter elasticsearchConverter,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
void addSpecialMethodParameters(BaseQuery query, ElasticsearchParametersParameterAccessor parameterAccessor,
ElasticsearchConverter elasticsearchConverter,
ValueEvaluationContextProvider evaluationContextProvider) {
if (hasAnnotatedHighlight()) {
var highlightQuery = getAnnotatedHighlightQuery(new HighlightConverter(parameterAccessor,
@@ -15,13 +15,8 @@
*/
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.QueryMethodEvaluationContextProvider;
import org.springframework.util.Assert;
import org.springframework.data.repository.query.ValueExpressionDelegate;
/**
* ElasticsearchStringQuery
@@ -32,43 +27,12 @@ import org.springframework.util.Assert;
* @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, QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(queryMethod, elasticsearchOperations, evaluationContextProvider);
Assert.notNull(queryString, "Query cannot be empty");
Assert.notNull(evaluationContextProvider, "ExpressionEvaluationContextProvider must not be null");
this.queryString = queryString;
String queryString, ValueExpressionDelegate valueExpressionDelegate) {
super(queryMethod, elasticsearchOperations, queryString, valueExpressionDelegate);
}
@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());
}
}
@@ -25,8 +25,8 @@ 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.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.util.Assert;
/**
@@ -38,13 +38,13 @@ public class HighlightConverter {
private final ElasticsearchParametersParameterAccessor parameterAccessor;
private final ConversionService conversionService;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final ValueEvaluationContextProvider evaluationContextProvider;
private final QueryMethod queryMethod;
HighlightConverter(ElasticsearchParametersParameterAccessor parameterAccessor,
ConversionService conversionService,
QueryMethodEvaluationContextProvider evaluationContextProvider,
QueryMethod queryMethod) {
ConversionService conversionService,
ValueEvaluationContextProvider evaluationContextProvider,
QueryMethod queryMethod) {
Assert.notNull(parameterAccessor, "parameterAccessor must not be null");
Assert.notNull(conversionService, "conversionService must not be null");
@@ -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.core.RepositoryMetadata;
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 class ReactiveElasticsearchQueryMethod extends ElasticsearchQueryMethod {
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 class ReactiveElasticsearchQueryMethod extends ElasticsearchQueryMethod {
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));
}
@@ -15,68 +15,26 @@
*/
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.QueryMethodEvaluationContextProvider;
import org.springframework.util.Assert;
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;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
@Deprecated(since = "5.5", forRemoval = true)
public class ReactiveElasticsearchStringQuery extends ReactiveRepositoryStringQuery {
public ReactiveElasticsearchStringQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations operations, QueryMethodEvaluationContextProvider evaluationContextProvider) {
this(queryMethod.getAnnotatedQuery(), queryMethod, operations, evaluationContextProvider);
ReactiveElasticsearchOperations operations, ValueExpressionDelegate valueExpressionDelegate) {
super(queryMethod, operations, valueExpressionDelegate);
}
public ReactiveElasticsearchStringQuery(String query, ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations operations, QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(queryMethod, operations, evaluationContextProvider);
Assert.notNull(query, "query must not be null");
Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null");
this.query = query;
this.evaluationContextProvider = evaluationContextProvider;
}
@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;
ReactiveElasticsearchOperations operations, ValueExpressionDelegate valueExpressionDelegate) {
super(query, queryMethod, operations, valueExpressionDelegate);
}
}
@@ -19,8 +19,8 @@ import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperatio
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.repository.query.parser.ElasticsearchQueryCreator;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.repository.query.parser.PartTree;
/**
@@ -35,8 +35,9 @@ public class ReactivePartTreeElasticsearchQuery extends AbstractReactiveElastics
public ReactivePartTreeElasticsearchQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations elasticsearchOperations,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(queryMethod, elasticsearchOperations, evaluationContextProvider);
ValueExpressionDelegate valueExpressionDelegate) {
super(queryMethod, elasticsearchOperations,
valueExpressionDelegate.createValueContextProvider(queryMethod.getParameters()));
ResultProcessor processor = queryMethod.getResultProcessor();
this.tree = new PartTree(queryMethod.getName(), processor.getReturnedType().getDomainType());
@@ -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<String, Object> 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<String, Object> 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<String, Object>();
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();
}
}

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