1
0
mirror of synced 2026-05-23 12:43:17 +00:00

Compare commits

..

97 Commits

Author SHA1 Message Date
Jens Schauder 44b1c9e848 Release version 5.3.2 (2024.0.2).
See #2932
2024-07-12 19:09:19 +02:00
Jens Schauder 1770f98a74 Prepare 5.3.2 (2024.0.2).
See #2932
2024-07-12 19:09:01 +02:00
Peter-Josef Meisch bad0a80313 Enable use of search_after with field_collapse.
Original Pull Request #2937
Closes #2935

(cherry picked from commit dd156b9e29)
2024-07-06 11:37:51 +02:00
Peter-Josef Meisch 92dd6e8599 Update migration-guide-5.2-5.3.adoc 2024-07-04 20:58:41 +02:00
Mark Paluch c793be8ab4 Switch to Broadcom docker proxy.
Closes #2934
2024-06-20 11:21:08 +02:00
Mark Paluch 07ae79f9ce After release cleanups.
See #2913
2024-06-14 10:48:00 +02:00
Mark Paluch 47c84b84af Prepare next development iteration.
See #2913
2024-06-14 10:47:59 +02:00
Mark Paluch 5ba1e5dc77 Release version 5.3.1 (2024.0.1).
See #2913
2024-06-14 10:45:40 +02:00
Mark Paluch 5ddcd55942 Prepare 5.3.1 (2024.0.1).
See #2913
2024-06-14 10:45:24 +02:00
Peter-Josef Meisch be4a77ad21 Update nav.adoc, add loink to migration guide 5.2 to 5.3 2024-05-27 19:39:26 +02:00
Peter-Josef Meisch 7fa3cb74a1 Upgrade to Elasticsearch 8.13.4.
Original Pull Request #2917
Closes #2916
2024-05-18 18:43:34 +00:00
Peter-Josef Meisch ba9edf8ec8 Fix max dim value for dense vector.
Closes #2911

