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

Compare commits

..

12 Commits

Author SHA1 Message Date
Mark Paluch 60b71dcb11 Release version 5.4.2 (2024.1.2).
See #3024
2025-01-17 11:37:36 +01:00
Mark Paluch 33a149ae2d Prepare 5.4.2 (2024.1.2).
See #3024
2025-01-17 11:37:19 +01:00
Peter-Josef Meisch 29158f9097 Update copyright notices to 2025.
Original Pull Request #3035
Closes #3033
2025-01-03 08:44:05 +01:00
Alfonso 5d52918a5c fix: use scripted field name to populate entity.
Original Pull Request: #3023
Closes: #3022

(cherry picked from commit 944e7e81dd)
2024-12-14 18:51:27 +01:00
Christoph Strobl 457c9b71b3 After release cleanups.
See #3004
2024-12-13 09:38:13 +01:00
Christoph Strobl 18b91d5da2 Prepare next development iteration.
See #3004
2024-12-13 09:38:11 +01:00
Christoph Strobl 62091be997 Release version 5.4.1 (2024.1.1).
See #3004
2024-12-13 09:34:47 +01:00
Christoph Strobl fdc7893817 Prepare 5.4.1 (2024.1.1).
See #3004
2024-12-13 09:34:27 +01:00
Peter-Josef Meisch 535d76faf0 Upgrade Elasticsearch to 8.15.5.
Original Pull Request #3018
Closes #3016
2024-12-01 11:59:53 +01:00
Peter-Josef Meisch 26bd770b8c Update versions documentation.
Original Pull request #3011 
Closes #3010
2024-11-23 22:46:46 +01:00
Mark Paluch aec03a3529 After release cleanups.
See #2990
2024-11-15 14:13:26 +01:00
Mark Paluch e3b26b2268 Prepare next development iteration.
See #2990
2024-11-15 14:13:25 +01:00
92 changed files with 708 additions and 2524 deletions
-1
View File
@@ -33,4 +33,3 @@ node
package-lock.json
.mvn/.develocity
/src/test/resources/testcontainers-local.properties
+1 -1
View File
@@ -3,6 +3,6 @@
<extension>
<groupId>io.spring.develocity.conventions</groupId>
<artifactId>develocity-conventions-maven-extension</artifactId>
<version>0.0.22</version>
<version>0.0.19</version>
</extension>
</extensions>
-14
View File
@@ -1,14 +0,0 @@
--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
--add-opens=java.base/java.util=ALL-UNNAMED
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED
--add-opens=java.base/java.text=ALL-UNNAMED
--add-opens=java.desktop/java.awt.font=ALL-UNNAMED
+2 -2
View File
@@ -1,3 +1,3 @@
#Thu Jul 17 14:00:55 CEST 2025
#Thu Nov 07 09:47:28 CET 2024
wrapperUrl=https\://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
Vendored
+1 -1
View File
@@ -9,7 +9,7 @@ pipeline {
triggers {
pollSCM 'H/10 * * * *'
upstream(upstreamProjects: "spring-data-commons/3.5.x", threshold: hudson.model.Result.SUCCESS)
upstream(upstreamProjects: "spring-data-commons/3.4.x", threshold: hudson.model.Result.SUCCESS)
}
options {
+2 -2
View File
@@ -62,7 +62,7 @@ public class MyService {
=== Using the RestClient
Please check the https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#elasticsearch.clients.configuration[official documentation].
Please check the [official documentation](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#elasticsearch.clients.configuration).
=== Maven configuration
@@ -168,7 +168,7 @@ Building the documentation builds also the project without running tests.
$ ./mvnw clean install -Pantora
----
The generated documentation is available from `target/site/index.html`.
The generated documentation is available from `target/antora/site/index.html`.
== Examples
+10 -6
View File
@@ -1,20 +1,24 @@
# Java versions
java.main.tag=17.0.15_6-jdk-focal
java.next.tag=24.0.1_9-jdk-noble
java.main.tag=17.0.13_11-jdk-focal
java.next.tag=23.0.1_11-jdk-noble
# Docker container images - standard
docker.java.main.image=library/eclipse-temurin:${java.main.tag}
docker.java.next.image=library/eclipse-temurin:${java.next.tag}
# Supported versions of MongoDB
docker.mongodb.6.0.version=6.0.23
docker.mongodb.7.0.version=7.0.20
docker.mongodb.8.0.version=8.0.9
docker.mongodb.4.4.version=4.4.25
docker.mongodb.5.0.version=5.0.21
docker.mongodb.6.0.version=6.0.10
docker.mongodb.7.0.version=7.0.2
docker.mongodb.8.0.version=8.0.0
# Supported versions of Redis
docker.redis.6.version=6.2.13
docker.redis.7.version=7.2.4
docker.valkey.8.version=8.1.1
# Supported versions of Cassandra
docker.cassandra.3.version=3.11.16
# Docker environment settings
docker.java.inside.basic=-v $HOME:/tmp/jenkins-home
+4 -16
View File
@@ -5,12 +5,12 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>5.5.2</version>
<version>5.4.2</version>
<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<version>3.5.2</version>
<version>3.4.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.5.2</springdata.commons>
<springdata.commons>3.4.2</springdata.commons>
<!-- version of the ElasticsearchClient -->
<elasticsearch-java>8.18.1</elasticsearch-java>
<elasticsearch-java>8.15.5</elasticsearch-java>
<hoverfly>0.19.0</hoverfly>
<log4j>2.23.1</log4j>
@@ -132,18 +132,6 @@
</exclusions>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>${elasticsearch-java}</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Jackson JSON Mapper -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
+1 -1
View File
@@ -17,7 +17,7 @@ content:
- url: https://github.com/spring-projects/spring-data-commons
# Refname matching:
# https://docs.antora.org/antora/latest/playbook/content-refname-matching/
branches: [ main, 3.4.x, 3.3.x ]
branches: [ main, 3.2.x ]
start_path: src/main/antora
asciidoc:
attributes:
-1
View File
@@ -11,7 +11,6 @@
*** xref:migration-guides/migration-guide-5.1-5.2.adoc[]
*** xref:migration-guides/migration-guide-5.2-5.3.adoc[]
*** xref:migration-guides/migration-guide-5.3-5.4.adoc[]
*** xref:migration-guides/migration-guide-5.4-5.5.adoc[]
* xref:elasticsearch.adoc[]
@@ -1,12 +1,9 @@
[[new-features]]
= What's new
[[new-features.5-5-0]]
== New in Spring Data Elasticsearch 5.5
* Upgrade to Elasticsearch 8.18.1.
* Add support for the `@SearchTemplateQuery` annotation on repository methods.
* Scripted field properties of type collection can be populated from scripts returning arrays.
[[new-features.5-4-1]]
== New in Spring Data Elasticsearch 5.4.1
* Upgrade to Elasticsearch 8.15.5.
[[new-features.5-4-0]]
== New in Spring Data Elasticsearch 5.4
@@ -365,8 +365,6 @@ operations.putScript( <.>
To use a search template in a search query, Spring Data Elasticsearch provides the `SearchTemplateQuery`, an implementation of the `org.springframework.data.elasticsearch.core.query.Query` interface.
NOTE: Although `SearchTemplateQuery` is an implementation of the `Query` interface, not all of the functionality provided by the base class is available for a `SearchTemplateQuery` like setting a `Pageable` or a `Sort`. Values for this functionality must be added to the stored script like shown in the following example for paging parameters. If these values are set on the `Query` object, they will be ignored.
In the following code, we will add a call using a search template query to a custom repository implementation (see
xref:repositories/custom-implementations.adoc[]) as an example how this can be integrated into a repository call.
@@ -451,3 +449,4 @@ var query = Query.findAll().addSort(Sort.by(order));
About the filter query: It is not possible to use a `CriteriaQuery` here, as this query would be converted into a Elasticsearch nested query which does not work in the filter context. So only `StringQuery` or `NativeQuery` can be used here. When using one of these, like the term query above, the Elasticsearch field names must be used, so take care, when these are redefined with the `@Field(name="...")` definition.
For the definition of the order path and the nested paths, the Java entity property names should be used.
@@ -10,9 +10,7 @@ The Elasticsearch module supports all basic query building feature as string que
=== Declared queries
Deriving the query from the method name is not always sufficient and/or may result in unreadable method names.
In this case one might make use of the `@Query` annotation (see xref:elasticsearch/repositories/elasticsearch-repository-queries.adoc#elasticsearch.query-methods.at-query[Using the @Query Annotation] ).
Another possibility is the use of a search-template, (see xref:elasticsearch/repositories/elasticsearch-repository-queries.adoc#elasticsearch.query-methods.at-searchtemplate-query[Using the @SearchTemplateQuery Annotation] ).
In this case one might make use of the `@Query` annotation (see xref:elasticsearch/repositories/elasticsearch-repository-queries.adoc#elasticsearch.query-methods.at-query[Using @Query Annotation] ).
[[elasticsearch.query-methods.criterions]]
== Query creation
@@ -314,13 +312,11 @@ Repository methods can be defined to have the following return types for returni
* `SearchPage<T>`
[[elasticsearch.query-methods.at-query]]
== Using the @Query Annotation
== Using @Query Annotation
.Declare query on the method using the `@Query` annotation.
====
The arguments passed to the method can be inserted into placeholders in the query string.
The placeholders are of the form `?0`, `?1`, `?2` etc. for the first, second, third parameter and so on.
The arguments passed to the method can be inserted into placeholders in the query string. The placeholders are of the form `?0`, `?1`, `?2` etc. for the first, second, third parameter and so on.
[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
@@ -345,20 +341,15 @@ It will be sent to Easticsearch as value of the query element; if for example th
}
----
====
.`@Query` annotation on a method taking a Collection argument
====
A repository method such as
[source,java]
----
@Query("{\"ids\": {\"values\": ?0 }}")
List<SampleEntity> getByIds(Collection<String> ids);
----
would make an https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ids-query.html[IDs query] to return all the matching documents.
So calling the method with a `List` of `["id1", "id2", "id3"]` would produce the query body
would make an https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ids-query.html[IDs query] to return all the matching documents. So calling the method with a `List` of `["id1", "id2", "id3"]` would produce the query body
[source,json]
----
{
@@ -378,6 +369,7 @@ So calling the method with a `List` of `["id1", "id2", "id3"]` would produce the
====
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> {
@@ -419,7 +411,6 @@ If for example the function is called with the parameter _John_, it would produc
.accessing parameter property.
====
Supposing that we have the following class as query parameter type:
[source,java]
----
public record QueryParameter(String value) {
@@ -453,9 +444,7 @@ We can pass `new QueryParameter("John")` as the parameter now, and it will produ
.accessing bean property.
====
https://docs.spring.io/spring-framework/reference/core/expressions/language-ref/bean-references.html[Bean property] is also supported to access.
Given that there is a bean named `queryParameter` of type `QueryParameter`, we can access the bean with symbol `@` rather than `#`, and there is no need to declare a parameter of type `QueryParameter` in the query method:
https://docs.spring.io/spring-framework/reference/core/expressions/language-ref/bean-references.html[Bean property] is also supported to access. Given that there is a bean named `queryParameter` of type `QueryParameter`, we can access the bean with symbol `@` rather than `#`, and there is no need to declare a parameter of type `QueryParameter` in the query method:
[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
@@ -504,7 +493,6 @@ interface BookRepository extends ElasticsearchRepository<Book, String> {
NOTE: collection values should not be quoted when declaring the elasticsearch json query.
A collection of `names` like `List.of("name1", "name2")` will produce the following terms query:
[source,json]
----
{
@@ -544,7 +532,6 @@ interface BookRepository extends ElasticsearchRepository<Book, String> {
Page<Book> findByName(Collection<QueryParameter> parameters, Pageable pageable);
}
----
This will extract all the `value` property values as a new `Collection` from `QueryParameter` collection, thus takes the same effect as above.
====
@@ -573,20 +560,3 @@ interface BookRepository extends ElasticsearchRepository<Book, String> {
----
====
[[elasticsearch.query-methods.at-searchtemplate-query]]
== Using the @SearchTemplateQuery Annotation
When using Elasticsearch search templates - (see xref:elasticsearch/misc.adoc#elasticsearch.misc.searchtemplates [Search Template support]) it is possible to specify that a repository method should use a template by adding the `@SearchTemplateQuery` annotation to that method.
Let's assume that there is a search template stored with the name "book-by-title" and this template need a parameter named "title", then a repository method using that search template can be defined like this:
[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
@SearchTemplateQuery(id = "book-by-title")
SearchHits<Book> findByTitle(String title);
}
----
The parameters of the repository method are sent to the seacrh template as key/value pairs where the key is the parameter name and the value is taken from the actual value when the method is invoked.
@@ -6,10 +6,9 @@ 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
| 2025.0 | 5.5.x | 8.18.1 | 6.2.x
| 2024.1 | 5.4.x | 8.15.5 | 6.1.x
| 2024.0 | 5.3.xfootnote:oom[Out of maintenance] | 8.13.4 | 6.1.x
| 2023.1 (Vaughan) | 5.2.xfootnote:oom[] | 8.11.1 | 6.1.x
| 2024.1 | 5.4.x | 8.15.5 | 6.2.x
| 2024.0 | 5.3.1 | 8.13.4 | 6.1.x
| 2023.1 (Vaughan) | 5.2.xfootnote:oom[Out of maintenance] | 8.11.1 | 6.1.x
| 2023.0 (Ullmann) | 5.1.xfootnote:oom[] | 8.7.1 | 6.0.x
| 2022.0 (Turing) | 5.0.xfootnote:oom[] | 8.5.3 | 6.0.x
| 2021.2 (Raj) | 4.4.xfootnote:oom[] | 7.17.3 | 5.3.x
@@ -1,30 +0,0 @@
[[elasticsearch-migration-guide-5.4-5.5]]
= Upgrading from 5.4.x to 5.5.x
This section describes breaking changes from version 5.4.x to 5.5.x and how removed features can be replaced by new introduced features.
[[elasticsearch-migration-guide-5.4-5.5.breaking-changes]]
== Breaking Changes
[[elasticsearch-migration-guide-5.4-5.5.deprecations]]
== Deprecations
Some classes that probably are not used by a library user have been renamed, the classes with the old names are still there, but are deprecated:
|===
|old name|new name
|ElasticsearchPartQuery|RepositoryPartQuery
|ElasticsearchStringQuery|RepositoryStringQuery
|ReactiveElasticsearchStringQuery|ReactiveRepositoryStringQuery
|===
=== Removals
The following methods that had been deprecated since release 5.3 have been removed:
```
DocumentOperations.delete(Query, Class<?>)
DocumentOperations.delete(Query, Class<?>, IndexCoordinates)
ReactiveDocumentOperations.delete(Query, Class<?>)
ReactiveDocumentOperations.delete(Query, Class<?>, IndexCoordinates)
```
@@ -1,42 +0,0 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.annotations;
import org.springframework.data.annotation.QueryAnnotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to mark a repository method as a search template method. The annotation defines the search template id,
* the parameters for the search template are taken from the method's arguments.
*
* @author P.J. Meisch (pj.meisch@sothawo.com)
* @since 5.5
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Documented
@QueryAnnotation
public @interface SearchTemplateQuery {
/**
* The id of the search template. Must not be empt or null.
*/
String id();
}
@@ -18,8 +18,6 @@ package org.springframework.data.elasticsearch.client.elc;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.transport.ElasticsearchTransport;
import java.io.IOException;
import org.elasticsearch.client.RestClient;
import org.springframework.util.Assert;
@@ -38,10 +36,7 @@ public class AutoCloseableElasticsearchClient extends ElasticsearchClient implem
}
@Override
public void close() throws IOException {
// since Elasticsearch 8.16 the ElasticsearchClient implements (through ApiClient) the Closeable interface and
// handles closing of the underlying transport. We now just call the base class, but keep this as we
// have been implementing AutoCloseable since 4.4 and won't change that to a mere Closeable
super.close();
public void close() throws Exception {
transport.close();
}
}
@@ -329,7 +329,7 @@ public final class ElasticsearchClients {
Assert.notNull(jsonpMapper, "jsonpMapper must not be null");
TransportOptions.Builder transportOptionsBuilder = transportOptions != null ? transportOptions.toBuilder()
: new RestClientOptions(RequestOptions.DEFAULT, false).toBuilder();
: new RestClientOptions(RequestOptions.DEFAULT).toBuilder();
RestClientOptions.Builder restClientOptionsBuilder = getRestClientOptionsBuilder(transportOptions);
@@ -135,6 +135,6 @@ public abstract class ElasticsearchConfiguration extends ElasticsearchConfigurat
* @return the options that should be added to every request. Must not be {@literal null}
*/
public TransportOptions transportOptions() {
return new RestClientOptions(RequestOptions.DEFAULT, false);
return new RestClientOptions(RequestOptions.DEFAULT);
}
}
@@ -181,6 +181,19 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
return delete(query, clazz, getIndexCoordinatesFor(clazz));
}
@Override
public ByQueryResponse delete(Query query, Class<?> clazz, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
DeleteByQueryRequest request = requestConverter.documentDeleteByQueryRequest(query, routingResolver.getRouting(),
clazz, index, getRefreshPolicy());
DeleteByQueryResponse response = execute(client -> client.deleteByQuery(request));
return responseConverter.byQueryResponse(response);
}
@Override
public ByQueryResponse delete(DeleteQuery query, Class<?> clazz, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
@@ -27,7 +27,6 @@ import co.elastic.clients.transport.endpoints.EndpointWithResponseMapperAttr;
import co.elastic.clients.util.ObjectBuilder;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.util.function.Function;
import org.springframework.lang.Nullable;
@@ -57,11 +56,8 @@ public class ReactiveElasticsearchClient extends ApiClient<ElasticsearchTranspor
}
@Override
public void close() throws IOException {
// since Elasticsearch 8.16 the ElasticsearchClient implements (through ApiClient) the Closeable interface and
// handles closing of the underlying transport. We now just call the base class, but keep this as we
// have been implementing AutoCloseable since 4.4 and won't change that to a mere Closeable
super.close();
public void close() throws Exception {
transport.close();
}
// region child clients
@@ -131,8 +127,7 @@ public class ReactiveElasticsearchClient extends ApiClient<ElasticsearchTranspor
// java.lang.Class<TDocument>)
// noinspection unchecked
JsonEndpoint<GetRequest, GetResponse<T>, ErrorResponse> endpoint = (JsonEndpoint<GetRequest, GetResponse<T>, ErrorResponse>) GetRequest._ENDPOINT;
endpoint = new EndpointWithResponseMapperAttr<>(endpoint,
"co.elastic.clients:Deserializer:_global.get.Response.TDocument",
endpoint = new EndpointWithResponseMapperAttr<>(endpoint, "co.elastic.clients:Deserializer:_global.get.Response.TDocument",
getDeserializer(tClass));
return Mono.fromFuture(transport.performRequestAsync(request, endpoint, transportOptions));
@@ -177,8 +172,7 @@ public class ReactiveElasticsearchClient extends ApiClient<ElasticsearchTranspor
// noinspection unchecked
JsonEndpoint<MgetRequest, MgetResponse<T>, ErrorResponse> endpoint = (JsonEndpoint<MgetRequest, MgetResponse<T>, ErrorResponse>) MgetRequest._ENDPOINT;
endpoint = new EndpointWithResponseMapperAttr<>(endpoint,
"co.elastic.clients:Deserializer:_global.mget.Response.TDocument",
endpoint = new EndpointWithResponseMapperAttr<>(endpoint, "co.elastic.clients:Deserializer:_global.mget.Response.TDocument",
this.getDeserializer(clazz));
return Mono.fromFuture(transport.performRequestAsync(request, endpoint, transportOptions));
@@ -125,6 +125,6 @@ public abstract class ReactiveElasticsearchConfiguration extends ElasticsearchCo
* @return the options that should be added to every request. Must not be {@literal null}
*/
public TransportOptions transportOptions() {
return new RestClientOptions(RequestOptions.DEFAULT, false).toBuilder().build();
return new RestClientOptions(RequestOptions.DEFAULT).toBuilder().build();
}
}
@@ -167,6 +167,16 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
.onErrorReturn(NoSuchIndexException.class, false);
}
@Override
public Mono<ByQueryResponse> delete(Query query, Class<?> entityType, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
DeleteByQueryRequest request = requestConverter.documentDeleteByQueryRequest(query, routingResolver.getRouting(),
entityType, index, getRefreshPolicy());
return Mono.from(execute(client -> client.deleteByQuery(request))).map(responseConverter::byQueryResponse);
}
@Override
public Mono<ByQueryResponse> delete(DeleteQuery query, Class<?> entityType, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
@@ -51,11 +51,9 @@ import co.elastic.clients.elasticsearch.indices.*;
import co.elastic.clients.elasticsearch.indices.ExistsIndexTemplateRequest;
import co.elastic.clients.elasticsearch.indices.ExistsRequest;
import co.elastic.clients.elasticsearch.indices.update_aliases.Action;
import co.elastic.clients.elasticsearch.sql.query.SqlFormat;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.json.JsonpDeserializer;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.util.NamedValue;
import co.elastic.clients.util.ObjectBuilder;
import jakarta.json.stream.JsonParser;
@@ -73,7 +71,6 @@ import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -120,6 +117,9 @@ class RequestConverter extends AbstractQueryProcessor {
private static final Log LOGGER = LogFactory.getLog(RequestConverter.class);
// the default max result window size of Elasticsearch
public static final Integer INDEX_MAX_RESULT_WINDOW = 10_000;
protected final JsonpMapper jsonpMapper;
protected final ElasticsearchConverter elasticsearchConverter;
@@ -533,22 +533,17 @@ class RequestConverter extends AbstractQueryProcessor {
public co.elastic.clients.elasticsearch.sql.QueryRequest sqlQueryRequest(SqlQuery query) {
Assert.notNull(query, "Query must not be null.");
return co.elastic.clients.elasticsearch.sql.QueryRequest.of(sqb -> sqb
.query(query.getQuery())
.catalog(query.getCatalog())
.columnar(query.getColumnar())
.cursor(query.getCursor())
.fetchSize(query.getFetchSize())
.fieldMultiValueLeniency(query.getFieldMultiValueLeniency())
.indexUsingFrozen(query.getIndexIncludeFrozen())
.keepAlive(time(query.getKeepAlive()))
.keepOnCompletion(query.getKeepOnCompletion())
.pageTimeout(time(query.getPageTimeout()))
.requestTimeout(time(query.getRequestTimeout()))
.waitForCompletionTimeout(time(query.getWaitForCompletionTimeout()))
.filter(getQuery(query.getFilter(), null))
.timeZone(Objects.toString(query.getTimeZone(), null))
.format(SqlFormat.Json));
return co.elastic.clients.elasticsearch.sql.QueryRequest.of(sqb -> {
sqb.query(query.getQuery()).catalog(query.getCatalog()).columnar(query.getColumnar()).cursor(query.getCursor())
.fetchSize(query.getFetchSize()).fieldMultiValueLeniency(query.getFieldMultiValueLeniency())
.indexUsingFrozen(query.getIndexIncludeFrozen()).keepAlive(time(query.getKeepAlive()))
.keepOnCompletion(query.getKeepOnCompletion()).pageTimeout(time(query.getPageTimeout()))
.requestTimeout(time(query.getRequestTimeout()))
.waitForCompletionTimeout(time(query.getWaitForCompletionTimeout())).filter(getQuery(query.getFilter(), null))
.timeZone(Objects.toString(query.getTimeZone(), null)).format("json");
return sqb;
});
}
// endregion
@@ -1292,8 +1287,11 @@ class RequestConverter extends AbstractQueryProcessor {
.timeout(timeStringMs(query.getTimeout())) //
;
bb.from((int) (query.getPageable().isPaged() ? query.getPageable().getOffset() : 0))
.size(query.getRequestSize());
if (query.getPageable().isPaged()) {
bb //
.from((int) query.getPageable().getOffset()) //
.size(query.getPageable().getPageSize());
}
if (!isEmpty(query.getFields())) {
bb.fields(fb -> {
@@ -1306,6 +1304,10 @@ class RequestConverter extends AbstractQueryProcessor {
bb.storedFields(query.getStoredFields());
}
if (query.isLimiting()) {
bb.size(query.getMaxResults());
}
if (query.getMinScore() > 0) {
bb.minScore((double) query.getMinScore());
}
@@ -1357,14 +1359,9 @@ class RequestConverter extends AbstractQueryProcessor {
}
if (!isEmpty(query.getIndicesBoost())) {
Stream<NamedValue<Double>> namedValueStream = query.getIndicesBoost().stream()
.map(indexBoost -> {
var namedValue = new NamedValue(indexBoost.getIndexName(),
Float.valueOf(indexBoost.getBoost()).doubleValue());
return namedValue;
});
List<NamedValue<Double>> namedValueList = namedValueStream.collect(Collectors.toList());
bb.indicesBoost(namedValueList);
bb.indicesBoost(query.getIndicesBoost().stream()
.map(indexBoost -> Map.of(indexBoost.getIndexName(), (double) indexBoost.getBoost()))
.collect(Collectors.toList()));
}
query.getScriptedFields().forEach(scriptedField -> bb.scriptFields(scriptedField.getFieldName(),
@@ -1463,8 +1460,13 @@ class RequestConverter extends AbstractQueryProcessor {
builder.seqNoPrimaryTerm(true);
}
builder.from((int) (query.getPageable().isPaged() ? query.getPageable().getOffset() : 0))
.size(query.getRequestSize());
if (query.getPageable().isPaged()) {
builder //
.from((int) query.getPageable().getOffset()) //
.size(query.getPageable().getPageSize());
} else {
builder.from(0).size(INDEX_MAX_RESULT_WINDOW);
}
if (!isEmpty(query.getFields())) {
var fieldAndFormats = query.getFields().stream().map(field -> FieldAndFormat.of(b -> b.field(field))).toList();
@@ -1479,6 +1481,10 @@ class RequestConverter extends AbstractQueryProcessor {
addIndicesOptions(builder, query.getIndicesOptions());
}
if (query.isLimiting()) {
builder.size(query.getMaxResults());
}
if (query.getMinScore() > 0) {
builder.minScore((double) query.getMinScore());
}
@@ -1564,14 +1570,9 @@ class RequestConverter extends AbstractQueryProcessor {
}
if (!isEmpty(query.getIndicesBoost())) {
Stream<NamedValue<Double>> namedValueStream = query.getIndicesBoost().stream()
.map(indexBoost -> {
var namedValue = new NamedValue(indexBoost.getIndexName(),
Float.valueOf(indexBoost.getBoost()).doubleValue());
return namedValue;
});
List<NamedValue<Double>> namedValueList = namedValueStream.collect(Collectors.toList());
builder.indicesBoost(namedValueList);
builder.indicesBoost(query.getIndicesBoost().stream()
.map(indexBoost -> Map.of(indexBoost.getIndexName(), (double) indexBoost.getBoost()))
.collect(Collectors.toList()));
}
if (!isEmpty(query.getDocValueFields())) {
@@ -2013,12 +2014,9 @@ class RequestConverter extends AbstractQueryProcessor {
private SourceConfig getSourceConfig(Query query) {
if (query.getSourceFilter() != null) {
return SourceConfig.of(s -> {
SourceFilter sourceFilter = query.getSourceFilter();
if (sourceFilter.fetchSource() != null) {
s.fetch(sourceFilter.fetchSource());
} else {
s.filter(sfb -> {
return SourceConfig.of(s -> s //
.filter(sfb -> {
SourceFilter sourceFilter = query.getSourceFilter();
String[] includes = sourceFilter.getIncludes();
String[] excludes = sourceFilter.getExcludes();
@@ -2031,10 +2029,7 @@ class RequestConverter extends AbstractQueryProcessor {
}
return sfb;
});
}
return s;
});
}));
} else {
return null;
}
@@ -92,7 +92,7 @@ class ResponseConverter {
return ClusterHealth.builder() //
.withActivePrimaryShards(healthResponse.activePrimaryShards()) //
.withActiveShards(healthResponse.activeShards()) //
.withActiveShardsPercent(healthResponse.activeShardsPercentAsNumber())//
.withActiveShardsPercent(Double.parseDouble(healthResponse.activeShardsPercentAsNumber()))//
.withClusterName(healthResponse.clusterName()) //
.withDelayedUnassignedShards(healthResponse.delayedUnassignedShards()) //
.withInitializingShards(healthResponse.initializingShards()) //
@@ -400,6 +400,7 @@ class ResponseConverter {
private ReindexResponse.Failure reindexResponseFailureOf(BulkIndexByScrollFailure failure) {
return ReindexResponse.Failure.builder() //
.withIndex(failure.index()) //
.withType(failure.type()) //
.withId(failure.id()) //
.withStatus(failure.status())//
.withErrorCause(toErrorCause(failure.cause())) //
@@ -410,6 +411,7 @@ class ResponseConverter {
private ByQueryResponse.Failure byQueryResponseFailureOf(BulkIndexByScrollFailure failure) {
return ByQueryResponse.Failure.builder() //
.withIndex(failure.index()) //
.withType(failure.type()) //
.withId(failure.id()) //
.withStatus(failure.status())//
.withErrorCause(toErrorCause(failure.cause())).build();
@@ -498,10 +500,6 @@ class ResponseConverter {
builder.withDeleted(response.deleted());
}
if(response.updated() != null) {
builder.withUpdated(response.updated());
}
if (response.batches() != null) {
builder.withBatches(Math.toIntExact(response.batches()));
}
@@ -298,6 +298,12 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
return this.delete(id, getIndexCoordinatesFor(entityType));
}
@Override
@Deprecated
public ByQueryResponse delete(Query query, Class<?> clazz) {
return delete(query, clazz, getIndexCoordinatesFor(clazz));
}
@Override
public String delete(Object entity) {
return delete(entity, getIndexCoordinatesFor(entity.getClass()));
@@ -233,7 +233,6 @@ abstract public class AbstractReactiveElasticsearchTemplate
.subscribe(new Subscriber<>() {
@Nullable private Subscription subscription = null;
private final AtomicBoolean upstreamComplete = new AtomicBoolean(false);
private final AtomicBoolean onNextHasBeenCalled = new AtomicBoolean(false);
@Override
public void onSubscribe(Subscription subscription) {
@@ -243,7 +242,6 @@ abstract public class AbstractReactiveElasticsearchTemplate
@Override
public void onNext(List<T> entityList) {
onNextHasBeenCalled.set(true);
saveAll(entityList, index)
.map(sink::tryEmitNext)
.doOnComplete(() -> {
@@ -269,10 +267,6 @@ abstract public class AbstractReactiveElasticsearchTemplate
@Override
public void onComplete() {
upstreamComplete.set(true);
if (!onNextHasBeenCalled.get()) {
// this happens when an empty flux is saved
sink.tryEmitComplete();
}
}
});
return sink.asFlux();
@@ -414,6 +408,12 @@ abstract public class AbstractReactiveElasticsearchTemplate
abstract protected Mono<String> doDeleteById(String id, @Nullable String routing, IndexCoordinates index);
@Override
@Deprecated
public Mono<ByQueryResponse> delete(Query query, Class<?> entityType) {
return delete(query, entityType, getIndexCoordinatesFor(entityType));
}
@Override
public Mono<ByQueryResponse> delete(DeleteQuery query, Class<?> entityType) {
return delete(query, entityType, getIndexCoordinatesFor(entityType));
@@ -272,6 +272,19 @@ public interface DocumentOperations {
*/
String delete(Object entity, IndexCoordinates index);
/**
* Delete all records matching the query.
*
* @param query query defining the objects
* @param clazz The entity class, must be annotated with
* {@link org.springframework.data.elasticsearch.annotations.Document}
* @return response with detailed information
* @since 4.1
* @deprecated since 5.3.0, use {@link #delete(DeleteQuery, Class)}
*/
@Deprecated
ByQueryResponse delete(Query query, Class<?> clazz);
/**
* Delete all records matching the query.
*
@@ -283,6 +296,19 @@ public interface DocumentOperations {
*/
ByQueryResponse delete(DeleteQuery query, Class<?> clazz);
/**
* Delete all records matching the query.
*
* @param query query defining the objects
* @param clazz The entity class, must be annotated with
* {@link org.springframework.data.elasticsearch.annotations.Document}
* @param index the index from which to delete
* @return response with detailed information
* @deprecated since 5.3.0, use {@link #delete(DeleteQuery, Class, IndexCoordinates)}
*/
@Deprecated
ByQueryResponse delete(Query query, Class<?> clazz, IndexCoordinates index);
/**
* Delete all records matching the query.
*
@@ -326,6 +326,17 @@ public interface ReactiveDocumentOperations {
*/
Mono<String> delete(String id, Class<?> entityType);
/**
* Delete the documents matching the given {@link Query} extracting index from entity metadata.
*
* @param query must not be {@literal null}.
* @param entityType must not be {@literal null}.
* @return a {@link Mono} emitting the number of the removed documents.
* @deprecated since 5.3.0, use {@link #delete(DeleteQuery, Class)}
*/
@Deprecated
Mono<ByQueryResponse> delete(Query query, Class<?> entityType);
/**
* Delete the documents matching the given {@link Query} extracting index from entity metadata.
*
@@ -336,6 +347,18 @@ public interface ReactiveDocumentOperations {
*/
Mono<ByQueryResponse> delete(DeleteQuery query, Class<?> entityType);
/**
* Delete the documents matching the given {@link Query} extracting index from entity metadata.
*
* @param query must not be {@literal null}.
* @param entityType must not be {@literal null}.
* @param index the target index, must not be {@literal null}
* @return a {@link Mono} emitting the number of the removed documents.
* @deprecated since 5.3.0, use {@link #delete(DeleteQuery, Class, IndexCoordinates)}
*/
@Deprecated
Mono<ByQueryResponse> delete(Query query, Class<?> entityType, IndexCoordinates index);
/**
* Delete the documents matching the given {@link Query} extracting index from entity metadata.
*
@@ -19,14 +19,14 @@ import java.time.temporal.TemporalAccessor;
/**
* Interface to convert from and to {@link TemporalAccessor}s.
*
*
* @author Peter-Josef Meisch
* @since 4.2
*/
public interface DateFormatter {
/**
* Formats a {@link TemporalAccessor} into a String.
*
*
* @param accessor must not be {@literal null}
* @return the formatted String
*/
@@ -34,7 +34,7 @@ public interface DateFormatter {
/**
* Parses a String into a {@link TemporalAccessor}.
*
*
* @param input the String to parse, must not be {@literal null}
* @param type the class of T
* @param <T> the {@link TemporalAccessor} implementation
@@ -38,7 +38,6 @@ import org.springframework.core.env.Environment;
import org.springframework.core.env.EnvironmentCapable;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.ScriptedField;
import org.springframework.data.elasticsearch.core.document.Document;
@@ -51,7 +50,6 @@ import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.Field;
import org.springframework.data.elasticsearch.core.query.Order;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.data.elasticsearch.core.query.SourceFilter;
@@ -394,7 +392,7 @@ public class MappingElasticsearchConverter
}
if (source instanceof SearchDocument searchDocument) {
populateScriptedFields(targetEntity, result, searchDocument);
populateScriptFields(targetEntity, result, searchDocument);
}
return result;
} catch (ConversionException e) {
@@ -652,16 +650,7 @@ public class MappingElasticsearchConverter
return conversionService.convert(value, target);
}
/**
* Checks if any of the properties of the entity is annotated with
*
* @{@link ScriptedField}. If so, the value of this property is set from the returned fields in the document.
* @param entity the entity to defining the persistent property
* @param result the rsult to populate
* @param searchDocument the search result caontaining the fields
* @param <T> the result type
*/
private <T> void populateScriptedFields(ElasticsearchPersistentEntity<?> entity, T result,
private <T> void populateScriptFields(ElasticsearchPersistentEntity<?> entity, T result,
SearchDocument searchDocument) {
Map<String, List<Object>> fields = searchDocument.getFields();
entity.doWithProperties((SimplePropertyHandler) property -> {
@@ -670,13 +659,8 @@ public class MappingElasticsearchConverter
// noinspection ConstantConditions
String name = scriptedField.name().isEmpty() ? property.getName() : scriptedField.name();
if (fields.containsKey(name)) {
if (property.isCollectionLike()) {
List<Object> values = searchDocument.getFieldValues(name);
entity.getPropertyAccessor(result).setProperty(property, values);
} else {
Object value = searchDocument.getFieldValue(name);
entity.getPropertyAccessor(result).setProperty(property, value);
}
Object value = searchDocument.getFieldValue(name);
entity.getPropertyAccessor(result).setProperty(property, value);
}
}
});
@@ -1247,7 +1231,7 @@ public class MappingElasticsearchConverter
return;
}
updatePropertiesInFieldsSortAndSourceFilter(query, domainClass);
updatePropertiesInFieldsAndSourceFilter(query, domainClass);
if (query instanceof CriteriaQuery criteriaQuery) {
updatePropertiesInCriteriaQuery(criteriaQuery, domainClass);
@@ -1258,14 +1242,7 @@ public class MappingElasticsearchConverter
}
}
/**
* replaces the names of fields in the query, the sort or soucre filters with the field names used in Elasticsearch
* when they are defined on the ElasticsearchProperties
*
* @param query the query to process
* @param domainClass the domain class (persistent entity)
*/
private void updatePropertiesInFieldsSortAndSourceFilter(Query query, Class<?> domainClass) {
private void updatePropertiesInFieldsAndSourceFilter(Query query, Class<?> domainClass) {
ElasticsearchPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(domainClass);
@@ -1273,12 +1250,12 @@ public class MappingElasticsearchConverter
List<String> fields = query.getFields();
if (!fields.isEmpty()) {
query.setFields(propertyToFieldNames(fields, persistentEntity));
query.setFields(updateFieldNames(fields, persistentEntity));
}
List<String> storedFields = query.getStoredFields();
if (!CollectionUtils.isEmpty(storedFields)) {
query.setStoredFields(propertyToFieldNames(storedFields, persistentEntity));
query.setStoredFields(updateFieldNames(storedFields, persistentEntity));
}
SourceFilter sourceFilter = query.getSourceFilter();
@@ -1289,60 +1266,37 @@ public class MappingElasticsearchConverter
String[] excludes = null;
if (sourceFilter.getIncludes() != null) {
includes = propertyToFieldNames(Arrays.asList(sourceFilter.getIncludes()), persistentEntity)
includes = updateFieldNames(Arrays.asList(sourceFilter.getIncludes()), persistentEntity)
.toArray(new String[] {});
}
if (sourceFilter.getExcludes() != null) {
excludes = propertyToFieldNames(Arrays.asList(sourceFilter.getExcludes()), persistentEntity)
excludes = updateFieldNames(Arrays.asList(sourceFilter.getExcludes()), persistentEntity)
.toArray(new String[] {});
}
query.addSourceFilter(new FetchSourceFilter(sourceFilter.fetchSource(), includes, excludes));
}
if (query.getSort() != null) {
var sort = query.getSort();
// stream the orders and map them to a new order with the changed names,
// then replace the existing sort with a new sort containing the new orders.
var newOrders = sort.stream().map(order -> {
var fieldNames = updateFieldNames(order.getProperty(), persistentEntity);
if (order instanceof Order springDataElasticsearchOrder) {
return springDataElasticsearchOrder.withProperty(fieldNames);
} else {
return new Sort.Order(order.getDirection(),
fieldNames,
order.isIgnoreCase(),
order.getNullHandling());
}
}).toList();
if (query instanceof BaseQuery baseQuery) {
baseQuery.setSort(Sort.by(newOrders));
}
query.addSourceFilter(new FetchSourceFilter(includes, excludes));
}
}
}
/**
* replaces property name of a property of the persistentEntity with the corresponding fieldname. If no such property
* exists, the original fieldName is kept.
* relaces the fieldName with the property name of a property of the persistentEntity with the corresponding
* fieldname. If no such property exists, the original fieldName is kept.
*
* @param propertyNames list of fieldnames
* @param fieldNames list of fieldnames
* @param persistentEntity the persistent entity to check
* @return an updated list of field names
*/
private List<String> propertyToFieldNames(List<String> propertyNames,
ElasticsearchPersistentEntity<?> persistentEntity) {
return propertyNames.stream().map(propertyName -> propertyToFieldName(persistentEntity, propertyName))
private List<String> updateFieldNames(List<String> fieldNames, ElasticsearchPersistentEntity<?> persistentEntity) {
return fieldNames.stream().map(fieldName -> updateFieldName(persistentEntity, fieldName))
.collect(Collectors.toList());
}
@NotNull
private String propertyToFieldName(ElasticsearchPersistentEntity<?> persistentEntity, String propertyName) {
ElasticsearchPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(propertyName);
return persistentProperty != null ? persistentProperty.getFieldName() : propertyName;
private String updateFieldName(ElasticsearchPersistentEntity<?> persistentEntity, String fieldName) {
ElasticsearchPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(fieldName);
return persistentProperty != null ? persistentProperty.getFieldName() : fieldName;
}
private void updatePropertiesInCriteriaQuery(CriteriaQuery criteriaQuery, Class<?> domainClass) {
@@ -1372,85 +1326,15 @@ public class MappingElasticsearchConverter
return;
}
var propertyNamesUpdate = updatePropertyNames(persistentEntity, field.getName());
var fieldNames = propertyNamesUpdate.names();
field.setName(String.join(".", fieldNames));
if (propertyNamesUpdate.propertyCount() > 1 && propertyNamesUpdate.nestedProperty()) {
List<String> propertyNames = Arrays.asList(fieldNames);
field.setPath(String.join(".", propertyNames.subList(0, propertyNamesUpdate.propertyCount - 1)));
}
if (propertyNamesUpdate.persistentProperty != null) {
if (propertyNamesUpdate.persistentProperty.hasPropertyValueConverter()) {
PropertyValueConverter propertyValueConverter = Objects
.requireNonNull(propertyNamesUpdate.persistentProperty.getPropertyValueConverter());
criteria.getQueryCriteriaEntries().forEach(criteriaEntry -> {
if (criteriaEntry.getKey().hasValue()) {
Object value = criteriaEntry.getValue();
if (value.getClass().isArray()) {
Object[] objects = (Object[]) value;
for (int i = 0; i < objects.length; i++) {
objects[i] = propertyValueConverter.write(objects[i]);
}
} else {
criteriaEntry.setValue(propertyValueConverter.write(value));
}
}
});
}
org.springframework.data.elasticsearch.annotations.Field fieldAnnotation = propertyNamesUpdate.persistentProperty
.findAnnotation(org.springframework.data.elasticsearch.annotations.Field.class);
if (fieldAnnotation != null) {
field.setFieldType(fieldAnnotation.type());
}
}
}
static record PropertyNamesUpdate(
String[] names,
Boolean nestedProperty,
Integer propertyCount,
ElasticsearchPersistentProperty persistentProperty) {
}
@Override
public String updateFieldNames(String propertyPath, ElasticsearchPersistentEntity<?> persistentEntity) {
Assert.notNull(propertyPath, "propertyPath must not be null");
Assert.notNull(persistentEntity, "persistentEntity must not be null");
var propertyNamesUpdate = updatePropertyNames(persistentEntity, propertyPath);
return String.join(".", propertyNamesUpdate.names());
}
/**
* Parse a propertyPath and replace the path values with the field names from a persistentEntity. path entries not
* found in the entity are kept as they are.
*
* @return the eventually modified names, a flag if a nested entity was encountered the number of processed
* propertiesand the last processed PersistentProperty.
*/
PropertyNamesUpdate updatePropertyNames(ElasticsearchPersistentEntity<?> persistentEntity, String propertyPath) {
String[] propertyNames = propertyPath.split("\\.");
String[] fieldNames = Arrays.copyOf(propertyNames, propertyNames.length);
String[] fieldNames = field.getName().split("\\.");
ElasticsearchPersistentEntity<?> currentEntity = persistentEntity;
ElasticsearchPersistentProperty persistentProperty = null;
int propertyCount = 0;
boolean isNested = false;
for (int i = 0; i < propertyNames.length; i++) {
persistentProperty = currentEntity.getPersistentProperty(propertyNames[i]);
for (int i = 0; i < fieldNames.length; i++) {
persistentProperty = currentEntity.getPersistentProperty(fieldNames[i]);
if (persistentProperty != null) {
propertyCount++;
@@ -1477,8 +1361,77 @@ public class MappingElasticsearchConverter
}
}
return new PropertyNamesUpdate(fieldNames, isNested, propertyCount, persistentProperty);
field.setName(String.join(".", fieldNames));
if (propertyCount > 1 && isNested) {
List<String> propertyNames = Arrays.asList(fieldNames);
field.setPath(String.join(".", propertyNames.subList(0, propertyCount - 1)));
}
if (persistentProperty != null) {
if (persistentProperty.hasPropertyValueConverter()) {
PropertyValueConverter propertyValueConverter = Objects
.requireNonNull(persistentProperty.getPropertyValueConverter());
criteria.getQueryCriteriaEntries().forEach(criteriaEntry -> {
if (criteriaEntry.getKey().hasValue()) {
Object value = criteriaEntry.getValue();
if (value.getClass().isArray()) {
Object[] objects = (Object[]) value;
for (int i = 0; i < objects.length; i++) {
objects[i] = propertyValueConverter.write(objects[i]);
}
} else {
criteriaEntry.setValue(propertyValueConverter.write(value));
}
}
});
}
org.springframework.data.elasticsearch.annotations.Field fieldAnnotation = persistentProperty
.findAnnotation(org.springframework.data.elasticsearch.annotations.Field.class);
if (fieldAnnotation != null) {
field.setFieldType(fieldAnnotation.type());
}
}
}
@Override
public String updateFieldNames(String propertyPath, ElasticsearchPersistentEntity<?> persistentEntity) {
Assert.notNull(propertyPath, "propertyPath must not be null");
Assert.notNull(persistentEntity, "persistentEntity must not be null");
var properties = propertyPath.split("\\.", 2);
if (properties.length > 0) {
var propertyName = properties[0];
var fieldName = updateFieldName(persistentEntity, propertyName);
if (properties.length > 1) {
var persistentProperty = persistentEntity.getPersistentProperty(propertyName);
if (persistentProperty != null) {
ElasticsearchPersistentEntity<?> nestedPersistentEntity = mappingContext
.getPersistentEntity(persistentProperty);
if (nestedPersistentEntity != null) {
return fieldName + '.' + updateFieldNames(properties[1], nestedPersistentEntity);
} else {
return fieldName;
}
}
}
return fieldName;
} else {
return propertyPath;
}
}
// endregion
@SuppressWarnings("ClassCanBeRecord")
@@ -57,20 +57,6 @@ public interface SearchDocument extends Document {
return (V) values.get(0);
}
/**
* @param name the field name
* @param <V> the type of elements
* @return the values of the given field.
*/
@Nullable
default <V> List<V> getFieldValues(final String name) {
List<Object> values = getFields().get(name);
if (values == null) {
return null;
}
return (List<V>) values;
}
/**
* @return the sort values for the search hit
*/
@@ -21,7 +21,7 @@ import org.springframework.data.elasticsearch.core.document.Document;
/**
* Interface definition for structures defined in <a href="https://geojson.org">GeoJSON</a>
* format. copied from Spring Data Mongodb
* format. copied from Spring Data Mongodb
*
* @author Christoph Strobl
* @since 1.7
@@ -69,7 +69,7 @@ public class GeoJsonLineString implements GeoJson<Iterable<Point>> {
Assert.notNull(second, "Second point must not be null!");
Assert.notNull(others, "Additional points must not be null!");
List<Point> points = new ArrayList<>(2 + others.length);
List<Point> points = new ArrayList<>();
points.add(first);
points.add(second);
points.addAll(Arrays.asList(others));
@@ -103,7 +103,7 @@ public class GeoJsonLineString implements GeoJson<Iterable<Point>> {
Assert.notNull(second, "Second point must not be null!");
Assert.notNull(others, "Additional points must not be null!");
List<Point> points = new ArrayList<>(2 + others.length);
List<Point> points = new ArrayList<>();
points.add(GeoPoint.toPoint(first));
points.add(GeoPoint.toPoint(second));
points.addAll(Arrays.stream(others).map(GeoPoint::toPoint).collect(Collectors.toList()));
@@ -69,7 +69,7 @@ public class GeoJsonMultiPoint implements GeoJson<Iterable<Point>> {
Assert.notNull(second, "Second point must not be null!");
Assert.notNull(others, "Additional points must not be null!");
List<Point> points = new ArrayList<>(2 + others.length);
List<Point> points = new ArrayList<>();
points.add(first);
points.add(second);
points.addAll(Arrays.asList(others));
@@ -103,7 +103,7 @@ public class GeoJsonMultiPoint implements GeoJson<Iterable<Point>> {
Assert.notNull(second, "Second point must not be null!");
Assert.notNull(others, "Additional points must not be null!");
List<Point> points = new ArrayList<>(2 + others.length);
List<Point> points = new ArrayList<>();
points.add(GeoPoint.toPoint(first));
points.add(GeoPoint.toPoint(second));
points.addAll(Arrays.stream(others).map(GeoPoint::toPoint).collect(Collectors.toList()));
@@ -189,13 +189,7 @@ public class GeoJsonPolygon implements GeoJson<Iterable<GeoJsonLineString>> {
@SafeVarargs
private static <T> List<T> asList(T first, T second, T third, T fourth, T... others) {
Assert.notNull(first, "First element must not be null!");
Assert.notNull(second, "Second element must not be null!");
Assert.notNull(third, "Third element must not be null!");
Assert.notNull(fourth, "Fourth element must not be null!");
Assert.notNull(others, "Additional elements must not be null!");
ArrayList<T> result = new ArrayList<>(4 + others.length);
ArrayList<T> result = new ArrayList<>(3 + others.length);
result.add(first);
result.add(second);
@@ -27,7 +27,6 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
@@ -48,15 +47,10 @@ import org.springframework.util.Assert;
*/
public class BaseQuery implements Query {
public static final int INDEX_MAX_RESULT_WINDOW = 10_000;
private static final int DEFAULT_REACTIVE_BATCH_SIZE = 500;
// the instance to mark the query pageable initial status, needed to distinguish between the initial
// value and a user-set unpaged value; values don't matter, the RequestConverter compares to the isntance.
private static final Pageable UNSET_PAGE = PageRequest.of(0, 1);
@Nullable protected Sort sort;
protected Pageable pageable = UNSET_PAGE;
protected Pageable pageable = DEFAULT_PAGE;
protected List<String> fields = new ArrayList<>();
@Nullable protected List<String> storedFields;
@Nullable protected SourceFilter sourceFilter;
@@ -84,7 +78,7 @@ public class BaseQuery implements Query {
private boolean queryIsUpdatedByConverter = false;
@Nullable private Integer reactiveBatchSize = null;
@Nullable private Boolean allowNoIndices = null;
private EnumSet<IndicesOptions.WildcardStates> expandWildcards = EnumSet.noneOf(IndicesOptions.WildcardStates.class);
private EnumSet<IndicesOptions.WildcardStates> expandWildcards;
private List<DocValueField> docValueFields = new ArrayList<>();
private List<ScriptedField> scriptedFields = new ArrayList<>();
@@ -93,7 +87,7 @@ public class BaseQuery implements Query {
public <Q extends BaseQuery, B extends BaseQueryBuilder<Q, B>> BaseQuery(BaseQueryBuilder<Q, B> builder) {
this.sort = builder.getSort();
// do a setPageable after setting the sort, because the pageable may contain an additional sort
this.setPageable(builder.getPageable() != null ? builder.getPageable() : UNSET_PAGE);
this.setPageable(builder.getPageable() != null ? builder.getPageable() : DEFAULT_PAGE);
this.fields = builder.getFields();
this.storedFields = builder.getStoredFields();
this.sourceFilter = builder.getSourceFilter();
@@ -209,7 +203,7 @@ public class BaseQuery implements Query {
@Override
@SuppressWarnings("unchecked")
public final <T extends Query> T addSort(@Nullable Sort sort) {
if (sort == null || sort.isUnsorted()) {
if (sort == null) {
return (T) this;
}
@@ -567,52 +561,4 @@ public class BaseQuery implements Query {
public List<ScriptedField> getScriptedFields() {
return scriptedFields;
}
@Override
public Integer getRequestSize() {
var pageable = getPageable();
Integer requestSize = null;
if (pageable.isPaged() && pageable != UNSET_PAGE) {
// pagesize defined by the user
if (!isLimiting()) {
// no maxResults
requestSize = pageable.getPageSize();
} else {
// if we have both a page size and a max results, we take the min, this is necessary for
// searchForStream to work correctly (#3098) as there the page size defines what is
// returned in a single request, and the max result determines the total number of
// documents returned.
requestSize = Math.min(pageable.getPageSize(), getMaxResults());
}
} else if (pageable == UNSET_PAGE) {
// no user defined pageable
if (isLimiting()) {
// maxResults
requestSize = getMaxResults();
} else {
requestSize = DEFAULT_PAGE_SIZE;
}
} else {
// explicitly set unpaged
if (!isLimiting()) {
// no maxResults
requestSize = INDEX_MAX_RESULT_WINDOW;
} else {
// if we have both a implicit page size and a max results, we take the min, this is necessary for
// searchForStream to work correctly (#3098) as there the page size defines what is
// returned in a single request, and the max result determines the total number of
// documents returned.
requestSize = Math.min(INDEX_MAX_RESULT_WINDOW, getMaxResults());
}
}
if (requestSize == null) {
// this should not happen
requestSize = DEFAULT_PAGE_SIZE;
}
return requestSize;
}
}
@@ -167,6 +167,7 @@ public class ByQueryResponse {
public static class Failure {
@Nullable private final String index;
@Nullable private final String type;
@Nullable private final String id;
@Nullable private final Exception cause;
@Nullable private final Integer status;
@@ -175,10 +176,11 @@ public class ByQueryResponse {
@Nullable private final Boolean aborted;
@Nullable private final ElasticsearchErrorCause elasticsearchErrorCause;
private Failure(@Nullable String index, @Nullable String id, @Nullable Exception cause,
private Failure(@Nullable String index, @Nullable String type, @Nullable String id, @Nullable Exception cause,
@Nullable Integer status, @Nullable Long seqNo, @Nullable Long term, @Nullable Boolean aborted,
@Nullable ElasticsearchErrorCause elasticsearchErrorCause) {
this.index = index;
this.type = type;
this.id = id;
this.cause = cause;
this.status = status;
@@ -193,6 +195,11 @@ public class ByQueryResponse {
return index;
}
@Nullable
public String getType() {
return type;
}
@Nullable
public String getId() {
return id;
@@ -243,6 +250,7 @@ public class ByQueryResponse {
*/
public static final class FailureBuilder {
@Nullable private String index;
@Nullable private String type;
@Nullable private String id;
@Nullable private Exception cause;
@Nullable private Integer status;
@@ -258,6 +266,11 @@ public class ByQueryResponse {
return this;
}
public FailureBuilder withType(String type) {
this.type = type;
return this;
}
public FailureBuilder withId(String id) {
this.id = id;
return this;
@@ -294,7 +307,7 @@ public class ByQueryResponse {
}
public Failure build() {
return new Failure(index, id, cause, status, seqNo, term, aborted, elasticsearchErrorCause);
return new Failure(index, type, id, cause, status, seqNo, term, aborted, elasticsearchErrorCause);
}
}
}
@@ -22,7 +22,6 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.elasticsearch.core.geo.GeoBox;
@@ -860,7 +859,6 @@ public class Criteria {
// endregion
// region equals/hashcode
@Override
public boolean equals(Object o) {
if (this == o)
@@ -876,8 +874,6 @@ public class Criteria {
return false;
if (!Objects.equals(field, criteria.field))
return false;
if (!criteriaChain.filter(this).equals(criteria.criteriaChain.filter(criteria)))
return false;
if (!queryCriteriaEntries.equals(criteria.queryCriteriaEntries))
return false;
if (!filterCriteriaEntries.equals(criteria.filterCriteriaEntries))
@@ -890,16 +886,11 @@ public class Criteria {
int result = field != null ? field.hashCode() : 0;
result = 31 * result + (boost != +0.0f ? Float.floatToIntBits(boost) : 0);
result = 31 * result + (negating ? 1 : 0);
// the criteriaChain contains "this" object, so we need to filter it out
// to avoid a stackoverflow here, because the hashcode implementation
// uses the element's hashcodes
result = 31 * result + criteriaChain.filter(this).hashCode();
result = 31 * result + queryCriteriaEntries.hashCode();
result = 31 * result + filterCriteriaEntries.hashCode();
result = 31 * result + subCriteria.hashCode();
return result;
}
// endregion
@Override
public String toString() {
@@ -947,17 +938,7 @@ public class Criteria {
*
* @since 4.1
*/
public static class CriteriaChain extends LinkedList<Criteria> {
/**
* return a copy of this list with the given element filtered out.
*
* @param criteria the element to filter
* @return the filtered list
*/
List<Criteria> filter(Criteria criteria) {
return this.stream().filter(c -> c != criteria).collect(Collectors.toList());
}
}
public static class CriteriaChain extends LinkedList<Criteria> {}
/**
* Operator to join the entries of the criteria chain
@@ -28,16 +28,14 @@ import org.springframework.util.Assert;
*/
public class FetchSourceFilter implements SourceFilter {
@Nullable private final Boolean fetchSource;
@Nullable private final String[] includes;
@Nullable private final String[] excludes;
/**
* @since 5.2
*/
public static SourceFilter of(@Nullable Boolean fetchSource, @Nullable final String[] includes,
@Nullable final String[] excludes) {
return new FetchSourceFilter(fetchSource, includes, excludes);
public static SourceFilter of(@Nullable final String[] includes, @Nullable final String[] excludes) {
return new FetchSourceFilter(includes, excludes);
}
/**
@@ -50,18 +48,11 @@ public class FetchSourceFilter implements SourceFilter {
return builderFunction.apply(new FetchSourceFilterBuilder()).build();
}
public FetchSourceFilter(@Nullable Boolean fetchSource, @Nullable final String[] includes,
@Nullable final String[] excludes) {
this.fetchSource = fetchSource;
public FetchSourceFilter(@Nullable final String[] includes, @Nullable final String[] excludes) {
this.includes = includes;
this.excludes = excludes;
}
@Override
public Boolean fetchSource() {
return fetchSource;
}
@Override
public String[] getIncludes() {
return includes;
@@ -25,7 +25,6 @@ import org.springframework.lang.Nullable;
*/
public class FetchSourceFilterBuilder {
@Nullable private Boolean fetchSource;
@Nullable private String[] includes;
@Nullable private String[] excludes;
@@ -39,17 +38,12 @@ public class FetchSourceFilterBuilder {
return this;
}
public FetchSourceFilterBuilder withFetchSource(Boolean fetchSource) {
this.fetchSource = fetchSource;
return this;
}
public SourceFilter build() {
if (includes == null)
includes = new String[0];
if (excludes == null)
excludes = new String[0];
return new FetchSourceFilter(fetchSource, includes, excludes);
return new FetchSourceFilter(includes, excludes);
}
}
@@ -54,13 +54,6 @@ public class IndexQuery {
this.indexName = indexName;
}
/**
* @since 5.5
*/
public static IndexQueryBuilder builder() {
return new IndexQueryBuilder();
}
@Nullable
public String getId() {
return id;
@@ -484,13 +484,6 @@ public interface Query {
*/
List<ScriptedField> getScriptedFields();
/**
* @return the number of documents that should be requested from Elasticsearch in this query. Depends wether a
* Pageable and/or maxResult size is set on the query.
* @since 5.4.8 5.5.2
*/
public Integer getRequestSize();
/**
* @since 4.3
*/
@@ -15,22 +15,22 @@
*/
package org.springframework.data.elasticsearch.core.query;
import java.util.Map;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
import java.util.Map;
/**
* @author Peter-Josef Meisch
* @since 5.1
*/
public class SearchTemplateQueryBuilder extends BaseQueryBuilder<SearchTemplateQuery, SearchTemplateQueryBuilder> {
@Nullable private String id;
@Nullable
private String id;
@Nullable String source;
@Nullable Map<String, Object> params;
@Nullable
Map<String, Object> params;
@Nullable
public String getId() {
@@ -62,18 +62,6 @@ public class SearchTemplateQueryBuilder extends BaseQueryBuilder<SearchTemplateQ
return this;
}
@Override
public SearchTemplateQueryBuilder withSort(Sort sort) {
throw new IllegalArgumentException(
"sort is not supported in a searchtemplate query. Sort values must be defined in the stored template");
}
@Override
public SearchTemplateQueryBuilder withPageable(Pageable pageable) {
throw new IllegalArgumentException(
"paging is not supported in a searchtemplate query. from and size values must be defined in the stored template");
}
@Override
public SearchTemplateQuery build() {
return new SearchTemplateQuery(this);
@@ -20,7 +20,7 @@ import org.springframework.lang.Nullable;
/**
* SourceFilter for providing includes and excludes. Using these helps in reducing the amount of data that is returned
* from Elasticsearch especially when the stored documents are large and only some fields from these documents are
* from Elasticsearch especially when the stored docuements are large and only some fields from these documents are
* needed. If the SourceFilter includes the name of a property that has a different name mapped in Elasticsearch (see
* {@link Field#name()} this will automatically be mapped.
*
@@ -40,15 +40,4 @@ public interface SourceFilter {
*/
@Nullable
String[] getExcludes();
/**
* Flag to set the _source parameter in a query to true or false. If this is not null, the values returned from
* getIncludes() and getExcludes() are ignored
*
* @since 5.5
*/
@Nullable
default Boolean fetchSource() {
return null;
}
}
@@ -80,10 +80,6 @@ public abstract class HighlightCommonParameters {
return boundaryScannerLocale;
}
/**
* @deprecated the underlying functionality is deprecated since Elasticsearch 8.8.
*/
@Deprecated(since = "5.5")
public boolean getForceSource() {
return forceSource;
}
@@ -177,10 +173,6 @@ public abstract class HighlightCommonParameters {
return (SELF) this;
}
/**
* @deprecated the underlying functionality is deprecated since Elasticsearch 8.8.
*/
@Deprecated(since = "5.5")
public SELF withForceSource(boolean forceSource) {
this.forceSource = forceSource;
return (SELF) this;
@@ -187,6 +187,7 @@ public class ReindexResponse {
public static class Failure {
@Nullable private final String index;
@Nullable private final String type;
@Nullable private final String id;
@Nullable private final Exception cause;
@Nullable private final Integer status;
@@ -195,10 +196,11 @@ public class ReindexResponse {
@Nullable private final Boolean aborted;
@Nullable private final ElasticsearchErrorCause elasticsearchErrorCause;
private Failure(@Nullable String index, @Nullable String id, @Nullable Exception cause,
private Failure(@Nullable String index, @Nullable String type, @Nullable String id, @Nullable Exception cause,
@Nullable Integer status, @Nullable Long seqNo, @Nullable Long term, @Nullable Boolean aborted,
@Nullable ElasticsearchErrorCause elasticsearchErrorCause) {
this.index = index;
this.type = type;
this.id = id;
this.cause = cause;
this.status = status;
@@ -213,6 +215,11 @@ public class ReindexResponse {
return index;
}
@Nullable
public String getType() {
return type;
}
@Nullable
public String getId() {
return id;
@@ -262,6 +269,7 @@ public class ReindexResponse {
*/
public static final class FailureBuilder {
@Nullable private String index;
@Nullable private String type;
@Nullable private String id;
@Nullable private Exception cause;
@Nullable private Integer status;
@@ -277,6 +285,11 @@ public class ReindexResponse {
return this;
}
public Failure.FailureBuilder withType(String type) {
this.type = type;
return this;
}
public Failure.FailureBuilder withId(String id) {
this.id = id;
return this;
@@ -313,7 +326,7 @@ public class ReindexResponse {
}
public Failure build() {
return new Failure(index, id, cause, status, seqNo, term, aborted, elasticsearchErrorCause);
return new Failure(index, type, id, cause, status, seqNo, term, aborted, elasticsearchErrorCause);
}
}
}
@@ -24,10 +24,9 @@ import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.DeleteQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.SearchTemplateQuery;
import org.springframework.data.expression.ValueEvaluationContextProvider;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.util.StreamUtils;
@@ -50,11 +49,11 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository
protected ElasticsearchQueryMethod queryMethod;
protected final ElasticsearchOperations elasticsearchOperations;
protected final ElasticsearchConverter elasticsearchConverter;
protected final ValueEvaluationContextProvider evaluationContextProvider;
protected final QueryMethodEvaluationContextProvider evaluationContextProvider;
public AbstractElasticsearchRepositoryQuery(ElasticsearchQueryMethod queryMethod,
ElasticsearchOperations elasticsearchOperations,
ValueEvaluationContextProvider evaluationContextProvider) {
QueryMethodEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(queryMethod, "queryMethod must not be null");
Assert.notNull(elasticsearchOperations, "elasticsearchOperations must not be null");
@@ -115,15 +114,11 @@ 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 (query instanceof SearchTemplateQuery) {
// we cannot get a count here, from and size would be in the template
if (parameterAccessor.getPageable().isUnpaged()) {
int itemCount = (int) elasticsearchOperations.count(query, clazz, index);
query.setPageable(PageRequest.of(0, Math.max(1, itemCount)));
} else {
if (parameterAccessor.getPageable().isUnpaged()) {
int itemCount = (int) elasticsearchOperations.count(query, clazz, index);
query.setPageable(PageRequest.of(0, Math.max(1, itemCount)));
} else {
query.setPageable(parameterAccessor.getPageable());
}
query.setPageable(parameterAccessor.getPageable());
}
result = elasticsearchOperations.search(query, clazz, index);
} else {
@@ -142,8 +137,7 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository
var query = createQuery(parameterAccessor);
Assert.notNull(query, "unsupported query");
queryMethod.addSpecialMethodParameters(query, parameterAccessor,
elasticsearchOperations.getElasticsearchConverter(),
queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter(),
evaluationContextProvider);
return query;
@@ -15,7 +15,6 @@
*/
package org.springframework.data.elasticsearch.repository.query;
import org.springframework.data.expression.ValueEvaluationContextProvider;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -53,11 +52,11 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor
protected final ReactiveElasticsearchQueryMethod queryMethod;
private final ReactiveElasticsearchOperations elasticsearchOperations;
protected final ValueEvaluationContextProvider evaluationContextProvider;
protected final QueryMethodEvaluationContextProvider evaluationContextProvider;
AbstractReactiveElasticsearchRepositoryQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations elasticsearchOperations,
ValueEvaluationContextProvider evaluationContextProvider) {
QueryMethodEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(queryMethod, "queryMethod must not be null");
Assert.notNull(elasticsearchOperations, "elasticsearchOperations must not be null");
@@ -106,7 +105,7 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor
var query = createQuery(parameterAccessor);
Assert.notNull(query, "unsupported query");
queryMethod.addSpecialMethodParameters(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter(),
queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter(),
evaluationContextProvider);
String indexName = queryMethod.getEntityInformation().getIndexName();
@@ -16,7 +16,12 @@
package org.springframework.data.elasticsearch.repository.query;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.repository.query.parser.ElasticsearchQueryCreator;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.parser.PartTree;
/**
* ElasticsearchPartQuery
@@ -28,12 +33,42 @@ import org.springframework.data.repository.query.ValueExpressionDelegate;
* @author Rasmus Faber-Espensen
* @author Peter-Josef Meisch
* @author Haibo Liu
* @deprecated since 5.5, use {@link RepositoryPartQuery} instead
*/
@Deprecated(forRemoval = true)
public class ElasticsearchPartQuery extends RepositoryPartQuery {
public class ElasticsearchPartQuery extends AbstractElasticsearchRepositoryQuery {
private final PartTree tree;
private final MappingContext<?, ElasticsearchPersistentProperty> mappingContext;
public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations,
ValueExpressionDelegate valueExpressionDelegate) {
super(method, elasticsearchOperations, valueExpressionDelegate);
QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, elasticsearchOperations, evaluationContextProvider);
this.tree = new PartTree(queryMethod.getName(), queryMethod.getResultProcessor().getReturnedType().getDomainType());
this.mappingContext = elasticsearchConverter.getMappingContext();
}
@Override
public boolean isCountQuery() {
return tree.isCountProjection();
}
@Override
protected boolean isDeleteQuery() {
return tree.isDelete();
}
@Override
protected boolean isExistsQuery() {
return tree.isExistsProjection();
}
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor) {
BaseQuery query = new ElasticsearchQueryCreator(tree, accessor, mappingContext).createQuery();
if (tree.getMaxResults() != null) {
query.setMaxResults(tree.getMaxResults());
}
return query;
}
}
@@ -28,7 +28,6 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.elasticsearch.annotations.Highlight;
import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.annotations.SearchTemplateQuery;
import org.springframework.data.elasticsearch.annotations.SourceFilters;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
@@ -44,7 +43,6 @@ import org.springframework.data.elasticsearch.core.query.RuntimeField;
import org.springframework.data.elasticsearch.core.query.ScriptedField;
import org.springframework.data.elasticsearch.core.query.SourceFilter;
import org.springframework.data.elasticsearch.repository.support.QueryStringProcessor;
import org.springframework.data.expression.ValueEvaluationContextProvider;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -86,7 +84,6 @@ public class ElasticsearchQueryMethod extends QueryMethod {
@Nullable private final Query queryAnnotation;
@Nullable private final Highlight highlightAnnotation;
@Nullable private final SourceFilters sourceFilters;
@Nullable private final SearchTemplateQuery searchTemplateQueryAnnotation;
public ElasticsearchQueryMethod(Method method, RepositoryMetadata repositoryMetadata, ProjectionFactory factory,
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext) {
@@ -101,11 +98,17 @@ public class ElasticsearchQueryMethod extends QueryMethod {
this.highlightAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Highlight.class);
this.sourceFilters = AnnotatedElementUtils.findMergedAnnotation(method, SourceFilters.class);
this.unwrappedReturnType = potentiallyUnwrapReturnTypeFor(repositoryMetadata, method);
this.searchTemplateQueryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, SearchTemplateQuery.class);
verifyCountQueryTypes();
}
@SuppressWarnings("removal")
@Override
@Deprecated
protected Parameters<?, ?> createParameters(Method method, TypeInformation<?> domainType) {
return new ElasticsearchParameters(ParametersSource.of(method));
}
@Override
protected Parameters<?, ?> createParameters(ParametersSource parametersSource) {
return new ElasticsearchParameters(parametersSource);
@@ -122,16 +125,12 @@ public class ElasticsearchQueryMethod extends QueryMethod {
}
}
/**
* @return if the method is annotated with the {@link Query} annotation.
*/
public boolean hasAnnotatedQuery() {
return this.queryAnnotation != null;
}
/**
* @return the query String defined in the {@link Query} annotation. Must not be {@literal null} when
* {@link #hasAnnotatedQuery()} returns true.
* @return the query String. Must not be {@literal null} when {@link #hasAnnotatedQuery()} returns true
*/
@Nullable
public String getAnnotatedQuery() {
@@ -159,27 +158,6 @@ public class ElasticsearchQueryMethod extends QueryMethod {
return new HighlightQuery(highlightConverter.convert(highlightAnnotation), getDomainClass());
}
/**
* @return if the method is annotated with the {@link SearchTemplateQuery} annotation.
* @since 5.5
*/
public boolean hasAnnotatedSearchTemplateQuery() {
return this.searchTemplateQueryAnnotation != null;
}
/**
* @return the {@link SearchTemplateQuery} annotation
* @throws IllegalArgumentException if no {@link SearchTemplateQuery} annotation is present on the method
* @since 5.5
*/
public SearchTemplateQuery getAnnotatedSearchTemplateQuery() {
Assert.isTrue(hasAnnotatedSearchTemplateQuery(), "no SearchTemplateQuery annotation present on " + getName());
Assert.notNull(searchTemplateQueryAnnotation, "highlsearchTemplateQueryAnnotationightAnnotation must not be null");
return searchTemplateQueryAnnotation;
}
/**
* @return the {@link ElasticsearchEntityMetadata} for the query methods {@link #getReturnedObjectType() return type}.
* @since 3.2
@@ -303,7 +281,7 @@ public class ElasticsearchQueryMethod extends QueryMethod {
/**
* @return {@literal true} if the method is annotated with
* {@link org.springframework.data.elasticsearch.annotations.CountQuery} or with {@link Query}(count = true)
* {@link org.springframework.data.elasticsearch.annotations.CountQuery} or with {@link Query}(count =true)
* @since 4.2
*/
public boolean hasCountQueryAnnotation() {
@@ -325,7 +303,7 @@ public class ElasticsearchQueryMethod extends QueryMethod {
@Nullable
SourceFilter getSourceFilter(ElasticsearchParametersParameterAccessor parameterAccessor,
ElasticsearchConverter converter,
ValueEvaluationContextProvider evaluationContextProvider) {
QueryMethodEvaluationContextProvider evaluationContextProvider) {
if (sourceFilters == null || (sourceFilters.includes().length == 0 && sourceFilters.excludes().length == 0)) {
return null;
@@ -348,7 +326,7 @@ public class ElasticsearchQueryMethod extends QueryMethod {
}
private String[] mapParameters(String[] source, ElasticsearchParametersParameterAccessor parameterAccessor,
ConversionService conversionService, ValueEvaluationContextProvider evaluationContextProvider) {
ConversionService conversionService, QueryMethodEvaluationContextProvider evaluationContextProvider) {
List<String> fieldNames = new ArrayList<>();
@@ -399,9 +377,9 @@ public class ElasticsearchQueryMethod extends QueryMethod {
}
}
void addSpecialMethodParameters(BaseQuery query, ElasticsearchParametersParameterAccessor parameterAccessor,
ElasticsearchConverter elasticsearchConverter,
ValueEvaluationContextProvider evaluationContextProvider) {
void addMethodParameter(BaseQuery query, ElasticsearchParametersParameterAccessor parameterAccessor,
ElasticsearchConverter elasticsearchConverter,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
if (hasAnnotatedHighlight()) {
var highlightQuery = getAnnotatedHighlightQuery(new HighlightConverter(parameterAccessor,
@@ -15,8 +15,13 @@
*/
package org.springframework.data.elasticsearch.repository.query;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.repository.support.QueryStringProcessor;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.util.Assert;
/**
* ElasticsearchStringQuery
@@ -27,12 +32,43 @@ import org.springframework.data.repository.query.ValueExpressionDelegate;
* @author Taylor Ono
* @author Peter-Josef Meisch
* @author Haibo Liu
* @deprecated since 5.5, use {@link RepositoryStringQuery}
*/
@Deprecated(since = "5.5", forRemoval = true)
public class ElasticsearchStringQuery extends RepositoryStringQuery {
public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQuery {
private final String queryString;
public ElasticsearchStringQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations,
String queryString, ValueExpressionDelegate valueExpressionDelegate) {
super(queryMethod, elasticsearchOperations, queryString, valueExpressionDelegate);
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;
}
@Override
public boolean isCountQuery() {
return queryMethod.hasCountQueryAnnotation();
}
@Override
protected boolean isDeleteQuery() {
return false;
}
@Override
protected boolean isExistsQuery() {
return false;
}
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
ConversionService conversionService = elasticsearchOperations.getElasticsearchConverter().getConversionService();
var processed = new QueryStringProcessor(queryString, queryMethod, conversionService, evaluationContextProvider)
.createQuery(parameterAccessor);
return new StringQuery(processed)
.addSort(parameterAccessor.getSort());
}
}
@@ -25,8 +25,8 @@ import org.springframework.data.elasticsearch.core.query.highlight.Highlight;
import org.springframework.data.elasticsearch.core.query.highlight.HighlightField;
import org.springframework.data.elasticsearch.core.query.highlight.HighlightParameters;
import org.springframework.data.elasticsearch.repository.support.QueryStringProcessor;
import org.springframework.data.expression.ValueEvaluationContextProvider;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.util.Assert;
/**
@@ -38,13 +38,13 @@ public class HighlightConverter {
private final ElasticsearchParametersParameterAccessor parameterAccessor;
private final ConversionService conversionService;
private final ValueEvaluationContextProvider evaluationContextProvider;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final QueryMethod queryMethod;
HighlightConverter(ElasticsearchParametersParameterAccessor parameterAccessor,
ConversionService conversionService,
ValueEvaluationContextProvider evaluationContextProvider,
QueryMethod queryMethod) {
ConversionService conversionService,
QueryMethodEvaluationContextProvider evaluationContextProvider,
QueryMethod queryMethod) {
Assert.notNull(parameterAccessor, "parameterAccessor must not be null");
Assert.notNull(conversionService, "conversionService must not be null");
@@ -15,6 +15,8 @@
*/
package org.springframework.data.elasticsearch.repository.query;
import static org.springframework.data.repository.util.ClassUtils.*;
import reactor.core.publisher.Mono;
import java.lang.reflect.Method;
@@ -34,7 +36,6 @@ import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.ReactiveWrappers;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.ClassUtils;
@@ -54,7 +55,7 @@ public class ReactiveElasticsearchQueryMethod extends ElasticsearchQueryMethod {
super(method, metadata, factory, mappingContext);
if (ReflectionUtils.hasParameterOfType(method, Pageable.class)) {
if (hasParameterOfType(method, Pageable.class)) {
TypeInformation<?> returnType = TypeInformation.fromReturnTypeOf(method);
boolean multiWrapper = ReactiveWrappers.isMultiValueType(returnType.getType());
@@ -74,7 +75,7 @@ public class ReactiveElasticsearchQueryMethod extends ElasticsearchQueryMethod {
method));
}
if (ReflectionUtils.hasParameterOfType(method, Sort.class)) {
if (hasParameterOfType(method, Sort.class)) {
throw new IllegalStateException(String.format("Method must not have Pageable *and* Sort parameter. "
+ "Use sorting capabilities on Pageable instead! Offending method: %s", method));
}
@@ -15,26 +15,68 @@
*/
package org.springframework.data.elasticsearch.repository.query;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.repository.support.QueryStringProcessor;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.util.Assert;
/**
* @author Christoph Strobl
* @author Taylor Ono
* @author Haibo Liu
* @since 3.2
* @deprecated since 5.5, use {@link ReactiveRepositoryStringQuery}
*/
@Deprecated(since = "5.5", forRemoval = true)
public class ReactiveElasticsearchStringQuery extends ReactiveRepositoryStringQuery {
public class ReactiveElasticsearchStringQuery extends AbstractReactiveElasticsearchRepositoryQuery {
private final String query;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
public ReactiveElasticsearchStringQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations operations, ValueExpressionDelegate valueExpressionDelegate) {
super(queryMethod, operations, valueExpressionDelegate);
ReactiveElasticsearchOperations operations, QueryMethodEvaluationContextProvider evaluationContextProvider) {
this(queryMethod.getAnnotatedQuery(), queryMethod, operations, evaluationContextProvider);
}
public ReactiveElasticsearchStringQuery(String query, ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations operations, ValueExpressionDelegate valueExpressionDelegate) {
super(query, queryMethod, operations, valueExpressionDelegate);
ReactiveElasticsearchOperations operations, QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(queryMethod, operations, evaluationContextProvider);
Assert.notNull(query, "query must not be null");
Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null");
this.query = query;
this.evaluationContextProvider = evaluationContextProvider;
}
@Override
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
ConversionService conversionService = getElasticsearchOperations().getElasticsearchConverter()
.getConversionService();
String processed = new QueryStringProcessor(query, queryMethod, conversionService, evaluationContextProvider)
.createQuery(parameterAccessor);
return new StringQuery(processed);
}
@Override
boolean isCountQuery() {
return queryMethod.hasCountQueryAnnotation();
}
@Override
boolean isDeleteQuery() {
return false;
}
@Override
boolean isExistsQuery() {
return false;
}
@Override
boolean isLimiting() {
return false;
}
}
@@ -19,8 +19,8 @@ import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperatio
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.repository.query.parser.ElasticsearchQueryCreator;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.repository.query.parser.PartTree;
/**
@@ -35,9 +35,8 @@ public class ReactivePartTreeElasticsearchQuery extends AbstractReactiveElastics
public ReactivePartTreeElasticsearchQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations elasticsearchOperations,
ValueExpressionDelegate valueExpressionDelegate) {
super(queryMethod, elasticsearchOperations,
valueExpressionDelegate.createValueContextProvider(queryMethod.getParameters()));
QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(queryMethod, elasticsearchOperations, evaluationContextProvider);
ResultProcessor processor = queryMethod.getResultProcessor();
this.tree = new PartTree(queryMethod.getName(), processor.getReturnedType().getDomainType());
@@ -1,93 +0,0 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.query;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.SearchTemplateQuery;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.util.Assert;
/**
* A reactive repository query that uses a search template already stored in Elasticsearch.
*
* @author P.J. Meisch (pj.meisch@sothawo.com)
* @since 5.5
*/
public class ReactiveRepositorySearchTemplateQuery extends AbstractReactiveElasticsearchRepositoryQuery {
private String id;
private Map<String, Object> params;
public ReactiveRepositorySearchTemplateQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations elasticsearchOperations,
ValueExpressionDelegate valueExpressionDelegate,
String id) {
super(queryMethod, elasticsearchOperations,
valueExpressionDelegate.createValueContextProvider(queryMethod.getParameters()));
Assert.hasLength(id, "id must not be null or empty");
this.id = id;
}
public String getId() {
return id;
}
public Map<String, Object> getParams() {
return params;
}
@Override
public boolean isCountQuery() {
return false;
}
@Override
protected boolean isDeleteQuery() {
return false;
}
@Override
protected boolean isExistsQuery() {
return false;
}
@Override
boolean isLimiting() {
return false;
}
@Override
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
var searchTemplateParameters = new LinkedHashMap<String, Object>();
var values = parameterAccessor.getValues();
parameterAccessor.getParameters().forEach(parameter -> {
if (!parameter.isSpecialParameter() && parameter.getName().isPresent() && parameter.getIndex() <= values.length) {
searchTemplateParameters.put(parameter.getName().get(), values[parameter.getIndex()]);
}
});
return SearchTemplateQuery.builder()
.withId(id)
.withParams(searchTemplateParameters)
.build();
}
}
@@ -1,82 +0,0 @@
/*
* Copyright 2019-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.query;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.repository.support.QueryStringProcessor;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.util.Assert;
/**
* Was originally named ReactiveElasticsearchStringQuery.
*
* @author Christoph Strobl
* @author Taylor Ono
* @author Haibo Liu
* @since 3.2
*/
public class ReactiveRepositoryStringQuery extends AbstractReactiveElasticsearchRepositoryQuery {
private final String query;
public ReactiveRepositoryStringQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations operations, ValueExpressionDelegate valueExpressionDelegate) {
this(queryMethod.getAnnotatedQuery(), queryMethod, operations, valueExpressionDelegate);
}
public ReactiveRepositoryStringQuery(String query, ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations operations, ValueExpressionDelegate valueExpressionDelegate) {
super(queryMethod, operations, valueExpressionDelegate.createValueContextProvider(queryMethod.getParameters()));
Assert.notNull(query, "query must not be null");
this.query = query;
}
@Override
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
ConversionService conversionService = getElasticsearchOperations().getElasticsearchConverter()
.getConversionService();
String processed = new QueryStringProcessor(query, queryMethod, conversionService, evaluationContextProvider)
.createQuery(parameterAccessor);
return new StringQuery(processed);
}
@Override
boolean isCountQuery() {
return queryMethod.hasCountQueryAnnotation();
}
@Override
boolean isDeleteQuery() {
return false;
}
@Override
boolean isExistsQuery() {
return false;
}
@Override
boolean isLimiting() {
return false;
}
}
@@ -1,76 +0,0 @@
/*
* Copyright 2013-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.query;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.repository.query.parser.ElasticsearchQueryCreator;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.repository.query.parser.PartTree;
/**
* A repository query that is built from the the method name in the repository definition. Was originally named
* ElasticsearchPartQuery.
*
* @author Rizwan Idrees
* @author Mohsin Husen
* @author Kevin Leturc
* @author Mark Paluch
* @author Rasmus Faber-Espensen
* @author Peter-Josef Meisch
* @author Haibo Liu
*/
public class RepositoryPartQuery extends AbstractElasticsearchRepositoryQuery {
private final PartTree tree;
private final MappingContext<?, ElasticsearchPersistentProperty> mappingContext;
public RepositoryPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations,
ValueExpressionDelegate valueExpressionDelegate) {
super(method, elasticsearchOperations,
valueExpressionDelegate.createValueContextProvider(method.getParameters()));
this.tree = new PartTree(queryMethod.getName(), queryMethod.getResultProcessor().getReturnedType().getDomainType());
this.mappingContext = elasticsearchConverter.getMappingContext();
}
@Override
public boolean isCountQuery() {
return tree.isCountProjection();
}
@Override
protected boolean isDeleteQuery() {
return tree.isDelete();
}
@Override
protected boolean isExistsQuery() {
return tree.isExistsProjection();
}
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor) {
BaseQuery query = new ElasticsearchQueryCreator(tree, accessor, mappingContext).createQuery();
if (tree.getMaxResults() != null) {
query.setMaxResults(tree.getMaxResults());
}
return query;
}
}
@@ -1,87 +0,0 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.query;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.SearchTemplateQuery;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.util.Assert;
/**
* A repository query that uses a search template already stored in Elasticsearch.
*
* @author P.J. Meisch (pj.meisch@sothawo.com)
* @since 5.5
*/
public class RepositorySearchTemplateQuery extends AbstractElasticsearchRepositoryQuery {
private String id;
private Map<String, Object> params;
public RepositorySearchTemplateQuery(ElasticsearchQueryMethod queryMethod,
ElasticsearchOperations elasticsearchOperations, ValueExpressionDelegate valueExpressionDelegate,
String id) {
super(queryMethod, elasticsearchOperations,
valueExpressionDelegate.createValueContextProvider(queryMethod.getParameters()));
Assert.hasLength(id, "id must not be null or empty");
this.id = id;
}
public String getId() {
return id;
}
public Map<String, Object> getParams() {
return params;
}
@Override
public boolean isCountQuery() {
return false;
}
@Override
protected boolean isDeleteQuery() {
return false;
}
@Override
protected boolean isExistsQuery() {
return false;
}
@Override
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
var searchTemplateParameters = new LinkedHashMap<String, Object>();
var values = parameterAccessor.getValues();
parameterAccessor.getParameters().forEach(parameter -> {
if (!parameter.isSpecialParameter() && parameter.getName().isPresent() && parameter.getIndex() <= values.length) {
searchTemplateParameters.put(parameter.getName().get(), values[parameter.getIndex()]);
}
});
return SearchTemplateQuery.builder()
.withId(id)
.withParams(searchTemplateParameters)
.build();
}
}
@@ -1,57 +0,0 @@
package org.springframework.data.elasticsearch.repository.query;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.repository.support.QueryStringProcessor;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.util.Assert;
/**
* A repository query that is defined by a String containing the query. Was originally named ElasticsearchStringQuery.
*
* @author Rizwan Idrees
* @author Mohsin Husen
* @author Mark Paluch
* @author Taylor Ono
* @author Peter-Josef Meisch
* @author Haibo Liu
*/
public class RepositoryStringQuery extends AbstractElasticsearchRepositoryQuery {
private final String queryString;
public RepositoryStringQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations,
String queryString, ValueExpressionDelegate valueExpressionDelegate) {
super(queryMethod, elasticsearchOperations,
valueExpressionDelegate.createValueContextProvider(queryMethod.getParameters()));
Assert.notNull(queryString, "Query cannot be empty");
this.queryString = queryString;
}
@Override
public boolean isCountQuery() {
return queryMethod.hasCountQueryAnnotation();
}
@Override
protected boolean isDeleteQuery() {
return false;
}
@Override
protected boolean isExistsQuery() {
return false;
}
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
ConversionService conversionService = elasticsearchOperations.getElasticsearchConverter().getConversionService();
var processed = new QueryStringProcessor(queryString, queryMethod, conversionService, evaluationContextProvider)
.createQuery(parameterAccessor);
return new StringQuery(processed)
.addSort(parameterAccessor.getSort());
}
}
@@ -22,10 +22,9 @@ 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;
import org.springframework.data.elasticsearch.repository.query.ElasticsearchQueryMethod;
import org.springframework.data.elasticsearch.repository.query.RepositoryPartQuery;
import org.springframework.data.elasticsearch.repository.query.RepositorySearchTemplateQuery;
import org.springframework.data.elasticsearch.repository.query.RepositoryStringQuery;
import org.springframework.data.elasticsearch.repository.query.ElasticsearchStringQuery;
import org.springframework.data.elasticsearch.repository.support.querybyexample.QueryByExampleElasticsearchExecutor;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
@@ -38,8 +37,8 @@ import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.repository.query.QueryByExampleExecutor;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -97,16 +96,16 @@ public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport {
@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
ValueExpressionDelegate valueExpressionDelegate) {
return Optional.of(new ElasticsearchQueryLookupStrategy(valueExpressionDelegate));
QueryMethodEvaluationContextProvider evaluationContextProvider) {
return Optional.of(new ElasticsearchQueryLookupStrategy(evaluationContextProvider));
}
private class ElasticsearchQueryLookupStrategy implements QueryLookupStrategy {
private final ValueExpressionDelegate valueExpressionDelegate;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
ElasticsearchQueryLookupStrategy(ValueExpressionDelegate valueExpressionDelegate) {
this.valueExpressionDelegate = valueExpressionDelegate;
ElasticsearchQueryLookupStrategy(QueryMethodEvaluationContextProvider evaluationContextProvider) {
this.evaluationContextProvider = evaluationContextProvider;
}
/*
@@ -123,17 +122,13 @@ public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport {
if (namedQueries.hasQuery(namedQueryName)) {
String namedQuery = namedQueries.getQuery(namedQueryName);
return new RepositoryStringQuery(queryMethod, elasticsearchOperations, namedQuery,
valueExpressionDelegate);
return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, namedQuery,
evaluationContextProvider);
} else if (queryMethod.hasAnnotatedQuery()) {
return new RepositoryStringQuery(queryMethod, elasticsearchOperations, queryMethod.getAnnotatedQuery(),
valueExpressionDelegate);
} else if (queryMethod.hasAnnotatedSearchTemplateQuery()) {
var searchTemplateQuery = queryMethod.getAnnotatedSearchTemplateQuery();
return new RepositorySearchTemplateQuery(queryMethod, elasticsearchOperations, valueExpressionDelegate,
searchTemplateQuery.id());
return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, queryMethod.getAnnotatedQuery(),
evaluationContextProvider);
}
return new RepositoryPartQuery(queryMethod, elasticsearchOperations, valueExpressionDelegate);
return new ElasticsearchPartQuery(queryMethod, elasticsearchOperations, evaluationContextProvider);
}
}
@@ -18,7 +18,6 @@ package org.springframework.data.elasticsearch.repository.support;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.repository.query.ElasticsearchParametersParameterAccessor;
import org.springframework.data.elasticsearch.repository.support.spel.QueryStringSpELEvaluator;
import org.springframework.data.expression.ValueEvaluationContextProvider;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.util.Assert;
@@ -35,10 +34,10 @@ public class QueryStringProcessor {
private final String query;
private final QueryMethod queryMethod;
private final ConversionService conversionService;
private final ValueEvaluationContextProvider evaluationContextProvider;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
public QueryStringProcessor(String query, QueryMethod queryMethod, ConversionService conversionService,
ValueEvaluationContextProvider evaluationContextProvider) {
QueryMethodEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(query, "query must not be null");
Assert.notNull(queryMethod, "queryMethod must not be null");
@@ -23,9 +23,8 @@ import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperatio
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchQueryMethod;
import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchStringQuery;
import org.springframework.data.elasticsearch.repository.query.ReactivePartTreeElasticsearchQuery;
import org.springframework.data.elasticsearch.repository.query.ReactiveRepositorySearchTemplateQuery;
import org.springframework.data.elasticsearch.repository.query.ReactiveRepositoryStringQuery;
import org.springframework.data.elasticsearch.repository.support.querybyexample.ReactiveQueryByExampleElasticsearchExecutor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.ProjectionFactory;
@@ -37,9 +36,9 @@ import org.springframework.data.repository.core.support.RepositoryComposition;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -93,10 +92,14 @@ public class ReactiveElasticsearchRepositoryFactory extends ReactiveRepositoryFa
return getTargetRepositoryViaReflection(information, entityInformation, operations);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getQueryLookupStrategy(org.springframework.data.repository.query.QueryLookupStrategy.Key, org.springframework.data.repository.query.EvaluationContextProvider)
*/
@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
ValueExpressionDelegate valueExpressionDelegate) {
return Optional.of(new ElasticsearchQueryLookupStrategy(operations, valueExpressionDelegate, mappingContext));
QueryMethodEvaluationContextProvider evaluationContextProvider) {
return Optional.of(new ElasticsearchQueryLookupStrategy(operations, evaluationContextProvider, mappingContext));
}
/*
@@ -127,19 +130,19 @@ public class ReactiveElasticsearchRepositoryFactory extends ReactiveRepositoryFa
private static class ElasticsearchQueryLookupStrategy implements QueryLookupStrategy {
private final ReactiveElasticsearchOperations operations;
private final ValueExpressionDelegate valueExpressionDelegate;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext;
public ElasticsearchQueryLookupStrategy(ReactiveElasticsearchOperations operations,
ValueExpressionDelegate valueExpressionDelegate,
QueryMethodEvaluationContextProvider evaluationContextProvider,
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext) {
Assert.notNull(operations, "operations must not be null");
Assert.notNull(valueExpressionDelegate, "evaluationContextProvider must not be null");
Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null");
Assert.notNull(mappingContext, "mappingContext must not be null");
this.operations = operations;
this.valueExpressionDelegate = valueExpressionDelegate;
this.evaluationContextProvider = evaluationContextProvider;
this.mappingContext = mappingContext;
}
@@ -158,16 +161,12 @@ public class ReactiveElasticsearchRepositoryFactory extends ReactiveRepositoryFa
if (namedQueries.hasQuery(namedQueryName)) {
String namedQuery = namedQueries.getQuery(namedQueryName);
return new ReactiveRepositoryStringQuery(namedQuery, queryMethod, operations,
valueExpressionDelegate);
return new ReactiveElasticsearchStringQuery(namedQuery, queryMethod, operations,
evaluationContextProvider);
} else if (queryMethod.hasAnnotatedQuery()) {
return new ReactiveRepositoryStringQuery(queryMethod, operations, valueExpressionDelegate);
} else if (queryMethod.hasAnnotatedSearchTemplateQuery()) {
var searchTemplateQuery = queryMethod.getAnnotatedSearchTemplateQuery();
return new ReactiveRepositorySearchTemplateQuery(queryMethod, operations, valueExpressionDelegate,
searchTemplateQuery.id());
return new ReactiveElasticsearchStringQuery(queryMethod, operations, evaluationContextProvider);
} else {
return new ReactivePartTreeElasticsearchQuery(queryMethod, operations, valueExpressionDelegate);
return new ReactivePartTreeElasticsearchQuery(queryMethod, operations, evaluationContextProvider);
}
}
}
@@ -384,7 +384,7 @@ public class SimpleElasticsearchRepository<T, ID> implements ElasticsearchReposi
public void deleteAll() {
executeAndRefresh((OperationsCallback<Void>) operations -> {
operations.delete(DeleteQuery.builder(Query.findAll()).build(), entityClass, getIndexCoordinates());
operations.delete(Query.findAll(), entityClass, getIndexCoordinates());
return null;
});
}
@@ -24,8 +24,8 @@ import org.springframework.data.elasticsearch.repository.query.ElasticsearchPara
import org.springframework.data.elasticsearch.repository.support.value.ElasticsearchCollectionValueToStringConverter;
import org.springframework.data.elasticsearch.repository.support.value.ElasticsearchQueryValueConversionService;
import org.springframework.data.elasticsearch.repository.support.value.ElasticsearchStringValueToStringConverter;
import org.springframework.data.expression.ValueEvaluationContextProvider;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
@@ -53,11 +53,11 @@ public class QueryStringSpELEvaluator {
private final String queryString;
private final ElasticsearchParametersParameterAccessor parameterAccessor;
private final QueryMethod queryMethod;
private final ValueEvaluationContextProvider evaluationContextProvider;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final TypeConverter elasticsearchSpELTypeConverter;
public QueryStringSpELEvaluator(String queryString, ElasticsearchParametersParameterAccessor parameterAccessor,
QueryMethod queryMethod, ValueEvaluationContextProvider evaluationContextProvider,
QueryMethod queryMethod, QueryMethodEvaluationContextProvider evaluationContextProvider,
ConversionService conversionService) {
Assert.notNull(queryString, "queryString must not be null");
@@ -83,8 +83,8 @@ public class QueryStringSpELEvaluator {
Expression expr = getQueryExpression(queryString);
if (expr != null) {
EvaluationContext context = evaluationContextProvider.getEvaluationContext(parameterAccessor.getValues())
.getRequiredEvaluationContext();
EvaluationContext context = evaluationContextProvider.getEvaluationContext(parameterAccessor.getParameters(),
parameterAccessor.getValues());
if (context instanceof StandardEvaluationContext standardEvaluationContext) {
standardEvaluationContext.setTypeConverter(elasticsearchSpELTypeConverter);
@@ -33,11 +33,11 @@ inline fun <reified T : Any> SearchOperations.searchOne(query: Query): SearchHit
inline fun <reified T : Any> SearchOperations.searchOne(query: Query, index: IndexCoordinates): SearchHit<T>? =
searchOne(query, T::class.java, index)
inline fun <reified T : Any> SearchOperations.multiSearch(queries: List<Query>): List<SearchHits<T>> =
inline fun <reified T : Any> SearchOperations.multiSearch(queries: List<out Query>): List<SearchHits<T>> =
multiSearch(queries, T::class.java)
inline fun <reified T : Any> SearchOperations.multiSearch(
queries: List<Query>,
queries: List<out Query>,
index: IndexCoordinates
): List<SearchHits<T>> =
multiSearch(queries, T::class.java, index)
+1 -5
View File
@@ -1,4 +1,4 @@
Spring Data Elasticsearch 5.5.2 (2025.0.2)
Spring Data Elasticsearch 5.4.2 (2024.1.2)
Copyright (c) [2013-2022] Pivotal Software, Inc.
This product is licensed to you under the Apache License, Version 2.0 (the "License").
@@ -24,10 +24,6 @@ conditions of the subcomponent's license, as noted in the LICENSE file.
@@ -15,7 +15,6 @@
*/
package org.springframework.data.elasticsearch.client.elc;
import static org.assertj.core.api.Assertions.*;
import static org.skyscreamer.jsonassert.JSONAssert.*;
import static org.springframework.data.elasticsearch.client.elc.JsonUtils.*;
@@ -23,7 +22,6 @@ import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
@@ -447,108 +445,6 @@ public class CriteriaQueryMappingUnitTests {
softly.assertAll();
}
// the following test failed because of a wrong implementation in Criteria
// equals and hscode methods.
@Test // #3083
@DisplayName("should map correct subcriteria")
void shouldMapCorrectSubcriteria() throws JSONException {
Criteria criteria = new Criteria("first").is("hello");
List<Criteria> criterias = new ArrayList<>();
criterias.add(new Criteria().or("second").exists());
List<Criteria> subCriterias = new ArrayList<>();
subCriterias.add(new Criteria("third").exists()
.and(new Criteria("fourth").is("ciao")));
subCriterias.add(new Criteria("third").exists()
.and(new Criteria("fourth").is("hi")));
Criteria result = Criteria.or();
for (Criteria c : criterias) {
result = result.or(c);
}
for (Criteria c : subCriterias) {
result = result.subCriteria(c);
}
criteria = criteria.subCriteria(result);
CriteriaQuery criteriaQuery = new CriteriaQuery(criteria);
String expected = """
{
"bool": {
"must": [
{
"query_string": {
"default_operator": "and",
"fields": [
"first"
],
"query": "hello"
}
},
{
"bool": {
"should": [
{
"exists": {
"field": "second"
}
},
{
"bool": {
"must": [
{
"exists": {
"field": "third"
}
},
{
"query_string": {
"default_operator": "and",
"fields": [
"fourth"
],
"query": "ciao"
}
}
]
}
},
{
"bool": {
"must": [
{
"exists": {
"field": "third"
}
},
{
"query_string": {
"default_operator": "and",
"fields": [
"fourth"
],
"query": "hi"
}
}
]
}
}
]
}
}
]
}
}
""";
mappingElasticsearchConverter.updateQuery(criteriaQuery, Person.class);
var queryString = queryToJson(CriteriaQueryProcessor.createQuery(criteriaQuery.getCriteria()), mapper);
assertEquals(expected, queryString, false);
}
// endregion
// region helper functions
@@ -77,7 +77,7 @@ public class DevTests {
private static final SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext();
private static final MappingElasticsearchConverter converter = new MappingElasticsearchConverter(mappingContext);
private final TransportOptions transportOptions = new RestClientOptions(RequestOptions.DEFAULT, false).toBuilder()
private final TransportOptions transportOptions = new RestClientOptions(RequestOptions.DEFAULT).toBuilder()
.addHeader("X-SpringDataElasticsearch-AlwaysThere", "true").setParameter("pretty", "true").build();
private final JsonpMapper jsonpMapper = new JacksonJsonpMapper();
@@ -21,7 +21,7 @@ import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.RepositoryPartQueryIntegrationTests;
import org.springframework.data.elasticsearch.core.query.ElasticsearchPartQueryIntegrationTests;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
@@ -29,7 +29,7 @@ import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplat
* @author Peter-Josef Meisch
* @since 4.4
*/
public class ElasticsearchPartQueryELCIntegrationTests extends RepositoryPartQueryIntegrationTests {
public class ElasticsearchPartQueryELCIntegrationTests extends ElasticsearchPartQueryIntegrationTests {
@Configuration
@Import({ ElasticsearchTemplateConfiguration.class })
@@ -105,6 +105,8 @@ import org.springframework.lang.Nullable;
@SpringIntegrationTest
public abstract class ElasticsearchIntegrationTests {
static final Integer INDEX_MAX_RESULT_WINDOW = 10_000;
private static final String MULTI_INDEX_PREFIX = "test-index";
private static final String MULTI_INDEX_ALL = MULTI_INDEX_PREFIX + "*";
private static final String MULTI_INDEX_1_NAME = MULTI_INDEX_PREFIX + "-1";
@@ -1634,9 +1636,7 @@ public abstract class ElasticsearchIntegrationTests {
.withParams(Collections.singletonMap("newMessage", messageAfterUpdate)).withAbortOnVersionConflict(true)
.build();
var byQueryResponse = operations.updateByQuery(updateQuery, IndexCoordinates.of(indexNameProvider.indexName()));
assertThat(byQueryResponse.getUpdated()).isEqualTo(1);
operations.updateByQuery(updateQuery, IndexCoordinates.of(indexNameProvider.indexName()));
SampleEntity indexedEntity = operations.get(documentId, SampleEntity.class,
IndexCoordinates.of(indexNameProvider.indexName()));
@@ -30,7 +30,6 @@ import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilterBuilder;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.SourceFilter;
@@ -187,38 +186,6 @@ public abstract class SourceFilterIntegrationTests {
assertThat(entity.getField3()).isNull();
}
@Test // #3009
@DisplayName("should not return any fields when source is set to false")
void shouldNotReturnAnyFieldsWhenSourceIsSetToFalse() {
Query query = Query.findAll();
query.addSourceFilter(FetchSourceFilter.of(b -> b.withFetchSource(false)));
SearchHits<Entity> entities = operations.search(query, Entity.class);
assertThat(entities).hasSize(1);
Entity entity = entities.getSearchHit(0).getContent();
assertThat(entity.getField1()).isNull();
assertThat(entity.getField2()).isNull();
assertThat(entity.getField3()).isNull();
}
@Test // #3009
@DisplayName("should return all fields when source is set to true")
void shouldReturnAllFieldsWhenSourceIsSetToTrue() {
Query query = Query.findAll();
query.addSourceFilter(FetchSourceFilter.of(b -> b.withFetchSource(true)));
SearchHits<Entity> entities = operations.search(query, Entity.class);
assertThat(entities).hasSize(1);
Entity entity = entities.getSearchHit(0).getContent();
assertThat(entity.getField1()).isNotNull();
assertThat(entity.getField2()).isNotNull();
assertThat(entity.getField3()).isNotNull();
}
@Document(indexName = "#{@indexNameProvider.indexName()}")
public static class Entity {
@Nullable
@@ -1,106 +0,0 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.query;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.elasticsearch.core.query.BaseQuery.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Pageable;
class BaseQueryTests {
private static final String MATCH_ALL_QUERY = "{\"match_all\":{}}";
@Test // #3127
@DisplayName("query with no Pageable and no maxResults requests 10 docs from 0")
void queryWithNoPageableAndNoMaxResultsRequests10DocsFrom0() {
var query = StringQuery.builder(MATCH_ALL_QUERY)
.build();
var requestSize = query.getRequestSize();
assertThat(requestSize).isEqualTo(10);
}
@Test // #3127
@DisplayName("query with a Pageable and no MaxResults request with values from Pageable")
void queryWithAPageableAndNoMaxResultsRequestWithValuesFromPageable() {
var query = StringQuery.builder(MATCH_ALL_QUERY)
.withPageable(Pageable.ofSize(42))
.build();
var requestSize = query.getRequestSize();
assertThat(requestSize).isEqualTo(42);
}
@Test // #3127
@DisplayName("query with no Pageable and maxResults requests maxResults")
void queryWithNoPageableAndMaxResultsRequestsMaxResults() {
var query = StringQuery.builder(MATCH_ALL_QUERY)
.withMaxResults(12_345)
.build();
var requestSize = query.getRequestSize();
assertThat(requestSize).isEqualTo(12_345);
}
@Test // #3127
@DisplayName("query with Pageable and maxResults requests with values from Pageable if Pageable is less than maxResults")
void queryWithPageableAndMaxResultsRequestsWithValuesFromPageableIfPageableIsLessThanMaxResults() {
var query = StringQuery.builder(MATCH_ALL_QUERY)
.withPageable(Pageable.ofSize(42))
.withMaxResults(123)
.build();
var requestSize = query.getRequestSize();
assertThat(requestSize).isEqualTo(42);
}
@Test // #3127
@DisplayName("query with Pageable and maxResults requests with values from maxResults if Pageable is more than maxResults")
void queryWithPageableAndMaxResultsRequestsWithValuesFromMaxResultsIfPageableIsMoreThanMaxResults() {
var query = StringQuery.builder(MATCH_ALL_QUERY)
.withPageable(Pageable.ofSize(420))
.withMaxResults(123)
.build();
var requestSize = query.getRequestSize();
assertThat(requestSize).isEqualTo(123);
}
@Test // #3127
@DisplayName("query with explicit unpaged request and no maxResults requests max request window size")
void queryWithExplicitUnpagedRequestAndNoMaxResultsRequestsMaxRequestWindowSize() {
var query = StringQuery.builder(MATCH_ALL_QUERY)
.withPageable(Pageable.unpaged())
.build();
var requestSize = query.getRequestSize();
assertThat(requestSize).isEqualTo(INDEX_MAX_RESULT_WINDOW);
}
}
@@ -15,7 +15,6 @@
*/
package org.springframework.data.elasticsearch.core.query;
import static org.assertj.core.api.Assertions.*;
import static org.skyscreamer.jsonassert.JSONAssert.*;
import java.lang.reflect.Method;
@@ -24,25 +23,23 @@ import java.util.Collection;
import java.util.List;
import org.json.JSONException;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.data.elasticsearch.repository.query.ElasticsearchPartQuery;
import org.springframework.data.elasticsearch.repository.query.ElasticsearchQueryMethod;
import org.springframework.data.elasticsearch.repository.query.RepositoryPartQuery;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.lang.Nullable;
/**
* Tests for {@link RepositoryPartQuery}. The tests make sure that queries are built according to the method naming.
* Tests for {@link ElasticsearchPartQuery}. The tests make sure that queries are built according to the method naming.
* Classes implementing this abstract class are in the packages of their request factories and converters as these are
* kept package private.
*
@@ -51,7 +48,7 @@ import org.springframework.lang.Nullable;
*/
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@SpringIntegrationTest
public abstract class RepositoryPartQueryIntegrationTests {
public abstract class ElasticsearchPartQueryIntegrationTests {
public static final String BOOK_TITLE = "Title";
public static final int BOOK_PRICE = 42;
@@ -642,84 +639,6 @@ public abstract class RepositoryPartQueryIntegrationTests {
assertEquals(expected, query, false);
}
@Test // #3072
@DisplayName("should build sort object with correct field names")
void shouldBuildSortObjectWithCorrectFieldNames() throws NoSuchMethodException, JSONException {
String methodName = "findByNameOrderBySortAuthor_SortName";
Class<?>[] parameterClasses = new Class[] { String.class };
Object[] parameters = new Object[] { BOOK_TITLE };
String query = getQueryString(methodName, parameterClasses, parameters);
String expected = """
{
"query": {
"bool": {
"must": [
{
"query_string": {
"query": "Title",
"fields": [
"name"
]
}
}
]
}
},
"sort": [
{
"sort_author.sort_name": {
"order": "asc"
}
}
]
}""";
assertEquals(expected, query, false);
}
@Test // #3081
@DisplayName("should build sort object with unknown field names")
void shouldBuildSortObjectWithUnknownFieldNames() throws NoSuchMethodException, JSONException {
String methodName = "findByName";
Class<?>[] parameterClasses = new Class[] { String.class, Sort.class };
Object[] parameters = new Object[] { BOOK_TITLE, Sort.by("sortAuthor.sortName.raw") };
String query = getQueryString(methodName, parameterClasses, parameters);
String expected = """
{
"query": {
"bool": {
"must": [
{
"query_string": {
"query": "Title",
"fields": [
"name"
]
}
}
]
}
},
"sort": [
{
"sort_author.sort_name.raw": {
"order": "asc"
}
}
]
}""";
assertEquals(expected, query, false);
}
private String getQueryString(String methodName, Class<?>[] parameterClasses, Object[] parameters)
throws NoSuchMethodException {
@@ -727,8 +646,8 @@ public abstract class RepositoryPartQueryIntegrationTests {
ElasticsearchQueryMethod queryMethod = new ElasticsearchQueryMethod(method,
new DefaultRepositoryMetadata(SampleRepository.class), new SpelAwareProxyProjectionFactory(),
operations.getElasticsearchConverter().getMappingContext());
RepositoryPartQuery partQuery = new RepositoryPartQuery(queryMethod, operations,
ValueExpressionDelegate.create());
ElasticsearchPartQuery partQuery = new ElasticsearchPartQuery(queryMethod, operations,
QueryMethodEvaluationContextProvider.DEFAULT);
Query query = partQuery.createQuery(parameters);
return buildQueryString(query, Book.class);
}
@@ -807,9 +726,6 @@ public abstract class RepositoryPartQueryIntegrationTests {
List<Book> findByAvailableTrueOrderByNameDesc();
List<Book> findByNameOrderBySortAuthor_SortName(String name);
List<Book> findByName(String name, Sort sort);
}
public static class Book {
@@ -819,10 +735,6 @@ public abstract class RepositoryPartQueryIntegrationTests {
@Nullable private Integer price;
@Field(type = FieldType.Boolean) private boolean available;
// this is needed for the #3072 test
@Nullable
@Field(name = "sort_author", type = FieldType.Object) private Author sortAuthor;
@Nullable
public String getId() {
return id;
@@ -854,32 +766,8 @@ public abstract class RepositoryPartQueryIntegrationTests {
return available;
}
@Nullable
public Author getSortAuthor() {
return sortAuthor;
}
public void setSortAuthor(@Nullable Author sortAuthor) {
this.sortAuthor = sortAuthor;
}
public void setAvailable(Boolean available) {
this.available = available;
}
}
public static class Author {
@Nullable
@Field(name = "sort_name", type = FieldType.Keyword) private String sortName;
@Nullable
public String getSortName() {
return sortName;
}
public void setSortName(@Nullable String sortName) {
this.sortName = sortName;
}
}
}
@@ -99,8 +99,6 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
@DisplayName("should use runtime-field without script")
void shouldUseRuntimeFieldWithoutScript() {
// a runtime field without a script can be used to redefine the type of a field for the search,
// here we change the type from text to double
insert("1", "11", 10);
Query query = new CriteriaQuery(new Criteria("description").matches(11.0));
RuntimeField runtimeField = new RuntimeField("description", "double");
@@ -135,25 +133,6 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
assertThat(foundPerson.getBirthDate()).isEqualTo(birthDate);
}
@Test // #3076
@DisplayName("should return scripted fields that are lists")
void shouldReturnScriptedFieldsThatAreLists() {
var person = new Person();
person.setFirstName("John");
person.setLastName("Doe");
operations.save(person);
var query = Query.findAll();
query.addFields("allNames");
query.addSourceFilter(new FetchSourceFilterBuilder().withIncludes("*").build());
var searchHits = operations.search(query, Person.class);
assertThat(searchHits.getTotalHits()).isEqualTo(1);
var foundPerson = searchHits.getSearchHit(0).getContent();
// the painless script seems to return the data sorted no matter in which order the values are emitted
assertThat(foundPerson.getAllNames()).containsExactlyInAnyOrderElementsOf(List.of("John", "Doe"));
}
@Test // #2035
@DisplayName("should use repository method with ScriptedField parameters")
void shouldUseRepositoryMethodWithScriptedFieldParameters() {
@@ -164,11 +143,9 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
repository.save(entity);
org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1 = buildScriptedField(
"scriptedValue1",
org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1 = getScriptedField("scriptedValue1",
2);
org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2 = buildScriptedField(
"scriptedValue2",
org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2 = getScriptedField("scriptedValue2",
3);
var searchHits = repository.findByValue(3, scriptedField1, scriptedField2);
@@ -180,6 +157,17 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
assertThat(foundEntity.getScriptedValue2()).isEqualTo(9);
}
@NotNull
private static org.springframework.data.elasticsearch.core.query.ScriptedField getScriptedField(String fieldName,
int factor) {
return org.springframework.data.elasticsearch.core.query.ScriptedField.of(
fieldName,
ScriptData.of(b -> b
.withType(ScriptType.INLINE)
.withScript("doc['value'].size() > 0 ? doc['value'].value * params['factor'] : 0")
.withParams(Map.of("factor", factor))));
}
@Test // #2035
@DisplayName("should use repository string query method with ScriptedField parameters")
void shouldUseRepositoryStringQueryMethodWithScriptedFieldParameters() {
@@ -190,11 +178,9 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
repository.save(entity);
org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1 = buildScriptedField(
"scriptedValue1",
org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField1 = getScriptedField("scriptedValue1",
2);
org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2 = buildScriptedField(
"scriptedValue2",
org.springframework.data.elasticsearch.core.query.ScriptedField scriptedField2 = getScriptedField("scriptedValue2",
3);
var searchHits = repository.findWithScriptedFields(3, scriptedField1, scriptedField2);
@@ -216,8 +202,8 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
repository.save(entity);
var runtimeField1 = buildRuntimeField("scriptedValue1", 3);
var runtimeField2 = buildRuntimeField("scriptedValue2", 4);
var runtimeField1 = getRuntimeField("scriptedValue1", 3);
var runtimeField2 = getRuntimeField("scriptedValue2", 4);
var searchHits = repository.findByValue(3, runtimeField1, runtimeField2);
@@ -228,6 +214,14 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
assertThat(foundEntity.getScriptedValue2()).isEqualTo(12);
}
@NotNull
private static RuntimeField getRuntimeField(String fieldName, int factor) {
return new RuntimeField(
fieldName,
"long",
String.format("emit(doc['value'].size() > 0 ? doc['value'].value * %d : 0)", factor));
}
@Test // #2035
@DisplayName("should use repository string query method with RuntimeField parameters")
void shouldUseRepositoryStringQueryMethodWithRuntimeFieldParameters() {
@@ -238,8 +232,8 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
repository.save(entity);
var runtimeField1 = buildRuntimeField("scriptedValue1", 3);
var runtimeField2 = buildRuntimeField("scriptedValue2", 4);
var runtimeField1 = getRuntimeField("scriptedValue1", 3);
var runtimeField2 = getRuntimeField("scriptedValue2", 4);
var searchHits = repository.findWithRuntimeFields(3, runtimeField1, runtimeField2);
@@ -269,7 +263,8 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
"priceWithTax",
"double",
"emit(doc['price'].value * params.tax)",
Map.of("tax", 1.19));
Map.of("tax", 1.19)
);
var query = CriteriaQuery.builder(
Criteria.where("priceWithTax").greaterThan(100.0))
.withRuntimeFields(List.of(runtimeField))
@@ -280,56 +275,6 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
assertThat(searchHits).hasSize(1);
}
@Test // #3076
@DisplayName("should use runtime fields in queries returning lists")
void shouldUseRuntimeFieldsInQueriesReturningLists() {
insert("1", "item 1", 80.0);
var runtimeField = new RuntimeField(
"someStrings",
"keyword",
"emit('foo'); emit('bar');",
null);
var query = Query.findAll();
query.addRuntimeField(runtimeField);
query.addFields("someStrings");
query.addSourceFilter(new FetchSourceFilterBuilder().withIncludes("*").build());
var searchHits = operations.search(query, SomethingToBuy.class);
assertThat(searchHits).hasSize(1);
var somethingToBuy = searchHits.getSearchHit(0).getContent();
assertThat(somethingToBuy.someStrings).containsExactlyInAnyOrder("foo", "bar");
}
/**
* build a {@link org.springframework.data.elasticsearch.core.query.ScriptedField} to return the product of the
* document's value property and the given factor
*/
@NotNull
private static org.springframework.data.elasticsearch.core.query.ScriptedField buildScriptedField(String fieldName,
int factor) {
return org.springframework.data.elasticsearch.core.query.ScriptedField.of(
fieldName,
ScriptData.of(b -> b
.withType(ScriptType.INLINE)
.withScript("doc['value'].size() > 0 ? doc['value'].value * params['factor'] : 0")
.withParams(Map.of("factor", factor))));
}
/**
* build a {@link RuntimeField} to return the product of the document's value property and the given factor
*/
@NotNull
private static RuntimeField buildRuntimeField(String fieldName, int factor) {
return new RuntimeField(
fieldName,
"long",
String.format("emit(doc['value'].size() > 0 ? doc['value'].value * %d : 0)", factor));
}
@SuppressWarnings("unused")
@Document(indexName = "#{@indexNameProvider.indexName()}-something-to-by")
private static class SomethingToBuy {
@@ -341,9 +286,6 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
@Nullable
@Field(type = FieldType.Double) private Double price;
@Nullable
@ScriptedField private List<String> someStrings;
@Nullable
public String getId() {
return id;
@@ -370,15 +312,6 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
public void setPrice(@Nullable Double price) {
this.price = price;
}
@Nullable
public List<String> getSomeStrings() {
return someStrings;
}
public void setSomeStrings(@Nullable List<String> someStrings) {
this.someStrings = someStrings;
}
}
@SuppressWarnings("unused")
@@ -387,13 +320,6 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
public static class Person {
@Nullable private String id;
// need keywords as we are using them in the script
@Nullable
@Field(type = FieldType.Keyword) private String firstName;
@Nullable
@Field(type = FieldType.Keyword) private String lastName;
@ScriptedField private List<String> allNames = List.of();
@Field(type = FieldType.Date, format = DateFormat.basic_date)
@Nullable private LocalDate birthDate;
@@ -409,24 +335,6 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
this.id = id;
}
@Nullable
public String getFirstName() {
return firstName;
}
public void setFirstName(@Nullable String firstName) {
this.firstName = firstName;
}
@Nullable
public String getLastName() {
return lastName;
}
public void setLastName(@Nullable String lastName) {
this.lastName = lastName;
}
@Nullable
public LocalDate getBirthDate() {
return birthDate;
@@ -444,14 +352,6 @@ public abstract class ScriptedAndRuntimeFieldsIntegrationTests {
public void setAge(@Nullable Integer age) {
this.age = age;
}
public List<String> getAllNames() {
return allNames;
}
public void setAllNames(List<String> allNames) {
this.allNames = allNames;
}
}
@SuppressWarnings("unused")
@@ -129,9 +129,6 @@ public class ClusterConnection implements ExtensionContext.Store.CloseableResour
Map<String, String> testcontainersProperties = testcontainersProperties(
"testcontainers-" + testcontainersConfiguration + ".properties");
var testcontainersPropertiesLocal = testcontainersProperties("testcontainers-local.properties");
testcontainersProperties.putAll(testcontainersPropertiesLocal);
DockerImageName dockerImageName = getDockerImageName(testcontainersProperties);
ElasticsearchContainer elasticsearchContainer = new SpringDataElasticsearchContainer(dockerImageName)
@@ -0,0 +1,78 @@
/*
* Copyright 2021-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.query;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchCustomConversions;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.lang.Nullable;
/**
* @author Peter-Josef Meisch
*/
public class ElasticsearchStringQueryUnitTestBase {
protected ElasticsearchConverter setupConverter() {
MappingElasticsearchConverter converter = new MappingElasticsearchConverter(
new SimpleElasticsearchMappingContext());
Collection<Converter<?, ?>> converters = new ArrayList<>();
converters.add(ElasticsearchStringQueryUnitTests.CarConverter.INSTANCE);
CustomConversions customConversions = new ElasticsearchCustomConversions(converters);
converter.setConversions(customConversions);
converter.afterPropertiesSet();
return converter;
}
static class Car {
@Nullable private String name;
@Nullable private String model;
@Nullable
public String getName() {
return name;
}
public void setName(@Nullable String name) {
this.name = name;
}
@Nullable
public String getModel() {
return model;
}
public void setModel(@Nullable String model) {
this.model = model;
}
}
enum CarConverter implements Converter<Car, String> {
INSTANCE;
@Override
public String convert(ElasticsearchStringQueryUnitTests.Car car) {
return (car.getName() != null ? car.getName() : "null") + '-'
+ (car.getModel() != null ? car.getModel() : "null");
}
}
}
@@ -16,7 +16,9 @@
package org.springframework.data.elasticsearch.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -24,26 +26,29 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.InnerField;
import org.springframework.data.elasticsearch.annotations.MultiField;
import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchCustomConversions;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.repositories.custommethod.QueryParameter;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.lang.Nullable;
/**
@@ -52,53 +57,14 @@ import org.springframework.lang.Nullable;
* @author Niklas Herder
* @author Haibo Liu
*/
public class RepositoryStringQueryUnitTests extends RepositoryStringQueryUnitTestsBase {
/**
* Adds some data class and custom conversion to the base class implementation.
*/
protected MappingElasticsearchConverter setupConverter() {
@ExtendWith(MockitoExtension.class)
public class ElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryUnitTestBase {
Collection<Converter<?, ?>> converters = new ArrayList<>();
converters.add(RepositoryStringQueryUnitTests.CarConverter.INSTANCE);
CustomConversions customConversions = new ElasticsearchCustomConversions(converters);
@Mock ElasticsearchOperations operations;
MappingElasticsearchConverter converter = super.setupConverter();
converter.setConversions(customConversions);
converter.afterPropertiesSet();
return converter;
}
static class Car {
@Nullable private String name;
@Nullable private String model;
@Nullable
public String getName() {
return name;
}
public void setName(@Nullable String name) {
this.name = name;
}
@Nullable
public String getModel() {
return model;
}
public void setModel(@Nullable String model) {
this.model = model;
}
}
enum CarConverter implements Converter<Car, String> {
INSTANCE;
@Override
public String convert(Car car) {
return (car.getName() != null ? car.getName() : "null") + '-'
+ (car.getModel() != null ? car.getModel() : "null");
}
@BeforeEach
public void setUp() {
when(operations.getElasticsearchConverter()).thenReturn(setupConverter());
}
@Test // DATAES-552
@@ -384,9 +350,8 @@ public class RepositoryStringQueryUnitTests extends RepositoryStringQueryUnitTes
throws NoSuchMethodException {
Class<?>[] argTypes = Arrays.stream(args).map(Object::getClass).toArray(Class[]::new);
ElasticsearchQueryMethod queryMethod = getQueryMethod(RepositoryStringQueryUnitTests.SampleRepository.class,
methodName, argTypes);
RepositoryStringQuery elasticsearchStringQuery = queryForMethod(queryMethod);
ElasticsearchQueryMethod queryMethod = getQueryMethod(methodName, argTypes);
ElasticsearchStringQuery elasticsearchStringQuery = queryForMethod(queryMethod);
return elasticsearchStringQuery.createQuery(new ElasticsearchParametersParameterAccessor(queryMethod, args));
}
@@ -405,9 +370,16 @@ public class RepositoryStringQueryUnitTests extends RepositoryStringQueryUnitTes
.isEqualTo("{ 'bool' : { 'must' : { 'term' : { 'car' : 'Toyota-Prius' } } } }");
}
private RepositoryStringQuery queryForMethod(ElasticsearchQueryMethod queryMethod) {
return new RepositoryStringQuery(queryMethod, operations, queryMethod.getAnnotatedQuery(),
ValueExpressionDelegate.create());
private ElasticsearchStringQuery queryForMethod(ElasticsearchQueryMethod queryMethod) {
return new ElasticsearchStringQuery(queryMethod, operations, queryMethod.getAnnotatedQuery(),
QueryMethodEvaluationContextProvider.DEFAULT);
}
private ElasticsearchQueryMethod getQueryMethod(String name, Class<?>... parameters) throws NoSuchMethodException {
Method method = SampleRepository.class.getMethod(name, parameters);
return new ElasticsearchQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class),
new SpelAwareProxyProjectionFactory(), operations.getElasticsearchConverter().getMappingContext());
}
private interface SampleRepository extends Repository<Person, String> {
@@ -16,11 +16,12 @@
package org.springframework.data.elasticsearch.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -28,27 +29,28 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.InnerField;
import org.springframework.data.elasticsearch.annotations.MultiField;
import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchCustomConversions;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.repositories.custommethod.QueryParameter;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.lang.Nullable;
@@ -58,54 +60,13 @@ import org.springframework.lang.Nullable;
* @author Haibo Liu
*/
@ExtendWith(MockitoExtension.class)
public class ReactiveRepositoryStringQueryUnitTests extends ReactiveRepositoryQueryUnitTestsBase {
public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryUnitTestBase {
/**
* Adds some data class and custom conversion to the base class implementation.
*/
protected MappingElasticsearchConverter setupConverter() {
@Mock ReactiveElasticsearchOperations operations;
Collection<Converter<?, ?>> converters = new ArrayList<>();
converters.add(CarConverter.INSTANCE);
CustomConversions customConversions = new ElasticsearchCustomConversions(converters);
MappingElasticsearchConverter converter = super.setupConverter();
converter.setConversions(customConversions);
converter.afterPropertiesSet();
return converter;
}
static class Car {
@Nullable private String name;
@Nullable private String model;
@Nullable
public String getName() {
return name;
}
public void setName(@Nullable String name) {
this.name = name;
}
@Nullable
public String getModel() {
return model;
}
public void setModel(@Nullable String model) {
this.model = model;
}
}
enum CarConverter implements Converter<Car, String> {
INSTANCE;
@Override
public String convert(Car car) {
return (car.getName() != null ? car.getName() : "null") + '-'
+ (car.getModel() != null ? car.getModel() : "null");
}
@BeforeEach
public void setUp() {
when(operations.getElasticsearchConverter()).thenReturn(setupConverter());
}
@Test // DATAES-519
@@ -406,21 +367,29 @@ public class ReactiveRepositoryStringQueryUnitTests extends ReactiveRepositoryQu
Class<?>[] argTypes = Arrays.stream(args).map(Object::getClass)
.map(clazz -> Collection.class.isAssignableFrom(clazz) ? List.class : clazz).toArray(Class[]::new);
ReactiveElasticsearchQueryMethod queryMethod = getQueryMethod(SampleRepository.class, methodName, argTypes);
ReactiveRepositoryStringQuery elasticsearchStringQuery = queryForMethod(queryMethod);
ReactiveElasticsearchQueryMethod queryMethod = getQueryMethod(methodName, argTypes);
ReactiveElasticsearchStringQuery elasticsearchStringQuery = queryForMethod(queryMethod);
return elasticsearchStringQuery.createQuery(new ElasticsearchParametersParameterAccessor(queryMethod, args));
}
private ReactiveRepositoryStringQuery createQueryForMethod(String name, Class<?>... parameters) throws Exception {
ReactiveElasticsearchQueryMethod queryMethod = getQueryMethod(SampleRepository.class, name, parameters);
return queryForMethod(queryMethod);
private ReactiveElasticsearchStringQuery queryForMethod(ReactiveElasticsearchQueryMethod queryMethod) {
return new ReactiveElasticsearchStringQuery(queryMethod, operations,
QueryMethodEvaluationContextProvider.DEFAULT);
}
private ReactiveRepositoryStringQuery queryForMethod(ReactiveElasticsearchQueryMethod queryMethod) {
return new ReactiveRepositoryStringQuery(queryMethod, operations,
ValueExpressionDelegate.create());
private ReactiveElasticsearchQueryMethod getQueryMethod(String name, Class<?>... parameters)
throws NoSuchMethodException {
Method method = SampleRepository.class.getMethod(name, parameters);
return new ReactiveElasticsearchQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class),
new SpelAwareProxyProjectionFactory(), operations.getElasticsearchConverter().getMappingContext());
}
private ReactiveElasticsearchStringQuery createQueryForMethod(String name, Class<?>... parameters) throws Exception {
ReactiveElasticsearchQueryMethod queryMethod = getQueryMethod(name, parameters);
return queryForMethod(queryMethod);
}
private interface SampleRepository extends Repository<Person, String> {
@@ -1,73 +0,0 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.query;
import static org.mockito.Mockito.*;
import java.lang.reflect.Method;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
@ExtendWith(MockitoExtension.class)
public class ReactiveRepositoryQueryUnitTestsBase {
@Mock ReactiveElasticsearchOperations operations;
/**
* set up the {operations} mock to return the {@link ElasticsearchConverter} from setupConverter().
*/
@BeforeEach
public void setUp() {
when(operations.getElasticsearchConverter()).thenReturn(setupConverter());
}
/**
* @return a simple {@link MappingElasticsearchConverter} with no special setup.
*/
protected MappingElasticsearchConverter setupConverter() {
return new MappingElasticsearchConverter(
new SimpleElasticsearchMappingContext());
}
/**
* Creates a {@link ReactiveElasticsearchQueryMethod} for the given method
*
* @param repositoryClass
* @param name
* @param parameters
* @return
* @throws NoSuchMethodException
*/
protected ReactiveElasticsearchQueryMethod getQueryMethod(Class<?> repositoryClass, String name,
Class<?>... parameters)
throws NoSuchMethodException {
Method method = repositoryClass.getMethod(name, parameters);
return new ReactiveElasticsearchQueryMethod(method,
new DefaultRepositoryMetadata(repositoryClass),
new SpelAwareProxyProjectionFactory(), operations.getElasticsearchConverter().getMappingContext());
}
}
@@ -1,113 +0,0 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.query;
import static org.assertj.core.api.Assertions.*;
import java.util.Arrays;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.SearchTemplateQuery;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.lang.Nullable;
public class ReactiveRepositorySearchTemplateQueryUnitTests extends ReactiveRepositoryQueryUnitTestsBase {
@Test // #2997
@DisplayName("should set searchtemplate id")
void shouldSetSearchTemplateId() throws NoSuchMethodException {
var query = createQuery("searchWithArgs", "answer", 42);
assertThat(query).isInstanceOf(org.springframework.data.elasticsearch.core.query.SearchTemplateQuery.class);
var searchTemplateQuery = (org.springframework.data.elasticsearch.core.query.SearchTemplateQuery) query;
assertThat(searchTemplateQuery.getId()).isEqualTo("searchtemplate-42");
}
@Test // #2997
@DisplayName("should set searchtemplate parameters")
void shouldSetSearchTemplateParameters() throws NoSuchMethodException {
var query = createQuery("searchWithArgs", "answer", 42);
assertThat(query).isInstanceOf(org.springframework.data.elasticsearch.core.query.SearchTemplateQuery.class);
var searchTemplateQuery = (org.springframework.data.elasticsearch.core.query.SearchTemplateQuery) query;
var params = searchTemplateQuery.getParams();
assertThat(params).isNotNull().hasSize(2);
assertThat(params.get("stringArg")).isEqualTo("answer");
assertThat(params.get("intArg")).isEqualTo(42);
}
// region helper methods
private Query createQuery(String methodName, Object... args) throws NoSuchMethodException {
Class<?>[] argTypes = Arrays.stream(args).map(Object::getClass).toArray(Class[]::new);
ReactiveElasticsearchQueryMethod queryMethod = getQueryMethod(SampleRepository.class, methodName, argTypes);
ReactiveRepositorySearchTemplateQuery repositorySearchTemplateQuery = queryForMethod(queryMethod);
return repositorySearchTemplateQuery.createQuery(new ElasticsearchParametersParameterAccessor(queryMethod, args));
}
private ReactiveRepositorySearchTemplateQuery queryForMethod(ReactiveElasticsearchQueryMethod queryMethod) {
return new ReactiveRepositorySearchTemplateQuery(queryMethod, operations, ValueExpressionDelegate.create(),
queryMethod.getAnnotatedSearchTemplateQuery().id());
}
// endregion
// region test data
private interface SampleRepository extends ElasticsearchRepository<SampleEntity, String> {
@SearchTemplateQuery(id = "searchtemplate-42")
SearchHits<SampleEntity> searchWithArgs(String stringArg, Integer intArg);
@SearchTemplateQuery(id = "searchtemplate-42")
SearchHits<SampleEntity> searchWithArgsAndSort(String stringArg, Integer intArg, Sort sort);
}
@Document(indexName = "not-relevant")
static class SampleEntity {
@Nullable
@Id String id;
@Nullable String data;
@Nullable
public String getId() {
return id;
}
public void setId(@Nullable String id) {
this.id = id;
}
@Nullable
public String getData() {
return data;
}
public void setData(@Nullable String data) {
this.data = data;
}
}
// endregion
}
@@ -1,71 +0,0 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.query;
import static org.mockito.Mockito.*;
import java.lang.reflect.Method;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
@ExtendWith(MockitoExtension.class)
public class RepositoryQueryUnitTestsBase {
@Mock ElasticsearchOperations operations;
/**
* set up the {operations} mock to return the {@link ElasticsearchConverter} from setupConverter().
*/
@BeforeEach
public void setUp() {
when(operations.getElasticsearchConverter()).thenReturn(setupConverter());
}
/**
* @return a simple {@link MappingElasticsearchConverter} with no special setup.
*/
protected MappingElasticsearchConverter setupConverter() {
return new MappingElasticsearchConverter(
new SimpleElasticsearchMappingContext());
}
/**
* Creates a {@link ElasticsearchQueryMethod} for the given method
*
* @param repositoryClass
* @param name
* @param parameters
* @return
* @throws NoSuchMethodException
*/
protected ElasticsearchQueryMethod getQueryMethod(Class<?> repositoryClass, String name, Class<?>... parameters)
throws NoSuchMethodException {
Method method = repositoryClass.getMethod(name, parameters);
return new ElasticsearchQueryMethod(method, new DefaultRepositoryMetadata(repositoryClass),
new SpelAwareProxyProjectionFactory(), operations.getElasticsearchConverter().getMappingContext());
}
}
@@ -1,113 +0,0 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.query;
import static org.assertj.core.api.Assertions.*;
import java.util.Arrays;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.SearchTemplateQuery;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.lang.Nullable;
public class RepositorySearchTemplateQueryUnitTests extends RepositoryQueryUnitTestsBase {
@Test // #2997
@DisplayName("should set searchtemplate id")
void shouldSetSearchTemplateId() throws NoSuchMethodException {
var query = createQuery("searchWithArgs", "answer", 42);
assertThat(query).isInstanceOf(org.springframework.data.elasticsearch.core.query.SearchTemplateQuery.class);
var searchTemplateQuery = (org.springframework.data.elasticsearch.core.query.SearchTemplateQuery) query;
assertThat(searchTemplateQuery.getId()).isEqualTo("searchtemplate-42");
}
@Test // #2997
@DisplayName("should set searchtemplate parameters")
void shouldSetSearchTemplateParameters() throws NoSuchMethodException {
var query = createQuery("searchWithArgs", "answer", 42);
assertThat(query).isInstanceOf(org.springframework.data.elasticsearch.core.query.SearchTemplateQuery.class);
var searchTemplateQuery = (org.springframework.data.elasticsearch.core.query.SearchTemplateQuery) query;
var params = searchTemplateQuery.getParams();
assertThat(params).isNotNull().hasSize(2);
assertThat(params.get("stringArg")).isEqualTo("answer");
assertThat(params.get("intArg")).isEqualTo(42);
}
// region helper methods
private Query createQuery(String methodName, Object... args) throws NoSuchMethodException {
Class<?>[] argTypes = Arrays.stream(args).map(Object::getClass).toArray(Class[]::new);
ElasticsearchQueryMethod queryMethod = getQueryMethod(SampleRepository.class, methodName, argTypes);
RepositorySearchTemplateQuery repositorySearchTemplateQuery = queryForMethod(queryMethod);
return repositorySearchTemplateQuery.createQuery(new ElasticsearchParametersParameterAccessor(queryMethod, args));
}
private RepositorySearchTemplateQuery queryForMethod(ElasticsearchQueryMethod queryMethod) {
return new RepositorySearchTemplateQuery(queryMethod, operations, ValueExpressionDelegate.create(),
queryMethod.getAnnotatedSearchTemplateQuery().id());
}
// endregion
// region test data
private interface SampleRepository extends ElasticsearchRepository<SampleEntity, String> {
@SearchTemplateQuery(id = "searchtemplate-42")
SearchHits<SampleEntity> searchWithArgs(String stringArg, Integer intArg);
@SearchTemplateQuery(id = "searchtemplate-42")
SearchHits<SampleEntity> searchWithArgsAndSort(String stringArg, Integer intArg, Sort sort);
}
@Document(indexName = "not-relevant")
static class SampleEntity {
@Nullable
@Id String id;
@Nullable String data;
@Nullable
public String getId() {
return id;
}
public void setId(@Nullable String id) {
this.id = id;
}
@Nullable
public String getData() {
return data;
}
public void setData(@Nullable String data) {
this.data = data;
}
}
// endregion
}
@@ -1,23 +0,0 @@
/*
* Copyright 2021-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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;
/**
* @author Peter-Josef Meisch
*/
public class RepositoryStringQueryUnitTestsBase extends RepositoryQueryUnitTestsBase {
}
@@ -1,42 +0,0 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.support;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchTemplateConfiguration;
import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.test.context.ContextConfiguration;
/**
* @since 5.5
*/
@ContextConfiguration(classes = ReactiveRepositoryQueryELCIntegrationTests.Config.class)
public class ReactiveRepositoryQueryELCIntegrationTests
extends ReactiveRepositoryQueryIntegrationTests {
@Configuration
@Import({ ReactiveElasticsearchTemplateConfiguration.class })
@EnableReactiveElasticsearchRepositories(considerNestedRepositories = true)
static class Config {
@Bean
IndexNameProvider indexNameProvider() {
return new IndexNameProvider("reactive-repository-query");
}
}
}
@@ -1,159 +0,0 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.support;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.elasticsearch.core.IndexOperationsAdapter.*;
import reactor.core.publisher.Flux;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.SearchTemplateQuery;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.script.Script;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.lang.Nullable;
/**
* @since 5.5
*/
@SpringIntegrationTest
abstract class ReactiveRepositoryQueryIntegrationTests {
@Autowired private SampleElasticsearchRepository repository;
@Autowired private ReactiveElasticsearchOperations operations;
@Autowired private IndexNameProvider indexNameProvider;
@BeforeEach
void before() {
indexNameProvider.increment();
blocking(operations.indexOps(LOTRCharacter.class)).createWithMapping();
}
@Test
@org.junit.jupiter.api.Order(Integer.MAX_VALUE)
public void cleanup() {
blocking(operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*"))).delete();
}
@Test // #2997
@DisplayName("should use searchtemplate query")
void shouldUseSearchtemplateQuery() {
// store some data
repository.saveAll(List.of(
new LOTRCharacter("1", "Frodo is a hobbit"),
new LOTRCharacter("2", "Legolas is an elf"),
new LOTRCharacter("3", "Gandalf is a wizard"),
new LOTRCharacter("4", "Bilbo is a hobbit"),
new LOTRCharacter("5", "Gimli is a dwarf")))
.blockLast();
// store a searchtemplate
String searchInCharacter = """
{
"query": {
"bool": {
"must": [
{
"match": {
"lotrCharacter": "{{word}}"
}
}
]
}
},
"from": 0,
"size": 100,
"sort": {
"id": {
"order": "desc"
}
}
}
""";
Script scriptSearchInCharacter = Script.builder() //
.withId("searchInCharacter") //
.withLanguage("mustache") //
.withSource(searchInCharacter) //
.build();
var success = operations.putScript(scriptSearchInCharacter).block();
assertThat(success).isTrue();
// search with repository for hobbits order by id descending
var searchHits = repository.searchInCharacter("hobbit")
.collectList().block();
// check result (bilbo, frodo)
assertThat(searchHits).isNotNull();
assertThat(searchHits.size()).isEqualTo(2);
assertThat(searchHits.get(0).getId()).isEqualTo("4");
assertThat(searchHits.get(1).getId()).isEqualTo("1");
}
@Document(indexName = "#{@indexNameProvider.indexName()}")
static class LOTRCharacter {
@Nullable
@Id
@Field(fielddata = true) // needed for the sort to work
private String id;
@Field(type = FieldType.Text)
@Nullable private String lotrCharacter;
public LOTRCharacter(@Nullable String id, @Nullable String lotrCharacter) {
this.id = id;
this.lotrCharacter = lotrCharacter;
}
@Nullable
public String getId() {
return id;
}
public void setId(@Nullable String id) {
this.id = id;
}
@Nullable
public String getLotrCharacter() {
return lotrCharacter;
}
public void setLotrCharacter(@Nullable String lotrCharacter) {
this.lotrCharacter = lotrCharacter;
}
}
interface SampleElasticsearchRepository
extends ReactiveElasticsearchRepository<LOTRCharacter, String> {
@SearchTemplateQuery(id = "searchInCharacter")
Flux<SearchHit<LOTRCharacter>> searchInCharacter(String word);
}
}
@@ -1,38 +0,0 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.support;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.test.context.ContextConfiguration;
@ContextConfiguration(classes = {RepositoryQueryELCIntegrationTests.Config.class })public class RepositoryQueryELCIntegrationTests extends RepositoryQueryIntegrationTests {
@Configuration
@Import({ElasticsearchTemplateConfiguration.class })
@EnableElasticsearchRepositories(basePackages = {"org.springframework.data.elasticsearch.repository.support" },
considerNestedRepositories = true)
static class Config {
@Bean
IndexNameProvider indexNameProvider() {
return new IndexNameProvider("repository-query");
}
}
}
@@ -1,151 +0,0 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.support;
import static org.assertj.core.api.Assertions.*;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.SearchTemplateQuery;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.script.Script;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.lang.Nullable;
@SpringIntegrationTest
abstract class RepositoryQueryIntegrationTests {
@Autowired private SampleElasticsearchRepository repository;
@Autowired private ElasticsearchOperations operations;
@Autowired private IndexNameProvider indexNameProvider;
@BeforeEach
void before() {
indexNameProvider.increment();
operations.indexOps(LOTRCharacter.class).createWithMapping();
}
@Test
@org.junit.jupiter.api.Order(Integer.MAX_VALUE)
public void cleanup() {
operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete();
}
@Test // #2997
@DisplayName("should use searchtemplate query")
void shouldUseSearchtemplateQuery() {
// store some data
repository.saveAll(List.of(
new LOTRCharacter("1", "Frodo is a hobbit"),
new LOTRCharacter("2", "Legolas is an elf"),
new LOTRCharacter("3", "Gandalf is a wizard"),
new LOTRCharacter("4", "Bilbo is a hobbit"),
new LOTRCharacter("5", "Gimli is a dwarf")));
// store a searchtemplate
String searchInCharacter = """
{
"query": {
"bool": {
"must": [
{
"match": {
"lotrCharacter": "{{word}}"
}
}
]
}
},
"from": 0,
"size": 100,
"sort": {
"id": {
"order": "desc"
}
}
}
""";
Script scriptSearchInCharacter = Script.builder() //
.withId("searchInCharacter") //
.withLanguage("mustache") //
.withSource(searchInCharacter) //
.build();
var success = operations.putScript(scriptSearchInCharacter);
assertThat(success).isTrue();
// search with repository for hobbits order by id descending
var searchHits = repository.searchInCharacter("hobbit");
// check result (bilbo, frodo)
assertThat(searchHits).isNotNull();
assertThat(searchHits.getTotalHits()).isEqualTo(2);
assertThat(searchHits.getSearchHit(0).getId()).isEqualTo("4");
assertThat(searchHits.getSearchHit(1).getId()).isEqualTo("1");
}
@Document(indexName = "#{@indexNameProvider.indexName()}")
static class LOTRCharacter {
@Nullable
@Id
@Field(fielddata = true) // needed for the sort to work
private String id;
@Field(type = FieldType.Text)
@Nullable private String lotrCharacter;
public LOTRCharacter(@Nullable String id, @Nullable String lotrCharacter) {
this.id = id;
this.lotrCharacter = lotrCharacter;
}
@Nullable
public String getId() {
return id;
}
public void setId(@Nullable String id) {
this.id = id;
}
@Nullable
public String getLotrCharacter() {
return lotrCharacter;
}
public void setLotrCharacter(@Nullable String lotrCharacter) {
this.lotrCharacter = lotrCharacter;
}
}
interface SampleElasticsearchRepository
extends ElasticsearchRepository<LOTRCharacter, String> {
@SearchTemplateQuery(id = "searchInCharacter")
SearchHits<LOTRCharacter> searchInCharacter(String word);
}
}
@@ -105,26 +105,6 @@ abstract class SimpleReactiveElasticsearchRepositoryIntegrationTests {
return operations.exists(id, IndexCoordinates.of(indexNameProvider.indexName()));
}
@Test // #3093
@DisplayName("should save all from empty collection")
void shouldSaveAllFromEmptyCollection() {
repository.saveAll(Collections.emptyList())
.as(StepVerifier::create)
.expectNextCount(0)
.verifyComplete();
}
@Test // #3093
@DisplayName("should save all from empty flux")
void shouldSaveAllFromEmptyFlux() {
repository.saveAll(Flux.empty())
.as(StepVerifier::create)
.expectNextCount(0)
.verifyComplete();
}
@Test // DATAES-519
void saveShouldComputeMultipleEntities() {
@@ -5,12 +5,5 @@
"lang": "painless",
"source": "Instant currentDate = Instant.ofEpochMilli(new Date().getTime()); Instant startDate = doc['birthDate'].value.toInstant(); emit(ChronoUnit.DAYS.between(startDate, currentDate) / 365);"
}
},
"allNames": {
"type": "keyword",
"script": {
"lang": "painless",
"source": "emit(doc['firstName'].value);emit(doc['lastName'].value);"
}
}
}
@@ -15,7 +15,7 @@
#
#
sde.testcontainers.image-name=docker.elastic.co/elasticsearch/elasticsearch
sde.testcontainers.image-version=8.18.1
sde.testcontainers.image-version=8.15.5
#
#
# needed as we do a DELETE /* at the end of the tests, will be required from 8.0 on, produces a warning since 7.13