1
0
mirror of synced 2026-05-23 20:53:17 +00:00

Compare commits

..

140 Commits

Author SHA1 Message Date
Christoph Strobl 62091be997 Release version 5.4.1 (2024.1.1).
See #3004
2024-12-13 09:34:47 +01:00
Christoph Strobl fdc7893817 Prepare 5.4.1 (2024.1.1).
See #3004
2024-12-13 09:34:27 +01:00
Peter-Josef Meisch 535d76faf0 Upgrade Elasticsearch to 8.15.5.
Original Pull Request #3018
Closes #3016
2024-12-01 11:59:53 +01:00
Peter-Josef Meisch 26bd770b8c Update versions documentation.
Original Pull request #3011 
Closes #3010
2024-11-23 22:46:46 +01:00
Mark Paluch aec03a3529 After release cleanups.
See #2990
2024-11-15 14:13:26 +01:00
Mark Paluch e3b26b2268 Prepare next development iteration.
See #2990
2024-11-15 14:13:25 +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
Mark Paluch 2b76762007 Release version 5.3 GA (2024.0.0).
See #2896
2024-05-17 11:49:25 +02:00
Mark Paluch baec1f1419 Prepare 5.3 GA (2024.0.0).
See #2896
2024-05-17 11:49:11 +02:00
Peter-Josef Meisch d693c4f81b Update README.adoc 2024-05-12 17:38:17 +02:00
Peter-Josef Meisch 94a40a7a75 Fix implementation of explicit refresh policy.
Original Pull Request #2908
Closes #2907
2024-05-10 09:33:39 +02:00
Peter-Josef Meisch dc5bf5a606 Add the filter parts of a CriteriaQuery to the query, not as post-filter.
Original Pull Request #2906
Closes #2857
2024-05-04 09:46:41 +02:00
Mingron 1d89054d12 fix scripted-and-runtime-fields.adoc
Original Pull Request #2902
Closes #2903
2024-04-27 11:13:58 +02:00
Mingron 106b513d11 Fix retrieving data in Join-Type implementation.
Original Pull request #2900
Closes #2901
2024-04-21 19:51:20 +02:00
Peter-Josef Meisch a16782ec73 Polishing. 2024-04-16 20:40:53 +02:00
Aouichaoui Youssef 2d5f8e8219 Support has_child and has_parent queries.
Original Pull Request: #2889
Closes #1472
2024-04-16 20:33:12 +02:00
Peter-Josef Meisch ad66510e9e Fix parameter in Order constructor.
Closes #2897
2024-04-15 21:06:04 +02:00
Mark Paluch e1537087bf After release cleanups.
See #2874
2024-04-12 10:53:00 +02:00
Mark Paluch 8eecbe6a32 Prepare next development iteration.
See #2874
2024-04-12 10:52:59 +02:00
Mark Paluch c79fe303db Release version 5.3 RC1 (2024.0.0).
See #2874
2024-04-12 10:50:42 +02:00
Mark Paluch b507abe327 Prepare 5.3 RC1 (2024.0.0).
See #2874
2024-04-12 10:50:27 +02:00
Peter-Josef Meisch bec3beb1eb Upgrade to Elasticsearch 8.13.2.
Original Pull Request #2893
Closes #2891
2024-04-11 20:11:55 +02:00
Peter-Josef Meisch 1d709f6c55 Code cleanup.
Original Pull Request #2890
Closes #2888
2024-04-09 23:24:30 +02:00
Peter-Josef Meisch 0beca99912 Update README.adoc 2024-04-02 17:32:55 +02:00
Peter-Josef Meisch 6d51e67948 Introduce MappingConversionException.
Original Pull Request #2882
Closes #2879
2024-03-31 19:27:05 +02:00
Seungheon Han 0a51dbab01 Correcting a typo in ElasticsearchAotPredicates class.
Original Pull Request #2881
Closes #2880
2024-03-30 10:36:07 +01:00
Peter-Josef Meisch c96423d5ba Polishing. 2024-03-26 19:53:14 +01:00
Aouichaoui Youssef 496b8d62a4 Support Delete by query with es parameters.
Original Pull Request #2875
Closes #2865
2024-03-26 19:18:26 +01:00
Peter-Josef Meisch d2b3ba94f6 Nullability annotation cleanup. 2024-03-25 19:51:30 +01:00
Peter-Josef Meisch 33973ec839 Polishing. 2024-03-24 18:46:04 +01:00
Peter-Josef Meisch 7f178238db Add environment variable to skip repository initialization.
Original Pull Request #2878
Closes #2876
2024-03-24 17:37:11 +01:00
mawen12 aa27bbec27 Remove unnecessary code.
Original Pull Request #2870
Closes #2869
2024-03-15 13:45:59 +01:00
Mark Paluch bd6b6e92f4 After release cleanups.
See #2851
2024-03-15 11:09:58 +01:00
Mark Paluch 87eb36a995 Prepare next development iteration.
See #2851
2024-03-15 11:09:57 +01:00
Mark Paluch 41cab97f78 Release version 5.3 M2 (2024.0.0).
See #2851
2024-03-15 11:07:31 +01:00
Mark Paluch f4d2ff7a99 Prepare 5.3 M2 (2024.0.0).
See #2851
2024-03-15 11:07:10 +01:00
Peter-Josef Meisch 9472161808 Add package-info.java files. 2024-03-05 20:03:42 +01:00
Peter-Josef Meisch debf04b499 Fix setting setting id in bulkrequest.
Original Pull Request #2862
Closes #2861
2024-02-28 20:58:43 +01:00
Aouichaoui Youssef 205d74b6db Implement the equals and hashCode contracts for Field.
Original Pull Request #2859
Closes #2858
2024-02-26 20:57:17 +01:00
Aouichaoui Youssef 7a8a9a15f1 Wrap the OrCriteria correctly.
Original Pull Request #2855
Closes #2854
2024-02-26 20:05:03 +01:00
Peter-Josef Meisch c965862e82 Polishing. 2024-02-26 19:07:53 +01:00
puppylpg 6af099ea34 Add SpEL support for highlight query and source filter.
Original Pull Request #2853
Closes #2852
2024-02-26 18:59:47 +01:00
Peter-Josef Meisch 96185f94ef Upgrade to Elasticsearch 8.12.2.
Original Pull Request #2856
Closes #2836
2024-02-24 12:49:06 +01:00
Christoph Strobl ca85729ea4 After release cleanups.
See #2772
2024-02-16 14:41:25 +01:00
Christoph Strobl f9d01df6f7 Prepare next development iteration.
See #2772
2024-02-16 14:41:24 +01:00
Christoph Strobl d16951eace Release version 5.3 M1 (2024.0.0).
See #2772
2024-02-16 14:37:38 +01:00
Christoph Strobl e1730ea7cc Prepare 5.3 M1 (2024.0.0).
See #2772
2024-02-16 14:37:01 +01:00
Peter-Josef Meisch 0f5497338a Add support for field aliases in the index mapping.
Original Pull Request #2847
Closes #2845
2024-02-07 20:26:17 +01:00
Peter-Josef Meisch e9ecebd9ef Fix criteria filter in native query.
Original Pulle Request #2846
Closes #2840
2024-02-06 20:56:41 +01:00
Peter-Josef Meisch 9a3f5dc4f5 Update elasticsearch-new.adoc 2024-02-01 07:11:45 +01:00
Peter-Josef Meisch 6390aaa739 Polishing. 2024-01-31 20:43:26 +01:00
puppylpg b391a4e844 Unify conversion services for value replacement when querying elasticsearch.
Original Pull Request #2834
Closes #2833
2024-01-31 20:08:04 +01:00
Peter-Josef Meisch 0a1e20579e Fix store null values implementation.
Original Pull Request #2841
Closes #2839
2024-01-31 15:28:49 +01:00
Mark Paluch 1f75016977 Refine Artifactory build name.
See #2772
2024-01-31 15:13:28 +01:00
Eric Haag 3878540394 Update Revved up by Develocity badge.
Original Pull Request #2835
2024-01-23 21:11:46 +01:00
Peter-Josef Meisch 957fe0531f Remove deprecated code; add arch unit tests.
Original Pull Request #2832
Closes #2831
2024-01-19 21:51:12 +01:00
Peter-Josef Meisch 460b4ac0f5 Polishing. 2024-01-19 19:38:56 +01:00
puppylpg e1a2412651 Add support for SpEL in @Query.
Original Pull Request #2826
Closes #2083
2024-01-19 19:19:03 +01:00
Peter-Josef Meisch c6041fb659 Upgrade to Elasticsearch 8.11.4.
Original Pull Request #2830
Closes #2828
2024-01-19 07:36:30 +01:00
Peter-Josef Meisch 8f745b19d1 Upgrade to Elasticsearch 8.11.3.
Original Pull Request #2822
Closes #2820
2024-01-06 17:06:17 +01:00
Mark Paluch c16024d779 Extend license header copyright years to 2024.
See #2819
2024-01-02 14:42:08 +01:00
Peter-Josef Meisch af1d2dd641 fix typo 2023-12-30 17:04:29 +01:00
Peter-Josef Meisch 06ede8d7ae documentation update 2023-12-30 16:18:21 +01:00
puppylpg 1554c3c94f Support multi search template API.
Original Pull Request #2807
Closes #2704
2023-12-30 16:10:36 +01:00
Peter-Josef Meisch 260dadd4d6 Make org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchTemplate.ClientCallback public.
Original Pull Request #2815
Closes #2814
2023-12-29 12:44:54 +01:00
Junghoon Ban b78588eec5 Remove duplicate declaration of identifying type for repository.
Original Pull Request #2813
Closes #2812
2023-12-28 13:59:25 +01:00
Peter-Josef Meisch b0c97ccf27 Polishing 2023-12-28 13:53:42 +01:00
puppylpg 433d52981e Expose search shard statistics in search hits.
Original Pull Request #2806
Closes #2605
2023-12-28 12:57:44 +01:00
Peter-Josef Meisch 6350514e7e Update documentation.
Original Pull Request #2811
Closes #2810
2023-12-24 14:47:14 +01:00
David Pilato 02bd3e60f8 ClientConfigurer is only available in MaybeSecureClientConfigurationBuilder.
The documentation code does not compile and I'm not sure if it's a bug in the code or a miss in the documentation.

When you want to configure a client, the doc says to use:

```java
ClientConfiguration.builder().withClientConfigurer( // ...
```

But `withClientConfigurer(ClientConfiguration.ClientConfigurationCallback<?> clientConfigurer)` is only available in `TerminalClientConfigurationBuilder` interface.