(cherry picked from commit e997b39f68)
2024-05-18 18:26:23 +02:00
Mark Paluch e4a39ae285 After release cleanups.
See #2896
2024-05-17 12:03:29 +02:00
Mark Paluch 7392222793 Prepare next development iteration.
See #2896
2024-05-17 11:51:48 +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
191 changed files with 6560 additions and 1545 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
#Thu Dec 14 08:34:15 CET 2023
#Thu Dec 14 08:40:44 CET 2023
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
Vendored
+1 -1
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.3.x", threshold: hudson.model.Result.SUCCESS)
}
options {
+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
+11 -4
View File
@@ -5,12 +5,12 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>5.2.8</version>
<version>5.3.2</version>
<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<version>3.2.8</version>
<version>3.3.2</version>
</parent>
<name>Spring Data Elasticsearch</name>
@@ -18,10 +18,10 @@
<url>https://github.com/spring-projects/spring-data-elasticsearch</url>
<properties>
<springdata.commons>3.2.8</springdata.commons>
<springdata.commons>3.3.2</springdata.commons>
<!-- version of the ElasticsearchClient -->
<elasticsearch-java>8.11.4</elasticsearch-java>
<elasticsearch-java>8.13.4</elasticsearch-java>
<blockhound-junit>1.0.8.RELEASE</blockhound-junit>
<hoverfly>0.14.4</hoverfly>
@@ -324,6 +324,13 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>${archunit}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
+1 -1
View File
@@ -9,7 +9,7 @@
*** 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:elasticsearch.adoc[]
** xref:elasticsearch/clients.adoc[]
@@ -150,7 +150,7 @@ ClientConfiguration clientConfiguration = ClientConfiguration.builder()
return headers;
})
.withClientConfigurer( <.>
ElasticsearchClientConfigurationCallback.from(clientBuilder -> {
ElasticsearchHttpClientConfigurationCallback.from(clientBuilder -> {
// ...
return clientBuilder;
}))
@@ -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,21 @@
[[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-3-1]]
== New in Spring Data Elasticsearch 5.3.1
[[new-features.5-2-2]]
== New in Spring Data Elasticsearch 5.2.2
* Upgrade to Elasticsearch 8.11.3
* Upgrade to Elasticsearch 8.13.4.
[[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);
""");
@@ -6,7 +6,8 @@ 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
| 2024.0 | 5.3.1 | 8.13.4 | 6.1.x
| 2023.1 (Vaughan) | 5.2.x | 8.11.1 | 6.1.x
| 2023.0 (Ullmann) | 5.1.x | 8.7.1 | 6.0.x
| 2022.0 (Turing) | 5.0.xfootnote:oom[Out of maintenance] | 8.5.3 | 6.0.x
| 2021.2 (Raj) | 4.4.xfootnote:oom[] | 7.17.3 | 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,18 @@
[[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.
@@ -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;
@@ -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
@@ -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,7 +246,7 @@ 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 //
@@ -259,6 +278,7 @@ class CriteriaQueryProcessor {
break;
case BETWEEN:
Object[] ranges = (Object[]) value;
Assert.notNull(value, "value for a between condition must not be null");
queryBuilder //
.range(rb -> {
rb.field(fieldName);
@@ -282,10 +302,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 +354,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 +405,7 @@ class CriteriaQueryProcessor {
if (item != null) {
if (sb.length() > 0) {
if (!sb.isEmpty()) {
sb.append(' ');
}
sb.append('"');
@@ -391,4 +437,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,7 @@ import org.springframework.util.Assert;
* {@link org.springframework.data.elasticsearch.core.document.Document}
*
* @author Peter-Josef Meisch
* @author Haibo Liu
* @since 4.4
*/
final class DocumentAdapters {
@@ -73,7 +74,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, 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);
@@ -29,6 +29,7 @@ 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,14 +51,7 @@ 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;
@@ -72,6 +66,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 {
@@ -175,6 +170,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 +188,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 +449,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 +463,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 +475,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 +492,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 +547,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 +585,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 +600,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) {
@@ -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()));
}
@@ -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;
@@ -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;
@@ -54,6 +55,7 @@ public class NativeQuery extends BaseQuery {
private Map<String, JsonData> searchExtensions = Collections.emptyMap();
@Nullable private KnnQuery knnQuery;
@Nullable private List<KnnSearch> knnSearches = Collections.emptyList();
public NativeQuery(NativeQueryBuilder builder) {
super(builder);
@@ -71,6 +73,7 @@ public class NativeQuery extends BaseQuery {
}
this.springDataQuery = builder.getSpringDataQuery();
this.knnQuery = builder.getKnnQuery();
this.knnSearches = builder.getKnnSearches();
}
public NativeQuery(@Nullable Query query) {
@@ -129,6 +132,14 @@ public class NativeQuery extends BaseQuery {
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;
@@ -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;
@@ -47,11 +49,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 +95,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;
@@ -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));
}
}
@@ -61,6 +61,7 @@ import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.BaseQueryBuilder;
import org.springframework.data.elasticsearch.core.query.BulkOptions;
import org.springframework.data.elasticsearch.core.query.ByQueryResponse;
import org.springframework.data.elasticsearch.core.query.DeleteQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.SearchTemplateQuery;
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
@@ -79,6 +80,7 @@ import org.springframework.util.StringUtils;
*
* @author Peter-Josef Meisch
* @author Illia Ulianov
* @author Junghoon Ban
* @since 4.4
*/
public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearchTemplate {
@@ -179,6 +181,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 +395,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);
@@ -645,7 +677,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);
@@ -44,6 +44,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 +55,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 +68,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -105,10 +108,11 @@ 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 +157,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) {
@@ -169,7 +172,6 @@ class RequestConverter {
private Alias.Builder buildAlias(AliasActionParameters parameters, Alias.Builder aliasBuilder) {
// noinspection DuplicatedCode
if (parameters.getRouting() != null) {
aliasBuilder.routing(parameters.getRouting());
}
@@ -396,10 +398,7 @@ 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));
Map<String, JsonData> settings = getTemplateParams(putTemplateRequest.getSettings().entrySet());
builder.settings(settings);
}
@@ -415,7 +414,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 +447,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) {
@@ -638,7 +635,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) {
@@ -967,6 +964,78 @@ 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()));
}
}
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) {
@@ -1141,11 +1210,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 +1255,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()))//
@@ -1284,6 +1361,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 && nativeQuery.getKnnQuery() != null) ? 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) {
@@ -1371,8 +1477,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 +1500,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)));
@@ -1495,7 +1609,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 +1619,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);
@@ -1613,7 +1727,18 @@ class RequestConverter {
;
if (query.getKnnQuery() != null) {
builder.knn(query.getKnnQuery());
var kq = query.getKnnQuery();
builder.knn(ksb -> ksb
.field(kq.field())
.queryVector(kq.queryVector())
.numCandidates(kq.numCandidates())
.filter(kq.filter())
.similarity(kq.similarity()));
}
if (!isEmpty(query.getKnnSearches())) {
builder.knn(query.getKnnSearches());
}
if (!isEmpty(query.getAggregations())) {
@@ -1634,7 +1759,18 @@ class RequestConverter {
.sort(query.getSortOptions());
if (query.getKnnQuery() != null) {
builder.knn(query.getKnnQuery());
var kq = query.getKnnQuery();
builder.knn(ksb -> ksb
.field(kq.field())
.queryVector(kq.queryVector())
.numCandidates(kq.numCandidates())
.filter(kq.filter())
.similarity(kq.similarity()));
}
if (!isEmpty(query.getKnnSearches())) {
builder.knn(query.getKnnSearches());
}
if (!isEmpty(query.getAggregations())) {
@@ -1647,51 +1783,23 @@ 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) {
@SuppressWarnings("StatementWithEmptyBody")
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 +1883,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 +1903,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 +1911,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;
@@ -36,7 +35,12 @@ import co.elastic.clients.elasticsearch.indices.get_index_template.IndexTemplate
import co.elastic.clients.elasticsearch.indices.get_mapping.IndexMappingRecord;
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,7 +51,11 @@ 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;
@@ -121,8 +129,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 +190,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 +333,7 @@ class ResponseConverter {
}
private TemplateResponseData indexGetComponentTemplateData(IndexTemplateSummary indexTemplateSummary,
List<String> composedOf) {
List<String> composedOf) {
var mapping = typeMapping(indexTemplateSummary.mappings());
Function<IndexSettings, Settings> indexSettingsToSettings = indexSettings -> {
@@ -541,7 +548,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;
@@ -36,6 +38,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 +55,7 @@ import org.springframework.util.CollectionUtils;
* Factory class to create {@link SearchDocumentResponse} instances.
*
* @author Peter-Josef Meisch
* @author Haibo Liu
* @since 4.4
*/
class SearchDocumentResponseBuilder {
@@ -78,8 +82,9 @@ class SearchDocumentResponseBuilder {
Map<String, Aggregate> aggregations = responseBody.aggregations();
Map<String, List<Suggestion<EntityAsMap>>> suggest = responseBody.suggest();
var pointInTimeId = responseBody.pitId();
var shards = responseBody.shards();
return from(hitsMetadata, scrollId, pointInTimeId, aggregations, suggest, entityCreator, jsonpMapper);
return from(hitsMetadata, shards, scrollId, pointInTimeId, aggregations, suggest, entityCreator, jsonpMapper);
}
/**
@@ -98,13 +103,14 @@ 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();
return from(hitsMetadata, scrollId, pointInTimeId, aggregations, suggest, entityCreator, jsonpMapper);
return from(hitsMetadata, shards, scrollId, pointInTimeId, aggregations, suggest, entityCreator, jsonpMapper);
}
/**
@@ -120,8 +126,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, @Nullable Map<String, Aggregate> aggregations,
Map<String, List<Suggestion<EntityAsMap>>> suggestES, SearchDocumentResponse.EntityCreator<T> entityCreator,
JsonpMapper jsonpMapper) {
@@ -155,8 +161,18 @@ class SearchDocumentResponseBuilder {
Suggest suggest = suggestFrom(suggestES, entityCreator);
SearchShardStatistics shardStatistics = shards != null ? shardsFrom(shards) : null;
return new SearchDocumentResponse(totalHits, totalHitsRelation, maxScore, scrollId, pointInTimeId, searchDocuments,
aggregationsContainer, suggest);
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 +234,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.
*
@@ -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.
*
@@ -22,5 +22,5 @@ package org.springframework.data.elasticsearch.core;
* @since 4.2
*/
public enum RefreshPolicy {
NONE, IMMEDIATE, WAIT_UNTIL;
NONE, IMMEDIATE, WAIT_UNTIL
}
@@ -46,6 +46,7 @@ import org.springframework.util.Assert;
* @author Matt Gilene
* @author Sascha Woo
* @author Jakob Hoeper
* @author Haibo Liu
* @since 4.0
*/
public class SearchHitMapping<T> {
@@ -84,6 +85,7 @@ 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();
String scrollId = searchDocumentResponse.getScrollId();
String pointInTimeId = searchDocumentResponse.getPointInTimeId();
@@ -103,7 +105,7 @@ public class SearchHitMapping<T> {
mapHitsInCompletionSuggestion(suggest);
return new SearchHitsImpl<>(totalHits, totalHitsRelation, maxScore, scrollId, pointInTimeId, searchHits,
aggregations, suggest);
aggregations, suggest, shardStatistics);
}
@SuppressWarnings("unchecked")
@@ -165,7 +167,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 +235,15 @@ 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(),
scrollId,
searchHits.getPointInTimeId(),
convertedSearchHits,
searchHits.getAggregations(),
searchHits.getSuggest(),
searchHits.getSearchShardStatistics());
}
} catch (Exception e) {
throw new UncategorizedElasticsearchException("Unable to convert inner hits.", e);
@@ -284,8 +287,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;
/**
@@ -27,6 +27,7 @@ import org.springframework.lang.Nullable;
*
* @param <T> the result data class.
* @author Sascha Woo
* @author Haibo Liu
* @since 4.0
*/
public interface SearchHits<T> extends Streamable<SearchHit<T>> {
@@ -108,4 +109,10 @@ public interface SearchHits<T> extends Streamable<SearchHit<T>> {
*/
@Nullable
String getPointInTimeId();
/**
* @return shard statistics for the search hit.
*/
@Nullable
SearchShardStatistics getSearchShardStatistics();
}
@@ -29,6 +29,7 @@ import org.springframework.util.Assert;
* @param <T> the result data class.
* @author Peter-Josef Meisch
* @author Sascha Woo
* @author Haibo Liu
* @since 4.0
*/
public class SearchHitsImpl<T> implements SearchScrollHits<T> {
@@ -41,7 +42,8 @@ public class SearchHitsImpl<T> implements SearchScrollHits<T> {
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
@@ -53,7 +55,8 @@ public class SearchHitsImpl<T> implements SearchScrollHits<T> {
*/
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) {
@Nullable AggregationsContainer<?> aggregations, @Nullable Suggest suggest,
@Nullable SearchShardStatistics searchShardStatistics) {
Assert.notNull(searchHits, "searchHits must not be null");
@@ -66,6 +69,7 @@ public class SearchHitsImpl<T> implements SearchScrollHits<T> {
this.aggregations = aggregations;
this.suggest = suggest;
this.unmodifiableSearchHits = Lazy.of(() -> Collections.unmodifiableList(searchHits));
this.searchShardStatistics = searchShardStatistics;
}
// region getter
@@ -118,6 +122,11 @@ public class SearchHitsImpl<T> implements SearchScrollHits<T> {
return pointInTimeId;
}
@Override
public SearchShardStatistics getSearchShardStatistics() {
return searchShardStatistics;
}
@Override
public String toString() {
return "SearchHits{" + //
@@ -128,6 +137,7 @@ public class SearchHitsImpl<T> implements SearchScrollHits<T> {
", pointInTimeId='" + pointInTimeId + '\'' + //
", searchHits={" + searchHits.size() + " elements}" + //
", aggregations=" + aggregations + //
", shardStatistics=" + searchShardStatistics + //
'}';
}
}
@@ -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;
}
}
}
@@ -61,10 +61,10 @@ abstract class StreamQueries {
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
@@ -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");
@@ -20,6 +20,7 @@ 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 +28,7 @@ import org.springframework.lang.Nullable;
* This represents the complete search response from Elasticsearch, including the returned documents.
*
* @author Peter-Josef Meisch
* @author Haibo Liu
* @since 4.0
*/
public class SearchDocumentResponse {
@@ -40,10 +42,12 @@ public class SearchDocumentResponse {
@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) {
@Nullable AggregationsContainer<?> aggregationsContainer, @Nullable Suggest suggest,
@Nullable SearchShardStatistics searchShardStatistics) {
this.totalHits = totalHits;
this.totalHitsRelation = totalHitsRelation;
this.maxScore = maxScore;
@@ -52,6 +56,7 @@ public class SearchDocumentResponse {
this.searchDocuments = searchDocuments;
this.aggregations = aggregationsContainer;
this.suggest = suggest;
this.searchShardStatistics = searchShardStatistics;
}
public long getTotalHits() {
@@ -93,6 +98,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);
@@ -214,8 +214,9 @@ public class MappingBuilder {
@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);
@@ -289,6 +290,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());
}
}
}
@@ -432,7 +443,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());
}
@@ -84,6 +84,8 @@ public final class MappingParameters {
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;
@@ -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;
@@ -73,8 +73,8 @@ 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<>();
@@ -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();
}
@@ -316,6 +316,9 @@ public class Criteria {
Criteria orCriteria = new OrCriteria(this.criteriaChain, criteria.getField());
orCriteria.queryCriteriaEntries.addAll(criteria.queryCriteriaEntries);
orCriteria.filterCriteriaEntries.addAll(criteria.filterCriteriaEntries);
orCriteria.subCriteria.addAll(criteria.subCriteria);
orCriteria.boost = criteria.boost;
orCriteria.negating = criteria.isNegating();
return orCriteria;
}
@@ -670,8 +673,8 @@ public class Criteria {
*/
public Criteria boundedBy(String topLeftGeohash, String bottomRightGeohash) {
Assert.isTrue(!StringUtils.isEmpty(topLeftGeohash), "topLeftGeohash must not be empty");
Assert.isTrue(!StringUtils.isEmpty(bottomRightGeohash), "bottomRightGeohash must not be empty");
Assert.isTrue(StringUtils.hasLength(topLeftGeohash), "topLeftGeohash must not be empty");
Assert.isTrue(StringUtils.hasLength(bottomRightGeohash), "bottomRightGeohash must not be empty");
filterCriteriaEntries
.add(new CriteriaEntry(OperationKey.BBOX, new Object[] { topLeftGeohash, bottomRightGeohash }));
@@ -754,7 +757,7 @@ public class Criteria {
*/
public Criteria within(String geoLocation, String distance) {
Assert.isTrue(!StringUtils.isEmpty(geoLocation), "geoLocation value must not be null");
Assert.isTrue(StringUtils.hasLength(geoLocation), "geoLocation value must not be null");
filterCriteriaEntries.add(new CriteriaEntry(OperationKey.WITHIN, new Object[] { geoLocation, distance }));
return this;
@@ -813,6 +816,32 @@ public class Criteria {
filterCriteriaEntries.add(new CriteriaEntry(OperationKey.GEO_CONTAINS, geoShape));
return this;
}
/**
* Adds a new filter CriteriaEntry for HAS_CHILD.
*
* @param query the has_child query.
* @return the current Criteria.
*/
public Criteria hasChild(HasChildQuery query) {
Assert.notNull(query, "has_child query must not be null.");
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.HAS_CHILD, query));
return this;
}
/**
* Adds a new filter CriteriaEntry for HAS_PARENT.
*
* @param query the has_parent query.
* @return the current Criteria.
*/
public Criteria hasParent(HasParentQuery query) {
Assert.notNull(query, "has_parent query must not be null.");
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.HAS_PARENT, query));
return this;
}
// endregion
// region helper functions
@@ -974,7 +1003,11 @@ public class Criteria {
/**
* @since 5.1
*/
REGEXP;
REGEXP,
/**
* @since 5.3
*/
HAS_CHILD, HAS_PARENT;
/**
* @return true if this key does not have an associated value
@@ -0,0 +1,676 @@
/*
* 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.query;
import java.time.Duration;
import java.util.EnumSet;
import java.util.List;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.query.Query.SearchType;
import org.springframework.data.elasticsearch.core.query.types.ConflictsType;
import org.springframework.data.elasticsearch.core.query.types.OperatorType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Defines a delete request.
*
* @author Aouichaoui Youssef
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete-by-query.html">docs</a>
* @since 5.3
*/
public class DeleteQuery {
// For Lucene query
/**
* Query in the Lucene query string syntax.
*/
@Nullable private final String q;
/**
* If true, wildcard and prefix queries are analyzed. Defaults to false. This parameter can only be used when the
* lucene query {@code q} parameter is specified.
*/
@Nullable private final Boolean analyzeWildcard;
/**
* Analyzer to use for the query string. This parameter can only be used when the lucene query {@code q} parameter is
* specified.
*/
@Nullable private final String analyzer;
/**
* The default operator for a query string query: {@literal AND} or {@literal OR}. Defaults to {@literal OR}. This
* parameter can only be used when the lucene query {@code q} parameter is specified.
*/
@Nullable private final OperatorType defaultOperator;
/**
* Field to be used as the default when no field prefix is specified in the query string. This parameter can only be
* used when the lucene query {@code q} parameter is specified.
* <p>
* e.g: {@code {"query":{"prefix":{"user.name":{"value":"es"}}}} }
*/
@Nullable private final String df;
/**
* If a query contains errors related to the format of the data being entered, they will be disregarded unless
* specified otherwise. By default, this feature is turned off.
*/
@Nullable private final Boolean lenient;
// For ES query
/**
* An error will occur if the condition is {@code false} and any of the following are true: a wildcard expression, an
* index alias, or the {@literal _all value} only targets missing or closed indices. By default, this is set to
* {@code true}.
*/
@Nullable private final Boolean allowNoIndices;
/**
* Define the types of conflicts that occur when a query encounters version conflicts: abort or proceed. Defaults to
* abort.
*/
@Nullable private final ConflictsType conflicts;
/**
* Type of index that wildcard patterns can match. Defaults to {@literal open}.
*/
@Nullable private final EnumSet<IndicesOptions.WildcardStates> expandWildcards;
/**
* An error occurs if it is directed at an index that is missing or closed when it is {@code false}. By default, this
* is set to {@code false}.
*/
@Nullable private final Boolean ignoreUnavailable;
/**
* Maximum number of documents to process. Defaults to all documents.
*/
@Nullable private final Long maxDocs;
/**
* Specifies the node or shard the operation should be performed on.
*/
@Nullable private final String preference;
/**
* Use the request cache when it is {@code true}. By default, use the index-level setting.
*/
@Nullable private final Boolean requestCache;
/**
* Refreshes all shards involved in the deleting by query after the request completes when it is {@code true}. By
* default, this is set to {@code false}.
*/
@Nullable private final Boolean refresh;
/**
* Limited this request to a certain number of sub-requests per second. By default, this is set to {@code -1} (no
* throttle).
*/
@Nullable private final Float requestsPerSecond;
/**
* Custom value used to route operations to a specific shard.
*/
@Nullable private final String routing;
/**
* Period to retain the search context for scrolling.
*/
@Nullable private final Duration scroll;
/**
* Size of the scroll request that powers the operation. By default, this is set to {@code 1000}.
*/
@Nullable private final Long scrollSize;
/**
* The type of the search operation.
*/
@Nullable private final SearchType searchType;
/**
* Explicit timeout for each search request. By default, this is set to no timeout.
*/
@Nullable private final Duration searchTimeout;
/**
* The number of slices this task should be divided into. By default, this is set to {@code 1} meaning the task isnt
* sliced into subtasks.
*/
@Nullable private final Integer slices;
/**
* Sort search results in a specific order.
*/
@Nullable private final Sort sort;
/**
* Specific {@code tag} of the request for logging and statistical purposes.
*/
@Nullable private final List<String> stats;
/**
* The Maximum number of documents that can be collected for each shard. If a query exceeds this limit, Elasticsearch
* will stop the query.
*/
@Nullable private final Long terminateAfter;
/**
* Period each deletion request waits for active shards. By default, this is set to {@code 1m} (one minute).
*/
@Nullable private final Duration timeout;
/**
* Returns the document version as part of a hit.
*/
@Nullable private final Boolean version;
// Body
/**
* Query that specifies the documents to delete.
*/
private final Query query;
public static Builder builder(Query query) {
return new Builder(query);
}
private DeleteQuery(Builder builder) {
this.q = builder.luceneQuery;
this.analyzeWildcard = builder.analyzeWildcard;
this.analyzer = builder.analyzer;
this.defaultOperator = builder.defaultOperator;
this.df = builder.defaultField;
this.lenient = builder.lenient;
this.allowNoIndices = builder.allowNoIndices;
this.conflicts = builder.conflicts;
this.expandWildcards = builder.expandWildcards;
this.ignoreUnavailable = builder.ignoreUnavailable;
this.maxDocs = builder.maxDocs;
this.preference = builder.preference;
this.requestCache = builder.requestCache;
this.refresh = builder.refresh;
this.requestsPerSecond = builder.requestsPerSecond;
this.routing = builder.routing;
this.scroll = builder.scrollTime;
this.scrollSize = builder.scrollSize;
this.searchType = builder.searchType;
this.searchTimeout = builder.searchTimeout;
this.slices = builder.slices;
this.sort = builder.sort;
this.stats = builder.stats;
this.terminateAfter = builder.terminateAfter;
this.timeout = builder.timeout;
this.version = builder.version;
this.query = builder.query;
}
@Nullable
public String getQ() {
return q;
}
@Nullable
public Boolean getAnalyzeWildcard() {
return analyzeWildcard;
}
@Nullable
public String getAnalyzer() {
return analyzer;
}
@Nullable
public OperatorType getDefaultOperator() {
return defaultOperator;
}
@Nullable
public String getDf() {
return df;
}
@Nullable
public Boolean getLenient() {
return lenient;
}
@Nullable
public Boolean getAllowNoIndices() {
return allowNoIndices;
}
@Nullable
public ConflictsType getConflicts() {
return conflicts;
}
@Nullable
public EnumSet<IndicesOptions.WildcardStates> getExpandWildcards() {
return expandWildcards;
}
@Nullable
public Boolean getIgnoreUnavailable() {
return ignoreUnavailable;
}
@Nullable
public Long getMaxDocs() {
return maxDocs;
}
@Nullable
public String getPreference() {
return preference;
}
@Nullable
public Boolean getRequestCache() {
return requestCache;
}
@Nullable
public Boolean getRefresh() {
return refresh;
}
@Nullable
public Float getRequestsPerSecond() {
return requestsPerSecond;
}
@Nullable
public String getRouting() {
return routing;
}
@Nullable
public Duration getScroll() {
return scroll;
}
@Nullable
public Long getScrollSize() {
return scrollSize;
}
@Nullable
public SearchType getSearchType() {
return searchType;
}
@Nullable
public Duration getSearchTimeout() {
return searchTimeout;
}
@Nullable
public Integer getSlices() {
return slices;
}
@Nullable
public Sort getSort() {
return sort;
}
@Nullable
public List<String> getStats() {
return stats;
}
@Nullable
public Long getTerminateAfter() {
return terminateAfter;
}
@Nullable
public Duration getTimeout() {
return timeout;
}
@Nullable
public Boolean getVersion() {
return version;
}
@Nullable
public Query getQuery() {
return query;
}
public static final class Builder {
// For Lucene query
@Nullable private String luceneQuery;
@Nullable private Boolean analyzeWildcard;
@Nullable private String analyzer;
@Nullable private OperatorType defaultOperator;
@Nullable private String defaultField;
@Nullable private Boolean lenient;
// For ES query
@Nullable private Boolean allowNoIndices;
@Nullable private ConflictsType conflicts;
@Nullable private EnumSet<IndicesOptions.WildcardStates> expandWildcards;
@Nullable private Boolean ignoreUnavailable;
@Nullable private Long maxDocs;
@Nullable private String preference;
@Nullable private Boolean requestCache;
@Nullable private Boolean refresh;
@Nullable private Float requestsPerSecond;
@Nullable private String routing;
@Nullable private Duration scrollTime;
@Nullable private Long scrollSize;
@Nullable private SearchType searchType;
@Nullable private Duration searchTimeout;
@Nullable private Integer slices;
@Nullable private Sort sort;
@Nullable private List<String> stats;
@Nullable private Long terminateAfter;
@Nullable private Duration timeout;
@Nullable private Boolean version;
// Body
private final Query query;
private Builder(Query query) {
Assert.notNull(query, "query must not be null");
this.query = query;
}
/**
* Query in the Lucene query string syntax.
*/
public Builder withLuceneQuery(@Nullable String luceneQuery) {
this.luceneQuery = luceneQuery;
return this;
}
/**
* If true, wildcard and prefix queries are analyzed. Defaults to false. This parameter can only be used when the
* lucene query {@code q} parameter is specified.
*/
public Builder withAnalyzeWildcard(@Nullable Boolean analyzeWildcard) {
this.analyzeWildcard = analyzeWildcard;
return this;
}
/**
* Analyzer to use for the query string. This parameter can only be used when the lucene query {@code q} parameter
* is specified.
*/
public Builder withAnalyzer(@Nullable String analyzer) {
this.analyzer = analyzer;
return this;
}
/**
* The default operator for a query string query: {@literal AND} or {@literal OR}. Defaults to {@literal OR}. This
* parameter can only be used when the lucene query {@code q} parameter is specified.
*/
public Builder withDefaultOperator(@Nullable OperatorType defaultOperator) {
this.defaultOperator = defaultOperator;
return this;
}
/**
* Field to be used as the default when no field prefix is specified in the query string. This parameter can only be
* used when the lucene query {@code q} parameter is specified.
* <p>
* e.g: {@code {"query":{"prefix":{"user.name":{"value":"es"}}}} }
*/
public Builder withDefaultField(@Nullable String defaultField) {
this.defaultField = defaultField;
return this;
}
/**
* If a query contains errors related to the format of the data being entered, they will be disregarded unless
* specified otherwise. By default, this feature is turned off.
*/
public Builder withLenient(@Nullable Boolean lenient) {
this.lenient = lenient;
return this;
}
/**
* An error will occur if the condition is {@code false} and any of the following are true: a wildcard expression,
* an index alias, or the {@literal _all value} only targets missing or closed indices. By default, this is set to
* {@code true}.
*/
public Builder withAllowNoIndices(@Nullable Boolean allowNoIndices) {
this.allowNoIndices = allowNoIndices;
return this;
}
/**
* Define the types of conflicts that occur when a query encounters version conflicts: abort or proceed. Defaults to
* abort.
*/
public Builder withConflicts(@Nullable ConflictsType conflicts) {
this.conflicts = conflicts;
return this;
}
/**
* Type of index that wildcard patterns can match. Defaults to {@literal open}.
*/
public Builder setExpandWildcards(@Nullable EnumSet<IndicesOptions.WildcardStates> expandWildcards) {
this.expandWildcards = expandWildcards;
return this;
}
/**
* An error occurs if it is directed at an index that is missing or closed when it is {@code false}. By default,
* this is set to {@code false}.
*/
public Builder withIgnoreUnavailable(@Nullable Boolean ignoreUnavailable) {
this.ignoreUnavailable = ignoreUnavailable;
return this;
}
/**
* Maximum number of documents to process. Defaults to all documents.
*/
public Builder withMaxDocs(@Nullable Long maxDocs) {
this.maxDocs = maxDocs;
return this;
}
/**
* Specifies the node or shard the operation should be performed on.
*/
public Builder withPreference(@Nullable String preference) {
this.preference = preference;
return this;
}
/**
* Use the request cache when it is {@code true}. By default, use the index-level setting.
*/
public Builder withRequestCache(@Nullable Boolean requestCache) {
this.requestCache = requestCache;
return this;
}
/**
* Refreshes all shards involved in the deleting by query after the request completes when it is {@code true}. By
* default, this is set to {@code false}.
*/
public Builder withRefresh(@Nullable Boolean refresh) {
this.refresh = refresh;
return this;
}
/**
* Limited this request to a certain number of sub-requests per second. By default, this is set to {@code -1} (no
* throttle).
*/
public Builder withRequestsPerSecond(@Nullable Float requestsPerSecond) {
this.requestsPerSecond = requestsPerSecond;
return this;
}
/**
* Custom value used to route operations to a specific shard.
*/
public Builder withRouting(@Nullable String routing) {
this.routing = routing;
return this;
}
/**
* Period to retain the search context for scrolling.
*/
public Builder withScrollTime(@Nullable Duration scrollTime) {
this.scrollTime = scrollTime;
return this;
}
/**
* Size of the scroll request that powers the operation. By default, this is set to {@code 1000}.
*/
public Builder withScrollSize(@Nullable Long scrollSize) {
this.scrollSize = scrollSize;
return this;
}
/**
* The type of the search operation.
*/
public Builder withSearchType(@Nullable SearchType searchType) {
this.searchType = searchType;
return this;
}
/**
* Explicit timeout for each search request. By default, this is set to no timeout.
*/
public Builder withSearchTimeout(@Nullable Duration searchTimeout) {
this.searchTimeout = searchTimeout;
return this;
}
/**
* The number of slices this task should be divided into. By default, this is set to {@code 1} meaning the task
* isnt sliced into subtasks.
*/
public Builder withSlices(@Nullable Integer slices) {
this.slices = slices;
return this;
}
/**
* Sort search results in a specific order.
*/
public Builder withSort(@Nullable Sort sort) {
this.sort = sort;
return this;
}
/**
* Specific {@code tag} of the request for logging and statistical purposes.
*/
public Builder withStats(@Nullable List<String> stats) {
this.stats = stats;
return this;
}
/**
* The Maximum number of documents that can be collected for each shard. If a query exceeds this limit,
* Elasticsearch will stop the query.
*/
public Builder withTerminateAfter(@Nullable Long terminateAfter) {
this.terminateAfter = terminateAfter;
return this;
}
/**
* Period each deletion request waits for active shards. By default, this is set to {@code 1m} (one minute).
*/
public Builder withTimeout(@Nullable Duration timeout) {
this.timeout = timeout;
return this;
}
/**
* Returns the document version as part of a hit.
*/
public Builder withVersion(@Nullable Boolean version) {
this.version = version;
return this;
}
public DeleteQuery build() {
if (luceneQuery == null) {
if (defaultField != null) {
throw new IllegalArgumentException("When defining the df parameter, you must include the Lucene query.");
}
if (analyzer != null) {
throw new IllegalArgumentException(
"When defining the analyzer parameter, you must include the Lucene query.");
}
if (analyzeWildcard != null) {
throw new IllegalArgumentException(
"When defining the analyzeWildcard parameter, you must include the Lucene query.");
}
if (defaultOperator != null) {
throw new IllegalArgumentException(
"When defining the defaultOperator parameter, you must include the Lucene query.");
}
if (lenient != null) {
throw new IllegalArgumentException("When defining the lenient parameter, you must include the Lucene query.");
}
}
return new DeleteQuery(this);
}
}
}
@@ -0,0 +1,204 @@
/*
* 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.query;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Defines a has_child request.
*
* @author Aouichaoui Youssef
* @see <a href=
* "https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-has-child-query.html">docs</a>
* @since 5.3
*/
public class HasChildQuery {
/**
* Name of the child relationship mapped for the join field.
*/
private final String type;
/**
* Query that specifies the documents to run on child documents of the {@link #type} field.
*/
private final Query query;
/**
* Indicates whether to ignore an unmapped {@link #type} and not return any documents instead of an error. Default,
* this is set to {@code false}.
*/
@Nullable private final Boolean ignoreUnmapped;
/**
* The Maximum number of child documents that match the {@link #query} allowed for a returned parent document. If the
* parent document exceeds this limit, it is excluded from the search results.
*/
@Nullable private final Integer maxChildren;
/**
* Minimum number of child documents that match the query required to match the {@link #query} for a returned parent
* document. If the parent document does not meet this limit, it is excluded from the search results.
*/
@Nullable private final Integer minChildren;
/**
* Indicates how scores for matching child documents affect the root parent documents relevance score.
*/
@Nullable private final ScoreMode scoreMode;
/**
* Obtaining nested objects and documents that have a parent-child relationship.
*/
@Nullable private final InnerHitsQuery innerHitsQuery;
public static Builder builder(String type) {
return new Builder(type);
}
private HasChildQuery(Builder builder) {
this.type = builder.type;
this.query = builder.query;
this.innerHitsQuery = builder.innerHitsQuery;
this.ignoreUnmapped = builder.ignoreUnmapped;
this.maxChildren = builder.maxChildren;
this.minChildren = builder.minChildren;
this.scoreMode = builder.scoreMode;
}
public String getType() {
return type;
}
public Query getQuery() {
return query;
}
@Nullable
public Boolean getIgnoreUnmapped() {
return ignoreUnmapped;
}
@Nullable
public Integer getMaxChildren() {
return maxChildren;
}
@Nullable
public Integer getMinChildren() {
return minChildren;
}
@Nullable
public ScoreMode getScoreMode() {
return scoreMode;
}
@Nullable
public InnerHitsQuery getInnerHitsQuery() {
return innerHitsQuery;
}
public enum ScoreMode {
Default, Avg, Max, Min, Sum
}
public static final class Builder {
private final String type;
private Query query;
@Nullable private Boolean ignoreUnmapped;
@Nullable private Integer maxChildren;
@Nullable private Integer minChildren;
@Nullable private ScoreMode scoreMode;
@Nullable private InnerHitsQuery innerHitsQuery;
private Builder(String type) {
Assert.notNull(type, "type must not be null");
this.type = type;
}
/**
* Query that specifies the documents to run on child documents of the {@link #type} field.
*/
public Builder withQuery(Query query) {
this.query = query;
return this;
}
/**
* Indicates whether to ignore an unmapped {@link #type} and not return any documents instead of an error. Default,
* this is set to {@code false}.
*/
public Builder withIgnoreUnmapped(@Nullable Boolean ignoreUnmapped) {
this.ignoreUnmapped = ignoreUnmapped;
return this;
}
/**
* The Maximum number of child documents that match the {@link #query} allowed for a returned parent document. If
* the parent document exceeds this limit, it is excluded from the search results.
*/
public Builder withMaxChildren(@Nullable Integer maxChildren) {
this.maxChildren = maxChildren;
return this;
}
/**
* Minimum number of child documents that match the query required to match the {@link #query} for a returned parent
* document. If the parent document does not meet this limit, it is excluded from the search results.
*/
public Builder withMinChildren(@Nullable Integer minChildren) {
this.minChildren = minChildren;
return this;
}
/**
* Indicates how scores for matching child documents affect the root parent documents relevance score.
*/
public Builder withScoreMode(@Nullable ScoreMode scoreMode) {
this.scoreMode = scoreMode;
return this;
}
/**
* Obtaining nested objects and documents that have a parent-child relationship.
*/
public Builder withInnerHitsQuery(@Nullable InnerHitsQuery innerHitsQuery) {
this.innerHitsQuery = innerHitsQuery;
return this;
}
public HasChildQuery build() {
Assert.notNull(query, "query must not be null.");
return new HasChildQuery(this);
}
}
}
@@ -0,0 +1,141 @@
/*
* 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.query;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Defines a has_parent request.
*
* @author Aouichaoui Youssef
* @see <a href=
* "https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-has-parent-query.html">docs</a>
* @since 5.3
*/
public class HasParentQuery {
/**
* Name of the parent relationship mapped for the join field.
*/
private final String parentType;
/**
* Query that specifies the documents to run on parent documents of the {@link #parentType} field.
*/
private final Query query;
/**
* Indicates whether the relevance score of a matching parent document is aggregated into its child documents.
* Default, this is set to {@code false}.
*/
@Nullable private final Boolean score;
/**
* Indicates whether to ignore an unmapped {@link #parentType} and not return any documents instead of an error.
* Default, this is set to {@code false}.
*/
@Nullable private final Boolean ignoreUnmapped;
/**
* Obtaining nested objects and documents that have a parent-child relationship.
*/
@Nullable private final InnerHitsQuery innerHitsQuery;
public static Builder builder(String parentType) {
return new Builder(parentType);
}
private HasParentQuery(Builder builder) {
this.parentType = builder.parentType;
this.query = builder.query;
this.innerHitsQuery = builder.innerHitsQuery;
this.score = builder.score;
this.ignoreUnmapped = builder.ignoreUnmapped;
}
public String getParentType() {
return parentType;
}
public Query getQuery() {
return query;
}
@Nullable
public Boolean getScore() {
return score;
}
@Nullable
public Boolean getIgnoreUnmapped() {
return ignoreUnmapped;
}
@Nullable
public InnerHitsQuery getInnerHitsQuery() {
return innerHitsQuery;
}
public static class Builder {
private final String parentType;
private Query query;
@Nullable private Boolean score;
@Nullable private Boolean ignoreUnmapped;
@Nullable private InnerHitsQuery innerHitsQuery;
private Builder(String parentType) {
Assert.notNull(parentType, "parent_type must not be null.");
this.parentType = parentType;
}
public Builder withQuery(Query query) {
this.query = query;
return this;
}
public Builder withScore(@Nullable Boolean score) {
this.score = score;
return this;
}
public Builder withIgnoreUnmapped(@Nullable Boolean ignoreUnmapped) {
this.ignoreUnmapped = ignoreUnmapped;
return this;
}
/**
* Obtaining nested objects and documents that have a parent-child relationship.
*/
public Builder withInnerHitsQuery(@Nullable InnerHitsQuery innerHitsQuery) {
this.innerHitsQuery = innerHitsQuery;
return this;
}
public HasParentQuery build() {
Assert.notNull(query, "query must not be null.");
return new HasParentQuery(this);
}
}
}
@@ -23,8 +23,8 @@ package org.springframework.data.elasticsearch.core.query;
*/
public class IndexBoost {
private String indexName;
private float boost;
private final String indexName;
private final float boost;
public IndexBoost(String indexName, float boost) {
this.indexName = indexName;
@@ -25,8 +25,8 @@ import java.util.EnumSet;
*/
public class IndicesOptions {
private EnumSet<Option> options;
private EnumSet<WildcardStates> expandWildcards;
private final EnumSet<Option> options;
private final EnumSet<WildcardStates> expandWildcards;
public static final IndicesOptions STRICT_EXPAND_OPEN = new IndicesOptions(
EnumSet.of(IndicesOptions.Option.ALLOW_NO_INDICES), EnumSet.of(IndicesOptions.WildcardStates.OPEN));
@@ -101,10 +101,10 @@ public class IndicesOptions {
}
public enum WildcardStates {
OPEN, CLOSED, HIDDEN, ALL, NONE;
OPEN, CLOSED, HIDDEN, ALL, NONE
}
public enum Option {
IGNORE_UNAVAILABLE, IGNORE_ALIASES, ALLOW_NO_INDICES, FORBID_ALIASES_TO_MULTIPLE_INDICES, FORBID_CLOSED_INDICES, IGNORE_THROTTLED;
IGNORE_UNAVAILABLE, IGNORE_ALIASES, ALLOW_NO_INDICES, FORBID_ALIASES_TO_MULTIPLE_INDICES, FORBID_CLOSED_INDICES, IGNORE_THROTTLED
}
}
@@ -0,0 +1,107 @@
/*
* 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.query;
import org.springframework.lang.Nullable;
/**
* Defines an inner_hits request.
*
* @author Aouichaoui Youssef
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/inner-hits.html">docs</a>
* @since 5.3
*/
public class InnerHitsQuery {
/**
* The name to be used for the particular inner hit definition in the response.
*/
@Nullable private final String name;
/**
* The maximum number of hits to return.
*/
@Nullable private final Integer size;
/**
* The offset from where the first hit to fetch.
*/
@Nullable private final Integer from;
public static Builder builder() {
return new Builder();
}
private InnerHitsQuery(Builder builder) {
this.name = builder.name;
this.from = builder.from;
this.size = builder.size;
}
@Nullable
public String getName() {
return name;
}
@Nullable
public Integer getSize() {
return size;
}
@Nullable
public Integer getFrom() {
return from;
}
public static final class Builder {
@Nullable private String name;
@Nullable private Integer size;
@Nullable private Integer from;
private Builder() {}
/**
* The name to be used for the particular inner hit definition in the response.
*/
public Builder withName(@Nullable String name) {
this.name = name;
return this;
}
/**
* The maximum number of hits to return.
*/
public Builder withSize(@Nullable Integer size) {
this.size = size;
return this;
}
/**
* The offset from where the first hit to fetch.
*/
public Builder withFrom(@Nullable Integer from) {
this.from = from;
return this;
}
public InnerHitsQuery build() {
return new InnerHitsQuery(this);
}
}
}
@@ -15,13 +15,12 @@
*/
package org.springframework.data.elasticsearch.core.query;
import java.util.function.Function;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import java.util.function.BiFunction;
import java.util.function.Function;
/**
* Extends the {@link Sort.Order} with properties that can be set on Elasticsearch order options.
*
@@ -143,18 +142,19 @@ public class Order extends Sort.Order {
}
public static class Nested {
private String path;
@Nullable private Query filter;
private final String path;
@Nullable private final Query filter;
@Nullable private Integer maxChildren = null;
@Nullable private Nested nested;
@Nullable private final Nested nested;
public static Nested of(String path, Function<Nested.Builder, Nested.Builder> builderFunction) {
Assert.notNull(path, "path must not be null");
Assert.notNull(builderFunction, "builderFunction must not be null");
Assert.notNull(path, "path must not be null");
Assert.notNull(builderFunction, "builderFunction must not be null");
return builderFunction.apply(builder(path)).build();
return builderFunction.apply(builder(path)).build();
}
public Nested(String path, @Nullable Query filter, @Nullable Integer maxChildren, @Nullable Nested nested) {
Assert.notNull(path, "path must not be null");
@@ -189,7 +189,7 @@ public class Order extends Sort.Order {
}
public static class Builder {
private String path;
private final String path;
@Nullable private Query filter = null;
@Nullable private Integer maxChildren = null;
@Nullable private Nested nested = null;
@@ -203,8 +203,9 @@ public class Order extends Sort.Order {
/**
* Sets the filter query for a nested sort.<br/>
* Note: This cannot be a {@link CriteriaQuery}, as that would be sent as a nested query within the filter,
* use a {@link org.springframework.data.elasticsearch.client.elc.NativeQuery} or {@link StringQuery} instead.
* Note: This cannot be a {@link CriteriaQuery}, as that would be sent as a nested query within the filter, use a
* {@link org.springframework.data.elasticsearch.client.elc.NativeQuery} or {@link StringQuery} instead.
*
* @param filter the filter to set
* @return this builder
* @throws IllegalArgumentException when a {@link CriteriaQuery} is passed.
@@ -447,7 +447,7 @@ public interface Query {
@Nullable
default PointInTime getPointInTime() {
return null;
};
}
/**
* returns the number of documents that are requested when the reactive code does a batched search operation. This is
@@ -469,7 +469,8 @@ public interface Query {
/**
* @since 5.1
*/
@Nullable EnumSet<IndicesOptions.WildcardStates> getExpandWildcards();
@Nullable
EnumSet<IndicesOptions.WildcardStates> getExpandWildcards();
/**
* @return a possible empty list of docvalue_field values to be set on the query.
@@ -19,6 +19,8 @@ import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import java.util.Objects;
/**
* The most trivial implementation of a Field. The {@link #name} is updatable, so it may be changed during query
* preparation by the {@link org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter}.
@@ -79,4 +81,16 @@ public class SimpleField implements Field {
public String toString() {
return getName();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SimpleField that)) return false;
return Objects.equals(name, that.name) && Objects.equals(fieldType, that.fieldType) && Objects.equals(path, that.path);
}
@Override
public int hashCode() {
return Objects.hash(name, fieldType, path);
}
}
@@ -26,7 +26,7 @@ import org.springframework.util.Assert;
*/
public class UpdateResponse {
private Result result;
private final Result result;
public UpdateResponse(Result result) {
@@ -47,6 +47,6 @@ public class UpdateResponse {
}
public enum Result {
CREATED, UPDATED, DELETED, NOT_FOUND, NOOP;
CREATED, UPDATED, DELETED, NOT_FOUND, NOOP
}
}
@@ -15,14 +15,13 @@
*/
package org.springframework.data.elasticsearch.core.query.highlight;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.util.Assert;
/**
* @author Peter-Josef Meisch
* @author Haibo Liu
* @since 4.3
*/
public class Highlight {
@@ -57,42 +56,4 @@ public class Highlight {
public List<HighlightField> getFields() {
return fields;
}
/**
* Creates a {@link Highlight} from an Annotation instance.
*
* @param highlight must not be {@literal null}
* @return highlight definition
*/
public static Highlight of(org.springframework.data.elasticsearch.annotations.Highlight highlight) {
Assert.notNull(highlight, "highlight must not be null");
org.springframework.data.elasticsearch.annotations.HighlightParameters parameters = highlight.parameters();
HighlightParameters highlightParameters = HighlightParameters.builder() //
.withBoundaryChars(parameters.boundaryChars()) //
.withBoundaryMaxScan(parameters.boundaryMaxScan()) //
.withBoundaryScanner(parameters.boundaryScanner()) //
.withBoundaryScannerLocale(parameters.boundaryScannerLocale()) //
.withEncoder(parameters.encoder()) //
.withForceSource(parameters.forceSource()) //
.withFragmenter(parameters.fragmenter()) //
.withFragmentSize(parameters.fragmentSize()) //
.withNoMatchSize(parameters.noMatchSize()) //
.withNumberOfFragments(parameters.numberOfFragments()) //
.withOrder(parameters.order()) //
.withPhraseLimit(parameters.phraseLimit()) //
.withPreTags(parameters.preTags()) //
.withPostTags(parameters.postTags()) //
.withRequireFieldMatch(parameters.requireFieldMatch()) //
.withTagsSchema(parameters.tagsSchema()) //
.withType(parameters.type()) //
.build();
List<HighlightField> highlightFields = Arrays.stream(highlight.fields()) //
.map(HighlightField::of) //
.collect(Collectors.toList());
return new Highlight(highlightParameters, highlightFields);
}
}
@@ -15,10 +15,13 @@
*/
package org.springframework.data.elasticsearch.core.query.highlight;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* @author Peter-Josef Meisch
* @author Haibo Liu
* @since 4.3
*/
public abstract class HighlightCommonParameters {
@@ -31,6 +34,7 @@ public abstract class HighlightCommonParameters {
private final int fragmentSize;
private final int noMatchSize;
private final int numberOfFragments;
@Nullable private final Query highlightQuery;
private final String order;
private final int phraseLimit;
private final String[] preTags;
@@ -51,6 +55,7 @@ public abstract class HighlightCommonParameters {
fragmentSize = builder.fragmentSize;
noMatchSize = builder.noMatchSize;
numberOfFragments = builder.numberOfFragments;
highlightQuery = builder.highlightQuery;
order = builder.order;
phraseLimit = builder.phraseLimit;
preTags = builder.preTags;
@@ -95,6 +100,11 @@ public abstract class HighlightCommonParameters {
return numberOfFragments;
}
@Nullable
public Query getHighlightQuery() {
return highlightQuery;
}
public String getOrder() {
return order;
}
@@ -130,6 +140,10 @@ public abstract class HighlightCommonParameters {
private int fragmentSize = -1;
private int noMatchSize = -1;
private int numberOfFragments = -1;
/**
* Only the search query part of the {@link Query} takes effect, others are just ignored.
*/
@Nullable private Query highlightQuery = null;
private String order = "";
private int phraseLimit = -1;
private String[] preTags = new String[0];
@@ -184,6 +198,11 @@ public abstract class HighlightCommonParameters {
return (SELF) this;
}
public SELF withHighlightQuery(@Nullable Query highlightQuery) {
this.highlightQuery = highlightQuery;
return (SELF) this;
}
public SELF withOrder(String order) {
this.order = order;
return (SELF) this;
@@ -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.core.query.types;
/**
* Define the types of conflicts that occur when a query encounters version conflicts.
*
* @author Aouichaoui Youssef
* @since 5.3
*/
public enum ConflictsType {
Abort, Proceed
}
@@ -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.core.query.types;
/**
* Define the default operator for a query string query.
*
* @author Aouichaoui Youssef
* @since 5.3
*/
public enum OperatorType {
And, Or
}
@@ -0,0 +1,3 @@
@org.springframework.lang.NonNullApi
@org.springframework.lang.NonNullFields
package org.springframework.data.elasticsearch.core.query.types;
@@ -19,6 +19,7 @@ import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
@@ -41,6 +42,7 @@ import org.w3c.dom.Element;
* @author Mohsin Husen
* @author Mark Paluch
* @author Christoph Strobl
* @author Junghoon Ban
*/
public class ElasticsearchRepositoryConfigExtension extends RepositoryConfigurationExtensionSupport {
@@ -106,7 +108,7 @@ public class ElasticsearchRepositoryConfigExtension extends RepositoryConfigurat
*/
@Override
protected Collection<Class<?>> getIdentifyingTypes() {
return Arrays.asList(ElasticsearchRepository.class, ElasticsearchRepository.class);
return List.of(ElasticsearchRepository.class);
}
/*
@@ -15,20 +15,18 @@
*/
package org.springframework.data.elasticsearch.repository.query;
import java.util.Collections;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHitSupport;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.SearchHitsImpl;
import org.springframework.data.elasticsearch.core.TotalHitsRelation;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.DeleteQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.util.StreamUtils;
@@ -42,6 +40,7 @@ import org.springframework.util.ClassUtils;
* @author Rizwan Idrees
* @author Mohsin Husen
* @author Peter-Josef Meisch
* @author Haibo Liu
*/
public abstract class AbstractElasticsearchRepositoryQuery implements RepositoryQuery {
@@ -50,12 +49,20 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository
protected ElasticsearchQueryMethod queryMethod;
protected final ElasticsearchOperations elasticsearchOperations;
protected final ElasticsearchConverter elasticsearchConverter;
protected final QueryMethodEvaluationContextProvider evaluationContextProvider;
public AbstractElasticsearchRepositoryQuery(ElasticsearchQueryMethod queryMethod,
ElasticsearchOperations elasticsearchOperations) {
ElasticsearchOperations elasticsearchOperations,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(queryMethod, "queryMethod must not be null");
Assert.notNull(elasticsearchOperations, "elasticsearchOperations must not be null");
Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null");
this.queryMethod = queryMethod;
this.elasticsearchOperations = elasticsearchOperations;
this.elasticsearchConverter = elasticsearchOperations.getElasticsearchConverter();
this.evaluationContextProvider = evaluationContextProvider;
}
@Override
@@ -88,7 +95,7 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository
if (isDeleteQuery()) {
result = countOrGetDocumentsForDelete(query, parameterAccessor);
elasticsearchOperations.delete(query, clazz, index);
elasticsearchOperations.delete(DeleteQuery.builder(query).build(), clazz, index);
elasticsearchOperations.indexOps(index).refresh();
} else if (isCountQuery()) {
result = elasticsearchOperations.count(query, clazz, index);
@@ -107,24 +114,13 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository
: PageRequest.of(0, DEFAULT_STREAM_BATCH_SIZE));
result = StreamUtils.createStreamFromIterator(elasticsearchOperations.searchForStream(query, clazz, index));
} else if (queryMethod.isCollectionQuery()) {
if (parameterAccessor.getPageable().isUnpaged()) {
int itemCount = (int) elasticsearchOperations.count(query, clazz, index);
if (itemCount == 0) {
result = new SearchHitsImpl<>(0, TotalHitsRelation.EQUAL_TO, Float.NaN, null,
query.getPointInTime() != null ? query.getPointInTime().id() : null, Collections.emptyList(), null, null);
} else {
query.setPageable(PageRequest.of(0, Math.max(1, itemCount)));
}
query.setPageable(PageRequest.of(0, Math.max(1, itemCount)));
} else {
query.setPageable(parameterAccessor.getPageable());
}
if (result == null) {
result = elasticsearchOperations.search(query, clazz, index);
}
result = elasticsearchOperations.search(query, clazz, index);
} else {
result = elasticsearchOperations.searchOne(query, clazz, index);
}
@@ -137,12 +133,12 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository
public Query createQuery(Object[] parameters) {
ElasticsearchParametersParameterAccessor parameterAccessor = getParameterAccessor(parameters);
ResultProcessor resultProcessor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor);
var query = createQuery(parameterAccessor);
Assert.notNull(query, "unsupported query");
queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter());
queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter(),
evaluationContextProvider);
return query;
}
@@ -28,12 +28,14 @@ import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersiste
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.ByQueryResponse;
import org.springframework.data.elasticsearch.core.query.DeleteQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchQueryExecution.ResultProcessingConverter;
import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchQueryExecution.ResultProcessingExecution;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.util.Assert;
@@ -43,18 +45,26 @@ import org.springframework.util.Assert;
*
* @author Christoph Strobl
* @author Peter-Josef Meisch
* @author Haibo Liu
* @since 3.2
*/
abstract class AbstractReactiveElasticsearchRepositoryQuery implements RepositoryQuery {
protected final ReactiveElasticsearchQueryMethod queryMethod;
private final ReactiveElasticsearchOperations elasticsearchOperations;
protected final QueryMethodEvaluationContextProvider evaluationContextProvider;
AbstractReactiveElasticsearchRepositoryQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations elasticsearchOperations) {
ReactiveElasticsearchOperations elasticsearchOperations,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(queryMethod, "queryMethod must not be null");
Assert.notNull(elasticsearchOperations, "elasticsearchOperations must not be null");
Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null");
this.queryMethod = queryMethod;
this.elasticsearchOperations = elasticsearchOperations;
this.evaluationContextProvider = evaluationContextProvider;
}
/*
@@ -95,7 +105,8 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor
var query = createQuery(parameterAccessor);
Assert.notNull(query, "unsupported query");
queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter());
queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter(),
evaluationContextProvider);
String indexName = queryMethod.getEntityInformation().getIndexName();
IndexCoordinates index = IndexCoordinates.of(indexName);
@@ -112,7 +123,7 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor
* @param accessor must not be {@literal null}.
* @return
*/
protected abstract BaseQuery createQuery(ElasticsearchParameterAccessor accessor);
protected abstract BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor);
private ReactiveElasticsearchQueryExecution getExecution(ElasticsearchParameterAccessor accessor,
Converter<Object, Object> resultProcessing) {
@@ -123,7 +134,8 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor
ReactiveElasticsearchOperations operations) {
if (isDeleteQuery()) {
return (query, type, targetType, indexCoordinates) -> operations.delete(query, type, indexCoordinates)
return (query, type, targetType, indexCoordinates) -> operations
.delete(DeleteQuery.builder(query).build(), type, indexCoordinates)
.map(ByQueryResponse::getDeleted);
} else if (isCountQuery()) {
return (query, type, targetType, indexCoordinates) -> operations.count(query, type, indexCoordinates);
@@ -29,7 +29,7 @@ import org.springframework.data.util.TypeInformation;
* @author Peter-Josef Meisch
* @since 5.2
*/
class ElasticsearchParameter extends Parameter {
public class ElasticsearchParameter extends Parameter {
/**
* Creates a new {@link ElasticsearchParameter}.
@@ -15,12 +15,12 @@
*/
package org.springframework.data.elasticsearch.repository.query;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.MethodParameter;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.ParametersSource;
import org.springframework.data.util.TypeInformation;
/**
@@ -33,10 +33,13 @@ public class ElasticsearchParameters extends Parameters<ElasticsearchParameters,
private final List<ElasticsearchParameter> scriptedFields = new ArrayList<>();
private final List<ElasticsearchParameter> runtimeFields = new ArrayList<>();
public ElasticsearchParameters(Method method, TypeInformation<?> domainType) {
public ElasticsearchParameters(ParametersSource parametersSource) {
super(method, parameter -> new ElasticsearchParameter(parameter, domainType));
super(parametersSource,
parameter -> new ElasticsearchParameter(parameter, parametersSource.getDomainTypeInformation()));
var domainType = parametersSource.getDomainTypeInformation();
var method = parametersSource.getMethod();
int parameterCount = method.getParameterCount();
for (int i = 0; i < parameterCount; i++) {
MethodParameter methodParameter = new MethodParameter(method, i);
@@ -50,7 +53,6 @@ public class ElasticsearchParameters extends Parameters<ElasticsearchParameters,
runtimeFields.add(parameter);
}
}
}
private ElasticsearchParameter parameterFactory(MethodParameter methodParameter, TypeInformation<?> domainType) {
@@ -21,7 +21,7 @@ import org.springframework.data.repository.query.ParametersParameterAccessor;
* @author Christoph Strobl
* @since 3.2
*/
class ElasticsearchParametersParameterAccessor extends ParametersParameterAccessor
public class ElasticsearchParametersParameterAccessor extends ParametersParameterAccessor
implements ElasticsearchParameterAccessor {
private final Object[] values;
@@ -18,11 +18,9 @@ package org.springframework.data.elasticsearch.repository.query;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.RuntimeField;
import org.springframework.data.elasticsearch.core.query.ScriptedField;
import org.springframework.data.elasticsearch.repository.query.parser.ElasticsearchQueryCreator;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.parser.PartTree;
/**
@@ -34,14 +32,16 @@ import org.springframework.data.repository.query.parser.PartTree;
* @author Mark Paluch
* @author Rasmus Faber-Espensen
* @author Peter-Josef Meisch
* @author Haibo Liu
*/
public class ElasticsearchPartQuery extends AbstractElasticsearchRepositoryQuery {
private final PartTree tree;
private final MappingContext<?, ElasticsearchPersistentProperty> mappingContext;
public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations) {
super(method, elasticsearchOperations);
public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, elasticsearchOperations, evaluationContextProvider);
this.tree = new PartTree(queryMethod.getName(), queryMethod.getResultProcessor().getReturnedType().getDomainType());
this.mappingContext = elasticsearchConverter.getMappingContext();
}
@@ -24,6 +24,7 @@ import java.util.List;
import java.util.stream.Stream;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.elasticsearch.annotations.Highlight;
import org.springframework.data.elasticsearch.annotations.Query;
@@ -41,16 +42,16 @@ import org.springframework.data.elasticsearch.core.query.HighlightQuery;
import org.springframework.data.elasticsearch.core.query.RuntimeField;
import org.springframework.data.elasticsearch.core.query.ScriptedField;
import org.springframework.data.elasticsearch.core.query.SourceFilter;
import org.springframework.data.elasticsearch.repository.support.StringQueryUtil;
import org.springframework.data.elasticsearch.repository.support.QueryStringProcessor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.ParametersSource;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.util.QueryExecutionConverters;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -66,12 +67,13 @@ import org.springframework.util.ClassUtils;
* @author Christoph Strobl
* @author Peter-Josef Meisch
* @author Alexander Torres
* @author Haibo Liu
*/
public class ElasticsearchQueryMethod extends QueryMethod {
// the following 2 variables exits in the base class, but are private. We need them for
// the following 2 variables exist in the base class, but are private. We need them for
// correct handling of return types (SearchHits), so we have our own values here.
// Alas this means that we have to copy code that initializes these variables and in the
// This means that we have to copy code that initializes these variables and in the
// base class uses them in order to use our variables
protected final Method method;
protected final Class<?> unwrappedReturnType;
@@ -81,8 +83,6 @@ public class ElasticsearchQueryMethod extends QueryMethod {
@Nullable private ElasticsearchEntityMetadata<?> metadata;
@Nullable private final Query queryAnnotation;
@Nullable private final Highlight highlightAnnotation;
private final Lazy<HighlightQuery> highlightQueryLazy = Lazy.of(this::createAnnotatedHighlightQuery);
@Nullable private final SourceFilters sourceFilters;
public ElasticsearchQueryMethod(Method method, RepositoryMetadata repositoryMetadata, ProjectionFactory factory,
@@ -102,9 +102,16 @@ public class ElasticsearchQueryMethod extends QueryMethod {
verifyCountQueryTypes();
}
@SuppressWarnings("removal")
@Override
@Deprecated
protected Parameters<?, ?> createParameters(Method method, TypeInformation<?> domainType) {
return new ElasticsearchParameters(method, domainType);
return new ElasticsearchParameters(ParametersSource.of(method));
}
@Override
protected Parameters<?, ?> createParameters(ParametersSource parametersSource) {
return new ElasticsearchParameters(parametersSource);
}
protected void verifyCountQueryTypes() {
@@ -143,20 +150,12 @@ public class ElasticsearchQueryMethod extends QueryMethod {
* @throws IllegalArgumentException if no {@link Highlight} annotation is present on the method
* @see #hasAnnotatedHighlight()
*/
public HighlightQuery getAnnotatedHighlightQuery() {
public HighlightQuery getAnnotatedHighlightQuery(HighlightConverter highlightConverter) {
Assert.isTrue(hasAnnotatedHighlight(), "no Highlight annotation present on " + getName());
return highlightQueryLazy.get();
}
private HighlightQuery createAnnotatedHighlightQuery() {
Assert.notNull(highlightAnnotation, "highlightAnnotation must not be null");
return new HighlightQuery(
org.springframework.data.elasticsearch.core.query.highlight.Highlight.of(highlightAnnotation),
getDomainClass());
return new HighlightQuery(highlightConverter.convert(highlightAnnotation), getDomainClass());
}
/**
@@ -296,42 +295,46 @@ public class ElasticsearchQueryMethod extends QueryMethod {
* @param parameterAccessor the accessor with the query method parameter details
* @param converter {@link ElasticsearchConverter} needed to convert entity property names to the Elasticsearch field
* names and for parameter conversion when the includes or excludes are defined as parameters
* @param evaluationContextProvider to provide an evaluation context for SpEL evaluation
* @return source filter with includes and excludes for a query, {@literal null} when no {@link SourceFilters}
* annotation was set on the method.
* @since 5.0
*/
@Nullable
SourceFilter getSourceFilter(ParameterAccessor parameterAccessor, ElasticsearchConverter converter) {
SourceFilter getSourceFilter(ElasticsearchParametersParameterAccessor parameterAccessor,
ElasticsearchConverter converter,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
if (sourceFilters == null || (sourceFilters.includes().length == 0 && sourceFilters.excludes().length == 0)) {
return null;
}
StringQueryUtil stringQueryUtil = new StringQueryUtil(converter.getConversionService());
ConversionService conversionService = converter.getConversionService();
FetchSourceFilterBuilder fetchSourceFilterBuilder = new FetchSourceFilterBuilder();
if (sourceFilters.includes().length > 0) {
fetchSourceFilterBuilder
.withIncludes(mapParameters(sourceFilters.includes(), parameterAccessor, stringQueryUtil));
fetchSourceFilterBuilder.withIncludes(mapParameters(sourceFilters.includes(), parameterAccessor,
conversionService, evaluationContextProvider));
}
if (sourceFilters.excludes().length > 0) {
fetchSourceFilterBuilder
.withExcludes(mapParameters(sourceFilters.excludes(), parameterAccessor, stringQueryUtil));
fetchSourceFilterBuilder.withExcludes(mapParameters(sourceFilters.excludes(), parameterAccessor,
conversionService, evaluationContextProvider));
}
return fetchSourceFilterBuilder.build();
}
private String[] mapParameters(String[] source, ParameterAccessor parameterAccessor,
StringQueryUtil stringQueryUtil) {
private String[] mapParameters(String[] source, ElasticsearchParametersParameterAccessor parameterAccessor,
ConversionService conversionService, QueryMethodEvaluationContextProvider evaluationContextProvider) {
List<String> fieldNames = new ArrayList<>();
for (String s : source) {
if (!s.isBlank()) {
String fieldName = stringQueryUtil.replacePlaceholders(s, parameterAccessor);
String fieldName = new QueryStringProcessor(s, this, conversionService, evaluationContextProvider)
.createQuery(parameterAccessor);
// this could be "[\"foo\",\"bar\"]", must be split
if (fieldName.startsWith("[") && fieldName.endsWith("]")) {
// noinspection RegExpRedundantEscape
@@ -352,7 +355,7 @@ public class ElasticsearchQueryMethod extends QueryMethod {
/*
* Copied from the QueryMethod class adding support for collections of SearchHit instances. No static method here.
*/
private Class<? extends Object> potentiallyUnwrapReturnTypeFor(RepositoryMetadata metadata, Method method) {
private Class<?> potentiallyUnwrapReturnTypeFor(RepositoryMetadata metadata, Method method) {
TypeInformation<?> returnType = metadata.getReturnType(method);
if (!QueryExecutionConverters.supports(returnType.getType())
&& !ReactiveWrapperConverters.supports(returnType.getType())) {
@@ -375,13 +378,16 @@ public class ElasticsearchQueryMethod extends QueryMethod {
}
void addMethodParameter(BaseQuery query, ElasticsearchParametersParameterAccessor parameterAccessor,
ElasticsearchConverter elasticsearchConverter) {
ElasticsearchConverter elasticsearchConverter,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
if (hasAnnotatedHighlight()) {
query.setHighlightQuery(getAnnotatedHighlightQuery());
var highlightQuery = getAnnotatedHighlightQuery(new HighlightConverter(parameterAccessor,
elasticsearchConverter.getConversionService(), evaluationContextProvider, this));
query.setHighlightQuery(highlightQuery);
}
var sourceFilter = getSourceFilter(parameterAccessor, elasticsearchConverter);
var sourceFilter = getSourceFilter(parameterAccessor, elasticsearchConverter, evaluationContextProvider);
if (sourceFilter != null) {
query.addSourceFilter(sourceFilter);
}
@@ -15,11 +15,12 @@
*/
package org.springframework.data.elasticsearch.repository.query;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.repository.support.StringQueryUtil;
import org.springframework.data.elasticsearch.repository.support.QueryStringProcessor;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.util.Assert;
/**
@@ -30,15 +31,19 @@ import org.springframework.util.Assert;
* @author Mark Paluch
* @author Taylor Ono
* @author Peter-Josef Meisch
* @author Haibo Liu
*/
public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQuery {
private final String queryString;
public ElasticsearchStringQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations,
String queryString) {
super(queryMethod, elasticsearchOperations);
String queryString, QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(queryMethod, elasticsearchOperations, evaluationContextProvider);
Assert.notNull(queryString, "Query cannot be empty");
Assert.notNull(evaluationContextProvider, "ExpressionEvaluationContextProvider must not be null");
this.queryString = queryString;
}
@@ -58,12 +63,12 @@ public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQue
}
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
ConversionService conversionService = elasticsearchOperations.getElasticsearchConverter().getConversionService();
var processed = new QueryStringProcessor(queryString, queryMethod, conversionService, evaluationContextProvider)
.createQuery(parameterAccessor);
String queryString = new StringQueryUtil(elasticsearchOperations.getElasticsearchConverter().getConversionService())
.replacePlaceholders(this.queryString, parameterAccessor);
var query = new StringQuery(queryString);
query.addSort(parameterAccessor.getSort());
return query;
return new StringQuery(processed)
.addSort(parameterAccessor.getSort());
}
}
@@ -0,0 +1,108 @@
/*
* 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.repository.query;
import java.util.Arrays;
import java.util.List;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.core.query.highlight.Highlight;
import org.springframework.data.elasticsearch.core.query.highlight.HighlightField;
import org.springframework.data.elasticsearch.core.query.highlight.HighlightParameters;
import org.springframework.data.elasticsearch.repository.support.QueryStringProcessor;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.util.Assert;
/**
* Convert {@link org.springframework.data.elasticsearch.annotations.Highlight} to {@link Highlight}.
*
* @author Haibo Liu
*/
public class HighlightConverter {
private final ElasticsearchParametersParameterAccessor parameterAccessor;
private final ConversionService conversionService;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final QueryMethod queryMethod;
HighlightConverter(ElasticsearchParametersParameterAccessor parameterAccessor,
ConversionService conversionService,
QueryMethodEvaluationContextProvider evaluationContextProvider,
QueryMethod queryMethod) {
Assert.notNull(parameterAccessor, "parameterAccessor must not be null");
Assert.notNull(conversionService, "conversionService must not be null");
Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null");
Assert.notNull(queryMethod, "queryMethod must not be null");
this.parameterAccessor = parameterAccessor;
this.conversionService = conversionService;
this.evaluationContextProvider = evaluationContextProvider;
this.queryMethod = queryMethod;
}
/**
* Creates a {@link Highlight} from an Annotation instance.
*
* @param highlight must not be {@literal null}
* @return highlight definition
*/
Highlight convert(org.springframework.data.elasticsearch.annotations.Highlight highlight) {
Assert.notNull(highlight, "highlight must not be null");
org.springframework.data.elasticsearch.annotations.HighlightParameters parameters = highlight.parameters();
// replace placeholders in highlight query with actual parameters
Query highlightQuery = null;
if (!parameters.highlightQuery().value().isEmpty()) {
String rawQuery = parameters.highlightQuery().value();
String query = new QueryStringProcessor(rawQuery, queryMethod, conversionService, evaluationContextProvider)
.createQuery(parameterAccessor);
highlightQuery = new StringQuery(query);
}
HighlightParameters highlightParameters = HighlightParameters.builder() //
.withBoundaryChars(parameters.boundaryChars()) //
.withBoundaryMaxScan(parameters.boundaryMaxScan()) //
.withBoundaryScanner(parameters.boundaryScanner()) //
.withBoundaryScannerLocale(parameters.boundaryScannerLocale()) //
.withEncoder(parameters.encoder()) //
.withForceSource(parameters.forceSource()) //
.withFragmenter(parameters.fragmenter()) //
.withFragmentSize(parameters.fragmentSize()) //
.withNoMatchSize(parameters.noMatchSize()) //
.withNumberOfFragments(parameters.numberOfFragments()) //
.withHighlightQuery(highlightQuery) //
.withOrder(parameters.order()) //
.withPhraseLimit(parameters.phraseLimit()) //
.withPreTags(parameters.preTags()) //
.withPostTags(parameters.postTags()) //
.withRequireFieldMatch(parameters.requireFieldMatch()) //
.withTagsSchema(parameters.tagsSchema()) //
.withType(parameters.type()) //
.build();
List<HighlightField> highlightFields = Arrays.stream(highlight.fields()) //
.map(HighlightField::of) //
.toList();
return new Highlight(highlightParameters, highlightFields);
}
}
@@ -22,7 +22,7 @@ import java.util.ArrayList;
import java.util.List;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import org.springframework.data.repository.util.ReactiveWrappers;
import org.springframework.data.util.ReactiveWrappers;
/**
* @author Christoph Strobl
@@ -101,11 +101,6 @@ public class ReactiveElasticsearchQueryMethod extends ElasticsearchQueryMethod {
}
}
@Override
protected ElasticsearchParameters createParameters(Method method, TypeInformation<?> domainType) {
return new ElasticsearchParameters(method, domainType);
}
/**
* Check if the given {@link org.springframework.data.repository.query.QueryMethod} receives a reactive parameter
* wrapper as one of its parameters.
@@ -15,42 +15,49 @@
*/
package org.springframework.data.elasticsearch.repository.query;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.repository.support.StringQueryUtil;
import org.springframework.data.elasticsearch.repository.support.QueryStringProcessor;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
/**
* @author Christoph Strobl
* @author Taylor Ono
* @author Haibo Liu
* @since 3.2
*/
public class ReactiveElasticsearchStringQuery extends AbstractReactiveElasticsearchRepositoryQuery {
private final String query;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
public ReactiveElasticsearchStringQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations operations, SpelExpressionParser expressionParser,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
ReactiveElasticsearchOperations operations, QueryMethodEvaluationContextProvider evaluationContextProvider) {
this(queryMethod.getAnnotatedQuery(), queryMethod, operations, expressionParser, evaluationContextProvider);
this(queryMethod.getAnnotatedQuery(), queryMethod, operations, evaluationContextProvider);
}
public ReactiveElasticsearchStringQuery(String query, ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations operations, SpelExpressionParser expressionParser,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
ReactiveElasticsearchOperations operations, QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(queryMethod, operations, evaluationContextProvider);
Assert.notNull(query, "query must not be null");
Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null");
super(queryMethod, operations);
this.query = query;
this.evaluationContextProvider = evaluationContextProvider;
}
@Override
protected StringQuery createQuery(ElasticsearchParameterAccessor parameterAccessor) {
String queryString = new StringQueryUtil(
getElasticsearchOperations().getElasticsearchConverter().getConversionService()).replacePlaceholders(this.query,
parameterAccessor);
return new StringQuery(queryString);
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
ConversionService conversionService = getElasticsearchOperations().getElasticsearchConverter()
.getConversionService();
String processed = new QueryStringProcessor(query, queryMethod, conversionService, evaluationContextProvider)
.createQuery(parameterAccessor);
return new StringQuery(processed);
}
@Override
@@ -19,12 +19,14 @@ import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperatio
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.repository.query.parser.ElasticsearchQueryCreator;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.parser.PartTree;
/**
* @author Christoph Strobl
* @author Peter-Josef Meisch
* @author Haibo Liu
* @since 3.2
*/
public class ReactivePartTreeElasticsearchQuery extends AbstractReactiveElasticsearchRepositoryQuery {
@@ -32,15 +34,16 @@ public class ReactivePartTreeElasticsearchQuery extends AbstractReactiveElastics
private final PartTree tree;
public ReactivePartTreeElasticsearchQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations elasticsearchOperations) {
super(queryMethod, elasticsearchOperations);
ReactiveElasticsearchOperations elasticsearchOperations,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(queryMethod, elasticsearchOperations, evaluationContextProvider);
ResultProcessor processor = queryMethod.getResultProcessor();
this.tree = new PartTree(queryMethod.getName(), processor.getReturnedType().getDomainType());
}
@Override
protected BaseQuery createQuery(ElasticsearchParameterAccessor accessor) {
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor) {
CriteriaQuery query = new ElasticsearchQueryCreator(tree, accessor, getMappingContext()).createQuery();
if (tree.isLimiting()) {
@@ -15,9 +15,6 @@
*/
package org.springframework.data.elasticsearch.repository.query.parser;
import java.util.Collection;
import java.util.Iterator;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.geo.GeoBox;
@@ -36,6 +33,9 @@ import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.lang.Nullable;
import java.util.Collection;
import java.util.Iterator;
/**
* ElasticsearchQueryCreator
*
@@ -44,168 +44,163 @@ import org.springframework.lang.Nullable;
* @author Franck Marchand
* @author Artur Konczak
* @author Peter-Josef Meisch
* @author Junghoon Ban
*/
public class ElasticsearchQueryCreator extends AbstractQueryCreator<CriteriaQuery, CriteriaQuery> {
private final MappingContext<?, ElasticsearchPersistentProperty> context;
private final MappingContext<?, ElasticsearchPersistentProperty> context;
public ElasticsearchQueryCreator(PartTree tree, ParameterAccessor parameters,
MappingContext<?, ElasticsearchPersistentProperty> context) {
super(tree, parameters);
this.context = context;
}
public ElasticsearchQueryCreator(PartTree tree, ParameterAccessor parameters,
MappingContext<?, ElasticsearchPersistentProperty> context) {
super(tree, parameters);
this.context = context;
}
public ElasticsearchQueryCreator(PartTree tree, MappingContext<?, ElasticsearchPersistentProperty> context) {
super(tree);
this.context = context;
}
public ElasticsearchQueryCreator(PartTree tree, MappingContext<?, ElasticsearchPersistentProperty> context) {
super(tree);
this.context = context;
}
@Override
protected CriteriaQuery create(Part part, Iterator<Object> iterator) {
PersistentPropertyPath<ElasticsearchPersistentProperty> path = context
.getPersistentPropertyPath(part.getProperty());
return new CriteriaQuery(from(part,
new Criteria(path.toDotPath(ElasticsearchPersistentProperty.QueryPropertyToFieldNameConverter.INSTANCE)),
iterator));
}
@Override
protected CriteriaQuery create(Part part, Iterator<Object> iterator) {
PersistentPropertyPath<ElasticsearchPersistentProperty> path = context.getPersistentPropertyPath(
part.getProperty());
return new CriteriaQuery(from(part,
new Criteria(path.toDotPath(ElasticsearchPersistentProperty.QueryPropertyToFieldNameConverter.INSTANCE)),
iterator));
}
@Override
protected CriteriaQuery and(Part part, CriteriaQuery base, Iterator<Object> iterator) {
if (base == null) {
return create(part, iterator);
}
PersistentPropertyPath<ElasticsearchPersistentProperty> path = context
.getPersistentPropertyPath(part.getProperty());
return base.addCriteria(from(part,
new Criteria(path.toDotPath(ElasticsearchPersistentProperty.QueryPropertyToFieldNameConverter.INSTANCE)),
iterator));
}
@Override
protected CriteriaQuery and(Part part, CriteriaQuery base, Iterator<Object> iterator) {
if (base == null) {
return create(part, iterator);
}
PersistentPropertyPath<ElasticsearchPersistentProperty> path = context.getPersistentPropertyPath(
part.getProperty());
return base.addCriteria(from(part,
new Criteria(path.toDotPath(ElasticsearchPersistentProperty.QueryPropertyToFieldNameConverter.INSTANCE)),
iterator));
}
@Override
protected CriteriaQuery or(CriteriaQuery base, CriteriaQuery query) {
return new CriteriaQuery(base.getCriteria().or(query.getCriteria()));
}
@Override
protected CriteriaQuery or(CriteriaQuery base, CriteriaQuery query) {
return new CriteriaQuery(base.getCriteria().or(query.getCriteria()));
}
@Override
protected CriteriaQuery complete(@Nullable CriteriaQuery query, Sort sort) {
@Override
protected CriteriaQuery complete(@Nullable CriteriaQuery query, Sort sort) {
if (query == null) {
// this is the case in a findAllByOrderByField method, add empty criteria
query = new CriteriaQuery(new Criteria());
}
return query.addSort(sort);
}
if (query == null) {
// this is the case in a findAllByOrderByField method, add empty criteria
query = new CriteriaQuery(new Criteria());
}
return query.addSort(sort);
}
private Criteria from(Part part, Criteria criteria, Iterator<?> parameters) {
private Criteria from(Part part, Criteria criteria, Iterator<?> parameters) {
Part.Type type = part.getType();
Part.Type type = part.getType();
switch (type) {
case TRUE:
return criteria.is(true);
case FALSE:
return criteria.is(false);
case NEGATING_SIMPLE_PROPERTY:
return criteria.is(parameters.next()).not();
case REGEX:
return criteria.expression(parameters.next().toString());
case LIKE:
case STARTING_WITH:
return criteria.startsWith(parameters.next().toString());
case ENDING_WITH:
return criteria.endsWith(parameters.next().toString());
case CONTAINING:
return criteria.contains(parameters.next().toString());
case GREATER_THAN:
return criteria.greaterThan(parameters.next());
case AFTER:
case GREATER_THAN_EQUAL:
return criteria.greaterThanEqual(parameters.next());
case LESS_THAN:
return criteria.lessThan(parameters.next());
case BEFORE:
case LESS_THAN_EQUAL:
return criteria.lessThanEqual(parameters.next());
case BETWEEN:
return criteria.between(parameters.next(), parameters.next());
case IN:
return criteria.in(asArray(parameters.next()));
case NOT_IN:
return criteria.notIn(asArray(parameters.next()));
case SIMPLE_PROPERTY:
case WITHIN: {
Object firstParameter = parameters.next();
Object secondParameter = null;
if (type == Part.Type.SIMPLE_PROPERTY) {
if (part.getProperty().getType() != GeoPoint.class) {
if (firstParameter != null) {
return criteria.is(firstParameter);
} else {
// searching for null is a must_not (exists)
return criteria.exists().not();
}
} else {
// it means it's a simple find with exact geopoint matching (e.g. findByLocation)
// and because Elasticsearch does not have any kind of query with just a geopoint
// as argument we use a "geo distance" query with a distance of one meter.
secondParameter = ".001km";
}
} else {
secondParameter = parameters.next();
}
return switch (type) {
case TRUE -> criteria.is(true);
case FALSE -> criteria.is(false);
case NEGATING_SIMPLE_PROPERTY -> criteria.is(parameters.next()).not();
case REGEX -> criteria.expression(parameters.next().toString());
case LIKE, STARTING_WITH -> criteria.startsWith(parameters.next().toString());
case ENDING_WITH -> criteria.endsWith(parameters.next().toString());
case CONTAINING -> criteria.contains(parameters.next().toString());
case GREATER_THAN -> criteria.greaterThan(parameters.next());
case AFTER, GREATER_THAN_EQUAL -> criteria.greaterThanEqual(parameters.next());
case LESS_THAN -> criteria.lessThan(parameters.next());
case BEFORE, LESS_THAN_EQUAL -> criteria.lessThanEqual(parameters.next());
case BETWEEN -> criteria.between(parameters.next(), parameters.next());
case IN -> criteria.in(asArray(parameters.next()));
case NOT_IN -> criteria.notIn(asArray(parameters.next()));
case SIMPLE_PROPERTY, WITHIN -> within(part, criteria, parameters);
case NEAR -> near(criteria, parameters);
case EXISTS, IS_NOT_NULL -> criteria.exists();
case IS_NULL -> criteria.not().exists();
case IS_EMPTY -> criteria.empty();
case IS_NOT_EMPTY -> criteria.notEmpty();
default -> throw new InvalidDataAccessApiUsageException("Illegal criteria found '" + type + "'.");
};
}
if (firstParameter instanceof GeoPoint && secondParameter instanceof String)
return criteria.within((GeoPoint) firstParameter, (String) secondParameter);
private Criteria within(Part part, Criteria criteria, Iterator<?> parameters) {
if (firstParameter instanceof Point && secondParameter instanceof Distance)
return criteria.within((Point) firstParameter, (Distance) secondParameter);
Object firstParameter = parameters.next();
Object secondParameter;
if (firstParameter instanceof String && secondParameter instanceof String)
return criteria.within((String) firstParameter, (String) secondParameter);
}
case NEAR: {
Object firstParameter = parameters.next();
if (part.getType() == Part.Type.SIMPLE_PROPERTY) {
if (part.getProperty().getType() != GeoPoint.class) {
if (firstParameter != null) {
return criteria.is(firstParameter);
} else {
// searching for null is a must_not (exists)
return criteria.exists().not();
}
} else {
// it means it's a simple find with exact geopoint matching (e.g. findByLocation)
// and because Elasticsearch does not have any kind of query with just a geopoint
// as argument we use a "geo distance" query with a distance of one meter.
secondParameter = ".001km";
}
} else {
secondParameter = parameters.next();
}
if (firstParameter instanceof GeoBox) {
return criteria.boundedBy((GeoBox) firstParameter);
}
return doWithinIfPossible(criteria, firstParameter, secondParameter);
}
if (firstParameter instanceof Box) {
return criteria.boundedBy(GeoBox.fromBox((Box) firstParameter));
}
private Criteria near(Criteria criteria, Iterator<?> parameters) {
Object secondParameter = parameters.next();
Object firstParameter = parameters.next();
// "near" query can be the same query as the "within" query
if (firstParameter instanceof GeoPoint && secondParameter instanceof String)
return criteria.within((GeoPoint) firstParameter, (String) secondParameter);
if (firstParameter instanceof GeoBox geoBox) {
return criteria.boundedBy(geoBox);
}
if (firstParameter instanceof Point && secondParameter instanceof Distance)
return criteria.within((Point) firstParameter, (Distance) secondParameter);
if (firstParameter instanceof Box box) {
return criteria.boundedBy(GeoBox.fromBox(box));
}
if (firstParameter instanceof String && secondParameter instanceof String)
return criteria.within((String) firstParameter, (String) secondParameter);
}
case EXISTS:
case IS_NOT_NULL:
return criteria.exists();
case IS_NULL:
return criteria.not().exists();
case IS_EMPTY:
return criteria.empty();
case IS_NOT_EMPTY:
return criteria.notEmpty();
default:
throw new InvalidDataAccessApiUsageException("Illegal criteria found '" + type + "'.");
}
}
Object secondParameter = parameters.next();
private Object[] asArray(Object o) {
if (o instanceof Collection) {
return ((Collection<?>) o).toArray();
} else if (o.getClass().isArray()) {
return (Object[]) o;
}
return new Object[] { o };
}
return doWithinIfPossible(criteria, firstParameter, secondParameter);
}
/**
* Do a within query if possible, otherwise return the criteria unchanged.
*
* @param criteria must not be {@literal null}
* @param firstParameter must not be {@literal null}
* @param secondParameter must not be {@literal null}
* @return the criteria with the within query applied if possible.
* @author Junghoon Ban
*/
private Criteria doWithinIfPossible(Criteria criteria, Object firstParameter, Object secondParameter) {
if (firstParameter instanceof GeoPoint geoPoint && secondParameter instanceof String string) {
return criteria.within(geoPoint, string);
}
if (firstParameter instanceof Point point && secondParameter instanceof Distance distance) {
return criteria.within(point, distance);
}
if (firstParameter instanceof String firstString && secondParameter instanceof String secondString) {
return criteria.within(firstString, secondString);
}
return criteria;
}
private Object[] asArray(Object o) {
if (o instanceof Collection) {
return ((Collection<?>) o).toArray();
} else if (o.getClass().isArray()) {
return (Object[]) o;
}
return new Object[]{o};
}
}
@@ -15,6 +15,11 @@
*/
package org.springframework.data.elasticsearch.repository.support;
import static org.springframework.data.querydsl.QuerydslUtils.*;
import java.lang.reflect.Method;
import java.util.Optional;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.data.elasticsearch.repository.query.ElasticsearchPartQuery;
@@ -37,11 +42,6 @@ import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import java.lang.reflect.Method;
import java.util.Optional;
import static org.springframework.data.querydsl.QuerydslUtils.QUERY_DSL_PRESENT;
/**
* Factory to create {@link ElasticsearchRepository}
*
@@ -54,6 +54,7 @@ import static org.springframework.data.querydsl.QuerydslUtils.QUERY_DSL_PRESENT;
* @author Sascha Woo
* @author Peter-Josef Meisch
* @author Ezequiel Antúnez Camacho
* @author Haibo Liu
*/
public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport {
@@ -96,11 +97,17 @@ public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport {
@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
return Optional.of(new ElasticsearchQueryLookupStrategy());
return Optional.of(new ElasticsearchQueryLookupStrategy(evaluationContextProvider));
}
private class ElasticsearchQueryLookupStrategy implements QueryLookupStrategy {
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
ElasticsearchQueryLookupStrategy(QueryMethodEvaluationContextProvider evaluationContextProvider) {
this.evaluationContextProvider = evaluationContextProvider;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.projection.ProjectionFactory, org.springframework.data.repository.core.NamedQueries)
@@ -115,11 +122,13 @@ public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport {
if (namedQueries.hasQuery(namedQueryName)) {
String namedQuery = namedQueries.getQuery(namedQueryName);
return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, namedQuery);
return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, namedQuery,
evaluationContextProvider);
} else if (queryMethod.hasAnnotatedQuery()) {
return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, queryMethod.getAnnotatedQuery());
return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, queryMethod.getAnnotatedQuery(),
evaluationContextProvider);
}
return new ElasticsearchPartQuery(queryMethod, elasticsearchOperations);
return new ElasticsearchPartQuery(queryMethod, elasticsearchOperations, evaluationContextProvider);
}
}
@@ -15,38 +15,52 @@
*/
package org.springframework.data.elasticsearch.repository.support;
import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.core.convert.ConversionException;
import org.springframework.data.elasticsearch.repository.support.value.ElasticsearchQueryValueConversionService;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.util.Assert;
import org.springframework.util.NumberUtils;
/**
* To replace the placeholders like `?0`, `?1, `?2` of the query string.
*
* @author Peter-Josef Meisch
* @author Niklas Herder
* @author Haibo Liu
*/
final public class StringQueryUtil {
final public class QueryStringPlaceholderReplacer {
private static final Pattern PARAMETER_PLACEHOLDER = Pattern.compile("\\?(\\d+)");
private final ConversionService conversionService;
public StringQueryUtil(ConversionService conversionService) {
this.conversionService = conversionService;
public QueryStringPlaceholderReplacer(ConversionService conversionService) {
Assert.notNull(conversionService, "conversionService must not be null");
this.conversionService = ElasticsearchQueryValueConversionService.getInstance(conversionService);
}
/**
* Replace the placeholders of the query string.
*
* @param input raw query string
* @param accessor parameter info
* @return a plain string with placeholders replaced
*/
public String replacePlaceholders(String input, ParameterAccessor accessor) {
Matcher matcher = PARAMETER_PLACEHOLDER.matcher(input);
String result = input;
while (matcher.find()) {
while (matcher.find()) {
String placeholder = Pattern.quote(matcher.group()) + "(?!\\d+)";
int index = NumberUtils.parseNumber(matcher.group(1), Integer.class);
String replacement = Matcher.quoteReplacement(getParameterWithIndex(accessor, index));
String replacement = Matcher.quoteReplacement(getParameterWithIndex(accessor, index, input));
result = result.replaceAll(placeholder, replacement);
// need to escape backslashes that are not escapes for quotes so that they are sent as double-backslashes
// to Elasticsearch
@@ -55,47 +69,17 @@ final public class StringQueryUtil {
return result;
}
private String getParameterWithIndex(ParameterAccessor accessor, int index) {
private String getParameterWithIndex(ParameterAccessor accessor, int index, String input) {
Object parameter = accessor.getBindableValue(index);
String parameterValue = "null";
String value = conversionService.convert(parameter, String.class);
if (parameter != null) {
parameterValue = convert(parameter);
}
return parameterValue;
}
private String convert(Object parameter) {
if (Collection.class.isAssignableFrom(parameter.getClass())) {
Collection<?> collectionParam = (Collection<?>) parameter;
StringBuilder sb = new StringBuilder("[");
sb.append(collectionParam.stream().map(o -> {
if (o instanceof String) {
return "\"" + convert(o) + "\"";
} else {
return convert(o);
}
}).collect(Collectors.joining(",")));
sb.append("]");
return sb.toString();
} else {
String parameterValue = "null";
if (conversionService.canConvert(parameter.getClass(), String.class)) {
String converted = conversionService.convert(parameter, String.class);
if (converted != null) {
parameterValue = converted;
}
} else {
parameterValue = parameter.toString();
}
parameterValue = parameterValue.replaceAll("\"", Matcher.quoteReplacement("\\\""));
return parameterValue;
if (value == null) {
throw new ConversionException(String.format(
"Parameter value can't be null for placeholder at index '%s' in query '%s' when querying elasticsearch",
index, input));
}
return value;
}
}
@@ -0,0 +1,67 @@
/*
* 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.repository.support;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.repository.query.ElasticsearchParametersParameterAccessor;
import org.springframework.data.elasticsearch.repository.support.spel.QueryStringSpELEvaluator;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.util.Assert;
/**
* To process query strings with placeholder replacement and SpEL evaluation by {@link QueryStringPlaceholderReplacer}
* and {@link QueryStringSpELEvaluator}.
*
* @since 5.3
* @author Haibo Liu
*/
public class QueryStringProcessor {
private final String query;
private final QueryMethod queryMethod;
private final ConversionService conversionService;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
public QueryStringProcessor(String query, QueryMethod queryMethod, ConversionService conversionService,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(query, "query must not be null");
Assert.notNull(queryMethod, "queryMethod must not be null");
Assert.notNull(conversionService, "conversionService must not be null");
Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null");
this.query = query;
this.queryMethod = queryMethod;
this.conversionService = conversionService;
this.evaluationContextProvider = evaluationContextProvider;
}
/**
* Process the query string with placeholder replacement and SpEL evaluation.
*
* @param parameterAccessor parameter info
* @return processed string
*/
public String createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
String queryString = new QueryStringPlaceholderReplacer(conversionService)
.replacePlaceholders(query, parameterAccessor);
QueryStringSpELEvaluator evaluator = new QueryStringSpELEvaluator(queryString, parameterAccessor, queryMethod,
evaluationContextProvider, conversionService);
return evaluator.evaluate();
}
}
@@ -39,7 +39,6 @@ import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -50,12 +49,11 @@ import org.springframework.util.Assert;
* @author Christoph Strobl
* @author Ivan Greene
* @author Ezequiel Antúnez Camacho
* @author Haibo Liu
* @since 3.2
*/
public class ReactiveElasticsearchRepositoryFactory extends ReactiveRepositoryFactorySupport {
private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private final ReactiveElasticsearchOperations operations;
private final MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext;
@@ -163,13 +161,12 @@ public class ReactiveElasticsearchRepositoryFactory extends ReactiveRepositoryFa
if (namedQueries.hasQuery(namedQueryName)) {
String namedQuery = namedQueries.getQuery(namedQueryName);
return new ReactiveElasticsearchStringQuery(namedQuery, queryMethod, operations, EXPRESSION_PARSER,
return new ReactiveElasticsearchStringQuery(namedQuery, queryMethod, operations,
evaluationContextProvider);
} else if (queryMethod.hasAnnotatedQuery()) {
return new ReactiveElasticsearchStringQuery(queryMethod, operations, EXPRESSION_PARSER,
evaluationContextProvider);
return new ReactiveElasticsearchStringQuery(queryMethod, operations, evaluationContextProvider);
} else {
return new ReactivePartTreeElasticsearchQuery(queryMethod, operations);
return new ReactivePartTreeElasticsearchQuery(queryMethod, operations, evaluationContextProvider);
}
}
}

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