From 8e118d6aa0c7ff1aa11af4d60bc534cf66ea1bcd Mon Sep 17 00:00:00 2001 From: Peter-Josef Meisch Date: Fri, 8 Nov 2024 16:01:16 +0100 Subject: [PATCH] Add repository method support for search templates. Signed-off-by: Peter-Josef Meisch --- .../elasticsearch/elasticsearch-new.adoc | 2 + .../ROOT/pages/elasticsearch/misc.adoc | 3 +- .../elasticsearch-repository-queries.adoc | 42 ++++- .../migration-guide-5.4-5.5.adoc | 10 ++ .../annotations/SearchTemplateQuery.java | 42 +++++ .../query/SearchTemplateQueryBuilder.java | 24 ++- .../AbstractElasticsearchRepositoryQuery.java | 16 +- ...tReactiveElasticsearchRepositoryQuery.java | 2 +- .../query/ElasticsearchPartQuery.java | 39 +---- .../query/ElasticsearchQueryMethod.java | 38 ++++- .../query/ElasticsearchStringQuery.java | 44 +---- .../ReactiveElasticsearchStringQuery.java | 52 +----- ...ReactiveRepositorySearchTemplateQuery.java | 92 ++++++++++ .../query/ReactiveRepositoryStringQuery.java | 83 +++++++++ .../repository/query/RepositoryPartQuery.java | 75 +++++++++ .../query/RepositorySearchTemplateQuery.java | 86 ++++++++++ .../query/RepositoryStringQuery.java | 58 +++++++ .../ElasticsearchRepositoryFactory.java | 15 +- ...eactiveElasticsearchRepositoryFactory.java | 12 +- ...ticsearchPartQueryELCIntegrationTests.java | 4 +- ... RepositoryPartQueryIntegrationTests.java} | 8 +- .../ElasticsearchStringQueryUnitTestBase.java | 78 --------- .../ReactiveRepositoryQueryUnitTestsBase.java | 73 ++++++++ ...epositorySearchTemplateQueryUnitTests.java | 113 +++++++++++++ ...activeRepositoryStringQueryUnitTests.java} | 86 +++++++--- .../query/RepositoryQueryUnitTestsBase.java | 71 ++++++++ ...epositorySearchTemplateQueryUnitTests.java | 113 +++++++++++++ ...va => RepositoryStringQueryUnitTests.java} | 80 ++++++--- .../RepositoryStringQueryUnitTestsBase.java | 23 +++ ...iveRepositoryQueryELCIntegrationTests.java | 42 +++++ ...activeRepositoryQueryIntegrationTests.java | 159 ++++++++++++++++++ .../RepositoryQueryELCIntegrationTests.java | 38 +++++ .../RepositoryQueryIntegrationTests.java | 151 +++++++++++++++++ 33 files changed, 1482 insertions(+), 292 deletions(-) create mode 100644 src/main/java/org/springframework/data/elasticsearch/annotations/SearchTemplateQuery.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositorySearchTemplateQuery.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositoryStringQuery.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/repository/query/RepositoryPartQuery.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/repository/query/RepositorySearchTemplateQuery.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/repository/query/RepositoryStringQuery.java rename src/test/java/org/springframework/data/elasticsearch/core/query/{ElasticsearchPartQueryIntegrationTests.java => RepositoryPartQueryIntegrationTests.java} (98%) delete mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQueryUnitTestBase.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositoryQueryUnitTestsBase.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositorySearchTemplateQueryUnitTests.java rename src/test/java/org/springframework/data/elasticsearch/repository/query/{ReactiveElasticsearchStringQueryUnitTests.java => ReactiveRepositoryStringQueryUnitTests.java} (89%) create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/query/RepositoryQueryUnitTestsBase.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/query/RepositorySearchTemplateQueryUnitTests.java rename src/test/java/org/springframework/data/elasticsearch/repository/query/{ElasticsearchStringQueryUnitTests.java => RepositoryStringQueryUnitTests.java} (89%) create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/query/RepositoryStringQueryUnitTestsBase.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/support/ReactiveRepositoryQueryELCIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/support/ReactiveRepositoryQueryIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/support/RepositoryQueryELCIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/support/RepositoryQueryIntegrationTests.java diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc index 4f481f347..d4a1b35a2 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc @@ -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 diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/misc.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/misc.adoc index 36567cda4..7f3ac8f0f 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/misc.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/misc.adoc @@ -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. - diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repository-queries.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repository-queries.adoc index b558e13bb..b22e17522 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repository-queries.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repository-queries.adoc @@ -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` [[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 { @@ -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 getByIds(Collection 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 { @@ -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 { @@ -493,6 +504,7 @@ interface BookRepository extends ElasticsearchRepository { 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 { Page findByName(Collection 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 { ---- ==== + +[[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 { + @SearchTemplateQuery(id = "book-by-title") + SearchHits 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. diff --git a/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.4-5.5.adoc b/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.4-5.5.adoc index 6fe54134d..494f6601f 100644 --- a/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.4-5.5.adoc +++ b/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.4-5.5.adoc @@ -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 diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/SearchTemplateQuery.java b/src/main/java/org/springframework/data/elasticsearch/annotations/SearchTemplateQuery.java new file mode 100644 index 000000000..f50675d97 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/SearchTemplateQuery.java @@ -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(); +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/SearchTemplateQueryBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/query/SearchTemplateQueryBuilder.java index 78fa4c3ad..a0b61e27e 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/SearchTemplateQueryBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/SearchTemplateQueryBuilder.java @@ -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 { - @Nullable - private String id; + @Nullable private String id; @Nullable String source; - @Nullable - Map params; + @Nullable Map params; @Nullable public String getId() { @@ -62,6 +62,18 @@ public SearchTemplateQueryBuilder withParams(@Nullable Map param 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); diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java index 4420c7e09..be7ca9f17 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java @@ -24,6 +24,7 @@ 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 Object execute(Object[] parameters) { : 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 Query createQuery(Object[] parameters) { var query = createQuery(parameterAccessor); Assert.notNull(query, "unsupported query"); - queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter(), + queryMethod.addSpecialMethodParameters(query, parameterAccessor, + elasticsearchOperations.getElasticsearchConverter(), evaluationContextProvider); return query; diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java index 74d3e78d7..384743562 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java @@ -105,7 +105,7 @@ private Object execute(ElasticsearchParametersParameterAccessor parameterAccesso 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(); diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java index f4bec2da9..c3efcd47d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java @@ -33,42 +33,11 @@ * @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 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; } } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java index 6b0f54311..6a3608bec 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java @@ -28,6 +28,7 @@ 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, ElasticsearchPersistentProperty> mappingContext) { @@ -98,6 +100,7 @@ public ElasticsearchQueryMethod(Method method, RepositoryMetadata repositoryMeta 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 @@ protected void verifyCountQueryTypes() { } } + /** + * @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 HighlightQuery getAnnotatedHighlightQuery(HighlightConverter highlightCon 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 boolean isNotSearchPageMethod() { /** * @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 @@ private Class potentiallyUnwrapReturnTypeFor(RepositoryMetadata metadata, Met } } - 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, diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java index 4bbbefab6..f9c1a830a 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java @@ -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 @@ * @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; - } - - @Override - public boolean isCountQuery() { - return queryMethod.hasCountQueryAnnotation(); - } - - @Override - protected boolean isDeleteQuery() { - return false; - } - - @Override - protected boolean isExistsQuery() { - return false; + super(queryMethod, elasticsearchOperations, queryString, evaluationContextProvider); } - - 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()); - } - } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQuery.java index 3fbbfff53..63651bb1c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQuery.java @@ -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); } } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositorySearchTemplateQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositorySearchTemplateQuery.java new file mode 100644 index 000000000..e6ba93826 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositorySearchTemplateQuery.java @@ -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 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 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(); + 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(); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositoryStringQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositoryStringQuery.java new file mode 100644 index 000000000..f6ea14d35 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositoryStringQuery.java @@ -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; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/RepositoryPartQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/RepositoryPartQuery.java new file mode 100644 index 000000000..1fe4b60b2 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/RepositoryPartQuery.java @@ -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 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; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/RepositorySearchTemplateQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/RepositorySearchTemplateQuery.java new file mode 100644 index 000000000..e42dacd5e --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/RepositorySearchTemplateQuery.java @@ -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 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 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(); + 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(); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/RepositoryStringQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/RepositoryStringQuery.java new file mode 100644 index 000000000..e1f53053a --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/RepositoryStringQuery.java @@ -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()); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java index 321cbff34..fb4ccf3ff 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java @@ -22,9 +22,10 @@ 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 RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, 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); } } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java index 49e1e18fc..a4808c78f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java @@ -23,8 +23,10 @@ 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 RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, 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); } diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchPartQueryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchPartQueryELCIntegrationTests.java index 86b395840..40f7dd7ce 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchPartQueryELCIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchPartQueryELCIntegrationTests.java @@ -21,7 +21,7 @@ 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 @@ * @author Peter-Josef Meisch * @since 4.4 */ -public class ElasticsearchPartQueryELCIntegrationTests extends ElasticsearchPartQueryIntegrationTests { +public class ElasticsearchPartQueryELCIntegrationTests extends RepositoryPartQueryIntegrationTests { @Configuration @Import({ ElasticsearchTemplateConfiguration.class }) diff --git a/src/test/java/org/springframework/data/elasticsearch/core/query/ElasticsearchPartQueryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/query/RepositoryPartQueryIntegrationTests.java similarity index 98% rename from src/test/java/org/springframework/data/elasticsearch/core/query/ElasticsearchPartQueryIntegrationTests.java rename to src/test/java/org/springframework/data/elasticsearch/core/query/RepositoryPartQueryIntegrationTests.java index d8501cb52..5a55d9c86 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/query/ElasticsearchPartQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/query/RepositoryPartQueryIntegrationTests.java @@ -31,15 +31,15 @@ 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 @@ */ @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 @@ private String getQueryString(String methodName, Class[] parameterClasses, Ob 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); diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQueryUnitTestBase.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQueryUnitTestBase.java deleted file mode 100644 index ef0f79cb7..000000000 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQueryUnitTestBase.java +++ /dev/null @@ -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> 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 { - INSTANCE; - - @Override - public String convert(ElasticsearchStringQueryUnitTests.Car car) { - return (car.getName() != null ? car.getName() : "null") + '-' - + (car.getModel() != null ? car.getModel() : "null"); - } - } - -} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositoryQueryUnitTestsBase.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositoryQueryUnitTestsBase.java new file mode 100644 index 000000000..4ab819632 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositoryQueryUnitTestsBase.java @@ -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()); + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositorySearchTemplateQueryUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositorySearchTemplateQueryUnitTests.java new file mode 100644 index 000000000..af144dbc3 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositorySearchTemplateQueryUnitTests.java @@ -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 { + @SearchTemplateQuery(id = "searchtemplate-42") + SearchHits searchWithArgs(String stringArg, Integer intArg); + + @SearchTemplateQuery(id = "searchtemplate-42") + SearchHits 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 +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQueryUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositoryStringQueryUnitTests.java similarity index 89% rename from src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQueryUnitTests.java rename to src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositoryStringQueryUnitTests.java index e5a9f626d..11d7d6939 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQueryUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveRepositoryStringQueryUnitTests.java @@ -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.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 @@ * @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() { + + Collection> 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; + } - @BeforeEach - public void setUp() { - when(operations.getElasticsearchConverter()).thenReturn(setupConverter()); + @Nullable + public String getModel() { + return model; + } + + public void setModel(@Nullable String model) { + this.model = model; + } + } + + enum CarConverter implements Converter { + INSTANCE; + + @Override + public String convert(Car car) { + return (car.getName() != null ? car.getName() : "null") + '-' + + (car.getModel() != null ? car.getModel() : "null"); + } } @Test // DATAES-519 @@ -367,29 +405,21 @@ private org.springframework.data.elasticsearch.core.query.Query createQuery(Stri 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 ReactiveElasticsearchQueryMethod getQueryMethod(String name, Class... parameters) - throws NoSuchMethodException { + private ReactiveRepositoryStringQuery createQueryForMethod(String name, Class... parameters) throws Exception { - Method method = SampleRepository.class.getMethod(name, parameters); - return new ReactiveElasticsearchQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class), - new SpelAwareProxyProjectionFactory(), operations.getElasticsearchConverter().getMappingContext()); + ReactiveElasticsearchQueryMethod queryMethod = getQueryMethod(SampleRepository.class, name, parameters); + return queryForMethod(queryMethod); } - private ReactiveElasticsearchStringQuery createQueryForMethod(String name, Class... parameters) throws Exception { - - ReactiveElasticsearchQueryMethod queryMethod = getQueryMethod(name, parameters); - return queryForMethod(queryMethod); + private ReactiveRepositoryStringQuery queryForMethod(ReactiveElasticsearchQueryMethod queryMethod) { + return new ReactiveRepositoryStringQuery(queryMethod, operations, + QueryMethodEvaluationContextProvider.DEFAULT); } private interface SampleRepository extends Repository { diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositoryQueryUnitTestsBase.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositoryQueryUnitTestsBase.java new file mode 100644 index 000000000..3d80c995a --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositoryQueryUnitTestsBase.java @@ -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()); + } + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositorySearchTemplateQueryUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositorySearchTemplateQueryUnitTests.java new file mode 100644 index 000000000..1b0f62109 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositorySearchTemplateQueryUnitTests.java @@ -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 { + @SearchTemplateQuery(id = "searchtemplate-42") + SearchHits searchWithArgs(String stringArg, Integer intArg); + + @SearchTemplateQuery(id = "searchtemplate-42") + SearchHits 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 +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQueryUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositoryStringQueryUnitTests.java similarity index 89% rename from src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQueryUnitTests.java rename to src/test/java/org/springframework/data/elasticsearch/repository/query/RepositoryStringQueryUnitTests.java index c15612ff6..eebc20a04 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQueryUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositoryStringQueryUnitTests.java @@ -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.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 @@ * @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() { + + Collection> converters = new ArrayList<>(); + converters.add(RepositoryStringQueryUnitTests.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; + } - @Mock ElasticsearchOperations operations; + @Nullable + public String getModel() { + return model; + } - @BeforeEach - public void setUp() { - when(operations.getElasticsearchConverter()).thenReturn(setupConverter()); + public void setModel(@Nullable String model) { + this.model = model; + } + } + + enum CarConverter implements Converter { + 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 @@ private org.springframework.data.elasticsearch.core.query.Query createQuery(Stri 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 @@ void shouldUseConverterOnParameters() throws NoSuchMethodException { .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 { @Query("{ 'bool' : { 'must' : { 'term' : { 'age' : ?0 } } } }") diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositoryStringQueryUnitTestsBase.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositoryStringQueryUnitTestsBase.java new file mode 100644 index 000000000..c6ae39743 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/RepositoryStringQueryUnitTestsBase.java @@ -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 { + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/ReactiveRepositoryQueryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/ReactiveRepositoryQueryELCIntegrationTests.java new file mode 100644 index 000000000..da0183438 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/ReactiveRepositoryQueryELCIntegrationTests.java @@ -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"); + } + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/ReactiveRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/ReactiveRepositoryQueryIntegrationTests.java new file mode 100644 index 000000000..674db9bfa --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/ReactiveRepositoryQueryIntegrationTests.java @@ -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 { + @SearchTemplateQuery(id = "searchInCharacter") + Flux> searchInCharacter(String word); + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/RepositoryQueryELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/RepositoryQueryELCIntegrationTests.java new file mode 100644 index 000000000..bc079feb7 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/RepositoryQueryELCIntegrationTests.java @@ -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"); + } + + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/RepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/RepositoryQueryIntegrationTests.java new file mode 100644 index 000000000..4787d7773 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/RepositoryQueryIntegrationTests.java @@ -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 { + @SearchTemplateQuery(id = "searchInCharacter") + SearchHits searchInCharacter(String word); + } +}