Add repository method support for search templates.

Original Pull Request #3049
Closes #2997 

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
This commit is contained in:
Peter-Josef Meisch 2025-02-08 12:17:42 +01:00 committed by GitHub
parent 5568c7bbc4
commit cb77b328ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 1483 additions and 293 deletions

View File

@ -3,7 +3,9 @@
[[new-features.5-5-0]]
== New in Spring Data Elasticsearch 5.5
* Upgrade to Elasticsearch 8.17.0.
* Add support for the `@SearchTemplateQuery` annotation on repository methods.
[[new-features.5-4-0]]
== New in Spring Data Elasticsearch 5.4

View File

@ -365,6 +365,8 @@ operations.putScript( <.>
To use a search template in a search query, Spring Data Elasticsearch provides the `SearchTemplateQuery`, an implementation of the `org.springframework.data.elasticsearch.core.query.Query` interface.
NOTE: Although `SearchTemplateQuery` is an implementation of the `Query` interface, not all of the functionality provided by the base class is available for a `SearchTemplateQuery` like setting a `Pageable` or a `Sort`. Values for this functionality must be added to the stored script like shown in the following example for paging parameters. If these values are set on the `Query` object, they will be ignored.
In the following code, we will add a call using a search template query to a custom repository implementation (see
xref:repositories/custom-implementations.adoc[]) as an example how this can be integrated into a repository call.
@ -449,4 +451,3 @@ var query = Query.findAll().addSort(Sort.by(order));
About the filter query: It is not possible to use a `CriteriaQuery` here, as this query would be converted into a Elasticsearch nested query which does not work in the filter context. So only `StringQuery` or `NativeQuery` can be used here. When using one of these, like the term query above, the Elasticsearch field names must be used, so take care, when these are redefined with the `@Field(name="...")` definition.
For the definition of the order path and the nested paths, the Java entity property names should be used.

View File

@ -10,7 +10,9 @@ The Elasticsearch module supports all basic query building feature as string que
=== Declared queries
Deriving the query from the method name is not always sufficient and/or may result in unreadable method names.
In this case one might make use of the `@Query` annotation (see xref:elasticsearch/repositories/elasticsearch-repository-queries.adoc#elasticsearch.query-methods.at-query[Using @Query Annotation] ).
In this case one might make use of the `@Query` annotation (see xref:elasticsearch/repositories/elasticsearch-repository-queries.adoc#elasticsearch.query-methods.at-query[Using the @Query Annotation] ).
Another possibility is the use of a search-template, (see xref:elasticsearch/repositories/elasticsearch-repository-queries.adoc#elasticsearch.query-methods.at-searchtemplate-query[Using the @SearchTemplateQuery Annotation] ).
[[elasticsearch.query-methods.criterions]]
== Query creation
@ -312,11 +314,13 @@ Repository methods can be defined to have the following return types for returni
* `SearchPage<T>`
[[elasticsearch.query-methods.at-query]]
== Using @Query Annotation
== Using the @Query Annotation
.Declare query on the method using the `@Query` annotation.
====
The arguments passed to the method can be inserted into placeholders in the query string. The placeholders are of the form `?0`, `?1`, `?2` etc. for the first, second, third parameter and so on.
The arguments passed to the method can be inserted into placeholders in the query string.
The placeholders are of the form `?0`, `?1`, `?2` etc. for the first, second, third parameter and so on.
[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
@ -341,15 +345,20 @@ It will be sent to Easticsearch as value of the query element; if for example th
}
----
====
.`@Query` annotation on a method taking a Collection argument
====
A repository method such as
[source,java]
----
@Query("{\"ids\": {\"values\": ?0 }}")
List<SampleEntity> getByIds(Collection<String> ids);
----
would make an https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ids-query.html[IDs query] to return all the matching documents. So calling the method with a `List` of `["id1", "id2", "id3"]` would produce the query body
would make an https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ids-query.html[IDs query] to return all the matching documents.
So calling the method with a `List` of `["id1", "id2", "id3"]` would produce the query body
[source,json]
----
{
@ -369,7 +378,6 @@ would make an https://www.elastic.co/guide/en/elasticsearch/reference/current/qu
====
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> {
@ -411,6 +419,7 @@ If for example the function is called with the parameter _John_, it would produc
.accessing parameter property.
====
Supposing that we have the following class as query parameter type:
[source,java]
----
public record QueryParameter(String value) {
@ -444,7 +453,9 @@ We can pass `new QueryParameter("John")` as the parameter now, and it will produ
.accessing bean property.
====
https://docs.spring.io/spring-framework/reference/core/expressions/language-ref/bean-references.html[Bean property] is also supported to access. Given that there is a bean named `queryParameter` of type `QueryParameter`, we can access the bean with symbol `@` rather than `#`, and there is no need to declare a parameter of type `QueryParameter` in the query method:
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> {
@ -493,6 +504,7 @@ interface BookRepository extends ElasticsearchRepository<Book, String> {
NOTE: collection values should not be quoted when declaring the elasticsearch json query.
A collection of `names` like `List.of("name1", "name2")` will produce the following terms query:
[source,json]
----
{
@ -532,6 +544,7 @@ interface BookRepository extends ElasticsearchRepository<Book, String> {
Page<Book> findByName(Collection<QueryParameter> parameters, Pageable pageable);
}
----
This will extract all the `value` property values as a new `Collection` from `QueryParameter` collection, thus takes the same effect as above.
====
@ -560,3 +573,20 @@ interface BookRepository extends ElasticsearchRepository<Book, String> {
----
====
[[elasticsearch.query-methods.at-searchtemplate-query]]
== Using the @SearchTemplateQuery Annotation
When using Elasticsearch search templates - (see xref:elasticsearch/misc.adoc#elasticsearch.misc.searchtemplates [Search Template support]) it is possible to specify that a repository method should use a template by adding the `@SearchTemplateQuery` annotation to that method.
Let's assume that there is a search template stored with the name "book-by-title" and this template need a parameter named "title", then a repository method using that search template can be defined like this:
[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
@SearchTemplateQuery(id = "book-by-title")
SearchHits<Book> findByTitle(String title);
}
----
The parameters of the repository method are sent to the seacrh template as key/value pairs where the key is the parameter name and the value is taken from the actual value when the method is invoked.

View File

@ -9,4 +9,14 @@ This section describes breaking changes from version 5.4.x to 5.5.x and how remo
[[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

View File

@ -0,0 +1,42 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.annotations;
import org.springframework.data.annotation.QueryAnnotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to mark a repository method as a search template method. The annotation defines the search template id,
* the parameters for the search template are taken from the method's arguments.
*
* @author P.J. Meisch (pj.meisch@sothawo.com)
* @since 5.5
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Documented
@QueryAnnotation
public @interface SearchTemplateQuery {
/**
* The id of the search template. Must not be empt or null.
*/
String id();
}

View File

@ -15,22 +15,22 @@
*/
package org.springframework.data.elasticsearch.core.query;
import org.springframework.lang.Nullable;
import java.util.Map;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
/**
* @author Peter-Josef Meisch
* @since 5.1
*/
public class SearchTemplateQueryBuilder extends BaseQueryBuilder<SearchTemplateQuery, SearchTemplateQueryBuilder> {
@Nullable
private String id;
@Nullable private String id;
@Nullable String source;
@Nullable
Map<String, Object> params;
@Nullable Map<String, Object> params;
@Nullable
public String getId() {
@ -62,6 +62,18 @@ public class SearchTemplateQueryBuilder extends BaseQueryBuilder<SearchTemplateQ
return this;
}
@Override
public SearchTemplateQueryBuilder withSort(Sort sort) {
throw new IllegalArgumentException(
"sort is not supported in a searchtemplate query. Sort values must be defined in the stored template");
}
@Override
public SearchTemplateQueryBuilder withPageable(Pageable pageable) {
throw new IllegalArgumentException(
"paging is not supported in a searchtemplate query. from and size values must be defined in the stored template");
}
@Override
public SearchTemplateQuery build() {
return new SearchTemplateQuery(this);

View File

@ -24,6 +24,7 @@ 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.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
@ -114,11 +115,15 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository
: PageRequest.of(0, DEFAULT_STREAM_BATCH_SIZE));
result = StreamUtils.createStreamFromIterator(elasticsearchOperations.searchForStream(query, clazz, index));
} else if (queryMethod.isCollectionQuery()) {
if (parameterAccessor.getPageable().isUnpaged()) {
int itemCount = (int) elasticsearchOperations.count(query, clazz, index);
query.setPageable(PageRequest.of(0, Math.max(1, itemCount)));
if (query instanceof SearchTemplateQuery) {
// we cannot get a count here, from and size would be in the template
} else {
query.setPageable(parameterAccessor.getPageable());
if (parameterAccessor.getPageable().isUnpaged()) {
int itemCount = (int) elasticsearchOperations.count(query, clazz, index);
query.setPageable(PageRequest.of(0, Math.max(1, itemCount)));
} else {
query.setPageable(parameterAccessor.getPageable());
}
}
result = elasticsearchOperations.search(query, clazz, index);
} else {
@ -137,7 +142,8 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository
var query = createQuery(parameterAccessor);
Assert.notNull(query, "unsupported query");
queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter(),
queryMethod.addSpecialMethodParameters(query, parameterAccessor,
elasticsearchOperations.getElasticsearchConverter(),
evaluationContextProvider);
return query;

View File

@ -105,7 +105,7 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor
var query = createQuery(parameterAccessor);
Assert.notNull(query, "unsupported query");
queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter(),
queryMethod.addSpecialMethodParameters(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter(),
evaluationContextProvider);
String indexName = queryMethod.getEntityInformation().getIndexName();

View File

@ -33,42 +33,11 @@ import org.springframework.data.repository.query.parser.PartTree;
* @author Rasmus Faber-Espensen
* @author Peter-Josef Meisch
* @author Haibo Liu
* @deprecated since 5.5, use {@link RepositoryPartQuery} instead
*/
public class ElasticsearchPartQuery extends AbstractElasticsearchRepositoryQuery {
private final PartTree tree;
private final MappingContext<?, ElasticsearchPersistentProperty> mappingContext;
public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
@Deprecated(forRemoval = true)
public class ElasticsearchPartQuery extends RepositoryPartQuery {
public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations, QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, elasticsearchOperations, evaluationContextProvider);
this.tree = new PartTree(queryMethod.getName(), queryMethod.getResultProcessor().getReturnedType().getDomainType());
this.mappingContext = elasticsearchConverter.getMappingContext();
}
@Override
public boolean isCountQuery() {
return tree.isCountProjection();
}
@Override
protected boolean isDeleteQuery() {
return tree.isDelete();
}
@Override
protected boolean isExistsQuery() {
return tree.isExistsProjection();
}
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor) {
BaseQuery query = new ElasticsearchQueryCreator(tree, accessor, mappingContext).createQuery();
if (tree.getMaxResults() != null) {
query.setMaxResults(tree.getMaxResults());
}
return query;
}
}

View File

@ -28,6 +28,7 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.elasticsearch.annotations.Highlight;
import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.annotations.SearchTemplateQuery;
import org.springframework.data.elasticsearch.annotations.SourceFilters;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
@ -84,6 +85,7 @@ public class ElasticsearchQueryMethod extends QueryMethod {
@Nullable private final Query queryAnnotation;
@Nullable private final Highlight highlightAnnotation;
@Nullable private final SourceFilters sourceFilters;
@Nullable private final SearchTemplateQuery searchTemplateQueryAnnotation;
public ElasticsearchQueryMethod(Method method, RepositoryMetadata repositoryMetadata, ProjectionFactory factory,
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext) {
@ -98,6 +100,7 @@ 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();
}
@ -125,12 +128,16 @@ public class ElasticsearchQueryMethod extends QueryMethod {
}
}
/**
* @return if the method is annotated with the {@link Query} annotation.
*/
public boolean hasAnnotatedQuery() {
return this.queryAnnotation != null;
}
/**
* @return the query String. Must not be {@literal null} when {@link #hasAnnotatedQuery()} returns true
* @return the query String defined in the {@link Query} annotation. Must not be {@literal null} when
* {@link #hasAnnotatedQuery()} returns true.
*/
@Nullable
public String getAnnotatedQuery() {
@ -158,6 +165,27 @@ public class ElasticsearchQueryMethod extends QueryMethod {
return new HighlightQuery(highlightConverter.convert(highlightAnnotation), getDomainClass());
}
/**
* @return if the method is annotated with the {@link SearchTemplateQuery} annotation.
* @since 5.5
*/
public boolean hasAnnotatedSearchTemplateQuery() {
return this.searchTemplateQueryAnnotation != null;
}
/**
* @return the {@link SearchTemplateQuery} annotation
* @throws IllegalArgumentException if no {@link SearchTemplateQuery} annotation is present on the method
* @since 5.5
*/
public SearchTemplateQuery getAnnotatedSearchTemplateQuery() {
Assert.isTrue(hasAnnotatedSearchTemplateQuery(), "no SearchTemplateQuery annotation present on " + getName());
Assert.notNull(searchTemplateQueryAnnotation, "highlsearchTemplateQueryAnnotationightAnnotation must not be null");
return searchTemplateQueryAnnotation;
}
/**
* @return the {@link ElasticsearchEntityMetadata} for the query methods {@link #getReturnedObjectType() return type}.
* @since 3.2
@ -281,7 +309,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() {
@ -377,9 +405,9 @@ public class ElasticsearchQueryMethod extends QueryMethod {
}
}
void addMethodParameter(BaseQuery query, ElasticsearchParametersParameterAccessor parameterAccessor,
ElasticsearchConverter elasticsearchConverter,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
void addSpecialMethodParameters(BaseQuery query, ElasticsearchParametersParameterAccessor parameterAccessor,
ElasticsearchConverter elasticsearchConverter,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
if (hasAnnotatedHighlight()) {
var highlightQuery = getAnnotatedHighlightQuery(new HighlightConverter(parameterAccessor,

View File

@ -15,13 +15,8 @@
*/
package org.springframework.data.elasticsearch.repository.query;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.repository.support.QueryStringProcessor;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.util.Assert;
/**
* ElasticsearchStringQuery
@ -32,43 +27,12 @@ import org.springframework.util.Assert;
* @author Taylor Ono
* @author Peter-Josef Meisch
* @author Haibo Liu
* @deprecated since 5.5, use {@link RepositoryStringQuery}
*/
public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQuery {
private final String queryString;
@Deprecated(since = "5.5", forRemoval = true)
public class ElasticsearchStringQuery extends RepositoryStringQuery {
public ElasticsearchStringQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations,
String queryString, QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(queryMethod, elasticsearchOperations, evaluationContextProvider);
Assert.notNull(queryString, "Query cannot be empty");
Assert.notNull(evaluationContextProvider, "ExpressionEvaluationContextProvider must not be null");
this.queryString = queryString;
super(queryMethod, elasticsearchOperations, queryString, evaluationContextProvider);
}
@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());
}
}

View File

@ -15,68 +15,26 @@
*/
package org.springframework.data.elasticsearch.repository.query;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.repository.support.QueryStringProcessor;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.util.Assert;
/**
* @author Christoph Strobl
* @author Taylor Ono
* @author Haibo Liu
* @since 3.2
* @deprecated since 5.5, use {@link ReactiveRepositoryStringQuery}
*/
public class ReactiveElasticsearchStringQuery extends AbstractReactiveElasticsearchRepositoryQuery {
private final String query;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
@Deprecated(since = "5.5", forRemoval = true)
public class ReactiveElasticsearchStringQuery extends ReactiveRepositoryStringQuery {
public ReactiveElasticsearchStringQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations operations, QueryMethodEvaluationContextProvider evaluationContextProvider) {
this(queryMethod.getAnnotatedQuery(), queryMethod, operations, evaluationContextProvider);
super(queryMethod, operations, evaluationContextProvider);
}
public ReactiveElasticsearchStringQuery(String query, ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations operations, QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(queryMethod, operations, evaluationContextProvider);
Assert.notNull(query, "query must not be null");
Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null");
this.query = query;
this.evaluationContextProvider = evaluationContextProvider;
}
@Override
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
ConversionService conversionService = getElasticsearchOperations().getElasticsearchConverter()
.getConversionService();
String processed = new QueryStringProcessor(query, queryMethod, conversionService, evaluationContextProvider)
.createQuery(parameterAccessor);
return new StringQuery(processed);
}
@Override
boolean isCountQuery() {
return queryMethod.hasCountQueryAnnotation();
}
@Override
boolean isDeleteQuery() {
return false;
}
@Override
boolean isExistsQuery() {
return false;
}
@Override
boolean isLimiting() {
return false;
super(query, queryMethod, operations, evaluationContextProvider);
}
}

View File

@ -0,0 +1,92 @@
/*
* 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.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.SearchTemplateQuery;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
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, QueryMethodEvaluationContextProvider evaluationContextProvider,
String id) {
super(queryMethod, elasticsearchOperations, evaluationContextProvider);
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();
}
}

View File

@ -0,0 +1,83 @@
/*
* 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.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;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
public ReactiveRepositoryStringQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations operations, QueryMethodEvaluationContextProvider evaluationContextProvider) {
this(queryMethod.getAnnotatedQuery(), queryMethod, operations, evaluationContextProvider);
}
public ReactiveRepositoryStringQuery(String query, ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations operations, QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(queryMethod, operations, evaluationContextProvider);
Assert.notNull(query, "query must not be null");
Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null");
this.query = query;
this.evaluationContextProvider = evaluationContextProvider;
}
@Override
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor parameterAccessor) {
ConversionService conversionService = getElasticsearchOperations().getElasticsearchConverter()
.getConversionService();
String processed = new QueryStringProcessor(query, queryMethod, conversionService, evaluationContextProvider)
.createQuery(parameterAccessor);
return new StringQuery(processed);
}
@Override
boolean isCountQuery() {
return queryMethod.hasCountQueryAnnotation();
}
@Override
boolean isDeleteQuery() {
return false;
}
@Override
boolean isExistsQuery() {
return false;
}
@Override
boolean isLimiting() {
return false;
}
}

View File

@ -0,0 +1,75 @@
/*
* 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.QueryMethodEvaluationContextProvider;
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,
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;
}
}

View File

@ -0,0 +1,86 @@
/*
* 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.QueryMethodEvaluationContextProvider;
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, QueryMethodEvaluationContextProvider evaluationContextProvider,
String id) {
super(queryMethod, elasticsearchOperations, evaluationContextProvider);
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();
}
}

View File

@ -0,0 +1,58 @@
package org.springframework.data.elasticsearch.repository.query;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.repository.support.QueryStringProcessor;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.util.Assert;
/**
* 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, 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());
}
}

View File

@ -22,9 +22,10 @@ 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.ElasticsearchStringQuery;
import org.springframework.data.elasticsearch.repository.query.RepositoryPartQuery;
import org.springframework.data.elasticsearch.repository.query.RepositorySearchTemplateQuery;
import org.springframework.data.elasticsearch.repository.query.RepositoryStringQuery;
import org.springframework.data.elasticsearch.repository.support.querybyexample.QueryByExampleElasticsearchExecutor;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
@ -122,13 +123,17 @@ public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport {
if (namedQueries.hasQuery(namedQueryName)) {
String namedQuery = namedQueries.getQuery(namedQueryName);
return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, namedQuery,
return new RepositoryStringQuery(queryMethod, elasticsearchOperations, namedQuery,
evaluationContextProvider);
} else if (queryMethod.hasAnnotatedQuery()) {
return new ElasticsearchStringQuery(queryMethod, elasticsearchOperations, queryMethod.getAnnotatedQuery(),
return new RepositoryStringQuery(queryMethod, elasticsearchOperations, queryMethod.getAnnotatedQuery(),
evaluationContextProvider);
} else if (queryMethod.hasAnnotatedSearchTemplateQuery()) {
var searchTemplateQuery = queryMethod.getAnnotatedSearchTemplateQuery();
return new RepositorySearchTemplateQuery(queryMethod, elasticsearchOperations, evaluationContextProvider,
searchTemplateQuery.id());
}
return new ElasticsearchPartQuery(queryMethod, elasticsearchOperations, evaluationContextProvider);
return new RepositoryPartQuery(queryMethod, elasticsearchOperations, evaluationContextProvider);
}
}

View File

@ -23,8 +23,10 @@ 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.query.RepositorySearchTemplateQuery;
import org.springframework.data.elasticsearch.repository.support.querybyexample.ReactiveQueryByExampleElasticsearchExecutor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.ProjectionFactory;
@ -161,10 +163,14 @@ public class ReactiveElasticsearchRepositoryFactory extends ReactiveRepositoryFa
if (namedQueries.hasQuery(namedQueryName)) {
String namedQuery = namedQueries.getQuery(namedQueryName);
return new ReactiveElasticsearchStringQuery(namedQuery, queryMethod, operations,
return new ReactiveRepositoryStringQuery(namedQuery, queryMethod, operations,
evaluationContextProvider);
} else if (queryMethod.hasAnnotatedQuery()) {
return new ReactiveElasticsearchStringQuery(queryMethod, operations, evaluationContextProvider);
return new ReactiveRepositoryStringQuery(queryMethod, operations, evaluationContextProvider);
} else if (queryMethod.hasAnnotatedSearchTemplateQuery()) {
var searchTemplateQuery = queryMethod.getAnnotatedSearchTemplateQuery();
return new ReactiveRepositorySearchTemplateQuery(queryMethod, operations, evaluationContextProvider,
searchTemplateQuery.id());
} else {
return new ReactivePartTreeElasticsearchQuery(queryMethod, operations, evaluationContextProvider);
}

View File

@ -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.ElasticsearchPartQueryIntegrationTests;
import org.springframework.data.elasticsearch.core.query.RepositoryPartQueryIntegrationTests;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
@ -29,7 +29,7 @@ import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplat
* @author Peter-Josef Meisch
* @since 4.4
*/
public class ElasticsearchPartQueryELCIntegrationTests extends ElasticsearchPartQueryIntegrationTests {
public class ElasticsearchPartQueryELCIntegrationTests extends RepositoryPartQueryIntegrationTests {
@Configuration
@Import({ ElasticsearchTemplateConfiguration.class })

View File

@ -31,15 +31,15 @@ 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.QueryMethodEvaluationContextProvider;
import org.springframework.lang.Nullable;
/**
* Tests for {@link ElasticsearchPartQuery}. The tests make sure that queries are built according to the method naming.
* Tests for {@link RepositoryPartQuery}. The tests make sure that queries are built according to the method naming.
* Classes implementing this abstract class are in the packages of their request factories and converters as these are
* kept package private.
*
@ -48,7 +48,7 @@ import org.springframework.lang.Nullable;
*/
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@SpringIntegrationTest
public abstract class ElasticsearchPartQueryIntegrationTests {
public abstract class RepositoryPartQueryIntegrationTests {
public static final String BOOK_TITLE = "Title";
public static final int BOOK_PRICE = 42;
@ -646,7 +646,7 @@ public abstract class ElasticsearchPartQueryIntegrationTests {
ElasticsearchQueryMethod queryMethod = new ElasticsearchQueryMethod(method,
new DefaultRepositoryMetadata(SampleRepository.class), new SpelAwareProxyProjectionFactory(),
operations.getElasticsearchConverter().getMappingContext());
ElasticsearchPartQuery partQuery = new ElasticsearchPartQuery(queryMethod, operations,
RepositoryPartQuery partQuery = new RepositoryPartQuery(queryMethod, operations,
QueryMethodEvaluationContextProvider.DEFAULT);
Query query = partQuery.createQuery(parameters);
return buildQueryString(query, Book.class);

View File

@ -1,78 +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;
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");
}
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.query;
import static org.mockito.Mockito.*;
import java.lang.reflect.Method;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
@ExtendWith(MockitoExtension.class)
public class ReactiveRepositoryQueryUnitTestsBase {
@Mock ReactiveElasticsearchOperations operations;
/**
* set up the {operations} mock to return the {@link ElasticsearchConverter} from setupConverter().
*/
@BeforeEach
public void setUp() {
when(operations.getElasticsearchConverter()).thenReturn(setupConverter());
}
/**
* @return a simple {@link MappingElasticsearchConverter} with no special setup.
*/
protected MappingElasticsearchConverter setupConverter() {
return new MappingElasticsearchConverter(
new SimpleElasticsearchMappingContext());
}
/**
* Creates a {@link ReactiveElasticsearchQueryMethod} for the given method
*
* @param repositoryClass
* @param name
* @param parameters
* @return
* @throws NoSuchMethodException
*/
protected ReactiveElasticsearchQueryMethod getQueryMethod(Class<?> repositoryClass, String name,
Class<?>... parameters)
throws NoSuchMethodException {
Method method = repositoryClass.getMethod(name, parameters);
return new ReactiveElasticsearchQueryMethod(method,
new DefaultRepositoryMetadata(repositoryClass),
new SpelAwareProxyProjectionFactory(), operations.getElasticsearchConverter().getMappingContext());
}
}

View File

@ -0,0 +1,113 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.query;
import static org.assertj.core.api.Assertions.*;
import java.util.Arrays;
import org.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.QueryMethodEvaluationContextProvider;
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, QueryMethodEvaluationContextProvider.DEFAULT,
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
}

View File

@ -16,12 +16,10 @@
package org.springframework.data.elasticsearch.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@ -29,28 +27,27 @@ 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;
@ -60,13 +57,54 @@ import org.springframework.lang.Nullable;
* @author Haibo Liu
*/
@ExtendWith(MockitoExtension.class)
public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryUnitTestBase {
public class ReactiveRepositoryStringQueryUnitTests extends ReactiveRepositoryQueryUnitTestsBase {
@Mock ReactiveElasticsearchOperations operations;
/**
* Adds some data class and custom conversion to the base class implementation.
*/
protected MappingElasticsearchConverter setupConverter() {
@BeforeEach
public void setUp() {
when(operations.getElasticsearchConverter()).thenReturn(setupConverter());
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");
}
}
@Test // DATAES-519
@ -367,31 +405,23 @@ public class ReactiveElasticsearchStringQueryUnitTests extends ElasticsearchStri
Class<?>[] argTypes = Arrays.stream(args).map(Object::getClass)
.map(clazz -> Collection.class.isAssignableFrom(clazz) ? List.class : clazz).toArray(Class[]::new);
ReactiveElasticsearchQueryMethod queryMethod = getQueryMethod(methodName, argTypes);
ReactiveElasticsearchStringQuery elasticsearchStringQuery = queryForMethod(queryMethod);
ReactiveElasticsearchQueryMethod queryMethod = getQueryMethod(SampleRepository.class, methodName, argTypes);
ReactiveRepositoryStringQuery elasticsearchStringQuery = queryForMethod(queryMethod);
return elasticsearchStringQuery.createQuery(new ElasticsearchParametersParameterAccessor(queryMethod, args));
}
private ReactiveElasticsearchStringQuery queryForMethod(ReactiveElasticsearchQueryMethod queryMethod) {
return new ReactiveElasticsearchStringQuery(queryMethod, operations,
QueryMethodEvaluationContextProvider.DEFAULT);
}
private ReactiveRepositoryStringQuery createQueryForMethod(String name, Class<?>... parameters) throws Exception {
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);
ReactiveElasticsearchQueryMethod queryMethod = getQueryMethod(SampleRepository.class, name, parameters);
return queryForMethod(queryMethod);
}
private ReactiveRepositoryStringQuery queryForMethod(ReactiveElasticsearchQueryMethod queryMethod) {
return new ReactiveRepositoryStringQuery(queryMethod, operations,
QueryMethodEvaluationContextProvider.DEFAULT);
}
private interface SampleRepository extends Repository<Person, String> {
@Query("{ 'bool' : { 'must' : { 'term' : { 'name' : '?0' } } } }")

View File

@ -0,0 +1,71 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.query;
import static org.mockito.Mockito.*;
import java.lang.reflect.Method;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
@ExtendWith(MockitoExtension.class)
public class RepositoryQueryUnitTestsBase {
@Mock ElasticsearchOperations operations;
/**
* set up the {operations} mock to return the {@link ElasticsearchConverter} from setupConverter().
*/
@BeforeEach
public void setUp() {
when(operations.getElasticsearchConverter()).thenReturn(setupConverter());
}
/**
* @return a simple {@link MappingElasticsearchConverter} with no special setup.
*/
protected MappingElasticsearchConverter setupConverter() {
return new MappingElasticsearchConverter(
new SimpleElasticsearchMappingContext());
}
/**
* Creates a {@link ElasticsearchQueryMethod} for the given method
*
* @param repositoryClass
* @param name
* @param parameters
* @return
* @throws NoSuchMethodException
*/
protected ElasticsearchQueryMethod getQueryMethod(Class<?> repositoryClass, String name, Class<?>... parameters)
throws NoSuchMethodException {
Method method = repositoryClass.getMethod(name, parameters);
return new ElasticsearchQueryMethod(method, new DefaultRepositoryMetadata(repositoryClass),
new SpelAwareProxyProjectionFactory(), operations.getElasticsearchConverter().getMappingContext());
}
}

View File

@ -0,0 +1,113 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.query;
import static org.assertj.core.api.Assertions.*;
import java.util.Arrays;
import org.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.QueryMethodEvaluationContextProvider;
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, QueryMethodEvaluationContextProvider.DEFAULT,
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
}

View File

@ -16,9 +16,7 @@
package org.springframework.data.elasticsearch.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@ -26,28 +24,25 @@ 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.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.lang.Nullable;
@ -57,14 +52,53 @@ import org.springframework.lang.Nullable;
* @author Niklas Herder
* @author Haibo Liu
*/
@ExtendWith(MockitoExtension.class)
public class ElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryUnitTestBase {
public class RepositoryStringQueryUnitTests extends RepositoryStringQueryUnitTestsBase {
/**
* Adds some data class and custom conversion to the base class implementation.
*/
protected MappingElasticsearchConverter setupConverter() {
@Mock ElasticsearchOperations operations;
Collection<Converter<?, ?>> converters = new ArrayList<>();
converters.add(RepositoryStringQueryUnitTests.CarConverter.INSTANCE);
CustomConversions customConversions = new ElasticsearchCustomConversions(converters);
@BeforeEach
public void setUp() {
when(operations.getElasticsearchConverter()).thenReturn(setupConverter());
MappingElasticsearchConverter converter = super.setupConverter();
converter.setConversions(customConversions);
converter.afterPropertiesSet();
return converter;
}
static class Car {
@Nullable private String name;
@Nullable private String model;
@Nullable
public String getName() {
return name;
}
public void setName(@Nullable String name) {
this.name = name;
}
@Nullable
public String getModel() {
return model;
}
public void setModel(@Nullable String model) {
this.model = model;
}
}
enum CarConverter implements Converter<Car, String> {
INSTANCE;
@Override
public String convert(Car car) {
return (car.getName() != null ? car.getName() : "null") + '-'
+ (car.getModel() != null ? car.getModel() : "null");
}
}
@Test // DATAES-552
@ -350,8 +384,9 @@ public class ElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryU
throws NoSuchMethodException {
Class<?>[] argTypes = Arrays.stream(args).map(Object::getClass).toArray(Class[]::new);
ElasticsearchQueryMethod queryMethod = getQueryMethod(methodName, argTypes);
ElasticsearchStringQuery elasticsearchStringQuery = queryForMethod(queryMethod);
ElasticsearchQueryMethod queryMethod = getQueryMethod(RepositoryStringQueryUnitTests.SampleRepository.class,
methodName, argTypes);
RepositoryStringQuery elasticsearchStringQuery = queryForMethod(queryMethod);
return elasticsearchStringQuery.createQuery(new ElasticsearchParametersParameterAccessor(queryMethod, args));
}
@ -370,18 +405,11 @@ public class ElasticsearchStringQueryUnitTests extends ElasticsearchStringQueryU
.isEqualTo("{ 'bool' : { 'must' : { 'term' : { 'car' : 'Toyota-Prius' } } } }");
}
private ElasticsearchStringQuery queryForMethod(ElasticsearchQueryMethod queryMethod) {
return new ElasticsearchStringQuery(queryMethod, operations, queryMethod.getAnnotatedQuery(),
private RepositoryStringQuery queryForMethod(ElasticsearchQueryMethod queryMethod) {
return new RepositoryStringQuery(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> {
@Query("{ 'bool' : { 'must' : { 'term' : { 'age' : ?0 } } } }")

View File

@ -0,0 +1,23 @@
/*
* 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 {
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.support;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchTemplateConfiguration;
import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.test.context.ContextConfiguration;
/**
* @since 5.5
*/
@ContextConfiguration(classes = ReactiveRepositoryQueryELCIntegrationTests.Config.class)
public class ReactiveRepositoryQueryELCIntegrationTests
extends ReactiveRepositoryQueryIntegrationTests {
@Configuration
@Import({ ReactiveElasticsearchTemplateConfiguration.class })
@EnableReactiveElasticsearchRepositories(considerNestedRepositories = true)
static class Config {
@Bean
IndexNameProvider indexNameProvider() {
return new IndexNameProvider("reactive-repository-query");
}
}
}

View File

@ -0,0 +1,159 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.support;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.elasticsearch.core.IndexOperationsAdapter.*;
import reactor.core.publisher.Flux;
import java.util.List;
import org.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);
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.support;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.test.context.ContextConfiguration;
@ContextConfiguration(classes = {RepositoryQueryELCIntegrationTests.Config.class })public class RepositoryQueryELCIntegrationTests extends RepositoryQueryIntegrationTests {
@Configuration
@Import({ElasticsearchTemplateConfiguration.class })
@EnableElasticsearchRepositories(basePackages = {"org.springframework.data.elasticsearch.repository.support" },
considerNestedRepositories = true)
static class Config {
@Bean
IndexNameProvider indexNameProvider() {
return new IndexNameProvider("repository-query");
}
}
}

View File

@ -0,0 +1,151 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.support;
import static org.assertj.core.api.Assertions.*;
import java.util.List;
import org.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);
}
}