And `ClientConfiguration.builder()` returns a `ClientConfigurationBuilderWithRequiredEndpoint` interface.
2023-12-21 14:50:18 +01:00
Peter-Josef Meisch 21a1fbca0f Clarified dependencies in the documentation 2023-12-18 15:18:37 +01:00
Peter-Josef Meisch 434de11f3d Polishing. 2023-12-17 18:19:51 +01:00
puppylpg 96b38652ab Support highlight query in @HighlightParameters annotation.
Original Pull Request #2802
2023-12-17 18:16:18 +01:00
Mark Paluch d0ed80dfde Update CI properties.
See #2772
2023-12-14 08:50:32 +01:00
Mark Paluch 362126e72d Upgrade to Maven Wrapper 3.9.6.
See #2801
2023-12-14 08:40:44 +01:00
puppylpg 0e419133a2 support highlight_query (#2793)
* support highlight_query

* implement highlight query with spring data elasticsearch query

* highight query by StringQuery

* split highligh fields assertion into different parts
2023-12-13 21:03:59 +01:00
Peter-Josef Meisch 8a3df63493 Upgrade to Elasticsearch 8.11.2.
Original Pull Request #2798
Closes #2797
2023-12-12 19:27:58 +01:00
Peter-Josef Meisch fb9ccf7b44 Polishing. 2023-12-12 18:58:34 +01:00
Junghoon Ban 1d6a1b0f2f Use switch expressions to simplify case branches.
Original Pull Request #2795
Closes #2794
2023-12-12 18:45:03 +01:00
Patrick Baumgartner 72e8f41de5 Fixes Typo ElasticsearchHttpClientConfigurationCallback.
Original Pull Request #2790
Closes #2792
2023-12-08 10:27:23 +01:00
李潇 4edf9bee41 Update elasticsearch-repositories.adoc.
Original Pull Request #2789
Closes #2791
2023-12-08 10:24:18 +01:00
Junghoon Ban 8613eb26e0 Use pattern matching instead of type casting.
Original Pull Request #2784
Closes #2785
2023-12-03 13:22:06 +01:00
Peter-Josef Meisch 415d5e0385 Removed junk characters from code. 2023-11-30 20:34:57 +01:00
Peter-Josef Meisch 3833975a1a Fix type of returned sort values.
Original Pull Request #2786
Closes #2777
2023-11-30 20:29:34 +01:00
Peter-Josef Meisch 05ca90ecc1 Add strict date formats.
Original Pull Requests #2782
Closes #2779
2023-11-27 22:02:17 +01:00
Mark Paluch 7af76338fc Introduce property for Jenkins user and Artifactory server details.
Closes #2781
2023-11-27 14:23:33 +01:00
Peter-Josef Meisch 1f4479092a Improve client ssl configuration.
Original Pull Request #2780
Closes #2778
2023-11-24 19:09:43 +01:00
Runbing ddd795a3d3 Fixed the URL for the Spring Data Commons documentation.
I fixed broken links in the Spring Data Elasticsearch documentation for Spring Data Commons.

Closes #2776
2023-11-20 11:31:45 +01:00
Mark Paluch 612cc50b88 After release cleanups.
See #2737
2023-11-17 14:33:27 +01:00
Mark Paluch d05b9f878a Prepare next development iteration.
See #2737
2023-11-17 14:33:25 +01:00
244 changed files with 9727 additions and 2063 deletions
+1 -2
View File
@@ -30,7 +30,6 @@ target
build/
node_modules
node
package.json
package-lock.json
.mvn/.gradle-enterprise
.mvn/.develocity
+3 -8
View File
@@ -1,13 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<extensions>
<extension>
<groupId>com.gradle</groupId>
<artifactId>gradle-enterprise-maven-extension</artifactId>
<version>1.19.2</version>
</extension>
<extension>
<groupId>com.gradle</groupId>
<artifactId>common-custom-user-data-maven-extension</artifactId>
<version>1.12.4</version>
<groupId>io.spring.develocity.conventions</groupId>
<artifactId>develocity-conventions-maven-extension</artifactId>
<version>0.0.19</version>
</extension>
</extensions>
-31
View File
@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<gradleEnterprise
xmlns="https://www.gradle.com/gradle-enterprise-maven" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.gradle.com/gradle-enterprise-maven https://www.gradle.com/schema/gradle-enterprise-maven.xsd">
<server>
<url>https://ge.spring.io</url>
</server>
<buildScan>
<backgroundBuildScanUpload>false</backgroundBuildScanUpload>
<captureGoalInputFiles>true</captureGoalInputFiles>
<publishIfAuthenticated>true</publishIfAuthenticated>
<obfuscation>
<ipAddresses>#{{'0.0.0.0'}}</ipAddresses>
</obfuscation>
</buildScan>
<buildCache>
<local>
<enabled>true</enabled>
</local>
<remote>
<server>
<credentials>
<username>${env.GRADLE_ENTERPRISE_CACHE_USERNAME}</username>
<password>${env.GRADLE_ENTERPRISE_CACHE_PASSWORD}</password>
</credentials>
</server>
<enabled>true</enabled>
<storeEnabled>#{env['GRADLE_ENTERPRISE_CACHE_USERNAME'] != null and env['GRADLE_ENTERPRISE_CACHE_PASSWORD'] != null}</storeEnabled>
</remote>
</buildCache>
</gradleEnterprise>
+2 -2
View File
@@ -1,3 +1,3 @@
#Thu Dec 14 08:34:15 CET 2023
#Thu Nov 07 09:47:28 CET 2024
wrapperUrl=https\://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
Vendored
+25 -23
View File
@@ -9,7 +9,7 @@ pipeline {
triggers {
pollSCM 'H/10 * * * *'
upstream(upstreamProjects: "spring-data-commons/3.2.x", threshold: hudson.model.Result.SUCCESS)
upstream(upstreamProjects: "spring-data-commons/3.4.x", threshold: hudson.model.Result.SUCCESS)
}
options {
@@ -33,15 +33,16 @@ pipeline {
environment {
ARTIFACTORY = credentials("${p['artifactory.credentials']}")
DEVELOCITY_CACHE = credentials("${p['develocity.cache.credentials']}")
DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}")
}
steps {
script {
docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.docker']) {
sh "PROFILE=none JENKINS_USER_NAME=${p['jenkins.user.name']} ci/verify.sh"
sh "JENKINS_USER_NAME=${p['jenkins.user.name']} ci/clean.sh"
docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) {
docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.docker']) {
sh "PROFILE=none JENKINS_USER_NAME=${p['jenkins.user.name']} ci/verify.sh"
sh "JENKINS_USER_NAME=${p['jenkins.user.name']} ci/clean.sh"
}
}
}
}
@@ -63,14 +64,15 @@ pipeline {
options { timeout(time: 30, unit: 'MINUTES') }
environment {
ARTIFACTORY = credentials("${p['artifactory.credentials']}")
DEVELOCITY_CACHE = credentials("${p['develocity.cache.credentials']}")
DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}")
}
steps {
script {
docker.image(p['docker.java.next.image']).inside(p['docker.java.inside.docker']) {
sh "PROFILE=none JENKINS_USER_NAME=${p['jenkins.user.name']} ci/verify.sh"
sh "JENKINS_USER_NAME=${p['jenkins.user.name']} ci/clean.sh"
docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) {
docker.image(p['docker.java.next.image']).inside(p['docker.java.inside.docker']) {
sh "PROFILE=none JENKINS_USER_NAME=${p['jenkins.user.name']} ci/verify.sh"
sh "JENKINS_USER_NAME=${p['jenkins.user.name']} ci/clean.sh"
}
}
}
}
@@ -92,24 +94,24 @@ pipeline {
options { timeout(time: 20, unit: 'MINUTES') }
environment {
ARTIFACTORY = credentials("${p['artifactory.credentials']}")
DEVELOCITY_CACHE = credentials("${p['develocity.cache.credentials']}")
DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}")
}
steps {
script {
docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.basic']) {
sh 'MAVEN_OPTS="-Duser.name=' + "${p['jenkins.user.name']}" + ' -Duser.home=/tmp/jenkins-home" ' +
"DEVELOCITY_CACHE_USERNAME=${DEVELOCITY_CACHE_USR} " +
"DEVELOCITY_CACHE_PASSWORD=${DEVELOCITY_CACHE_PSW} " +
"GRADLE_ENTERPRISE_ACCESS_KEY=${DEVELOCITY_ACCESS_KEY} " +
"./mvnw -s settings.xml -Pci,artifactory -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-elasticsearch-non-root " +
"-Dartifactory.server=${p['artifactory.url']} " +
"-Dartifactory.username=${ARTIFACTORY_USR} " +
"-Dartifactory.password=${ARTIFACTORY_PSW} " +
"-Dartifactory.staging-repository=${p['artifactory.repository.snapshot']} " +
"-Dartifactory.build-name=spring-data-elasticsearch " +
"-Dartifactory.build-number=spring-data-elasticsearch-${BRANCH_NAME}-build-${BUILD_NUMBER} " +
"-Dmaven.test.skip=true clean deploy -U -B"
docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) {
docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.docker']) {
sh 'MAVEN_OPTS="-Duser.name=' + "${p['jenkins.user.name']}" + ' -Duser.home=/tmp/jenkins-home" ' +
"./mvnw -s settings.xml -Pci,artifactory " +
"-Ddevelocity.storage.directory=/tmp/jenkins-home/.develocity-root " +
"-Dartifactory.server=${p['artifactory.url']} " +
"-Dartifactory.username=${ARTIFACTORY_USR} " +
"-Dartifactory.password=${ARTIFACTORY_PSW} " +
"-Dartifactory.staging-repository=${p['artifactory.repository.snapshot']} " +
"-Dartifactory.build-name=spring-data-elasticsearch " +
"-Dartifactory.build-number=spring-data-elasticsearch-${BRANCH_NAME}-build-${BUILD_NUMBER} " +
"-Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-elasticsearch " +
"-Dmaven.test.skip=true clean deploy -U -B"
}
}
}
}
+2 -5
View File
@@ -1,6 +1,4 @@
image:https://spring.io/badges/spring-data-elasticsearch/ga.svg[Spring Data Elasticsearch,link=https://projects.spring.io/spring-data-elasticsearch#quick-start] image:https://spring.io/badges/spring-data-elasticsearch/snapshot.svg[Spring Data Elasticsearch,link=https://projects.spring.io/spring-data-elasticsearch#quick-start]
= Spring Data for Elasticsearch image:https://jenkins.spring.io/buildStatus/icon?job=spring-data-elasticsearch%2Fmain&subject=Build[link=https://jenkins.spring.io/view/SpringData/job/spring-data-elasticsearch/] https://gitter.im/spring-projects/spring-data[image:https://badges.gitter.im/spring-projects/spring-data.svg[Gitter]] image:https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Gradle Enterprise", link="https://ge.spring.io/scans?search.rootProjectNames=Spring Data Elasticsearch"]
= Spring Data for Elasticsearch image:https://jenkins.spring.io/buildStatus/icon?job=spring-data-elasticsearch%2Fmain&subject=Build[link=https://jenkins.spring.io/view/SpringData/job/spring-data-elasticsearch/] https://gitter.im/spring-projects/spring-data[image:https://badges.gitter.im/spring-projects/spring-data.svg[Gitter]] image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Develocity", link="https://ge.spring.io/scans?search.rootProjectNames=Spring Data Elasticsearch"]
The primary goal of the https://projects.spring.io/spring-data[Spring Data] project is to make it easier to build Spring-powered applications that use new data access technologies such as non-relational databases, map-reduce frameworks, and cloud based data services.
@@ -126,8 +124,7 @@ Wed love to help!
https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/[reference documentation], and https://docs.spring.io/spring-data/elasticsearch/docs/current/api/[Javadocs].
* Learn the Spring basics Spring Data builds on Spring Framework, check the https://spring.io[spring.io] web-site for a wealth of reference documentation.
If you are just starting out with Spring, try one of the https://spring.io/guides[guides].
* Ask a question - we monitor https://stackoverflow.com[stackoverflow.com] for questions tagged with https://stackoverflow.com/tags/spring-data[`spring-data-elasticsearch`].
You can also chat with the community on https://gitter.im/spring-projects/spring-data[Gitter].
* Ask a question or chat with the community on https://app.gitter.im/#/room/#spring-projects_spring-data:gitter.im[Gitter].
* Report bugs with Spring Data for Elasticsearch at https://github.com/spring-projects/spring-data-elasticsearch/issues[https://github.com/spring-projects/spring-data-elasticsearch/issues].
== Reporting Issues
+1 -6
View File
@@ -2,12 +2,7 @@
set -euo pipefail
export DEVELOCITY_CACHE_USERNAME=${DEVELOCITY_CACHE_USR}
export DEVELOCITY_CACHE_PASSWORD=${DEVELOCITY_CACHE_PSW}
export JENKINS_USER=${JENKINS_USER_NAME}
# The environment variable to configure access key is still GRADLE_ENTERPRISE_ACCESS_KEY
export GRADLE_ENTERPRISE_ACCESS_KEY=${DEVELOCITY_ACCESS_KEY}
MAVEN_OPTS="-Duser.name=${JENKINS_USER} -Duser.home=/tmp/jenkins-home" \
./mvnw -s settings.xml clean -Dscan=false -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-elasticsearch
./mvnw -s settings.xml clean -Dscan=false -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-elasticsearch -Ddevelocity.storage.directory=/tmp/jenkins-home/.develocity-root
+8 -5
View File
@@ -1,19 +1,21 @@
# Java versions
java.main.tag=17.0.9_9-jdk-focal
java.next.tag=21.0.1_12-jdk-jammy
java.main.tag=17.0.13_11-jdk-focal
java.next.tag=23.0.1_11-jdk-noble
# Docker container images - standard
docker.java.main.image=harbor-repo.vmware.com/dockerhub-proxy-cache/library/eclipse-temurin:${java.main.tag}
docker.java.next.image=harbor-repo.vmware.com/dockerhub-proxy-cache/library/eclipse-temurin:${java.next.tag}
docker.java.main.image=library/eclipse-temurin:${java.main.tag}
docker.java.next.image=library/eclipse-temurin:${java.next.tag}
# Supported versions of MongoDB
docker.mongodb.4.4.version=4.4.25
docker.mongodb.5.0.version=5.0.21
docker.mongodb.6.0.version=6.0.10
docker.mongodb.7.0.version=7.0.2
docker.mongodb.8.0.version=8.0.0
# 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
@@ -25,9 +27,10 @@ docker.java.inside.docker=-u root -v /var/run/docker.sock:/var/run/docker.sock -
# Credentials
docker.registry=
docker.credentials=hub.docker.com-springbuildmaster
docker.proxy.registry=https://docker-hub.usw1.packages.broadcom.com
docker.proxy.credentials=usw1_packages_broadcom_com-jenkins-token
artifactory.credentials=02bd1690-b54f-4c9f-819d-a77cb7a9822c
artifactory.url=https://repo.spring.io
artifactory.repository.snapshot=libs-snapshot-local
develocity.cache.credentials=gradle_enterprise_cache_user
develocity.access-key=gradle_enterprise_secret_access_key
jenkins.user.name=spring-builds+jenkins
+1 -8
View File
@@ -3,15 +3,8 @@
set -euo pipefail
mkdir -p /tmp/jenkins-home/.m2/spring-data-elasticsearch
chown -R 1001:1001 .
export DEVELOCITY_CACHE_USERNAME=${DEVELOCITY_CACHE_USR}
export DEVELOCITY_CACHE_PASSWORD=${DEVELOCITY_CACHE_PSW}
export JENKINS_USER=${JENKINS_USER_NAME}
# The environment variable to configure access key is still GRADLE_ENTERPRISE_ACCESS_KEY
export GRADLE_ENTERPRISE_ACCESS_KEY=${DEVELOCITY_ACCESS_KEY}
MAVEN_OPTS="-Duser.name=${JENKINS_USER} -Duser.home=/tmp/jenkins-home" \
./mvnw -s settings.xml \
-P${PROFILE} clean dependency:list verify -Dsort -U -B -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-elasticsearch
-P${PROFILE} clean dependency:list verify -Dsort -U -B -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-elasticsearch -Ddevelocity.storage.directory=/tmp/jenkins-home/.develocity-root
+10
View File
@@ -0,0 +1,10 @@
{
"dependencies": {
"antora": "3.2.0-alpha.6",
"@antora/atlas-extension": "1.0.0-alpha.2",
"@antora/collector-extension": "1.0.0-alpha.7",
"@asciidoctor/tabs": "1.0.0-beta.6",
"@springio/antora-extensions": "1.13.0",
"@springio/asciidoctor-extensions": "1.0.0-alpha.11"
}
}
+19 -50
View File
@@ -5,12 +5,12 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>5.2.6</version>
<version>5.4.1</version>
<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<version>3.2.6</version>
<version>3.4.1</version>
</parent>
<name>Spring Data Elasticsearch</name>
@@ -18,17 +18,16 @@
<url>https://github.com/spring-projects/spring-data-elasticsearch</url>
<properties>
<springdata.commons>3.2.6</springdata.commons>
<springdata.commons>3.4.1</springdata.commons>
<!-- version of the ElasticsearchClient -->
<elasticsearch-java>8.11.4</elasticsearch-java>
<elasticsearch-java>8.15.5</elasticsearch-java>
<blockhound-junit>1.0.8.RELEASE</blockhound-junit>
<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>
@@ -132,17 +131,6 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId> <!-- is Apache 2-->
<version>${elasticsearch-java}</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Jackson JSON Mapper -->
<dependency>
@@ -248,13 +236,6 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor.tools</groupId>
<artifactId>blockhound-junit-platform</artifactId>
<version>${blockhound-junit}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.skyscreamer</groupId>
<artifactId>jsonassert</artifactId>
@@ -263,8 +244,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>
@@ -324,6 +305,13 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>${archunit}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@@ -436,25 +424,6 @@
</build>
</profile>
<profile>
<id>jdk13+</id>
<!-- on jDK13+, Blockhound needs this JVM flag set -->
<activation>
<jdk>[13,)</jdk>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-XX:+AllowRedefinitionToAddDeleteMethods</argLine>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>antora-process-resources</id>
<build>
@@ -472,7 +441,7 @@
<build>
<plugins>
<plugin>
<groupId>io.spring.maven.antora</groupId>
<groupId>org.antora</groupId>
<artifactId>antora-maven-plugin</artifactId>
</plugin>
</plugins>
+3 -5
View File
@@ -3,8 +3,7 @@
# The purpose of this Antora playbook is to build the docs in the current branch.
antora:
extensions:
- '@antora/collector-extension'
- require: '@springio/antora-extensions/root-component-extension'
- require: '@springio/antora-extensions'
root_component_name: 'data-elasticsearch'
site:
title: Spring Data Elasticsearch
@@ -22,13 +21,12 @@ content:
start_path: src/main/antora
asciidoc:
attributes:
page-pagination: ''
hide-uri-scheme: '@'
tabs-sync-option: '@'
chomp: 'all'
extensions:
- '@asciidoctor/tabs'
- '@springio/asciidoctor-extensions'
- '@springio/asciidoctor-extensions/javadoc-extension'
sourcemap: true
urls:
latest_version_segment: ''
@@ -38,5 +36,5 @@ runtime:
format: pretty
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.3.5/ui-bundle.zip
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.16/ui-bundle.zip
snapshot: true
+5
View File
@@ -10,3 +10,8 @@ ext:
local: true
scan:
dir: target/classes/
- run:
command: ./mvnw package -Pdistribute
local: true
scan:
dir: target/antora
+4 -1
View File
@@ -9,6 +9,8 @@
*** xref:migration-guides/migration-guide-4.4-5.0.adoc[]
*** xref:migration-guides/migration-guide-5.0-5.1.adoc[]
*** xref:migration-guides/migration-guide-5.1-5.2.adoc[]
*** xref:migration-guides/migration-guide-5.2-5.3.adoc[]
*** xref:migration-guides/migration-guide-5.3-5.4.adoc[]
* xref:elasticsearch.adoc[]
@@ -39,4 +41,5 @@
** xref:repositories/query-keywords-reference.adoc[]
** xref:repositories/query-return-types-reference.adoc[]
* https://github.com/spring-projects/spring-data-commons/wiki[Wiki]
* xref:attachment$api/java/index.html[Javadoc,role=link-external,window=_blank]
* https://github.com/spring-projects/spring-data-commons/wiki[Wiki,role=link-external,window=_blank]
@@ -31,7 +31,7 @@ public class MyClientConfig extends ElasticsearchConfiguration {
<.> for a detailed description of the builder methods see xref:elasticsearch/clients.adoc#elasticsearch.clients.configuration[Client Configuration]
====
The `ElasticsearchConfiguration` class allows further configuration by overriding for example the `jsonpMapper()` or `transportOptions()` methods.
The javadoc:org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration[]] class allows further configuration by overriding for example the `jsonpMapper()` or `transportOptions()` methods.
The following beans can then be injected in other Spring components:
@@ -52,13 +52,13 @@ RestClient restClient; <.>
JsonpMapper jsonpMapper; <.>
----
<.> an implementation of `ElasticsearchOperations`
<.> an implementation of javadoc:org.springframework.data.elasticsearch.core.ElasticsearchOperations[]
<.> the `co.elastic.clients.elasticsearch.ElasticsearchClient` that is used.
<.> the low level `RestClient` from the Elasticsearch libraries
<.> the `JsonpMapper` user by the Elasticsearch `Transport`
====
Basically one should just use the `ElasticsearchOperations` to interact with the Elasticsearch cluster.
Basically one should just use the javadoc:org.springframework.data.elasticsearch.core.ElasticsearchOperations[] to interact with the Elasticsearch cluster.
When using repositories, this instance is used under the hood as well.
[[elasticsearch.clients.reactiverestclient]]
@@ -86,7 +86,7 @@ public class MyClientConfig extends ReactiveElasticsearchConfiguration {
<.> for a detailed description of the builder methods see xref:elasticsearch/clients.adoc#elasticsearch.clients.configuration[Client Configuration]
====
The `ReactiveElasticsearchConfiguration` class allows further configuration by overriding for example the `jsonpMapper()` or `transportOptions()` methods.
The javadoc:org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchConfiguration[] class allows further configuration by overriding for example the `jsonpMapper()` or `transportOptions()` methods.
The following beans can then be injected in other Spring components:
@@ -108,20 +108,20 @@ JsonpMapper jsonpMapper; <.>
the following can be injected:
<.> an implementation of `ReactiveElasticsearchOperations`
<.> an implementation of javadoc:org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations[]
<.> the `org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchClient` that is used.
This is a reactive implementation based on the Elasticsearch client implementation.
<.> the low level `RestClient` from the Elasticsearch libraries
<.> the `JsonpMapper` user by the Elasticsearch `Transport`
====
Basically one should just use the `ReactiveElasticsearchOperations` to interact with the Elasticsearch cluster.
Basically one should just use the javadoc:org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations[] to interact with the Elasticsearch cluster.
When using repositories, this instance is used under the hood as well.
[[elasticsearch.clients.configuration]]
== Client Configuration
Client behaviour can be changed via the `ClientConfiguration` that allows to set options for SSL, connect and socket timeouts, headers and other parameters.
Client behaviour can be changed via the javadoc:org.springframework.data.elasticsearch.client.ClientConfiguration[] that allows to set options for SSL, connect and socket timeouts, headers and other parameters.
.Client Configuration
====
@@ -150,7 +150,7 @@ ClientConfiguration clientConfiguration = ClientConfiguration.builder()
return headers;
})
.withClientConfigurer( <.>
ElasticsearchClientConfigurationCallback.from(clientBuilder -> {
ElasticsearchHttpClientConfigurationCallback.from(clientBuilder -> {
// ...
return clientBuilder;
}))
@@ -178,7 +178,7 @@ If this is used in the reactive setup, the supplier function *must not* block!
[[elasticsearch.clients.configuration.callbacks]]
=== Client configuration callbacks
The `ClientConfiguration` class offers the most common parameters to configure the client.
The javadoc:org.springframework.data.elasticsearch.client.ClientConfiguration[] class offers the most common parameters to configure the client.
In the case this is not enough, the user can add callback functions by using the `withClientConfigurer(ClientConfigurationCallback<?>)` method.
The following callbacks are provided:
@@ -192,6 +192,7 @@ This callback provides a `org.elasticsearch.client.RestClientBuilder` that can b
[source,java]
----
ClientConfiguration.builder()
.connectedTo("localhost:9200", "localhost:9291")
.withClientConfigurer(ElasticsearchClients.ElasticsearchRestClientConfigurationCallback.from(restClientBuilder -> {
// configure the Elasticsearch RestClient
return restClientBuilder;
@@ -210,6 +211,7 @@ used by the `RestClient`.
[source,java]
----
ClientConfiguration.builder()
.connectedTo("localhost:9200", "localhost:9291")
.withClientConfigurer(ElasticsearchClients.ElasticsearchHttpClientConfigurationCallback.from(httpAsyncClientBuilder -> {
// configure the HttpAsyncClient
return httpAsyncClientBuilder;
@@ -1,13 +1,28 @@
[[new-features]]
= What's new
[[new-features.5-2-3]]
== New in Spring Data Elasticsearch 5.2.2
* Upgrade to Elasticsearch 8.11.4
[[new-features.5-4-1]]
== New in Spring Data Elasticsearch 5.4.1
* Upgrade to Elasticsearch 8.15.5.
[[new-features.5-2-2]]
== New in Spring Data Elasticsearch 5.2.2
* Upgrade to Elasticsearch 8.11.3
[[new-features.5-4-0]]
== New in Spring Data Elasticsearch 5.4
* Upgrade to Elasticsearch 8.15.3.
* Allow to customize the mapped type name for `@InnerField` and `@Field` annotations.
* Support for Elasticsearch SQL.
* Add support for retrieving request executionDuration.
[[new-features.5-3-0]]
== New in Spring Data Elasticsearch 5.3
* Upgrade to Elasticsearch 8.13.2.
* Add support for highlight queries in highlighting.
* Add shard statistics to the `SearchHit` class.
* Add support for multi search template API.
* Add support for SpEL in @Query.
* Add support for field aliases in the index mapping.
* Add support for has_child and has_parent queries.
[[new-features.5-2-0]]
== New in Spring Data Elasticsearch 5.2
@@ -52,7 +52,7 @@ public class Statement {
return routing;
}
public void setRouting(Routing routing) {
public void setRouting(String routing) {
this.routing = routing;
}
@@ -199,7 +199,7 @@ void init() {
repository.save(
Statement.builder()
.withText("+1 for the sun")
,withRouting(savedWeather.getId())
.withRouting(savedWeather.getId())
.withRelation(new JoinField<>("vote", sunnyAnswer.getId())) <5>
.build());
}
@@ -226,6 +226,7 @@ SearchHits<Statement> hasVotes() {
Query query = NativeQuery.builder()
.withQuery(co.elastic.clients.elasticsearch._types.query_dsl.Query.of(qb -> qb
.hasChild(hc -> hc
.type("answer")
.queryName("vote")
.query(matchAllQueryAsQuery())
.scoreMode(ChildScoreMode.None)
@@ -192,8 +192,7 @@ public String getProperty() {
This annotation can be set on a String property of an entity.
This property will not be written to the mapping, it will not be stored in Elasticsearch and its value will not be read from an Elasticsearch document.
After an entity is persisted, for example with a call to `ElasticsearchOperations.save(T entity)`, the entity
returned from that call will contain the name of the index that an entity was saved to in that property.
After an entity is persisted, for example with a call to `ElasticsearchOperations.save(T entity)`, the entity returned from that call will contain the name of the index that an entity was saved to in that property.
This is useful when the index name is dynamically set by a bean, or when writing to a write alias.
Putting some value into such a property does not set the index into which an entity is stored!
@@ -423,7 +422,6 @@ Looking at the `Configuration` from the xref:elasticsearch/object-mapping.adoc#e
@Configuration
public class Config extends ElasticsearchConfiguration {
@NonNull
@Override
public ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder() //
@@ -12,10 +12,10 @@ class Book {
@Id
private String id;
@Field(type = FieldType.text)
@Field(type = FieldType.Text)
private String name;
@Field(type = FieldType.text)
@Field(type = FieldType.Text)
private String summary;
@Field(type = FieldType.Integer)
@@ -316,7 +316,7 @@ Repository methods can be defined to have the following return types for returni
.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> {
@@ -361,3 +361,202 @@ would make an https://www.elastic.co/guide/en/elasticsearch/reference/current/qu
}
----
====
[[elasticsearch.query-methods.at-query.spel]]
=== Using SpEL Expressions
.Declare query on the method using the `@Query` annotation with SpEL expression.
====
https://docs.spring.io/spring-framework/reference/core/expressions.html[SpEL expression] is also supported when defining query in `@Query`.
[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
@Query("""
{
"bool":{
"must":[
{
"term":{
"name": "#{#name}"
}
}
]
}
}
""")
Page<Book> findByName(String name, Pageable pageable);
}
----
If for example the function is called with the parameter _John_, it would produce the following query body:
[source,json]
----
{
"bool":{
"must":[
{
"term":{
"name": "John"
}
}
]
}
}
----
====
.accessing parameter property.
====
Supposing that we have the following class as query parameter type:
[source,java]
----
public record QueryParameter(String value) {
}
----
It's easy to access the parameter by `#` symbol, then reference the property `value` with a simple `.`:
[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
@Query("""
{
"bool":{
"must":[
{
"term":{
"name": "#{#parameter.value}"
}
}
]
}
}
""")
Page<Book> findByName(QueryParameter parameter, Pageable pageable);
}
----
We can pass `new QueryParameter("John")` as the parameter now, and it will produce the same query string as above.
====
.accessing bean property.
====
https://docs.spring.io/spring-framework/reference/core/expressions/language-ref/bean-references.html[Bean property] is also supported to access. Given that there is a bean named `queryParameter` of type `QueryParameter`, we can access the bean with symbol `@` rather than `#`, and there is no need to declare a parameter of type `QueryParameter` in the query method:
[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
@Query("""
{
"bool":{
"must":[
{
"term":{
"name": "#{@queryParameter.value}"
}
}
]
}
}
""")
Page<Book> findByName(Pageable pageable);
}
----
====
.SpEL and `Collection` param.
====
`Collection` parameter is also supported and is as easy to use as normal `String`, such as the following `terms` query:
[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
@Query("""
{
"bool":{
"must":[
{
"terms":{
"name": #{#names}
}
}
]
}
}
""")
Page<Book> findByName(Collection<String> names, Pageable pageable);
}
----
NOTE: collection values should not be quoted when declaring the elasticsearch json query.
A collection of `names` like `List.of("name1", "name2")` will produce the following terms query:
[source,json]
----
{
"bool":{
"must":[
{
"terms":{
"name": ["name1", "name2"]
}
}
]
}
}
----
====
.access property in the `Collection` param.
====
https://docs.spring.io/spring-framework/reference/core/expressions/language-ref/collection-projection.html[SpEL Collection Projection] is convenient to use when values in the `Collection` parameter is not plain `String`:
[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
@Query("""
{
"bool":{
"must":[
{
"terms":{
"name": #{#parameters.![value]}
}
}
]
}
}
""")
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.
====
.alter parameter name by using `@Param`
====
When accessing the parameter by SpEL, it's also useful to alter the parameter name to another one by `@Param` annotation in Sping Data:
[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
@Query("""
{
"bool":{
"must":[
{
"terms":{
"name": #{#another.![value]}
}
}
]
}
}
""")
Page<Book> findByName(@Param("another") Collection<QueryParameter> parameters, Pageable pageable);
}
----
====
@@ -194,7 +194,7 @@ In the following code this is used to run a query for a given gender and maximum
var runtimeField = new RuntimeField("age", "long", """ <.>
Instant currentDate = Instant.ofEpochMilli(new Date().getTime());
Instant startDate = doc['birth-date'].value.toInstant();
Instant startDate = doc['birthDate'].value.toInstant();
emit (ChronoUnit.DAYS.between(startDate, currentDate) / 365);
""");
@@ -3,10 +3,10 @@
Spring Data Elasticsearch uses several interfaces to define the operations that can be called against an Elasticsearch index (for a description of the reactive interfaces see xref:elasticsearch/reactive-template.adoc[]).
* `IndexOperations` defines actions on index level like creating or deleting an index.
* `DocumentOperations` defines actions to store, update and retrieve entities based on their id.
* `SearchOperations` define the actions to search for multiple entities using queries
* `ElasticsearchOperations` combines the `DocumentOperations` and `SearchOperations` interfaces.
* javadoc:org.springframework.data.elasticsearch.core.IndexOperations[] defines actions on index level like creating or deleting an index.
* javadoc:org.springframework.data.elasticsearch.core.DocumentOperations[] defines actions to store, update and retrieve entities based on their id.
* javadoc:org.springframework.data.elasticsearch.core.SearchOperations[] define the actions to search for multiple entities using queries
* javadoc:org.springframework.data.elasticsearch.core.ElasticsearchOperations[] combines the `DocumentOperations` and `SearchOperations` interfaces.
These interfaces correspond to the structuring of the https://www.elastic.co/guide/en/elasticsearch/reference/current/rest-apis.html[Elasticsearch API].
@@ -81,7 +81,7 @@ When a document is retrieved with the methods of the `DocumentOperations` inter
When searching with the methods of the `SearchOperations` interface, additional information is available for each entity, for example the _score_ or the _sortValues_ of the found entity.
In order to return this information, each entity is wrapped in a `SearchHit` object that contains this entity-specific additional information.
These `SearchHit` objects themselves are returned within a `SearchHits` object which additionally contains informations about the whole search like the _maxScore_ or requested aggregations.
These `SearchHit` objects themselves are returned within a `SearchHits` object which additionally contains informations about the whole search like the _maxScore_ or requested aggregations or the execution duration it took to complete the request.
The following classes and interfaces are now available:
.SearchHit<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
| 2023.1 (Vaughan) | 5.2.x | 8.11.4 | 6.1.x
| 2023.0 (Ullmann) | 5.1.x | 8.7.1 | 6.0.x
| 2022.0 (Turing) | 5.0.xfootnote:oom[Out of maintenance] | 8.5.3 | 6.0.x
| 2024.1 | 5.4.x | 8.15.5 | 6.2.x
| 2024.0 | 5.3.1 | 8.13.4 | 6.1.x
| 2023.1 (Vaughan) | 5.2.xfootnote:oom[Out of maintenance] | 8.11.1 | 6.1.x
| 2023.0 (Ullmann) | 5.1.xfootnote:oom[] | 8.7.1 | 6.0.x
| 2022.0 (Turing) | 5.0.xfootnote:oom[] | 8.5.3 | 6.0.x
| 2021.2 (Raj) | 4.4.xfootnote:oom[] | 7.17.3 | 5.3.x
| 2021.1 (Q) | 4.3.xfootnote:oom[] | 7.15.2 | 5.3.x
| 2021.0 (Pascal) | 4.2.xfootnote:oom[] | 7.12.0 | 5.3.x
@@ -49,7 +49,7 @@ Also the reactive implementation that was provided up to now has been moved here
If you are using `ElasticsearchRestTemplate` directly and not the `ElasticsearchOperations` interface you'll need to adjust your imports as well.
When working with the `NativeSearchQuery` class, you'll need to switch to the `NativeQuery` class, which can take a
`Query` instance comign from the new Elasticsearch client libraries.
`Query` instance coming from the new Elasticsearch client libraries.
You'll find plenty of examples in the test code.
[[elasticsearch-migration-guide-4.4-5.0.breaking-changes-records]]
@@ -0,0 +1,21 @@
[[elasticsearch-migration-guide-5.2-5.3]]
= Upgrading from 5.2.x to 5.3.x
This section describes breaking changes from version 5.2.x to 5.3.x and how removed features can be replaced by new introduced features.
[[elasticsearch-migration-guide-5.2-5.3.breaking-changes]]
== Breaking Changes
During the parameter replacement in `@Query` annotated repository methods previous versions wrote the String `"null"` into the query that was sent to Elasticsearch when the actual parameter value was `null`.
As Elasticsearch does not store `null` values, this behaviour could lead to problems, for example whent the fields to be searched contains the string `"null"`.
In Version 5.3 a `null` value in a parameter will cause a `ConversionException` to be thrown.
If you are using `"null"` as the
`null_value` defined in a field mapping, then pass that string into the query instead of a Java `null`.
[[elasticsearch-migration-guide-5.2-5.3.deprecations]]
== Deprecations
=== Removals
The deprecated classes `org.springframework.data.elasticsearch.ELCQueries`
and `org.springframework.data.elasticsearch.client.elc.QueryBuilders` have been removed, use `org.springframework.data.elasticsearch.client.elc.Queries` instead.
@@ -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,79 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.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 the original author 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();
}
@@ -39,41 +39,173 @@ public enum DateFormat {
basic_t_time("'T'HHmmss.SSSXXX"), //
basic_t_time_no_millis("'T'HHmmssXXX"), //
basic_week_date("YYYY'W'wwe"), // week-based-year!
/**
* @since 5.3
*/
strict_basic_week_date("YYYY'W'wwe"), // week-based-year!
basic_week_date_time("YYYY'W'wwe'T'HHmmss.SSSX"), // here Elasticsearch uses a different zone format
/**
* @since 5.3
*/
strict_basic_week_date_time("YYYY'W'wwe'T'HHmmss.SSSX"), // here Elasticsearch uses a different zone format
basic_week_date_time_no_millis("YYYY'W'wwe'T'HHmmssX"), //
/**
* @since 5.3
*/
strict_basic_week_date_time_no_millis("YYYY'W'wwe'T'HHmmssX"), //
date("uuuu-MM-dd"), //
/**
* @since 5.3
*/
strict_date("uuuu-MM-dd"), //
date_hour("uuuu-MM-dd'T'HH"), //
/**
* @since 5.3
*/
strict_date_hour("uuuu-MM-dd'T'HH"), //
date_hour_minute("uuuu-MM-dd'T'HH:mm"), //
/**
* @since 5.3
*/
strict_date_hour_minute("uuuu-MM-dd'T'HH:mm"), //
date_hour_minute_second("uuuu-MM-dd'T'HH:mm:ss"), //
/**
* @since 5.3
*/
strict_date_hour_minute_second("uuuu-MM-dd'T'HH:mm:ss"), //
date_hour_minute_second_fraction("uuuu-MM-dd'T'HH:mm:ss.SSS"), //
/**
* @since 5.3
*/
strict_date_hour_minute_second_fraction("uuuu-MM-dd'T'HH:mm:ss.SSS"), //
date_hour_minute_second_millis("uuuu-MM-dd'T'HH:mm:ss.SSS"), //
/**
* @since 5.3
*/
strict_date_hour_minute_second_millis("uuuu-MM-dd'T'HH:mm:ss.SSS"), //
date_optional_time("uuuu-MM-dd['T'HH:mm:ss.SSSXXX]"), //
/**
* @since 5.3
*/
strict_date_optional_time("uuuu-MM-dd['T'HH:mm:ss.SSSXXX]"), //
strict_date_optional_time_nanos("uuuu-MM-dd['T'HH:mm:ss.SSSSSSXXX]"), //
date_time("uuuu-MM-dd'T'HH:mm:ss.SSSXXX"), //
/**
* @since 5.3
*/
strict_date_time("uuuu-MM-dd'T'HH:mm:ss.SSSXXX"), //
date_time_no_millis("uuuu-MM-dd'T'HH:mm:ssVV"), // here Elasticsearch uses the zone-id in its implementation
/**
* @since 5.3
*/
strict_date_time_no_millis("uuuu-MM-dd'T'HH:mm:ssVV"), // here Elasticsearch uses the zone-id in its implementation
epoch_millis("epoch_millis"), //
epoch_second("epoch_second"), //
hour("HH"), //
/**
* @since 5.3
*/
strict_hour("HH"), //
hour_minute("HH:mm"), //
/**
* @since 5.3
*/
strict_hour_minute("HH:mm"), //
hour_minute_second("HH:mm:ss"), //
/**
* @since 5.3
*/
strict_hour_minute_second("HH:mm:ss"), //
hour_minute_second_fraction("HH:mm:ss.SSS"), //
/**
* @since 5.3
*/
strict_hour_minute_second_fraction("HH:mm:ss.SSS"), //
hour_minute_second_millis("HH:mm:ss.SSS"), //
/**
* @since 5.3
*/
strict_hour_minute_second_millis("HH:mm:ss.SSS"), //
ordinal_date("uuuu-DDD"), //
/**
* @since 5.3
*/
strict_ordinal_date("uuuu-DDD"), //
ordinal_date_time("uuuu-DDD'T'HH:mm:ss.SSSXXX"), //
/**
* @since 5.3
*/
strict_ordinal_date_time("uuuu-DDD'T'HH:mm:ss.SSSXXX"), //
ordinal_date_time_no_millis("uuuu-DDD'T'HH:mm:ssXXX"), //
/**
* @since 5.3
*/
strict_ordinal_date_time_no_millis("uuuu-DDD'T'HH:mm:ssXXX"), //
time("HH:mm:ss.SSSXXX"), //
/**
* @since 5.3
*/
strict_time("HH:mm:ss.SSSXXX"), //
time_no_millis("HH:mm:ssXXX"), //
/**
* @since 5.3
*/
strict_time_no_millis("HH:mm:ssXXX"), //
t_time("'T'HH:mm:ss.SSSXXX"), //
/**
* @since 5.3
*/
strict_t_time("'T'HH:mm:ss.SSSXXX"), //
t_time_no_millis("'T'HH:mm:ssXXX"), //
/**
* @since 5.3
*/
strict_t_time_no_millis("'T'HH:mm:ssXXX"), //
week_date("YYYY-'W'ww-e"), //
/**
* @since 5.3
*/
strict_week_date("YYYY-'W'ww-e"), //
week_date_time("YYYY-'W'ww-e'T'HH:mm:ss.SSSXXX"), //
/**
* @since 5.3
*/
strict_week_date_time("YYYY-'W'ww-e'T'HH:mm:ss.SSSXXX"), //
week_date_time_no_millis("YYYY-'W'ww-e'T'HH:mm:ssXXX"), //
/**
* @since 5.3
*/
strict_week_date_time_no_millis("YYYY-'W'ww-e'T'HH:mm:ssXXX"), //
weekyear(""), // no TemporalAccessor available for these 3
/**
* @since 5.3
*/
strict_weekyear(""), // no TemporalAccessor available for these 3
weekyear_week(""), //
/**
* @since 5.3
*/
strict_weekyear_week(""), //
weekyear_week_day(""), //
/**
* @since 5.3
*/
strict_strict_weekyear_week_day(""), //
year("uuuu"), //
/**
* @since 5.3
*/
strict_year("uuuu"), //
year_month("uuuu-MM"), //
year_month_day("uuuu-MM-dd"); //
/**
* @since 5.3
*/
strict_year_month("uuuu-MM"), //
year_month_day("uuuu-MM-dd"), //
/**
* @since 5.3
*/
strict_year_month_day("uuuu-MM-dd"); //
private final String pattern;
@@ -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 the original author 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 the original author 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 "";
}
@@ -21,6 +21,7 @@ import java.lang.annotation.RetentionPolicy;
/**
* @author Peter-Josef Meisch
* @author Haibo Liu
* @since 4.0
*/
@Documented
@@ -59,6 +60,8 @@ public @interface HighlightParameters {
int numberOfFragments() default -1;
Query highlightQuery() default @Query;
String order() default "";
int phraseLimit() default -1;
@@ -15,9 +15,6 @@
*/
package org.springframework.data.elasticsearch.annotations;
import org.springframework.data.annotation.ReadOnlyProperty;
import org.springframework.data.annotation.Transient;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -25,10 +22,10 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to mark a String property of an entity to be filled with the name of the index where the entity was
* stored after it is indexed into Elasticsearch. This can be used when the name of the index is dynamically created
* or when a document was indexed into a write alias.
*
* Annotation to mark a String property of an entity to be filled with the name of the index where the entity was stored
* after it is indexed into Elasticsearch. This can be used when the name of the index is dynamically created or when a
* document was indexed into a write alias.
* <p>
* This can not be used to specify the index where an entity should be written to.
*
* @author Peter-Josef Meisch
@@ -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 the original author 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 the original author 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 the original author 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;
}
}
@@ -74,7 +74,14 @@ public @interface Mapping {
*/
String runtimeFieldsPath() default "";
/**
* field alias definitions to be written to the index mapping
*
* @since 5.3
*/
MappingAlias[] aliases() default {};
enum Detection {
DEFAULT, TRUE, FALSE;
DEFAULT, TRUE, FALSE
}
}
@@ -0,0 +1,45 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Defines a field alias in the index mapping.
*
* @author Peter-Josef Meisch
* @since 5.3
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Inherited
public @interface MappingAlias {
/**
* the name of the alias.
*/
String name();
/**
* the path of the alias.
*/
String path();
}
@@ -25,11 +25,11 @@ import org.springframework.data.util.ReactiveWrappers;
*/
public class ElasticsearchAotPredicates {
public static final Predicate<ReactiveWrappers.ReactiveLibrary> IS_REACTIVE_LIBARARY_AVAILABLE = (
public static final Predicate<ReactiveWrappers.ReactiveLibrary> IS_REACTIVE_LIBRARY_AVAILABLE = (
lib) -> ReactiveWrappers.isAvailable(lib);
public static boolean isReactorPresent() {
return IS_REACTIVE_LIBARARY_AVAILABLE.test(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR);
return IS_REACTIVE_LIBRARY_AVAILABLE.test(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR);
}
}
@@ -233,6 +233,15 @@ public interface ClientConfiguration {
*/
TerminalClientConfigurationBuilder usingSsl();
/**
* Connects using https if flag is true.
*
* @param flag whether to use https in the connection
* @return the {@link TerminalClientConfigurationBuilder}
* @since 5.3
*/
TerminalClientConfigurationBuilder usingSsl(boolean flag);
/**
* Connect via {@literal https} using the given {@link SSLContext}.<br />
* <strong>NOTE</strong> You need to leave out the protocol in
@@ -25,7 +25,6 @@ import java.util.function.Supplier;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import org.elasticsearch.client.RestClientBuilder.HttpClientConfigCallback;
import org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationBuilderWithRequiredEndpoint;
import org.springframework.data.elasticsearch.client.ClientConfiguration.MaybeSecureClientConfigurationBuilder;
import org.springframework.data.elasticsearch.client.ClientConfiguration.TerminalClientConfigurationBuilder;
@@ -106,6 +105,13 @@ class ClientConfigurationBuilder
return this;
}
@Override
public TerminalClientConfigurationBuilder usingSsl(boolean flag) {
this.useSsl = flag;
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.client.ClientConfiguration.MaybeSecureClientConfigurationBuilder#usingSsl(javax.net.ssl.SSLContext)
@@ -0,0 +1,70 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.client.elc;
import java.util.function.Consumer;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.lang.Nullable;
/**
* An abstract class that serves as a base for query processors. It provides a common interface and basic functionality
* for query processing.
*
* @author Aouichaoui Youssef
* @since 5.3
*/
public abstract class AbstractQueryProcessor {
/**
* Convert a spring-data-elasticsearch {@literal query} to an Elasticsearch {@literal query}.
*
* @param query spring-data-elasticsearch {@literal query}.
* @param queryConverter correct mapped field names and the values to the converted values.
* @return an Elasticsearch {@literal query}.
*/
@Nullable
static co.elastic.clients.elasticsearch._types.query_dsl.Query getEsQuery(@Nullable Query query,
@Nullable Consumer<Query> queryConverter) {
if (query == null) {
return null;
}
if (queryConverter != null) {
queryConverter.accept(query);
}
co.elastic.clients.elasticsearch._types.query_dsl.Query esQuery = null;
if (query instanceof CriteriaQuery criteriaQuery) {
esQuery = CriteriaQueryProcessor.createQuery(criteriaQuery.getCriteria());
} else if (query instanceof StringQuery stringQuery) {
esQuery = Queries.wrapperQueryAsQuery(stringQuery.getSource());
} else if (query instanceof NativeQuery nativeQuery) {
if (nativeQuery.getQuery() != null) {
esQuery = nativeQuery.getQuery();
} else if (nativeQuery.getSpringDataQuery() != null) {
esQuery = getEsQuery(nativeQuery.getSpringDataQuery(), queryConverter);
}
} else {
throw new IllegalArgumentException("unhandled Query implementation " + query.getClass().getName());
}
return esQuery;
}
}
@@ -16,7 +16,6 @@
package org.springframework.data.elasticsearch.client.elc;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.cluster.ElasticsearchClusterClient;
import co.elastic.clients.transport.ElasticsearchTransport;
import org.elasticsearch.client.RestClient;
@@ -40,9 +39,4 @@ public class AutoCloseableElasticsearchClient extends ElasticsearchClient implem
public void close() throws Exception {
transport.close();
}
@Override
public ElasticsearchClusterClient cluster() {
return super.cluster();
}
}
@@ -39,6 +39,7 @@ import org.springframework.data.elasticsearch.core.geo.GeoBox;
import org.springframework.data.elasticsearch.core.geo.GeoJson;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.utils.geohash.Geohash;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metrics;
@@ -50,6 +51,7 @@ import org.springframework.util.Assert;
* filter.
*
* @author Peter-Josef Meisch
* @author Junghoon Ban
* @since 4.4
*/
class CriteriaFilterProcessor {
@@ -68,10 +70,17 @@ class CriteriaFilterProcessor {
for (Criteria chainedCriteria : criteria.getCriteriaChain()) {
if (chainedCriteria.isOr()) {
BoolQuery.Builder boolQueryBuilder = QueryBuilders.bool();
queriesForEntries(chainedCriteria).forEach(boolQueryBuilder::should);
filterQueries.add(new Query(boolQueryBuilder.build()));
Collection<? extends Query> queriesForEntries = queriesForEntries(chainedCriteria);
if (!queriesForEntries.isEmpty()) {
BoolQuery.Builder boolQueryBuilder = QueryBuilders.bool();
queriesForEntries.forEach(boolQueryBuilder::should);
filterQueries.add(new Query(boolQueryBuilder.build()));
}
} else if (chainedCriteria.isNegating()) {
Assert.notNull(criteria.getField(), "criteria must have a field");
Collection<? extends Query> negatingFilters = buildNegatingFilter(criteria.getField().getName(),
criteria.getFilterCriteriaEntries());
filterQueries.addAll(negatingFilters);
@@ -115,6 +124,7 @@ class CriteriaFilterProcessor {
private static Collection<? extends Query> queriesForEntries(Criteria criteria) {
Assert.notNull(criteria.getField(), "criteria must have a field");
String fieldName = criteria.getField().getName();
Assert.notNull(fieldName, "Unknown field");
@@ -169,17 +179,17 @@ class CriteriaFilterProcessor {
Assert.isTrue(values[1] instanceof String || values[1] instanceof Distance,
"Second element of a geo distance filter must be a text or a Distance");
String dist = (values[1] instanceof Distance) ? extractDistanceString((Distance) values[1]) : (String) values[1];
String dist = (values[1] instanceof Distance distance) ? extractDistanceString(distance) : (String) values[1];
return QueryBuilders.geoDistance() //
.field(fieldName) //
.distance(dist) //
.distanceType(GeoDistanceType.Plane) //
.location(location -> {
if (values[0]instanceof GeoPoint loc) {
if (values[0] instanceof GeoPoint loc) {
location.latlon(latlon -> latlon.lat(loc.getLat()).lon(loc.getLon()));
} else if (values[0] instanceof Point) {
GeoPoint loc = GeoPoint.fromPoint((Point) values[0]);
} else if (values[0] instanceof Point point) {
GeoPoint loc = GeoPoint.fromPoint(point);
location.latlon(latlon -> latlon.lat(loc.getLat()).lon(loc.getLon()));
} else {
String loc = (String) values[0];
@@ -220,8 +230,8 @@ class CriteriaFilterProcessor {
"single-element of boundedBy filter must be type of GeoBox or Box");
GeoBox geoBBox;
if (value instanceof Box) {
geoBBox = GeoBox.fromBox((Box) value);
if (value instanceof Box box) {
geoBBox = GeoBox.fromBox(box);
} else {
geoBBox = (GeoBox) value;
}
@@ -244,7 +254,7 @@ class CriteriaFilterProcessor {
Assert.isTrue(allElementsAreOfType(values, GeoPoint.class) || allElementsAreOfType(values, String.class),
" both elements of boundedBy filter must be type of GeoPoint or text(format lat,lon or geohash)");
if (values[0]instanceof GeoPoint topLeft) {
if (values[0] instanceof GeoPoint topLeft) {
GeoPoint bottomRight = (GeoPoint) values[1];
queryBuilder.boundingBox(bb -> bb //
.tlbr(tlbr -> tlbr //
@@ -266,7 +276,10 @@ class CriteriaFilterProcessor {
.tlbr(tlbr -> tlbr //
.topLeft(glb -> {
if (isGeoHash) {
glb.geohash(gh -> gh.geohash(topLeft));
// although the builder in 8.13.2 supports geohash, the server throws an error, so we convert to a
// lat,lon string here
glb.text(Geohash.toLatLon(topLeft));
// glb.geohash(gh -> gh.geohash(topLeft));
} else {
glb.text(topLeft);
}
@@ -274,7 +287,8 @@ class CriteriaFilterProcessor {
}) //
.bottomRight(glb -> {
if (isGeoHash) {
glb.geohash(gh -> gh.geohash(bottomRight));
glb.text(Geohash.toLatLon(bottomRight));
// glb.geohash(gh -> gh.geohash(bottomRight));
} else {
glb.text(bottomRight);
}
@@ -16,21 +16,27 @@
package org.springframework.data.elasticsearch.client.elc;
import static org.springframework.data.elasticsearch.client.elc.Queries.*;
import static org.springframework.data.elasticsearch.client.elc.TypeUtils.*;
import static org.springframework.util.StringUtils.*;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.query_dsl.ChildScoreMode;
import co.elastic.clients.elasticsearch._types.query_dsl.Operator;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch.core.search.InnerHits;
import co.elastic.clients.json.JsonData;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.Field;
import org.springframework.data.elasticsearch.core.query.HasChildQuery;
import org.springframework.data.elasticsearch.core.query.HasParentQuery;
import org.springframework.data.elasticsearch.core.query.InnerHitsQuery;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -42,7 +48,7 @@ import org.springframework.util.Assert;
* @author Ezequiel Antúnez Camacho
* @since 4.4
*/
class CriteriaQueryProcessor {
class CriteriaQueryProcessor extends AbstractQueryProcessor {
/**
* creates a query from the criteria
@@ -110,11 +116,18 @@ class CriteriaQueryProcessor {
}
}
var filterQuery = CriteriaFilterProcessor.createQuery(criteria);
if (shouldQueries.isEmpty() && mustNotQueries.isEmpty() && mustQueries.isEmpty()) {
return null;
if (filterQuery.isEmpty()) {
return null;
}
// we need something to add the filter to
mustQueries.add(Query.of(qb -> qb.matchAll(m -> m)));
}
Query query = new Query.Builder().bool(boolQueryBuilder -> {
return new Query.Builder().bool(boolQueryBuilder -> {
if (!shouldQueries.isEmpty()) {
boolQueryBuilder.should(shouldQueries);
@@ -128,10 +141,10 @@ class CriteriaQueryProcessor {
boolQueryBuilder.must(mustQueries);
}
filterQuery.ifPresent(boolQueryBuilder::filter);
return boolQueryBuilder;
}).build();
return query;
}
@Nullable
@@ -174,6 +187,12 @@ class CriteriaQueryProcessor {
.scoreMode(ChildScoreMode.Avg));
}
if (criteria.isNegating() && criteria.isOr()) {
final Query query = queryBuilder.build();
queryBuilder = new Query.Builder();
queryBuilder.bool(mnqb -> mnqb.mustNot(query));
}
return queryBuilder.build();
}
@@ -227,51 +246,57 @@ class CriteriaQueryProcessor {
queryBuilder.queryString(queryStringQuery(fieldName, '*' + searchText, true, boost));
break;
case EXPRESSION:
queryBuilder.queryString(queryStringQuery(fieldName, value.toString(), boost));
queryBuilder.queryString(queryStringQuery(fieldName, Objects.requireNonNull(value).toString(), boost));
break;
case LESS:
queryBuilder //
.range(rb -> rb //
.field(fieldName) //
.lt(JsonData.of(value)) //
.boost(boost)); //
queryBuilder
.range(rb -> rb
.untyped(ut -> ut
.field(fieldName)
.lt(JsonData.of(value))
.boost(boost)));
break;
case LESS_EQUAL:
queryBuilder //
.range(rb -> rb //
.field(fieldName) //
.lte(JsonData.of(value)) //
.boost(boost)); //
queryBuilder
.range(rb -> rb
.untyped(ut -> ut
.field(fieldName)
.lte(JsonData.of(value))
.boost(boost)));
break;
case GREATER:
queryBuilder //
.range(rb -> rb //
.field(fieldName) //
.gt(JsonData.of(value)) //
.boost(boost)); //
queryBuilder
.range(rb -> rb
.untyped(ut -> ut
.field(fieldName)
.gt(JsonData.of(value))
.boost(boost)));
break;
case GREATER_EQUAL:
queryBuilder //
.range(rb -> rb //
.field(fieldName) //
.gte(JsonData.of(value)) //
.boost(boost)); //
queryBuilder
.range(rb -> rb
.untyped(ut -> ut
.field(fieldName)
.gte(JsonData.of(value))
.boost(boost)));
break;
case BETWEEN:
Object[] ranges = (Object[]) value;
queryBuilder //
.range(rb -> {
rb.field(fieldName);
if (ranges[0] != null) {
rb.gte(JsonData.of(ranges[0]));
}
Assert.notNull(value, "value for a between condition must not be null");
queryBuilder
.range(rb -> rb
.untyped(ut -> {
ut.field(fieldName);
if (ranges[0] != null) {
ut.gte(JsonData.of(ranges[0]));
}
if (ranges[1] != null) {
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:
@@ -282,10 +307,10 @@ class CriteriaQueryProcessor {
.boost(boost)); //
break;
case MATCHES:
queryBuilder.match(matchQuery(fieldName, value.toString(), Operator.Or, boost));
queryBuilder.match(matchQuery(fieldName, Objects.requireNonNull(value).toString(), Operator.Or, boost));
break;
case MATCHES_ALL:
queryBuilder.match(matchQuery(fieldName, value.toString(), Operator.And, boost));
queryBuilder.match(matchQuery(fieldName, Objects.requireNonNull(value).toString(), Operator.And, boost));
break;
case IN:
@@ -334,9 +359,35 @@ class CriteriaQueryProcessor {
queryBuilder //
.regexp(rb -> rb //
.field(fieldName) //
.value(value.toString()) //
.value(Objects.requireNonNull(value).toString()) //
.boost(boost)); //
break;
case HAS_CHILD:
if (value instanceof HasChildQuery query) {
queryBuilder.hasChild(hcb -> hcb
.type(query.getType())
.query(getEsQuery(query.getQuery(), null))
.innerHits(getInnerHits(query.getInnerHitsQuery()))
.ignoreUnmapped(query.getIgnoreUnmapped())
.minChildren(query.getMinChildren())
.maxChildren(query.getMaxChildren())
.scoreMode(scoreMode(query.getScoreMode())));
} else {
throw new CriteriaQueryException("value for " + fieldName + " is not a has_child query");
}
break;
case HAS_PARENT:
if (value instanceof HasParentQuery query) {
queryBuilder.hasParent(hpb -> hpb
.parentType(query.getParentType())
.query(getEsQuery(query.getQuery(), null))
.innerHits(getInnerHits(query.getInnerHitsQuery()))
.ignoreUnmapped(query.getIgnoreUnmapped())
.score(query.getScore()));
} else {
throw new CriteriaQueryException("value for " + fieldName + " is not a has_parent query");
}
break;
default:
throw new CriteriaQueryException("Could not build query for " + entry);
}
@@ -359,7 +410,7 @@ class CriteriaQueryProcessor {
if (item != null) {
if (sb.length() > 0) {
if (!sb.isEmpty()) {
sb.append(' ');
}
sb.append('"');
@@ -391,4 +442,19 @@ class CriteriaQueryProcessor {
return sb.toString();
}
/**
* Convert a spring-data-elasticsearch {@literal inner_hits} to an Elasticsearch {@literal inner_hits} query.
*
* @param query spring-data-elasticsearch {@literal inner_hits}.
* @return an Elasticsearch {@literal inner_hits} query.
*/
@Nullable
private static InnerHits getInnerHits(@Nullable InnerHitsQuery query) {
if (query == null) {
return null;
}
return InnerHits.of(iqb -> iqb.from(query.getFrom()).size(query.getSize()).name(query.getName()));
}
}
@@ -49,6 +49,8 @@ import org.springframework.util.Assert;
* {@link org.springframework.data.elasticsearch.core.document.Document}
*
* @author Peter-Josef Meisch
* @author Haibo Liu
* @author Mohamed El Harrougui
* @since 4.4
*/
final class DocumentAdapters {
@@ -73,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,
innerHits.put(name, SearchDocumentResponseBuilder.from(innerHitsResult.hits(), null, null, null, 0, null, null,
searchDocument -> null, jsonpMapper));
});
@@ -40,6 +40,7 @@ import org.springframework.data.elasticsearch.VersionConflictException;
* appropriate: any other exception may have resulted from user code, and should not be translated.
*
* @author Peter-Josef Meisch
* @author Junghoon Ban
* @since 4.4
*/
public class ElasticsearchExceptionTranslator implements PersistenceExceptionTranslator {
@@ -59,7 +60,7 @@ public class ElasticsearchExceptionTranslator implements PersistenceExceptionTra
*/
public RuntimeException translateException(Throwable throwable) {
RuntimeException runtimeException = throwable instanceof RuntimeException ? (RuntimeException) throwable
RuntimeException runtimeException = throwable instanceof RuntimeException ex ? ex
: new RuntimeException(throwable.getMessage(), throwable);
RuntimeException potentiallyTranslatedException = translateExceptionIfPossible(runtimeException);
@@ -23,12 +23,15 @@ 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;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -50,18 +53,12 @@ import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverte
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.BaseQueryBuilder;
import org.springframework.data.elasticsearch.core.query.BulkOptions;
import org.springframework.data.elasticsearch.core.query.ByQueryResponse;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.MoreLikeThisQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.SearchTemplateQuery;
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.data.elasticsearch.core.query.UpdateResponse;
import org.springframework.data.elasticsearch.core.reindex.ReindexRequest;
import org.springframework.data.elasticsearch.core.reindex.ReindexResponse;
import org.springframework.data.elasticsearch.core.script.Script;
import org.springframework.data.elasticsearch.core.sql.SqlResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -72,6 +69,7 @@ import org.springframework.util.Assert;
* @author Peter-Josef Meisch
* @author Hamid Rahimi
* @author Illia Ulianov
* @author Haibo Liu
* @since 4.4
*/
public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
@@ -79,6 +77,7 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
private static final Log LOGGER = LogFactory.getLog(ElasticsearchTemplate.class);
private final ElasticsearchClient client;
private final ElasticsearchSqlClient sqlClient;
private final RequestConverter requestConverter;
private final ResponseConverter responseConverter;
private final JsonpMapper jsonpMapper;
@@ -90,6 +89,7 @@ public 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);
@@ -102,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,6 +176,11 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
doBulkOperation(queries, bulkOptions, index);
}
@Override
public ByQueryResponse delete(DeleteQuery query, Class<?> clazz) {
return delete(query, clazz, getIndexCoordinatesFor(clazz));
}
@Override
public ByQueryResponse delete(Query query, Class<?> clazz, IndexCoordinates index) {
@@ -188,6 +194,18 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
return responseConverter.byQueryResponse(response);
}
@Override
public ByQueryResponse delete(DeleteQuery query, Class<?> clazz, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
DeleteByQueryRequest request = requestConverter.documentDeleteByQueryRequest(query, routingResolver.getRouting(),
clazz, index, getRefreshPolicy());
DeleteByQueryResponse response = execute(client -> client.deleteByQuery(request));
return responseConverter.byQueryResponse(response);
}
@Override
public UpdateResponse update(UpdateQuery updateQuery, IndexCoordinates index) {
@@ -437,13 +455,10 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
Assert.notNull(queries, "queries must not be null");
Assert.notNull(clazz, "clazz must not be null");
List<MultiSearchQueryParameter> multiSearchQueryParameters = new ArrayList<>(queries.size());
for (Query query : queries) {
multiSearchQueryParameters.add(new MultiSearchQueryParameter(query, clazz, getIndexCoordinatesFor(clazz)));
}
int size = queries.size();
// noinspection unchecked
return doMultiSearch(multiSearchQueryParameters).stream().map(searchHits -> (SearchHits<T>) searchHits)
return multiSearch(queries, Collections.nCopies(size, clazz), Collections.nCopies(size, index))
.stream().map(searchHits -> (SearchHits<T>) searchHits)
.collect(Collectors.toList());
}
@@ -454,14 +469,7 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
Assert.notNull(classes, "classes must not be null");
Assert.isTrue(queries.size() == classes.size(), "queries and classes must have the same size");
List<MultiSearchQueryParameter> multiSearchQueryParameters = new ArrayList<>(queries.size());
Iterator<Class<?>> it = classes.iterator();
for (Query query : queries) {
Class<?> clazz = it.next();
multiSearchQueryParameters.add(new MultiSearchQueryParameter(query, clazz, getIndexCoordinatesFor(clazz)));
}
return doMultiSearch(multiSearchQueryParameters);
return multiSearch(queries, classes, classes.stream().map(this::getIndexCoordinatesFor).toList());
}
@Override
@@ -473,14 +481,7 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
Assert.notNull(index, "index must not be null");
Assert.isTrue(queries.size() == classes.size(), "queries and classes must have the same size");
List<MultiSearchQueryParameter> multiSearchQueryParameters = new ArrayList<>(queries.size());
Iterator<Class<?>> it = classes.iterator();
for (Query query : queries) {
Class<?> clazz = it.next();
multiSearchQueryParameters.add(new MultiSearchQueryParameter(query, clazz, index));
}
return doMultiSearch(multiSearchQueryParameters);
return multiSearch(queries, classes, Collections.nCopies(queries.size(), index));
}
@Override
@@ -497,16 +498,50 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
Iterator<Class<?>> it = classes.iterator();
Iterator<IndexCoordinates> indexesIt = indexes.iterator();
Assert.isTrue(!queries.isEmpty(), "queries should have at least 1 query");
boolean isSearchTemplateQuery = queries.get(0) instanceof SearchTemplateQuery;
for (Query query : queries) {
Assert.isTrue((query instanceof SearchTemplateQuery) == isSearchTemplateQuery,
"SearchTemplateQuery can't be mixed with other types of query in multiple search");
Class<?> clazz = it.next();
IndexCoordinates index = indexesIt.next();
multiSearchQueryParameters.add(new MultiSearchQueryParameter(query, clazz, index));
}
return doMultiSearch(multiSearchQueryParameters);
return multiSearch(multiSearchQueryParameters, isSearchTemplateQuery);
}
private List<SearchHits<?>> multiSearch(List<MultiSearchQueryParameter> multiSearchQueryParameters,
boolean isSearchTemplateQuery) {
return isSearchTemplateQuery ? doMultiTemplateSearch(multiSearchQueryParameters.stream()
.map(p -> new MultiSearchTemplateQueryParameter((SearchTemplateQuery) p.query, p.clazz, p.index))
.toList())
: doMultiSearch(multiSearchQueryParameters);
}
private List<SearchHits<?>> doMultiTemplateSearch(
List<MultiSearchTemplateQueryParameter> mSearchTemplateQueryParameters) {
MsearchTemplateRequest request = requestConverter.searchMsearchTemplateRequest(mSearchTemplateQueryParameters,
routingResolver.getRouting());
MsearchTemplateResponse<EntityAsMap> response = execute(
client -> client.msearchTemplate(request, EntityAsMap.class));
List<MultiSearchResponseItem<EntityAsMap>> responseItems = response.responses();
Assert.isTrue(mSearchTemplateQueryParameters.size() == responseItems.size(),
"number of response items does not match number of requests");
int size = mSearchTemplateQueryParameters.size();
List<Class<?>> classes = mSearchTemplateQueryParameters
.stream().map(MultiSearchTemplateQueryParameter::clazz).collect(Collectors.toList());
List<IndexCoordinates> indices = mSearchTemplateQueryParameters
.stream().map(MultiSearchTemplateQueryParameter::index).collect(Collectors.toList());
return getSearchHitsFromMsearchResponse(size, classes, indices, responseItems);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private List<SearchHits<?>> doMultiSearch(List<MultiSearchQueryParameter> multiSearchQueryParameters) {
MsearchRequest request = requestConverter.searchMsearchRequest(multiSearchQueryParameters,
@@ -518,22 +553,37 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
Assert.isTrue(multiSearchQueryParameters.size() == responseItems.size(),
"number of response items does not match number of requests");
List<SearchHits<?>> searchHitsList = new ArrayList<>(multiSearchQueryParameters.size());
int size = multiSearchQueryParameters.size();
List<Class<?>> classes = multiSearchQueryParameters
.stream().map(MultiSearchQueryParameter::clazz).collect(Collectors.toList());
List<IndexCoordinates> indices = multiSearchQueryParameters
.stream().map(MultiSearchQueryParameter::index).collect(Collectors.toList());
Iterator<MultiSearchQueryParameter> queryIterator = multiSearchQueryParameters.iterator();
return getSearchHitsFromMsearchResponse(size, classes, indices, responseItems);
}
/**
* {@link MsearchResponse} and {@link MsearchTemplateResponse} share the same {@link MultiSearchResponseItem}
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private List<SearchHits<?>> getSearchHitsFromMsearchResponse(int size, List<Class<?>> classes,
List<IndexCoordinates> indices, List<MultiSearchResponseItem<EntityAsMap>> responseItems) {
List<SearchHits<?>> searchHitsList = new ArrayList<>(size);
Iterator<Class<?>> clazzIter = classes.iterator();
Iterator<IndexCoordinates> indexIter = indices.iterator();
Iterator<MultiSearchResponseItem<EntityAsMap>> responseIterator = responseItems.iterator();
while (queryIterator.hasNext()) {
MultiSearchQueryParameter queryParameter = queryIterator.next();
while (clazzIter.hasNext() && indexIter.hasNext()) {
MultiSearchResponseItem<EntityAsMap> responseItem = responseIterator.next();
if (responseItem.isResult()) {
Class clazz = queryParameter.clazz;
Class clazz = clazzIter.next();
IndexCoordinates index = indexIter.next();
ReadDocumentCallback<?> documentCallback = new ReadDocumentCallback<>(elasticsearchConverter, clazz,
queryParameter.index);
index);
SearchDocumentResponseCallback<SearchHits<?>> callback = new ReadSearchDocumentResponseCallback<>(clazz,
queryParameter.index);
index);
SearchHits<?> searchHits = callback.doWith(
SearchDocumentResponseBuilder.from(responseItem.result(), getEntityCreator(documentCallback), jsonpMapper));
@@ -541,8 +591,8 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
searchHitsList.add(searchHits);
} else {
if (LOGGER.isWarnEnabled()) {
LOGGER
.warn(String.format("multisearch responsecontains failure: {}", responseItem.failure().error().reason()));
LOGGER.warn(String.format("multisearch response contains failure: %s",
responseItem.failure().error().reason()));
}
}
}
@@ -556,6 +606,12 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
record MultiSearchQueryParameter(Query query, Class<?> clazz, IndexCoordinates index) {
}
/**
* value class combining the information needed for a single query in a template multisearch request.
*/
record MultiSearchTemplateQueryParameter(SearchTemplateQuery query, Class<?> clazz, IndexCoordinates index) {
}
@Override
public String openPointInTime(IndexCoordinates index, Duration keepAlive, Boolean ignoreUnavailable) {
@@ -606,6 +662,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
@@ -35,14 +35,17 @@ import org.springframework.util.StringUtils;
* {@link co.elastic.clients.elasticsearch.core.search.Highlight}.
*
* @author Peter-Josef Meisch
* @author Haibo Liu
* @since 4.4
*/
class HighlightQueryBuilder {
private final MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext;
private final RequestConverter requestConverter;
HighlightQueryBuilder(
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext) {
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext, RequestConverter requestConverter) {
this.mappingContext = mappingContext;
this.requestConverter = requestConverter;
}
public co.elastic.clients.elasticsearch.core.search.Highlight getHighlight(Highlight highlight,
@@ -52,7 +55,7 @@ class HighlightQueryBuilder {
// in the old implementation we could use one addParameters method, but in the new Elasticsearch client
// the builder for highlight and highlightfield share no code
addParameters(highlight.getParameters(), highlightBuilder);
addParameters(highlight.getParameters(), highlightBuilder, type);
for (HighlightField highlightField : highlight.getFields()) {
String mappedName = mapFieldName(highlightField.getName(), type);
@@ -69,7 +72,7 @@ class HighlightQueryBuilder {
* the builder for highlight and highlight fields don't share code, so we have these two methods here that basically are almost copies
*/
private void addParameters(HighlightParameters parameters,
co.elastic.clients.elasticsearch.core.search.Highlight.Builder builder) {
co.elastic.clients.elasticsearch.core.search.Highlight.Builder builder, @Nullable Class<?> type) {
if (StringUtils.hasLength(parameters.getBoundaryChars())) {
builder.boundaryChars(parameters.getBoundaryChars());
@@ -103,6 +106,10 @@ class HighlightQueryBuilder {
builder.numberOfFragments(parameters.getNumberOfFragments());
}
if (parameters.getHighlightQuery() != null) {
builder.highlightQuery(requestConverter.getQuery(parameters.getHighlightQuery(), type));
}
if (StringUtils.hasLength(parameters.getOrder())) {
builder.order(highlighterOrder(parameters.getOrder()));
}
@@ -174,6 +181,10 @@ class HighlightQueryBuilder {
builder.numberOfFragments(parameters.getNumberOfFragments());
}
if (parameters.getHighlightQuery() != null) {
builder.highlightQuery(requestConverter.getQuery(parameters.getHighlightQuery(), type));
}
if (StringUtils.hasLength(parameters.getOrder())) {
builder.order(highlighterOrder(parameters.getOrder()));
}
@@ -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
}
@@ -19,7 +19,6 @@ import co.elastic.clients.json.JsonpMapper;
import jakarta.json.stream.JsonGenerator;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import org.apache.commons.logging.Log;
@@ -44,17 +43,13 @@ final class JsonUtils {
mapper.serialize(object, generator);
generator.close();
String json = "{}";
try {
json = baos.toString("UTF-8");
} catch (UnsupportedEncodingException e) {
LOGGER.warn("could not read json", e);
}
json = baos.toString(StandardCharsets.UTF_8);
return json;
}
@Nullable
public static String queryToJson(@Nullable co.elastic.clients.elasticsearch._types.query_dsl.Query query, JsonpMapper mapper) {
public static String queryToJson(@Nullable co.elastic.clients.elasticsearch._types.query_dsl.Query query,
JsonpMapper mapper) {
if (query == null) {
return null;
@@ -15,7 +15,7 @@
*/
package org.springframework.data.elasticsearch.client.elc;
import co.elastic.clients.elasticsearch._types.KnnQuery;
import co.elastic.clients.elasticsearch._types.KnnSearch;
import co.elastic.clients.elasticsearch._types.SortOptions;
import co.elastic.clients.elasticsearch._types.aggregations.Aggregation;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
@@ -29,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;
@@ -39,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 {
@@ -53,7 +53,7 @@ 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) {
super(builder);
@@ -70,7 +70,7 @@ public class NativeQuery extends BaseQuery {
"Cannot add an NativeQuery in a NativeQuery");
}
this.springDataQuery = builder.getSpringDataQuery();
this.knnQuery = builder.getKnnQuery();
this.knnSearches = builder.getKnnSearches();
}
public NativeQuery(@Nullable Query query) {
@@ -122,11 +122,11 @@ public class NativeQuery extends BaseQuery {
}
/**
* @since 5.1
* @since 5.3.1
*/
@Nullable
public KnnQuery getKnnQuery() {
return knnQuery;
public List<KnnSearch> getKnnSearches() {
return knnSearches;
}
@Nullable
@@ -16,6 +16,7 @@
package org.springframework.data.elasticsearch.client.elc;
import co.elastic.clients.elasticsearch._types.KnnQuery;
import co.elastic.clients.elasticsearch._types.KnnSearch;
import co.elastic.clients.elasticsearch._types.SortOptions;
import co.elastic.clients.elasticsearch._types.aggregations.Aggregation;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
@@ -26,6 +27,7 @@ import co.elastic.clients.util.ObjectBuilder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -38,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> {
@@ -47,11 +50,12 @@ public class NativeQueryBuilder extends BaseQueryBuilder<NativeQuery, NativeQuer
private final Map<String, Aggregation> aggregations = new LinkedHashMap<>();
@Nullable private Suggester suggester;
@Nullable private FieldCollapse fieldCollapse;
private List<SortOptions> sortOptions = new ArrayList<>();
private Map<String, JsonData> searchExtensions = new LinkedHashMap<>();
private final List<SortOptions> sortOptions = new ArrayList<>();
private final Map<String, JsonData> searchExtensions = new LinkedHashMap<>();
@Nullable private org.springframework.data.elasticsearch.core.query.Query springDataQuery;
@Nullable private KnnQuery knnQuery;
@Nullable private List<KnnSearch> knnSearches = Collections.emptyList();
public NativeQueryBuilder() {}
@@ -92,6 +96,14 @@ public class NativeQueryBuilder extends BaseQueryBuilder<NativeQuery, NativeQuer
return knnQuery;
}
/**
* @since 5.3.1
*/
@Nullable
public List<KnnSearch> getKnnSearches() {
return knnSearches;
}
@Nullable
public org.springframework.data.elasticsearch.core.query.Query getSpringDataQuery() {
return springDataQuery;
@@ -202,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);
@@ -1,174 +0,0 @@
/*
* Copyright 2022-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.client.elc;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.LatLonGeoLocation;
import co.elastic.clients.elasticsearch._types.query_dsl.IdsQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.MatchAllQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.MatchQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.Operator;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch._types.query_dsl.QueryStringQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.TermQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.WildcardQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.WrapperQuery;
import co.elastic.clients.util.ObjectBuilder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import java.util.function.Function;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Utility class simplifying the creation of some more complex queries and type.
*
* @author Peter-Josef Meisch
* @since 4.4
* @deprecated since 5.1, use {@link Queries} instead.
*/
@Deprecated(forRemoval = true)
public final class QueryBuilders {
private QueryBuilders() {}
public static IdsQuery idsQuery(List<String> ids) {
Assert.notNull(ids, "ids must not be null");
return IdsQuery.of(i -> i.values(ids));
}
public static Query idsQueryAsQuery(List<String> ids) {
Assert.notNull(ids, "ids must not be null");
Function<Query.Builder, ObjectBuilder<Query>> builder = b -> b.ids(idsQuery(ids));
return builder.apply(new Query.Builder()).build();
}
public static MatchQuery matchQuery(String fieldName, String query, @Nullable Operator operator,
@Nullable Float boost) {
Assert.notNull(fieldName, "fieldName must not be null");
Assert.notNull(query, "query must not be null");
return MatchQuery.of(mb -> mb.field(fieldName).query(FieldValue.of(query)).operator(operator).boost(boost));
}
public static Query matchQueryAsQuery(String fieldName, String query, @Nullable Operator operator,
@Nullable Float boost) {
Function<Query.Builder, ObjectBuilder<Query>> builder = b -> b.match(matchQuery(fieldName, query, operator, boost));
return builder.apply(new Query.Builder()).build();
}
public static MatchAllQuery matchAllQuery() {
return MatchAllQuery.of(b -> b);
}
public static Query matchAllQueryAsQuery() {
Function<Query.Builder, ObjectBuilder<Query>> builder = b -> b.matchAll(matchAllQuery());
return builder.apply(new Query.Builder()).build();
}
public static QueryStringQuery queryStringQuery(String fieldName, String query, @Nullable Float boost) {
return queryStringQuery(fieldName, query, null, null, boost);
}
public static QueryStringQuery queryStringQuery(String fieldName, String query, Operator defaultOperator,
@Nullable Float boost) {
return queryStringQuery(fieldName, query, null, defaultOperator, boost);
}
public static QueryStringQuery queryStringQuery(String fieldName, String query, @Nullable Boolean analyzeWildcard,
@Nullable Float boost) {
return queryStringQuery(fieldName, query, analyzeWildcard, null, boost);
}
public static QueryStringQuery queryStringQuery(String fieldName, String query, @Nullable Boolean analyzeWildcard,
@Nullable Operator defaultOperator, @Nullable Float boost) {
Assert.notNull(fieldName, "fieldName must not be null");
Assert.notNull(query, "query must not be null");
return QueryStringQuery.of(qs -> qs.fields(fieldName).query(query).analyzeWildcard(analyzeWildcard)
.defaultOperator(defaultOperator).boost(boost));
}
public static TermQuery termQuery(String fieldName, String value) {
Assert.notNull(fieldName, "fieldName must not be null");
Assert.notNull(value, "value must not be null");
return TermQuery.of(t -> t.field(fieldName).value(FieldValue.of(value)));
}
public static Query termQueryAsQuery(String fieldName, String value) {
Function<Query.Builder, ObjectBuilder<Query>> builder = q -> q.term(termQuery(fieldName, value));
return builder.apply(new Query.Builder()).build();
}
public static WildcardQuery wildcardQuery(String field, String value) {
Assert.notNull(field, "field must not be null");
Assert.notNull(value, "value must not be null");
return WildcardQuery.of(w -> w.field(field).wildcard(value));
}
public static Query wildcardQueryAsQuery(String field, String value) {
Function<Query.Builder, ObjectBuilder<Query>> builder = q -> q.wildcard(wildcardQuery(field, value));
return builder.apply(new Query.Builder()).build();
}
public static Query wrapperQueryAsQuery(String query) {
Function<Query.Builder, ObjectBuilder<Query>> builder = q -> q.wrapper(wrapperQuery(query));
return builder.apply(new Query.Builder()).build();
}
public static WrapperQuery wrapperQuery(String query) {
Assert.notNull(query, "query must not be null");
String encodedValue = Base64.getEncoder().encodeToString(query.getBytes(StandardCharsets.UTF_8));
return WrapperQuery.of(wq -> wq.query(encodedValue));
}
public static LatLonGeoLocation latLon(GeoPoint geoPoint) {
Assert.notNull(geoPoint, "geoPoint must not be null");
return latLon(geoPoint.getLat(), geoPoint.getLon());
}
public static LatLonGeoLocation latLon(double lat, double lon) {
return LatLonGeoLocation.of(_0 -> _0.lat(lat).lon(lon));
}
}
@@ -36,6 +36,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>
@@ -69,6 +70,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 +127,7 @@ 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 +146,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 +172,7 @@ 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 +228,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 +303,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));
}
@@ -0,0 +1,72 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.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,17 +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.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;
@@ -79,6 +74,7 @@ import org.springframework.util.StringUtils;
*
* @author Peter-Josef Meisch
* @author Illia Ulianov
* @author Junghoon Ban
* @since 4.4
*/
public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearchTemplate {
@@ -86,6 +82,7 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
private static final Log LOGGER = LogFactory.getLog(ReactiveElasticsearchTemplate.class);
private final ReactiveElasticsearchClient client;
private final ReactiveElasticsearchSqlClient sqlClient;
private final RequestConverter requestConverter;
private final ResponseConverter responseConverter;
private final JsonpMapper jsonpMapper;
@@ -97,6 +94,7 @@ public 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);
@@ -179,6 +177,15 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
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");
DeleteByQueryRequest request = requestConverter.documentDeleteByQueryRequest(query, routingResolver.getRouting(),
entityType, index, getRefreshPolicy());
return Mono.from(execute(client -> client.deleteByQuery(request))).map(responseConverter::byQueryResponse);
}
@Override
public <T> Mono<T> get(String id, Class<T> entityType, IndexCoordinates index) {
@@ -384,7 +391,28 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
Function<PitSearchAfter, Publisher<? extends ResponseBody<EntityAsMap>>> resourceClosure = psa -> {
baseQuery.setPointInTime(new Query.PointInTime(psa.getPit(), pitKeepAlive));
baseQuery.addSort(Sort.by("_shard_doc"));
// only add _shard_doc if there is not a field_collapse and a sort with the same name
boolean addShardDoc = true;
if (query instanceof NativeQuery nativeQuery && nativeQuery.getFieldCollapse() != null) {
var field = nativeQuery.getFieldCollapse().field();
if (nativeQuery.getSortOptions().stream()
.anyMatch(sortOptions -> sortOptions.isField() && sortOptions.field().field().equals(field))) {
addShardDoc = false;
}
if (query.getSort() != null
&& query.getSort().stream().anyMatch(order -> order.getProperty().equals(field))) {
addShardDoc = false;
}
}
if (addShardDoc) {
baseQuery.addSort(Sort.by("_shard_doc"));
}
SearchRequest firstSearchRequest = requestConverter.searchRequest(baseQuery, routingResolver.getRouting(),
clazz, index, false, true);
@@ -614,6 +642,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}.
@@ -645,7 +681,7 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
*/
private RuntimeException translateException(Throwable throwable) {
RuntimeException runtimeException = throwable instanceof RuntimeException ? (RuntimeException) throwable
RuntimeException runtimeException = throwable instanceof RuntimeException ex ? ex
: new RuntimeException(throwable.getMessage(), throwable);
RuntimeException potentiallyTranslatedException = exceptionTranslator
.translateExceptionIfPossible(runtimeException);
@@ -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;
@@ -44,6 +43,7 @@ import co.elastic.clients.elasticsearch.core.bulk.IndexOperation;
import co.elastic.clients.elasticsearch.core.bulk.UpdateOperation;
import co.elastic.clients.elasticsearch.core.mget.MultiGetOperation;
import co.elastic.clients.elasticsearch.core.msearch.MultisearchBody;
import co.elastic.clients.elasticsearch.core.msearch.MultisearchHeader;
import co.elastic.clients.elasticsearch.core.search.Highlight;
import co.elastic.clients.elasticsearch.core.search.Rescore;
import co.elastic.clients.elasticsearch.core.search.SourceConfig;
@@ -54,6 +54,7 @@ import co.elastic.clients.elasticsearch.indices.update_aliases.Action;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.json.JsonpDeserializer;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.util.ObjectBuilder;
import jakarta.json.stream.JsonParser;
import java.io.ByteArrayInputStream;
@@ -66,6 +67,8 @@ 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;
@@ -85,6 +88,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;
@@ -105,10 +110,10 @@ import org.springframework.util.StringUtils;
* @author Sascha Woo
* @author cdalxndr
* @author scoobyzhang
* @author Haibo Liu
* @since 4.4
*/
@SuppressWarnings("ClassCanBeRecord")
class RequestConverter {
class RequestConverter extends AbstractQueryProcessor {
private static final Log LOGGER = LogFactory.getLog(RequestConverter.class);
@@ -153,7 +158,6 @@ class RequestConverter {
aliasActions.getActions().forEach(aliasAction -> {
if (aliasAction instanceof AliasAction.Add add) {
var parameters = add.getParameters();
// noinspection DuplicatedCode
String[] parametersAliases = parameters.getAliases();
if (parametersAliases != null) {
for (String aliasName : parametersAliases) {
@@ -167,9 +171,9 @@ class RequestConverter {
}));
}
private Alias.Builder buildAlias(AliasActionParameters parameters, Alias.Builder aliasBuilder) {
private co.elastic.clients.elasticsearch.indices.Alias.Builder buildAlias(AliasActionParameters parameters,
co.elastic.clients.elasticsearch.indices.Alias.Builder aliasBuilder) {
// noinspection DuplicatedCode
if (parameters.getRouting() != null) {
aliasBuilder.routing(parameters.getRouting());
}
@@ -232,17 +236,25 @@ class RequestConverter {
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();
}
@@ -396,11 +408,8 @@ class RequestConverter {
.order(putTemplateRequest.getOrder());
if (putTemplateRequest.getSettings() != null) {
Function<Map.Entry<String, Object>, String> keyMapper = Map.Entry::getKey;
Function<Map.Entry<String, Object>, JsonData> valueMapper = entry -> JsonData.of(entry.getValue(), jsonpMapper);
Map<String, JsonData> settings = putTemplateRequest.getSettings().entrySet().stream()
.collect(Collectors.toMap(keyMapper, valueMapper));
builder.settings(settings);
Map<String, JsonData> settings = getTemplateParams(putTemplateRequest.getSettings().entrySet());
builder.settings(sb -> sb.otherSettings(settings));
}
if (putTemplateRequest.getMappings() != null) {
@@ -415,7 +424,6 @@ class RequestConverter {
if (aliasActions != null) {
aliasActions.getActions().forEach(aliasAction -> {
AliasActionParameters parameters = aliasAction.getParameters();
// noinspection DuplicatedCode
String[] parametersAliases = parameters.getAliases();
if (parametersAliases != null) {
@@ -449,7 +457,6 @@ class RequestConverter {
aliasActions.getActions().forEach(aliasAction -> {
if (aliasAction instanceof AliasAction.Add add) {
var parameters = add.getParameters();
// noinspection DuplicatedCode
String[] parametersAliases = parameters.getAliases();
if (parametersAliases != null) {
for (String aliasName : parametersAliases) {
@@ -523,6 +530,22 @@ class RequestConverter {
.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("json");
return sqb;
});
}
// endregion
// region documents
@@ -638,7 +661,7 @@ class RequestConverter {
Object queryObject = query.getObject();
if (queryObject != null) {
builder
builder
.id(StringUtils.hasText(query.getId()) ? query.getId() : getPersistentEntityId(queryObject))
.document(elasticsearchConverter.mapObject(queryObject));
} else if (query.getSource() != null) {
@@ -720,16 +743,12 @@ class RequestConverter {
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;
});
@@ -901,7 +920,9 @@ class RequestConverter {
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())) //
@@ -967,6 +988,81 @@ class RequestConverter {
});
}
public DeleteByQueryRequest documentDeleteByQueryRequest(DeleteQuery query, @Nullable String routing, Class<?> clazz,
IndexCoordinates index, @Nullable RefreshPolicy refreshPolicy) {
Assert.notNull(query, "query must not be null");
Assert.notNull(index, "index must not be null");
return DeleteByQueryRequest.of(dqb -> {
dqb.index(Arrays.asList(index.getIndexNames())) //
.query(getQuery(query.getQuery(), clazz))//
.refresh(deleteByQueryRefresh(refreshPolicy))
.requestsPerSecond(query.getRequestsPerSecond())
.maxDocs(query.getMaxDocs())
.scroll(time(query.getScroll()))
.scrollSize(query.getScrollSize());
if (query.getRouting() != null) {
dqb.routing(query.getRouting());
} else if (StringUtils.hasText(routing)) {
dqb.routing(routing);
}
if (query.getQ() != null) {
dqb.q(query.getQ())
.analyzer(query.getAnalyzer())
.analyzeWildcard(query.getAnalyzeWildcard())
.defaultOperator(operator(query.getDefaultOperator()))
.df(query.getDf())
.lenient(query.getLenient());
}
if (query.getExpandWildcards() != null && !query.getExpandWildcards().isEmpty()) {
dqb.expandWildcards(expandWildcards(query.getExpandWildcards()));
}
if (query.getStats() != null && !query.getStats().isEmpty()) {
dqb.stats(query.getStats());
}
if (query.getSlices() != null) {
dqb.slices(sb -> sb.value(query.getSlices()));
}
if (query.getSort() != null) {
ElasticsearchPersistentEntity<?> persistentEntity = getPersistentEntity(clazz);
List<SortOptions> sortOptions = getSortOptions(query.getSort(), persistentEntity);
if (!sortOptions.isEmpty()) {
dqb.sort(
sortOptions.stream()
.map(sortOption -> {
String order = "asc";
var sortField = sortOption.field();
if (sortField.order() != null) {
order = sortField.order().jsonValue();
}
return sortField.field() + ':' + order;
})
.collect(Collectors.toList()));
}
}
if (query.getRefresh() != null) {
dqb.refresh(query.getRefresh());
}
dqb.allowNoIndices(query.getAllowNoIndices())
.conflicts(conflicts(query.getConflicts()))
.ignoreUnavailable(query.getIgnoreUnavailable())
.preference(query.getPreference())
.requestCache(query.getRequestCache())
.searchType(searchType(query.getSearchType()))
.searchTimeout(time(query.getSearchTimeout()))
.terminateAfter(query.getTerminateAfter())
.timeout(time(query.getTimeout()))
.version(query.getVersion());
return dqb;
});
}
public UpdateRequest<Document, ?> documentUpdateRequest(UpdateQuery query, IndexCoordinates index,
@Nullable RefreshPolicy refreshPolicy, @Nullable String routing) {
@@ -982,21 +1078,15 @@ class RequestConverter {
}
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 //
@@ -1141,11 +1231,40 @@ class RequestConverter {
builder.routing(routing);
}
addFilter(query, builder);
addPostFilter(query, builder);
return builder.build();
}
public MsearchTemplateRequest searchMsearchTemplateRequest(
List<ElasticsearchTemplate.MultiSearchTemplateQueryParameter> multiSearchTemplateQueryParameters,
@Nullable String routing) {
// basically the same stuff as in template search
return MsearchTemplateRequest.of(mtrb -> {
multiSearchTemplateQueryParameters.forEach(param -> {
var query = param.query();
mtrb.searchTemplates(stb -> stb
.header(msearchHeaderBuilder(query, param.index(), routing))
.body(bb -> {
bb //
.explain(query.getExplain()) //
.id(query.getId()) //
.source(query.getSource()) //
;
if (!CollectionUtils.isEmpty(query.getParams())) {
Map<String, JsonData> params = getTemplateParams(query.getParams().entrySet());
bb.params(params);
}
return bb;
}));
});
return mtrb;
});
}
public MsearchRequest searchMsearchRequest(
List<ElasticsearchTemplate.MultiSearchQueryParameter> multiSearchQueryParameters, @Nullable String routing) {
@@ -1157,28 +1276,7 @@ class RequestConverter {
var query = param.query();
mrb.searches(sb -> sb //
.header(h -> {
var searchType = (query instanceof NativeQuery nativeQuery && nativeQuery.getKnnQuery() != null) ? null
: searchType(query.getSearchType());
h //
.index(Arrays.asList(param.index().getIndexNames())) //
.searchType(searchType) //
.requestCache(query.getRequestCache()) //
;
if (StringUtils.hasText(query.getRoute())) {
h.routing(query.getRoute());
} else if (StringUtils.hasText(routing)) {
h.routing(routing);
}
if (query.getPreference() != null) {
h.preference(query.getPreference());
}
return h;
}) //
.header(msearchHeaderBuilder(query, param.index(), routing)) //
.body(bb -> {
bb //
.query(getQuery(query, param.clazz()))//
@@ -1243,17 +1341,16 @@ class RequestConverter {
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);
@@ -1284,6 +1381,35 @@ class RequestConverter {
});
}
/**
* {@link MsearchRequest} and {@link MsearchTemplateRequest} share the same {@link MultisearchHeader}
*/
private Function<MultisearchHeader.Builder, ObjectBuilder<MultisearchHeader>> msearchHeaderBuilder(Query query,
IndexCoordinates index, @Nullable String routing) {
return h -> {
var searchType = (query instanceof NativeQuery nativeQuery && !isEmpty(nativeQuery.getKnnSearches())) ? null
: searchType(query.getSearchType());
h //
.index(Arrays.asList(index.getIndexNames())) //
.searchType(searchType) //
.requestCache(query.getRequestCache()) //
;
if (StringUtils.hasText(query.getRoute())) {
h.routing(query.getRoute());
} else if (StringUtils.hasText(routing)) {
h.routing(routing);
}
if (query.getPreference() != null) {
h.preference(query.getPreference());
}
return h;
};
}
private <T> void prepareSearchRequest(Query query, @Nullable String routing, @Nullable Class<T> clazz,
IndexCoordinates indexCoordinates, SearchRequest.Builder builder, boolean forCount, boolean forBatchedSearch) {
@@ -1293,7 +1419,7 @@ class RequestConverter {
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 //
@@ -1371,8 +1497,8 @@ class RequestConverter {
if (query instanceof NativeQuery nativeQuery) {
prepareNativeSearch(nativeQuery, builder);
}
// query.getSort() must be checked after prepareNativeSearch as this already might hav a sort set that must have
// higher priority
// query.getSort() must be checked after prepareNativeSearch as this already might have a sort set
// that must have higher priority
if (query.getSort() != null) {
List<SortOptions> sortOptions = getSortOptions(query.getSort(), persistentEntity);
@@ -1394,7 +1520,15 @@ class RequestConverter {
}
if (!isEmpty(query.getSearchAfter())) {
builder.searchAfter(query.getSearchAfter().stream().map(TypeUtils::toFieldValue).toList());
var fieldValues = query.getSearchAfter().stream().map(TypeUtils::toFieldValue).toList();
// when there is a field collapse on a native query, and we have a search_after, then the search_after
// must only have one entry
if (query instanceof NativeQuery nativeQuery && nativeQuery.getFieldCollapse() != null) {
builder.searchAfter(fieldValues.get(0));
} else {
builder.searchAfter(fieldValues);
}
}
query.getRescorerQueries().forEach(rescorerQuery -> builder.rescore(getRescore(rescorerQuery)));
@@ -1407,16 +1541,14 @@ class RequestConverter {
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;
@@ -1495,7 +1627,7 @@ class RequestConverter {
private void addHighlight(Query query, SearchRequest.Builder builder) {
Highlight highlight = query.getHighlightQuery()
.map(highlightQuery -> new HighlightQueryBuilder(elasticsearchConverter.getMappingContext())
.map(highlightQuery -> new HighlightQueryBuilder(elasticsearchConverter.getMappingContext(), this)
.getHighlight(highlightQuery.getHighlight(), highlightQuery.getType()))
.orElse(null);
@@ -1505,7 +1637,7 @@ class RequestConverter {
private void addHighlight(Query query, MultisearchBody.Builder builder) {
Highlight highlight = query.getHighlightQuery()
.map(highlightQuery -> new HighlightQueryBuilder(elasticsearchConverter.getMappingContext())
.map(highlightQuery -> new HighlightQueryBuilder(elasticsearchConverter.getMappingContext(), this)
.getHighlight(highlightQuery.getHighlight(), highlightQuery.getType()))
.orElse(null);
@@ -1612,8 +1744,8 @@ class RequestConverter {
.sort(query.getSortOptions()) //
;
if (query.getKnnQuery() != null) {
builder.knn(query.getKnnQuery());
if (!isEmpty(query.getKnnSearches())) {
builder.knn(query.getKnnSearches());
}
if (!isEmpty(query.getAggregations())) {
@@ -1633,8 +1765,8 @@ class RequestConverter {
.collapse(query.getFieldCollapse()) //
.sort(query.getSortOptions());
if (query.getKnnQuery() != null) {
builder.knn(query.getKnnQuery());
if (!isEmpty(query.getKnnSearches())) {
builder.knn(query.getKnnSearches());
}
if (!isEmpty(query.getAggregations())) {
@@ -1647,51 +1779,22 @@ class RequestConverter {
}
@Nullable
private co.elastic.clients.elasticsearch._types.query_dsl.Query getQuery(@Nullable Query query,
co.elastic.clients.elasticsearch._types.query_dsl.Query getQuery(@Nullable Query query,
@Nullable Class<?> clazz) {
if (query == null) {
return null;
}
elasticsearchConverter.updateQuery(query, clazz);
co.elastic.clients.elasticsearch._types.query_dsl.Query esQuery = null;
if (query instanceof CriteriaQuery) {
esQuery = CriteriaQueryProcessor.createQuery(((CriteriaQuery) query).getCriteria());
} else if (query instanceof StringQuery) {
esQuery = Queries.wrapperQueryAsQuery(((StringQuery) query).getSource());
} else if (query instanceof NativeQuery nativeQuery) {
if (nativeQuery.getQuery() != null) {
esQuery = nativeQuery.getQuery();
} else if (nativeQuery.getSpringDataQuery() != null) {
esQuery = getQuery(nativeQuery.getSpringDataQuery(), clazz);
}
} else {
throw new IllegalArgumentException("unhandled Query implementation " + query.getClass().getName());
}
return esQuery;
return getEsQuery(query, (q) -> elasticsearchConverter.updateQuery(q, clazz));
}
private void addFilter(Query query, SearchRequest.Builder builder) {
private void addPostFilter(Query query, SearchRequest.Builder builder) {
if (query instanceof CriteriaQuery) {
CriteriaFilterProcessor.createQuery(((CriteriaQuery) query).getCriteria()).ifPresent(builder::postFilter);
} else // noinspection StatementWithEmptyBody
if (query instanceof StringQuery) {
// no filter for StringQuery
} else if (query instanceof NativeQuery nativeQuery) {
// we only need to handle NativeQuery here. filter from a CriteriaQuery are added into the query and not as post
// filter anymore, StringQuery do not have post filters
if (query instanceof NativeQuery nativeQuery) {
if (nativeQuery.getFilter() != null) {
builder.postFilter(nativeQuery.getFilter());
} else if (nativeQuery.getSpringDataQuery() != null) {
addFilter(nativeQuery.getSpringDataQuery(), builder);
addPostFilter(nativeQuery.getSpringDataQuery(), builder);
}
} else {
throw new IllegalArgumentException("unhandled Query implementation " + query.getClass().getName());
}
}
@@ -1775,7 +1878,8 @@ class RequestConverter {
.id(query.getId()) //
.index(Arrays.asList(index.getIndexNames())) //
.preference(query.getPreference()) //
.searchType(searchType(query.getSearchType())).source(query.getSource()) //
.searchType(searchType(query.getSearchType())) //
.source(query.getSource()) //
;
if (query.getRoute() != null) {
@@ -1794,10 +1898,7 @@ class RequestConverter {
}
if (!CollectionUtils.isEmpty(query.getParams())) {
Function<Map.Entry<String, Object>, String> keyMapper = Map.Entry::getKey;
Function<Map.Entry<String, Object>, JsonData> valueMapper = entry -> JsonData.of(entry.getValue(), jsonpMapper);
Map<String, JsonData> params = query.getParams().entrySet().stream()
.collect(Collectors.toMap(keyMapper, valueMapper));
Map<String, JsonData> params = getTemplateParams(query.getParams().entrySet());
builder.params(params);
}
@@ -1805,6 +1906,14 @@ class RequestConverter {
});
}
@NotNull
private Map<String, JsonData> getTemplateParams(Set<Map.Entry<String, Object>> query) {
Function<Map.Entry<String, Object>, String> keyMapper = Map.Entry::getKey;
Function<Map.Entry<String, Object>, JsonData> valueMapper = entry -> JsonData.of(entry.getValue(), jsonpMapper);
return query.stream()
.collect(Collectors.toMap(keyMapper, valueMapper));
}
// endregion
public PutScriptRequest scriptPut(Script script) {
@@ -15,9 +15,8 @@
*/
package org.springframework.data.elasticsearch.client.elc;
import static org.springframework.data.elasticsearch.client.elc.JsonUtils.toJson;
import static org.springframework.data.elasticsearch.client.elc.TypeUtils.removePrefixFromJson;
import static org.springframework.data.elasticsearch.client.elc.TypeUtils.typeMapping;
import static org.springframework.data.elasticsearch.client.elc.JsonUtils.*;
import static org.springframework.data.elasticsearch.client.elc.TypeUtils.*;
import co.elastic.clients.elasticsearch._types.BulkIndexByScrollFailure;
import co.elastic.clients.elasticsearch._types.ErrorCause;
@@ -34,9 +33,16 @@ 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.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -47,12 +53,17 @@ import org.springframework.data.elasticsearch.core.IndexInformation;
import org.springframework.data.elasticsearch.core.MultiGetItem;
import org.springframework.data.elasticsearch.core.cluster.ClusterHealth;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.index.*;
import org.springframework.data.elasticsearch.core.index.AliasData;
import org.springframework.data.elasticsearch.core.index.Settings;
import org.springframework.data.elasticsearch.core.index.TemplateData;
import org.springframework.data.elasticsearch.core.index.TemplateResponse;
import org.springframework.data.elasticsearch.core.index.TemplateResponseData;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.ByQueryResponse;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.core.reindex.ReindexResponse;
import org.springframework.data.elasticsearch.core.script.Script;
import org.springframework.data.elasticsearch.core.sql.SqlResponse;
import org.springframework.data.elasticsearch.support.DefaultStringObjectMap;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -121,8 +132,7 @@ class ResponseConverter {
.build();
}
private TemplateResponseData clusterGetComponentTemplateData(
ComponentTemplateSummary componentTemplateSummary) {
private TemplateResponseData clusterGetComponentTemplateData(ComponentTemplateSummary componentTemplateSummary) {
var mapping = typeMapping(componentTemplateSummary.mappings());
var settings = new Settings();
@@ -183,7 +193,7 @@ class ResponseConverter {
Map<String, IndexMappingRecord> mappings = getMappingResponse.result();
if (mappings == null || mappings.size() == 0) {
if (mappings == null || mappings.isEmpty()) {
return Document.create();
}
@@ -326,7 +336,7 @@ class ResponseConverter {
}
private TemplateResponseData indexGetComponentTemplateData(IndexTemplateSummary indexTemplateSummary,
List<String> composedOf) {
List<String> composedOf) {
var mapping = typeMapping(indexTemplateSummary.mappings());
Function<IndexSettings, Settings> indexSettingsToSettings = indexSettings -> {
@@ -529,6 +539,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) {
@@ -541,7 +574,7 @@ class ResponseConverter {
}
@Nullable
private static ElasticsearchErrorCause toErrorCause(@Nullable ErrorCause errorCause) {
static ElasticsearchErrorCause toErrorCause(@Nullable ErrorCause errorCause) {
if (errorCause != null) {
return new ElasticsearchErrorCause( //
@@ -15,6 +15,8 @@
*/
package org.springframework.data.elasticsearch.client.elc;
import co.elastic.clients.elasticsearch._types.ShardFailure;
import co.elastic.clients.elasticsearch._types.ShardStatistics;
import co.elastic.clients.elasticsearch._types.aggregations.Aggregate;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.SearchTemplateResponse;
@@ -27,6 +29,7 @@ import co.elastic.clients.elasticsearch.core.search.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;
@@ -36,6 +39,7 @@ import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.data.elasticsearch.core.SearchShardStatistics;
import org.springframework.data.elasticsearch.core.TotalHitsRelation;
import org.springframework.data.elasticsearch.core.document.SearchDocument;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
@@ -52,6 +56,8 @@ import org.springframework.util.CollectionUtils;
* Factory class to create {@link SearchDocumentResponse} instances.
*
* @author Peter-Josef Meisch
* @author Haibo Liu
* @author Mohamed El Harrougui
* @since 4.4
*/
class SearchDocumentResponseBuilder {
@@ -78,8 +84,11 @@ class SearchDocumentResponseBuilder {
Map<String, Aggregate> aggregations = responseBody.aggregations();
Map<String, List<Suggestion<EntityAsMap>>> suggest = responseBody.suggest();
var pointInTimeId = responseBody.pitId();
var shards = responseBody.shards();
var executionDurationInMillis = responseBody.took();
return from(hitsMetadata, scrollId, pointInTimeId, aggregations, suggest, entityCreator, jsonpMapper);
return from(hitsMetadata, shards, scrollId, pointInTimeId, executionDurationInMillis, aggregations, suggest,
entityCreator, jsonpMapper);
}
/**
@@ -98,13 +107,16 @@ class SearchDocumentResponseBuilder {
Assert.notNull(entityCreator, "entityCreator must not be null");
Assert.notNull(jsonpMapper, "jsonpMapper must not be null");
var shards = response.shards();
var hitsMetadata = response.hits();
var scrollId = response.scrollId();
var aggregations = response.aggregations();
var suggest = response.suggest();
var pointInTimeId = response.pitId();
var executionDurationInMillis = response.took();
return from(hitsMetadata, scrollId, pointInTimeId, aggregations, suggest, entityCreator, jsonpMapper);
return from(hitsMetadata, shards, scrollId, pointInTimeId, executionDurationInMillis, aggregations, suggest,
entityCreator, jsonpMapper);
}
/**
@@ -120,8 +132,8 @@ class SearchDocumentResponseBuilder {
* @param jsonpMapper to map JsonData objects
* @return the {@link SearchDocumentResponse}
*/
public static <T> SearchDocumentResponse from(HitsMetadata<?> hitsMetadata, @Nullable String scrollId,
@Nullable String pointInTimeId, @Nullable Map<String, Aggregate> aggregations,
public static <T> SearchDocumentResponse from(HitsMetadata<?> hitsMetadata, @Nullable ShardStatistics shards,
@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) {
@@ -145,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));
@@ -155,8 +169,18 @@ class SearchDocumentResponseBuilder {
Suggest suggest = suggestFrom(suggestES, entityCreator);
return new SearchDocumentResponse(totalHits, totalHitsRelation, maxScore, scrollId, pointInTimeId, searchDocuments,
aggregationsContainer, suggest);
SearchShardStatistics shardStatistics = shards != null ? shardsFrom(shards) : null;
return new SearchDocumentResponse(totalHits, totalHitsRelation, maxScore, executionDuration, scrollId, pointInTimeId, searchDocuments,
aggregationsContainer, suggest, shardStatistics);
}
private static SearchShardStatistics shardsFrom(ShardStatistics shards) {
List<ShardFailure> failures = shards.failures();
List<SearchShardStatistics.Failure> searchFailures = failures.stream().map(f -> SearchShardStatistics.Failure
.of(f.index(), f.node(), f.status(), f.shard(), null, ResponseConverter.toErrorCause(f.reason()))).toList();
return SearchShardStatistics.of(shards.failed(), shards.successful(), shards.total(), shards.skipped(),
searchFailures);
}
@Nullable
@@ -218,9 +242,8 @@ class SearchDocumentResponseBuilder {
var phraseSuggest = suggestionES.phrase();
var phraseSuggestOptions = phraseSuggest.options();
List<PhraseSuggestion.Entry.Option> options = new ArrayList<>();
phraseSuggestOptions.forEach(optionES -> options
.add(new PhraseSuggestion.Entry.Option(optionES.text(), optionES.highlighted(), optionES.score(),
optionES.collateMatch())));
phraseSuggestOptions.forEach(optionES -> options.add(new PhraseSuggestion.Entry.Option(optionES.text(),
optionES.highlighted(), optionES.score(), optionES.collateMatch())));
entries.add(new PhraseSuggestion.Entry(phraseSuggest.text(), phraseSuggest.offset(), phraseSuggest.length(),
options, null));
});
@@ -18,6 +18,8 @@ package org.springframework.data.elasticsearch.client.elc;
import co.elastic.clients.elasticsearch._types.*;
import co.elastic.clients.elasticsearch._types.mapping.FieldType;
import co.elastic.clients.elasticsearch._types.mapping.TypeMapping;
import co.elastic.clients.elasticsearch._types.query_dsl.ChildScoreMode;
import co.elastic.clients.elasticsearch._types.query_dsl.Operator;
import co.elastic.clients.elasticsearch.core.search.BoundaryScanner;
import co.elastic.clients.elasticsearch.core.search.HighlighterEncoder;
import co.elastic.clients.elasticsearch.core.search.HighlighterFragmenter;
@@ -40,12 +42,15 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.RefreshPolicy;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.query.GeoDistanceOrder;
import org.springframework.data.elasticsearch.core.query.HasChildQuery;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.IndicesOptions;
import org.springframework.data.elasticsearch.core.query.Order;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.RescorerQuery;
import org.springframework.data.elasticsearch.core.query.UpdateResponse;
import org.springframework.data.elasticsearch.core.query.types.ConflictsType;
import org.springframework.data.elasticsearch.core.query.types.OperatorType;
import org.springframework.data.elasticsearch.core.reindex.ReindexRequest;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -500,4 +505,48 @@ final class TypeUtils {
});
return mappedParams;
}
/**
* Convert a spring-data-elasticsearch operator to an Elasticsearch operator.
*
* @param operator spring-data-elasticsearch operator.
* @return an Elasticsearch Operator.
* @since 5.3
*/
@Nullable
static Operator operator(@Nullable OperatorType operator) {
return operator != null ? Operator.valueOf(operator.name()) : null;
}
/**
* Convert a spring-data-elasticsearch {@literal conflicts} to an Elasticsearch {@literal conflicts}.
*
* @param conflicts spring-data-elasticsearch {@literal conflicts}.
* @return an Elasticsearch {@literal conflicts}.
* @since 5.3
*/
@Nullable
static Conflicts conflicts(@Nullable ConflictsType conflicts) {
return conflicts != null ? Conflicts.valueOf(conflicts.name()) : null;
}
/**
* Convert a spring-data-elasticsearch {@literal scoreMode} to an Elasticsearch {@literal scoreMode}.
*
* @param scoreMode spring-data-elasticsearch {@literal scoreMode}.
* @return an Elasticsearch {@literal scoreMode}.
*/
static ChildScoreMode scoreMode(@Nullable HasChildQuery.ScoreMode scoreMode) {
if (scoreMode == null) {
return ChildScoreMode.None;
}
return switch (scoreMode) {
case Avg -> ChildScoreMode.Avg;
case Max -> ChildScoreMode.Max;
case Min -> ChildScoreMode.Min;
case Sum -> ChildScoreMode.Sum;
default -> ChildScoreMode.None;
};
}
}
@@ -57,7 +57,6 @@ import org.springframework.data.elasticsearch.support.VersionInfo;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.util.Streamable;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -300,6 +299,7 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
}
@Override
@Deprecated
public ByQueryResponse delete(Query query, Class<?> clazz) {
return delete(query, clazz, getIndexCoordinatesFor(clazz));
}
@@ -779,8 +779,7 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
}
protected interface SearchDocumentResponseCallback<T> {
@NonNull
T doWith(@NonNull SearchDocumentResponse response);
T doWith(SearchDocumentResponse response);
}
protected class ReadSearchDocumentResponseCallback<T> implements SearchDocumentResponseCallback<SearchHits<T>> {
@@ -795,7 +794,6 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
this.type = type;
}
@NonNull
@Override
public SearchHits<T> doWith(SearchDocumentResponse response) {
List<T> entities = response.getSearchDocuments().stream().map(delegate::doWith).collect(Collectors.toList());
@@ -816,7 +814,6 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
this.type = type;
}
@NonNull
@Override
public SearchScrollHits<T> doWith(SearchDocumentResponse response) {
List<T> entities = response.getSearchDocuments().stream().map(delegate::doWith).collect(Collectors.toList());
@@ -46,6 +46,7 @@ import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersiste
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.elasticsearch.core.query.ByQueryResponse;
import org.springframework.data.elasticsearch.core.query.DeleteQuery;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
@@ -55,7 +56,6 @@ import org.springframework.data.elasticsearch.core.script.Script;
import org.springframework.data.elasticsearch.core.suggest.response.Suggest;
import org.springframework.data.elasticsearch.support.VersionInfo;
import org.springframework.data.mapping.callback.ReactiveEntityCallbacks;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -409,9 +409,15 @@ 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));
}
// endregion
// region SearchDocument
@@ -575,7 +581,6 @@ abstract public class AbstractReactiveElasticsearchTemplate
* @param document the document to convert
* @return a Mono of the entity
*/
@NonNull
Mono<T> toEntity(@Nullable Document document);
}
@@ -593,7 +598,6 @@ abstract public class AbstractReactiveElasticsearchTemplate
this.index = index;
}
@NonNull
public Mono<T> toEntity(@Nullable Document document) {
if (document == null) {
return Mono.empty();
@@ -21,6 +21,7 @@ import java.util.List;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.BulkOptions;
import org.springframework.data.elasticsearch.core.query.ByQueryResponse;
import org.springframework.data.elasticsearch.core.query.DeleteQuery;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
@@ -279,9 +280,22 @@ public interface DocumentOperations {
* {@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.
*
* @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 5.3
*/
ByQueryResponse delete(DeleteQuery query, Class<?> clazz);
/**
* Delete all records matching the query.
*
@@ -290,9 +304,23 @@ public interface DocumentOperations {
* {@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.
*
* @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
* @since 5.3
*/
ByQueryResponse delete(DeleteQuery query, Class<?> clazz, IndexCoordinates index);
/**
* Partially update a document by the given entity.
*
@@ -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
@@ -25,6 +25,7 @@ import java.util.List;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.BulkOptions;
import org.springframework.data.elasticsearch.core.query.ByQueryResponse;
import org.springframework.data.elasticsearch.core.query.DeleteQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
import org.springframework.data.elasticsearch.core.query.UpdateResponse;
@@ -331,9 +332,21 @@ public interface ReactiveDocumentOperations {
* @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.
*
* @param query must not be {@literal null}.
* @param entityType must not be {@literal null}.
* @return a {@link Mono} emitting the number of the removed documents.
* @since 5.3
*/
Mono<ByQueryResponse> delete(DeleteQuery query, Class<?> entityType);
/**
* Delete the documents matching the given {@link Query} extracting index from entity metadata.
*
@@ -341,9 +354,22 @@ public interface ReactiveDocumentOperations {
* @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.
*
* @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.
* @since 5.3
*/
Mono<ByQueryResponse> delete(DeleteQuery query, Class<?> entityType, IndexCoordinates index);
/**
* Partial update of the document.
*
@@ -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();
@@ -22,5 +22,5 @@ package org.springframework.data.elasticsearch.core;
* @since 4.2
*/
public enum RefreshPolicy {
NONE, IMMEDIATE, WAIT_UNTIL;
NONE, IMMEDIATE, WAIT_UNTIL
}
@@ -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;
@@ -46,6 +47,8 @@ import org.springframework.util.Assert;
* @author Matt Gilene
* @author Sascha Woo
* @author Jakob Hoeper
* @author Haibo Liu
* @author Mohamed El Harrougui
* @since 4.0
*/
public class SearchHitMapping<T> {
@@ -84,7 +87,9 @@ public class SearchHitMapping<T> {
"Count of documents must match the count of entities");
long totalHits = searchDocumentResponse.getTotalHits();
SearchShardStatistics shardStatistics = searchDocumentResponse.getSearchShardStatistics();
float maxScore = searchDocumentResponse.getMaxScore();
Duration executionDuration = searchDocumentResponse.getExecutionDuration();
String scrollId = searchDocumentResponse.getScrollId();
String pointInTimeId = searchDocumentResponse.getPointInTimeId();
@@ -102,8 +107,8 @@ public class SearchHitMapping<T> {
Suggest suggest = searchDocumentResponse.getSuggest();
mapHitsInCompletionSuggestion(suggest);
return new SearchHitsImpl<>(totalHits, totalHitsRelation, maxScore, scrollId, pointInTimeId, searchHits,
aggregations, suggest);
return new SearchHitsImpl<>(totalHits, totalHitsRelation, maxScore, executionDuration, scrollId, pointInTimeId,
searchHits, aggregations, suggest, shardStatistics);
}
@SuppressWarnings("unchecked")
@@ -165,7 +170,7 @@ public class SearchHitMapping<T> {
Map<String, SearchHits<?>> innerHits = new LinkedHashMap<>();
Map<String, SearchDocumentResponse> documentInnerHits = searchDocument.getInnerHits();
if (documentInnerHits != null && documentInnerHits.size() > 0) {
if (documentInnerHits != null && !documentInnerHits.isEmpty()) {
SearchHitMapping<SearchDocument> searchDocumentSearchHitMapping = SearchHitMapping
.mappingFor(SearchDocument.class, converter);
@@ -233,14 +238,16 @@ public class SearchHitMapping<T> {
scrollId = searchHitsImpl.getScrollId();
}
return new SearchHitsImpl<>(searchHits.getTotalHits(), //
searchHits.getTotalHitsRelation(), //
searchHits.getMaxScore(), //
scrollId, //
searchHits.getPointInTimeId(), //
convertedSearchHits, //
searchHits.getAggregations(), //
searchHits.getSuggest());
return new SearchHitsImpl<>(searchHits.getTotalHits(),
searchHits.getTotalHitsRelation(),
searchHits.getMaxScore(),
searchHits.getExecutionDuration(),
scrollId,
searchHits.getPointInTimeId(),
convertedSearchHits,
searchHits.getAggregations(),
searchHits.getSuggest(),
searchHits.getSearchShardStatistics());
}
} catch (Exception e) {
throw new UncategorizedElasticsearchException("Unable to convert inner hits.", e);
@@ -284,8 +291,8 @@ public class SearchHitMapping<T> {
}
private static class ElasticsearchPersistentEntityWithNestedMetaData {
@Nullable private ElasticsearchPersistentEntity<?> entity;
private NestedMetaData nestedMetaData;
@Nullable private final ElasticsearchPersistentEntity<?> entity;
private final NestedMetaData nestedMetaData;
public ElasticsearchPersistentEntityWithNestedMetaData(@Nullable ElasticsearchPersistentEntity<?> entity,
NestedMetaData nestedMetaData) {
@@ -23,8 +23,8 @@ import java.util.stream.Stream;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.util.ReactiveWrappers;
import org.springframework.data.util.CloseableIterator;
import org.springframework.data.util.ReactiveWrappers;
import org.springframework.lang.Nullable;
/**
@@ -15,6 +15,7 @@
*/
package org.springframework.data.elasticsearch.core;
import java.time.Duration;
import java.util.Iterator;
import java.util.List;
@@ -27,6 +28,8 @@ 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>> {
@@ -42,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}
@@ -108,4 +116,10 @@ public interface SearchHits<T> extends Streamable<SearchHit<T>> {
*/
@Nullable
String getPointInTimeId();
/**
* @return shard statistics for the search hit.
*/
@Nullable
SearchShardStatistics getSearchShardStatistics();
}
@@ -15,6 +15,7 @@
*/
package org.springframework.data.elasticsearch.core;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
@@ -29,6 +30,8 @@ import org.springframework.util.Assert;
* @param <T> the result data class.
* @author Peter-Josef Meisch
* @author Sascha Woo
* @author Haibo Liu
* @author Mohamed El Harrougui
* @since 4.0
*/
public class SearchHitsImpl<T> implements SearchScrollHits<T> {
@@ -36,36 +39,42 @@ 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;
@Nullable private final AggregationsContainer<?> aggregations;
@Nullable private final Suggest suggest;
@Nullable private String pointInTimeId;
@Nullable private final String pointInTimeId;
@Nullable private final SearchShardStatistics searchShardStatistics;
/**
* @param totalHits the number of total hits for the search
* @param totalHitsRelation the relation {@see TotalHitsRelation}, must not be {@literal null}
* @param maxScore the maximum score
* @param executionDuration the execution duration it took to complete the request
* @param scrollId the scroll id if available
* @param searchHits must not be {@literal null}
* @param aggregations the aggregations if available
*/
public SearchHitsImpl(long totalHits, TotalHitsRelation totalHitsRelation, float maxScore, @Nullable String scrollId,
@Nullable String pointInTimeId, List<? extends SearchHit<T>> searchHits,
@Nullable AggregationsContainer<?> aggregations, @Nullable Suggest suggest) {
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) {
Assert.notNull(searchHits, "searchHits must not be null");
this.totalHits = totalHits;
this.totalHitsRelation = totalHitsRelation;
this.maxScore = maxScore;
this.executionDuration = executionDuration;
this.scrollId = scrollId;
this.pointInTimeId = pointInTimeId;
this.searchHits = searchHits;
this.aggregations = aggregations;
this.suggest = suggest;
this.unmodifiableSearchHits = Lazy.of(() -> Collections.unmodifiableList(searchHits));
this.searchShardStatistics = searchShardStatistics;
}
// region getter
@@ -84,6 +93,11 @@ public class SearchHitsImpl<T> implements SearchScrollHits<T> {
return maxScore;
}
@Override
public Duration getExecutionDuration() {
return executionDuration;
}
@Override
@Nullable
public String getScrollId() {
@@ -118,16 +132,23 @@ public class SearchHitsImpl<T> implements SearchScrollHits<T> {
return pointInTimeId;
}
@Override
public SearchShardStatistics getSearchShardStatistics() {
return searchShardStatistics;
}
@Override
public String toString() {
return "SearchHits{" + //
"totalHits=" + totalHits + //
", totalHitsRelation=" + totalHitsRelation + //
", maxScore=" + maxScore + //
", executionDuration=" + executionDuration + //
", scrollId='" + scrollId + '\'' + //
", pointInTimeId='" + pointInTimeId + '\'' + //
", searchHits={" + searchHits.size() + " elements}" + //
", aggregations=" + aggregations + //
", shardStatistics=" + searchShardStatistics + //
'}';
}
}
@@ -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.
*/
@@ -0,0 +1,130 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core;
import java.util.List;
import org.springframework.data.elasticsearch.ElasticsearchErrorCause;
import org.springframework.lang.Nullable;
/**
* @author Haibo Liu
* @since 5.3
*/
public class SearchShardStatistics {
private final Number failed;
private final Number successful;
private final Number total;
@Nullable private final Number skipped;
private final List<Failure> failures;
private SearchShardStatistics(Number failed, Number successful, Number total, @Nullable Number skipped,
List<Failure> failures) {
this.failed = failed;
this.successful = successful;
this.total = total;
this.skipped = skipped;
this.failures = failures;
}
public static SearchShardStatistics of(Number failed, Number successful, Number total, @Nullable Number skipped,
List<Failure> failures) {
return new SearchShardStatistics(failed, successful, total, skipped, failures);
}
public Number getFailed() {
return failed;
}
public Number getSuccessful() {
return successful;
}
public Number getTotal() {
return total;
}
@Nullable
public Number getSkipped() {
return skipped;
}
public boolean isFailed() {
return failed.intValue() > 0;
}
public List<Failure> getFailures() {
return failures;
}
public static class Failure {
@Nullable private final String index;
@Nullable private final String node;
@Nullable private final String status;
private final int shard;
@Nullable private final Exception exception;
@Nullable private final ElasticsearchErrorCause elasticsearchErrorCause;
private Failure(@Nullable String index, @Nullable String node, @Nullable String status, int shard,
@Nullable Exception exception, @Nullable ElasticsearchErrorCause elasticsearchErrorCause) {
this.index = index;
this.node = node;
this.status = status;
this.shard = shard;
this.exception = exception;
this.elasticsearchErrorCause = elasticsearchErrorCause;
}
public static SearchShardStatistics.Failure of(@Nullable String index, @Nullable String node,
@Nullable String status, int shard, @Nullable Exception exception,
@Nullable ElasticsearchErrorCause elasticsearchErrorCause) {
return new SearchShardStatistics.Failure(index, node, status, shard, exception, elasticsearchErrorCause);
}
@Nullable
public String getIndex() {
return index;
}
@Nullable
public String getNode() {
return node;
}
@Nullable
public String getStatus() {
return status;
}
@Nullable
public Exception getException() {
return exception;
}
public int getShard() {
return shard;
}
@Nullable
public ElasticsearchErrorCause getElasticsearchErrorCause() {
return elasticsearchErrorCause;
}
}
}
@@ -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,15 +58,16 @@ abstract class StreamQueries {
AggregationsContainer<?> aggregations = searchHits.getAggregations();
float maxScore = searchHits.getMaxScore();
Duration executionDuration = searchHits.getExecutionDuration();
long totalHits = searchHits.getTotalHits();
TotalHitsRelation totalHitsRelation = searchHits.getTotalHitsRelation();
return new SearchHitsIterator<>() {
private volatile AtomicInteger currentCount = new AtomicInteger();
private final AtomicInteger currentCount = new AtomicInteger();
private volatile Iterator<SearchHit<T>> currentScrollHits = searchHits.iterator();
private volatile boolean continueScroll = currentScrollHits.hasNext();
private volatile ScrollState scrollState = new ScrollState(searchHits.getScrollId());
private final ScrollState scrollState = new ScrollState(searchHits.getScrollId());
private volatile boolean isClosed = false;
@Override
@@ -86,6 +89,11 @@ abstract class StreamQueries {
return maxScore;
}
@Override
public Duration getExecutionDuration() {
return executionDuration;
}
@Override
public long getTotalHits() {
return totalHits;
@@ -110,6 +110,6 @@ public interface ElasticsearchConverter
* @return a String wihere the property names are replaced with field names
* @since 5.2
*/
public String updateFieldNames(String propertyPath, ElasticsearchPersistentEntity<?> persistentEntity);
String updateFieldNames(String propertyPath, ElasticsearchPersistentEntity<?> persistentEntity);
// endregion
}
@@ -129,7 +129,7 @@ public class ElasticsearchCustomConversions extends CustomConversions {
@WritingConverter
enum ByteArrayToBase64Converter implements Converter<byte[], String> {
INSTANCE,;
INSTANCE;
@Override
public String convert(byte[] source) {
@@ -344,7 +344,7 @@ public class GeoConverters {
String type = GeoConverters.getGeoJsonType(source);
Assert.isTrue(type.equalsIgnoreCase(GeoJsonPolygon.TYPE), "does not contain a type 'Polygon'");
List<GeoJsonLineString> lines = geoJsonLineStringsFromMap(source);
Assert.isTrue(lines.size() > 0, "no linestrings defined in polygon");
Assert.isTrue(!lines.isEmpty(), "no linestrings defined in polygon");
GeoJsonPolygon geoJsonPolygon = GeoJsonPolygon.of(lines.get(0));
for (int i = 1; i < lines.size(); i++) {
geoJsonPolygon = geoJsonPolygon.withInnerRing(lines.get(i));
@@ -0,0 +1,40 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.convert;
import org.springframework.lang.Nullable;
/**
* @since 5.3
* @author Peter-Josef Meisch
*/
public class MappingConversionException extends RuntimeException {
private final String documentId;
public MappingConversionException(@Nullable String documentId, Throwable cause) {
super(cause);
this.documentId = documentId != null ? documentId : "\"null\"";
}
public String getDocumentId() {
return documentId;
}
@Override
public String getMessage() {
return "Conversion exception when converting document id " + documentId;
}
}
@@ -34,6 +34,9 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.env.Environment;
import org.springframework.core.env.EnvironmentCapable;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.ScriptedField;
@@ -58,6 +61,7 @@ import org.springframework.data.mapping.SimplePropertyHandler;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.*;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.format.datetime.DateFormatterRegistrar;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -80,10 +84,11 @@ import org.springframework.util.ObjectUtils;
* @author Marc Vanbrabant
* @author Anton Naydenov
* @author vdisk
* @author Junghoon Ban
* @since 3.2
*/
public class MappingElasticsearchConverter
implements ElasticsearchConverter, ApplicationContextAware, InitializingBean {
implements ElasticsearchConverter, ApplicationContextAware, InitializingBean, EnvironmentCapable {
private static final String INCOMPATIBLE_TYPES = "Cannot convert %1$s of type %2$s into an instance of %3$s! Implement a custom Converter<%2$s, %3$s> and register it with the CustomConversions.";
private static final String INVALID_TYPE_TO_READ = "Expected to read Document %s into type %s but didn't find a PersistentEntity for the latter!";
@@ -93,7 +98,14 @@ public class MappingElasticsearchConverter
private final MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext;
private final GenericConversionService conversionService;
private CustomConversions conversions = new ElasticsearchCustomConversions(Collections.emptyList());
protected @Nullable Environment environment;
private final SpELContext spELContext = new SpELContext(new MapAccessor());
private final SpelExpressionParser expressionParser = new SpelExpressionParser();
private final CachingValueExpressionEvaluatorFactory expressionEvaluatorFactory = new CachingValueExpressionEvaluatorFactory(
expressionParser, this, spELContext);
private final EntityInstantiators instantiators = new EntityInstantiators();
private final ElasticsearchTypeMapper typeMapper;
@@ -121,6 +133,14 @@ public class MappingElasticsearchConverter
}
}
@Override
public Environment getEnvironment() {
if (environment == null) {
environment = new StandardEnvironment();
}
return environment;
}
@Override
public MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> getMappingContext() {
return mappingContext;
@@ -159,7 +179,8 @@ public class MappingElasticsearchConverter
@Override
public <R> R read(Class<R> type, Document source) {
Reader reader = new Reader(mappingContext, conversionService, conversions, typeMapper, spELContext, instantiators);
Reader reader = new Reader(mappingContext, conversionService, conversions, typeMapper, expressionEvaluatorFactory,
instantiators);
return reader.read(type, source);
}
@@ -199,29 +220,29 @@ public class MappingElasticsearchConverter
*/
private static class Reader extends Base {
private final SpELContext spELContext;
private final EntityInstantiators instantiators;
private final CachingValueExpressionEvaluatorFactory expressionEvaluatorFactory;
public Reader(
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext,
GenericConversionService conversionService, CustomConversions conversions, ElasticsearchTypeMapper typeMapper,
SpELContext spELContext, EntityInstantiators instantiators) {
CachingValueExpressionEvaluatorFactory expressionEvaluatorFactory, EntityInstantiators instantiators) {
super(mappingContext, conversionService, conversions, typeMapper);
this.spELContext = spELContext;
this.expressionEvaluatorFactory = expressionEvaluatorFactory;
this.instantiators = instantiators;
}
@SuppressWarnings("unchecked")
/**
* Reads the given source into the given type.
*
* @param type they type to convert the given source to.
* @param type the type to convert the given source to.
* @param source the source to create an object of the given type from.
* @return the object that was read
*/
<R> R read(Class<R> type, Document source) {
// noinspection unchecked
TypeInformation<R> typeInformation = TypeInformation.of((Class<R>) ClassUtils.getUserClass(type));
R r = read(typeInformation, source);
@@ -313,8 +334,7 @@ public class MappingElasticsearchConverter
private <R> R readEntity(ElasticsearchPersistentEntity<?> entity, Map<String, Object> source) {
ElasticsearchPersistentEntity<?> targetEntity = computeClosestEntity(entity, source);
SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(source, spELContext);
ValueExpressionEvaluator evaluator = expressionEvaluatorFactory.create(source);
MapValueAccessor accessor = new MapValueAccessor(source);
InstanceCreatorMetadata<?> creatorMetadata = entity.getInstanceCreatorMetadata();
@@ -332,50 +352,56 @@ public class MappingElasticsearchConverter
return instance;
}
Document document = (source instanceof Document) ? (Document) source : null;
ElasticsearchPropertyValueProvider valueProvider = new ElasticsearchPropertyValueProvider(accessor, evaluator);
R result = readProperties(targetEntity, instance, valueProvider);
try {
R result = readProperties(targetEntity, instance, valueProvider);
if (source instanceof Document document) {
if (document.hasId()) {
ElasticsearchPersistentProperty idProperty = targetEntity.getIdProperty();
PersistentPropertyAccessor<R> propertyAccessor = new ConvertingPropertyAccessor<>(
targetEntity.getPropertyAccessor(result), conversionService);
// Only deal with String because ES generated Ids are strings !
if (idProperty != null && idProperty.isReadable() && idProperty.getType().isAssignableFrom(String.class)) {
propertyAccessor.setProperty(idProperty, document.getId());
if (document != null) {
if (document.hasId()) {
ElasticsearchPersistentProperty idProperty = targetEntity.getIdProperty();
PersistentPropertyAccessor<R> propertyAccessor = new ConvertingPropertyAccessor<>(
targetEntity.getPropertyAccessor(result), conversionService);
// Only deal with String because ES generated Ids are strings !
if (idProperty != null && idProperty.isReadable() && idProperty.getType().isAssignableFrom(String.class)) {
propertyAccessor.setProperty(idProperty, document.getId());
}
}
if (document.hasVersion()) {
long version = document.getVersion();
ElasticsearchPersistentProperty versionProperty = targetEntity.getVersionProperty();
// Only deal with Long because ES versions are longs !
if (versionProperty != null && versionProperty.getType().isAssignableFrom(Long.class)) {
// check that a version was actually returned in the response, -1 would indicate that
// a search didn't request the version ids in the response, which would be an issue
Assert.isTrue(version != -1, "Version in response is -1");
targetEntity.getPropertyAccessor(result).setProperty(versionProperty, version);
}
}
if (targetEntity.hasSeqNoPrimaryTermProperty() && document.hasSeqNo() && document.hasPrimaryTerm()) {
if (isAssignedSeqNo(document.getSeqNo()) && isAssignedPrimaryTerm(document.getPrimaryTerm())) {
SeqNoPrimaryTerm seqNoPrimaryTerm = new SeqNoPrimaryTerm(document.getSeqNo(), document.getPrimaryTerm());
ElasticsearchPersistentProperty property = targetEntity.getRequiredSeqNoPrimaryTermProperty();
targetEntity.getPropertyAccessor(result).setProperty(property, seqNoPrimaryTerm);
}
}
}
if (document.hasVersion()) {
long version = document.getVersion();
ElasticsearchPersistentProperty versionProperty = targetEntity.getVersionProperty();
// Only deal with Long because ES versions are longs !
if (versionProperty != null && versionProperty.getType().isAssignableFrom(Long.class)) {
// check that a version was actually returned in the response, -1 would indicate that
// a search didn't request the version ids in the response, which would be an issue
Assert.isTrue(version != -1, "Version in response is -1");
targetEntity.getPropertyAccessor(result).setProperty(versionProperty, version);
}
}
if (targetEntity.hasSeqNoPrimaryTermProperty() && document.hasSeqNo() && document.hasPrimaryTerm()) {
if (isAssignedSeqNo(document.getSeqNo()) && isAssignedPrimaryTerm(document.getPrimaryTerm())) {
SeqNoPrimaryTerm seqNoPrimaryTerm = new SeqNoPrimaryTerm(document.getSeqNo(), document.getPrimaryTerm());
ElasticsearchPersistentProperty property = targetEntity.getRequiredSeqNoPrimaryTermProperty();
targetEntity.getPropertyAccessor(result).setProperty(property, seqNoPrimaryTerm);
}
if (source instanceof SearchDocument searchDocument) {
populateScriptFields(targetEntity, result, searchDocument);
}
return result;
} catch (ConversionException e) {
String documentId = (document != null && document.hasId()) ? document.getId() : null;
throw new MappingConversionException(documentId, e);
}
if (source instanceof SearchDocument searchDocument) {
populateScriptFields(targetEntity, result, searchDocument);
}
return result;
}
private ParameterValueProvider<ElasticsearchPersistentProperty> getParameterProvider(
ElasticsearchPersistentEntity<?> entity, MapValueAccessor source, SpELExpressionEvaluator evaluator) {
ElasticsearchPersistentEntity<?> entity, MapValueAccessor source, ValueExpressionEvaluator evaluator) {
ElasticsearchPropertyValueProvider provider = new ElasticsearchPropertyValueProvider(source, evaluator);
@@ -384,7 +410,7 @@ public class MappingElasticsearchConverter
PersistentEntityParameterValueProvider<ElasticsearchPersistentProperty> parameterProvider = new PersistentEntityParameterValueProvider<>(
entity, provider, null);
return new ConverterAwareSpELExpressionParameterValueProvider(evaluator, conversionService, parameterProvider);
return new ConverterAwareValueExpressionParameterValueProvider(evaluator, conversionService, parameterProvider);
}
private boolean isAssignedSeqNo(long seqNo) {
@@ -466,7 +492,7 @@ public class MappingElasticsearchConverter
TypeInformation<?> collectionComponentType = getCollectionComponentType(type);
if (collectionComponentType != null) {
Object o = read(collectionComponentType, (Map<String, Object>) value);
return getCollectionWithSingleElement(type, collectionComponentType, o);
return (o != null) ? getCollectionWithSingleElement(type, collectionComponentType, o) : null;
}
return (T) read(type, (Map<String, Object>) value);
} else {
@@ -475,7 +501,7 @@ public class MappingElasticsearchConverter
if (collectionComponentType != null
&& collectionComponentType.isAssignableFrom(TypeInformation.of(value.getClass()))) {
Object o = getPotentiallyConvertedSimpleRead(value, collectionComponentType);
return getCollectionWithSingleElement(type, collectionComponentType, o);
return (o != null) ? getCollectionWithSingleElement(type, collectionComponentType, o) : null;
}
return (T) getPotentiallyConvertedSimpleRead(value, rawType);
@@ -493,7 +519,7 @@ public class MappingElasticsearchConverter
/**
* @param type the type to check
* @return true if type is a collectoin, null otherwise,
* @return the collection type if type is a collection, null otherwise,
*/
@Nullable
TypeInformation<?> getCollectionComponentType(TypeInformation<?> type) {
@@ -503,17 +529,15 @@ public class MappingElasticsearchConverter
private Object propertyConverterRead(ElasticsearchPersistentProperty property, Object source) {
PropertyValueConverter propertyValueConverter = Objects.requireNonNull(property.getPropertyValueConverter());
if (source instanceof String[]) {
if (source instanceof String[] strings) {
// convert to a List
source = Arrays.asList((String[]) source);
source = Arrays.asList(strings);
}
if (source instanceof List) {
source = ((List<?>) source).stream().map(it -> convertOnRead(propertyValueConverter, it))
.collect(Collectors.toList());
} else if (source instanceof Set) {
source = ((Set<?>) source).stream().map(it -> convertOnRead(propertyValueConverter, it))
.collect(Collectors.toSet());
if (source instanceof List<?> list) {
source = list.stream().map(it -> convertOnRead(propertyValueConverter, it)).collect(Collectors.toList());
} else if (source instanceof Set<?> set) {
source = set.stream().map(it -> convertOnRead(propertyValueConverter, it)).collect(Collectors.toSet());
} else {
source = convertOnRead(propertyValueConverter, source);
}
@@ -611,9 +635,10 @@ public class MappingElasticsearchConverter
* but will be removed from spring-data-commons, so we do it here
*/
@Nullable
private Object convertFromCollectionToObject(Object value, @Nullable Class<?> target) {
private Object convertFromCollectionToObject(Object value, Class<?> target) {
if (value.getClass().isArray()) {
// noinspection ArraysAsListWithZeroOrOneArgument
value = Arrays.asList(value);
}
@@ -663,9 +688,9 @@ public class MappingElasticsearchConverter
class ElasticsearchPropertyValueProvider implements PropertyValueProvider<ElasticsearchPersistentProperty> {
final MapValueAccessor accessor;
final SpELExpressionEvaluator evaluator;
final ValueExpressionEvaluator evaluator;
ElasticsearchPropertyValueProvider(MapValueAccessor accessor, SpELExpressionEvaluator evaluator) {
ElasticsearchPropertyValueProvider(MapValueAccessor accessor, ValueExpressionEvaluator evaluator) {
this.accessor = accessor;
this.evaluator = evaluator;
}
@@ -685,33 +710,29 @@ public class MappingElasticsearchConverter
}
/**
* Extension of {@link SpELExpressionParameterValueProvider} to recursively trigger value conversion on the raw
* Extension of {@link ValueExpressionParameterValueProvider} to recursively trigger value conversion on the raw
* resolved SpEL value.
*
* @author Mark Paluch
*/
private class ConverterAwareSpELExpressionParameterValueProvider
extends SpELExpressionParameterValueProvider<ElasticsearchPersistentProperty> {
private class ConverterAwareValueExpressionParameterValueProvider
extends ValueExpressionParameterValueProvider<ElasticsearchPersistentProperty> {
/**
* Creates a new {@link ConverterAwareSpELExpressionParameterValueProvider}.
* Creates a new {@link ConverterAwareValueExpressionParameterValueProvider}.
*
* @param evaluator must not be {@literal null}.
* @param conversionService must not be {@literal null}.
* @param delegate must not be {@literal null}.
*/
public ConverterAwareSpELExpressionParameterValueProvider(SpELExpressionEvaluator evaluator,
public ConverterAwareValueExpressionParameterValueProvider(ValueExpressionEvaluator evaluator,
ConversionService conversionService, ParameterValueProvider<ElasticsearchPersistentProperty> delegate) {
super(evaluator, conversionService, delegate);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.model.SpELExpressionParameterValueProvider#potentiallyConvertSpelValue(java.lang.Object, org.springframework.data.mapping.PreferredConstructor.Parameter)
*/
@Override
protected <T> T potentiallyConvertSpelValue(Object object,
protected <T> T potentiallyConvertExpressionValue(Object object,
Parameter<T, ElasticsearchPersistentProperty> parameter) {
return readValue(object, parameter.getType());
}
@@ -988,12 +1009,8 @@ public class MappingElasticsearchConverter
private static boolean hasEmptyValue(Object value) {
if (value instanceof String s && s.isEmpty() || value instanceof Collection<?> c && c.isEmpty()
|| value instanceof Map<?, ?> m && m.isEmpty()) {
return true;
}
return false;
return value instanceof String s && s.isEmpty() || value instanceof Collection<?> c && c.isEmpty()
|| value instanceof Map<?, ?> m && m.isEmpty();
}
@SuppressWarnings("unchecked")
@@ -1395,12 +1412,18 @@ public class MappingElasticsearchConverter
if (properties.length > 1) {
var persistentProperty = persistentEntity.getPersistentProperty(propertyName);
return (persistentProperty != null)
? fieldName + "." + updateFieldNames(properties[1], mappingContext.getPersistentEntity(persistentProperty))
: fieldName;
} else {
return fieldName;
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;
}
@@ -1409,6 +1432,7 @@ public class MappingElasticsearchConverter
// endregion
@SuppressWarnings("ClassCanBeRecord")
static class MapValueAccessor {
final Map<String, Object> target;
@@ -18,11 +18,7 @@ package org.springframework.data.elasticsearch.core.document;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.function.IntSupplier;
import java.util.function.LongSupplier;
import java.util.function.Supplier;
import org.springframework.data.elasticsearch.core.convert.ConversionException;
import org.springframework.data.elasticsearch.support.StringObjectMap;
@@ -30,12 +26,12 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* A representation of a Elasticsearch document as extended {@link StringObjectMap Map}. All iterators preserve original
* insertion order.
* A representation of an Elasticsearch document as extended {@link StringObjectMap Map}. All iterators preserve
* original insertion order.
* <p>
* Document does not allow {@code null} keys. It allows {@literal null} values.
* <p>
* Implementing classes can bei either mutable or immutable. In case a subclass is immutable, its methods may throw
* Implementing classes can be either mutable or immutable. In case a subclass is immutable, its methods may throw
* {@link UnsupportedOperationException} when calling modifying methods.
*
* @author Mark Paluch
@@ -60,7 +56,7 @@ public interface Document extends StringObjectMap<Document> {
* @param map source map containing key-value pairs and sub-documents. must not be {@literal null}.
* @return a new {@link Document}.
*/
static Document from(Map<String, ? extends Object> map) {
static Document from(Map<String, ?> map) {
Assert.notNull(map, "Map must not be null");
@@ -15,11 +15,13 @@
*/
package org.springframework.data.elasticsearch.core.document;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import org.springframework.data.elasticsearch.core.AggregationsContainer;
import org.springframework.data.elasticsearch.core.SearchShardStatistics;
import org.springframework.data.elasticsearch.core.suggest.response.Suggest;
import org.springframework.lang.Nullable;
@@ -27,6 +29,8 @@ import org.springframework.lang.Nullable;
* This represents the complete search response from Elasticsearch, including the returned documents.
*
* @author Peter-Josef Meisch
* @author Haibo Liu
* @author Mohamed El Harrougui
* @since 4.0
*/
public class SearchDocumentResponse {
@@ -34,24 +38,29 @@ public class SearchDocumentResponse {
private final long totalHits;
private final String totalHitsRelation;
private final float maxScore;
private final Duration executionDuration;
@Nullable private final String scrollId;
private final List<SearchDocument> searchDocuments;
@Nullable private final AggregationsContainer<?> aggregations;
@Nullable private final Suggest suggest;
@Nullable String pointInTimeId;
@Nullable private final SearchShardStatistics searchShardStatistics;
public SearchDocumentResponse(long totalHits, String totalHitsRelation, float maxScore, @Nullable String scrollId,
@Nullable String pointInTimeId, List<SearchDocument> searchDocuments,
@Nullable AggregationsContainer<?> aggregationsContainer, @Nullable Suggest suggest) {
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;
this.aggregations = aggregationsContainer;
this.suggest = suggest;
this.searchShardStatistics = searchShardStatistics;
}
public long getTotalHits() {
@@ -66,6 +75,10 @@ public class SearchDocumentResponse {
return maxScore;
}
public Duration getExecutionDuration() {
return executionDuration;
}
@Nullable
public String getScrollId() {
return scrollId;
@@ -93,6 +106,11 @@ public class SearchDocumentResponse {
return pointInTimeId;
}
@Nullable
public SearchShardStatistics getSearchShardStatistics() {
return searchShardStatistics;
}
/**
* A function to convert a {@link SearchDocument} async into an entity. Asynchronous so that it can be used from the
* imperative and the reactive code.
@@ -24,8 +24,8 @@ import org.springframework.data.geo.Box;
*/
public class GeoBox {
private GeoPoint topLeft;
private GeoPoint bottomRight;
private final GeoPoint topLeft;
private final GeoPoint bottomRight;
public GeoBox(GeoPoint topLeft, GeoPoint bottomRight) {
this.topLeft = topLeft;
@@ -33,7 +33,7 @@ public class GeoJsonMultiPolygon implements GeoJson<Iterable<GeoJsonPolygon>> {
public static final String TYPE = "MultiPolygon";
private List<GeoJsonPolygon> coordinates = new ArrayList<>();
private final List<GeoJsonPolygon> coordinates = new ArrayList<>();
private GeoJsonMultiPolygon(List<GeoJsonPolygon> polygons) {
this.coordinates.addAll(polygons);
@@ -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,11 +214,13 @@ 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 {
if (entity != null && entity.isAnnotationPresent(Mapping.class)) {
Mapping mappingAnnotation = entity.getRequiredAnnotation(Mapping.class);
var mappingAnnotation = entity != null ? entity.findAnnotation(Mapping.class) : null;
if (mappingAnnotation != null) {
if (!mappingAnnotation.enabled()) {
objectNode.put(MAPPING_ENABLED, false);
@@ -243,7 +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);
@@ -289,6 +294,16 @@ public class MappingBuilder {
LOGGER.warn(String.format("error mapping property with name %s", property.getName()), e);
}
});
}
// write the alias entries after the properties
if (mappingAnnotation != null) {
for (MappingAlias mappingAlias : mappingAnnotation.aliases()) {
var aliasNode = propertiesNode.putObject(mappingAlias.name());
aliasNode.put(FIELD_PARAM_TYPE, FIELD_PARAM_TYPE_ALIAS);
aliasNode.put(FIELD_PARAM_PATH, mappingAlias.path());
}
}
}
@@ -334,8 +349,10 @@ public class MappingBuilder {
: nestedPropertyPrefix + '.' + property.getFieldName();
Field fieldAnnotation = property.findAnnotation(Field.class);
MultiField multiFieldAnnotation = property.findAnnotation(MultiField.class);
if (fieldAnnotation != null && fieldAnnotation.excludeFromSource()) {
if ((fieldAnnotation != null && fieldAnnotation.excludeFromSource()) ||
multiFieldAnnotation != null && multiFieldAnnotation.mainField().excludeFromSource()) {
excludeFromSource.add(nestedPropertyPath);
}
@@ -359,15 +376,13 @@ 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;
}
}
MultiField multiField = property.findAnnotation(MultiField.class);
if (isCompletionProperty) {
CompletionField completionField = property.findAnnotation(CompletionField.class);
applyCompletionFieldMapping(propertiesNode, property, completionField);
@@ -375,8 +390,8 @@ public class MappingBuilder {
if (isRootObject && fieldAnnotation != null && property.isIdProperty()) {
applyDefaultIdFieldMapping(propertiesNode, property);
} else if (multiField != null) {
addMultiFieldMapping(propertiesNode, property, multiField, isNestedOrObjectProperty, dynamicMapping);
} else if (multiFieldAnnotation != null) {
addMultiFieldMapping(propertiesNode, property, multiFieldAnnotation, isNestedOrObjectProperty, dynamicMapping);
} else if (fieldAnnotation != null) {
addSingleFieldMapping(propertiesNode, property, fieldAnnotation, isNestedOrObjectProperty, dynamicMapping);
}
@@ -432,7 +447,7 @@ public class MappingBuilder {
contextNode.put(FIELD_CONTEXT_NAME, context.name());
contextNode.put(FIELD_CONTEXT_TYPE, context.type().getMappedName());
if (context.precision().length() > 0) {
if (!context.precision().isEmpty()) {
contextNode.put(FIELD_CONTEXT_PRECISION, context.precision());
}
@@ -462,7 +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) //
);
@@ -471,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,12 +71,18 @@ public final class MappingParameters {
static final String FIELD_PARAM_ORIENTATION = "orientation";
static final String FIELD_PARAM_POSITIVE_SCORE_IMPACT = "positive_score_impact";
static final String FIELD_PARAM_DIMS = "dims";
static final String FIELD_PARAM_ELEMENT_TYPE = "element_type";
static final String FIELD_PARAM_M = "m";
static final String FIELD_PARAM_EF_CONSTRUCTION = "ef_construction";
static final String FIELD_PARAM_CONFIDENCE_INTERVAL = "confidence_interval";
static final String FIELD_PARAM_SCALING_FACTOR = "scaling_factor";
static final String FIELD_PARAM_SEARCH_ANALYZER = "search_analyzer";
static final String FIELD_PARAM_STORE = "store";
static final String FIELD_PARAM_SIMILARITY = "similarity";
static final String FIELD_PARAM_TERM_VECTOR = "term_vector";
static final String FIELD_PARAM_TYPE = "type";
static final String FIELD_PARAM_PATH = "path";
static final String FIELD_PARAM_TYPE_ALIAS = "alias";
private final String analyzer;
private final boolean coerce;
@@ -108,12 +107,16 @@ public final class MappingParameters {
private final Integer positionIncrementGap;
private final boolean positiveScoreImpact;
private final Integer dims;
private final String elementType;
private final KnnSimilarity knnSimilarity;
@Nullable private final KnnIndexOptions knnIndexOptions;
private final String searchAnalyzer;
private final double scalingFactor;
private final String similarity;
private final boolean store;
private final TermVector termVector;
private final FieldType type;
private final String mappedTypeName;
/**
* extracts the mapping parameters from the relevant annotations.
@@ -139,6 +142,7 @@ 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();
@@ -169,9 +173,12 @@ public final class MappingParameters {
positiveScoreImpact = field.positiveScoreImpact();
dims = field.dims();
if (type == FieldType.Dense_Vector) {
Assert.isTrue(dims >= 1 && dims <= 2048,
"Invalid required parameter! Dense_Vector value \"dims\" must be between 1 and 2048.");
Assert.isTrue(dims >= 1 && dims <= 4096,
"Invalid required parameter! Dense_Vector value \"dims\" must be between 1 and 4096.");
}
elementType = field.elementType();
knnSimilarity = field.knnSimilarity();
knnIndexOptions = field.knnIndexOptions().length > 0 ? field.knnIndexOptions()[0] : null;
Assert.isTrue(field.enabled() || type == FieldType.Object, "enabled false is only allowed for field type object");
enabled = field.enabled();
eagerGlobalOrdinals = field.eagerGlobalOrdinals();
@@ -182,6 +189,7 @@ 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();
@@ -212,9 +220,12 @@ public final class MappingParameters {
positiveScoreImpact = field.positiveScoreImpact();
dims = field.dims();
if (type == FieldType.Dense_Vector) {
Assert.isTrue(dims >= 1 && dims <= 2048,
"Invalid required parameter! Dense_Vector value \"dims\" must be between 1 and 2048.");
Assert.isTrue(dims >= 1 && dims <= 4096,
"Invalid required parameter! Dense_Vector value \"dims\" must be between 1 and 4096.");
}
elementType = field.elementType();
knnSimilarity = field.knnSimilarity();
knnIndexOptions = field.knnIndexOptions().length > 0 ? field.knnIndexOptions()[0] : null;
enabled = true;
eagerGlobalOrdinals = field.eagerGlobalOrdinals();
}
@@ -237,7 +248,7 @@ public 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<>();
@@ -354,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) {
@@ -17,7 +17,7 @@ package org.springframework.data.elasticsearch.core.join;
import java.util.Objects;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.annotation.PersistenceCreator;
import org.springframework.lang.Nullable;
/**
@@ -39,7 +39,7 @@ public class JoinField<ID> {
this(name, null);
}
@PersistenceConstructor
@PersistenceCreator
public JoinField(String name, @Nullable ID parent) {
this.name = name;
this.parent = parent;
@@ -0,0 +1,218 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.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 the original author 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;
@@ -73,13 +77,14 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
private @Nullable ElasticsearchPersistentProperty joinFieldProperty;
private @Nullable ElasticsearchPersistentProperty indexedIndexNameProperty;
private @Nullable Document.VersionType versionType;
private boolean createIndexAndMapping;
private boolean alwaysWriteMapping;
private final boolean createIndexAndMapping;
private final boolean alwaysWriteMapping;
private final Dynamic dynamic;
private final Map<String, ElasticsearchPersistentProperty> fieldNamePropertyCache = new ConcurrentHashMap<>();
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
@@ -230,11 +230,17 @@ public class ByQueryResponse {
return aborted;
}
@Nullable
public ElasticsearchErrorCause getElasticsearchErrorCause() {
return elasticsearchErrorCause;
}
/**
* Create a new {@link FailureBuilder} to build {@link Failure}
*
* @return a new {@link FailureBuilder} to build {@link Failure}
*/
public static FailureBuilder builder() {
return new FailureBuilder();
}

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