Skip to content

Commit cb77b32

Browse files
authored
Add repository method support for search templates.
Original Pull Request #3049 Closes #2997 Signed-off-by: Peter-Josef Meisch <[email protected]>
1 parent 5568c7b commit cb77b32

33 files changed

+1482
-292
lines changed

src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc

+2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
[[new-features.5-5-0]]
55
== New in Spring Data Elasticsearch 5.5
6+
67
* Upgrade to Elasticsearch 8.17.0.
8+
* Add support for the `@SearchTemplateQuery` annotation on repository methods.
79

810
[[new-features.5-4-0]]
911
== New in Spring Data Elasticsearch 5.4

src/main/antora/modules/ROOT/pages/elasticsearch/misc.adoc

+2-1
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,8 @@ operations.putScript( <.>
365365

366366
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.
367367

368+
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.
369+
368370
In the following code, we will add a call using a search template query to a custom repository implementation (see
369371
xref:repositories/custom-implementations.adoc[]) as an example how this can be integrated into a repository call.
370372

@@ -449,4 +451,3 @@ var query = Query.findAll().addSort(Sort.by(order));
449451
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.
450452

451453
For the definition of the order path and the nested paths, the Java entity property names should be used.
452-

src/main/antora/modules/ROOT/pages/elasticsearch/repositories/elasticsearch-repository-queries.adoc

+36-6
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ The Elasticsearch module supports all basic query building feature as string que
1010
=== Declared queries
1111

1212
Deriving the query from the method name is not always sufficient and/or may result in unreadable method names.
13-
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] ).
13+
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] ).
14+
15+
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] ).
1416

1517
[[elasticsearch.query-methods.criterions]]
1618
== Query creation
@@ -312,11 +314,13 @@ Repository methods can be defined to have the following return types for returni
312314
* `SearchPage<T>`
313315

314316
[[elasticsearch.query-methods.at-query]]
315-
== Using @Query Annotation
317+
== Using the @Query Annotation
316318

317319
.Declare query on the method using the `@Query` annotation.
318320
====
319-
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.
321+
The arguments passed to the method can be inserted into placeholders in the query string.
322+
The placeholders are of the form `?0`, `?1`, `?2` etc. for the first, second, third parameter and so on.
323+
320324
[source,java]
321325
----
322326
interface BookRepository extends ElasticsearchRepository<Book, String> {
@@ -341,15 +345,20 @@ It will be sent to Easticsearch as value of the query element; if for example th
341345
}
342346
----
343347
====
348+
344349
.`@Query` annotation on a method taking a Collection argument
345350
====
346351
A repository method such as
352+
347353
[source,java]
348354
----
349355
@Query("{\"ids\": {\"values\": ?0 }}")
350356
List<SampleEntity> getByIds(Collection<String> ids);
351357
----
352-
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
358+
359+
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.
360+
So calling the method with a `List` of `["id1", "id2", "id3"]` would produce the query body
361+
353362
[source,json]
354363
----
355364
{
@@ -369,7 +378,6 @@ would make an https://www.elastic.co/guide/en/elasticsearch/reference/current/qu
369378
====
370379
https://docs.spring.io/spring-framework/reference/core/expressions.html[SpEL expression] is also supported when defining query in `@Query`.
371380
372-
373381
[source,java]
374382
----
375383
interface BookRepository extends ElasticsearchRepository<Book, String> {
@@ -411,6 +419,7 @@ If for example the function is called with the parameter _John_, it would produc
411419
.accessing parameter property.
412420
====
413421
Supposing that we have the following class as query parameter type:
422+
414423
[source,java]
415424
----
416425
public record QueryParameter(String value) {
@@ -444,7 +453,9 @@ We can pass `new QueryParameter("John")` as the parameter now, and it will produ
444453

445454
.accessing bean property.
446455
====
447-
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:
456+
https://docs.spring.io/spring-framework/reference/core/expressions/language-ref/bean-references.html[Bean property] is also supported to access.
457+
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:
458+
448459
[source,java]
449460
----
450461
interface BookRepository extends ElasticsearchRepository<Book, String> {
@@ -493,6 +504,7 @@ interface BookRepository extends ElasticsearchRepository<Book, String> {
493504
NOTE: collection values should not be quoted when declaring the elasticsearch json query.
494505
495506
A collection of `names` like `List.of("name1", "name2")` will produce the following terms query:
507+
496508
[source,json]
497509
----
498510
{
@@ -532,6 +544,7 @@ interface BookRepository extends ElasticsearchRepository<Book, String> {
532544
Page<Book> findByName(Collection<QueryParameter> parameters, Pageable pageable);
533545
}
534546
----
547+
535548
This will extract all the `value` property values as a new `Collection` from `QueryParameter` collection, thus takes the same effect as above.
536549
====
537550

@@ -560,3 +573,20 @@ interface BookRepository extends ElasticsearchRepository<Book, String> {
560573
----
561574
562575
====
576+
577+
[[elasticsearch.query-methods.at-searchtemplate-query]]
578+
== Using the @SearchTemplateQuery Annotation
579+
580+
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.
581+
582+
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:
583+
584+
[source,java]
585+
----
586+
interface BookRepository extends ElasticsearchRepository<Book, String> {
587+
@SearchTemplateQuery(id = "book-by-title")
588+
SearchHits<Book> findByTitle(String title);
589+
}
590+
----
591+
592+
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.

src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.4-5.5.adoc

+10
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,14 @@ This section describes breaking changes from version 5.4.x to 5.5.x and how remo
99
[[elasticsearch-migration-guide-5.4-5.5.deprecations]]
1010
== Deprecations
1111

12+
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:
13+
14+
|===
15+
|old name|new name
16+
17+
|ElasticsearchPartQuery|RepositoryPartQuery
18+
|ElasticsearchStringQuery|RepositoryStringQuery
19+
|ReactiveElasticsearchStringQuery|ReactiveRepositoryStringQuery
20+
|===
21+
1222
=== Removals
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.elasticsearch.annotations;
17+
18+
import org.springframework.data.annotation.QueryAnnotation;
19+
20+
import java.lang.annotation.Documented;
21+
import java.lang.annotation.ElementType;
22+
import java.lang.annotation.Retention;
23+
import java.lang.annotation.RetentionPolicy;
24+
import java.lang.annotation.Target;
25+
26+
/**
27+
* Annotation to mark a repository method as a search template method. The annotation defines the search template id,
28+
* the parameters for the search template are taken from the method's arguments.
29+
*
30+
* @author P.J. Meisch ([email protected])
31+
* @since 5.5
32+
*/
33+
@Retention(RetentionPolicy.RUNTIME)
34+
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
35+
@Documented
36+
@QueryAnnotation
37+
public @interface SearchTemplateQuery {
38+
/**
39+
* The id of the search template. Must not be empt or null.
40+
*/
41+
String id();
42+
}

src/main/java/org/springframework/data/elasticsearch/core/query/SearchTemplateQueryBuilder.java

+18-6
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,22 @@
1515
*/
1616
package org.springframework.data.elasticsearch.core.query;
1717

18-
import org.springframework.lang.Nullable;
19-
2018
import java.util.Map;
2119

20+
import org.springframework.data.domain.Pageable;
21+
import org.springframework.data.domain.Sort;
22+
import org.springframework.lang.Nullable;
23+
2224
/**
2325
* @author Peter-Josef Meisch
2426
* @since 5.1
2527
*/
2628
public class SearchTemplateQueryBuilder extends BaseQueryBuilder<SearchTemplateQuery, SearchTemplateQueryBuilder> {
2729

28-
@Nullable
29-
private String id;
30+
@Nullable private String id;
3031
@Nullable String source;
3132

32-
@Nullable
33-
Map<String, Object> params;
33+
@Nullable Map<String, Object> params;
3434

3535
@Nullable
3636
public String getId() {
@@ -62,6 +62,18 @@ public SearchTemplateQueryBuilder withParams(@Nullable Map<String, Object> param
6262
return this;
6363
}
6464

65+
@Override
66+
public SearchTemplateQueryBuilder withSort(Sort sort) {
67+
throw new IllegalArgumentException(
68+
"sort is not supported in a searchtemplate query. Sort values must be defined in the stored template");
69+
}
70+
71+
@Override
72+
public SearchTemplateQueryBuilder withPageable(Pageable pageable) {
73+
throw new IllegalArgumentException(
74+
"paging is not supported in a searchtemplate query. from and size values must be defined in the stored template");
75+
}
76+
6577
@Override
6678
public SearchTemplateQuery build() {
6779
return new SearchTemplateQuery(this);

src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java

+11-5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.data.elasticsearch.core.query.BaseQuery;
2525
import org.springframework.data.elasticsearch.core.query.DeleteQuery;
2626
import org.springframework.data.elasticsearch.core.query.Query;
27+
import org.springframework.data.elasticsearch.core.query.SearchTemplateQuery;
2728
import org.springframework.data.repository.query.ParametersParameterAccessor;
2829
import org.springframework.data.repository.query.QueryMethod;
2930
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
@@ -114,11 +115,15 @@ public Object execute(Object[] parameters) {
114115
: PageRequest.of(0, DEFAULT_STREAM_BATCH_SIZE));
115116
result = StreamUtils.createStreamFromIterator(elasticsearchOperations.searchForStream(query, clazz, index));
116117
} else if (queryMethod.isCollectionQuery()) {
117-
if (parameterAccessor.getPageable().isUnpaged()) {
118-
int itemCount = (int) elasticsearchOperations.count(query, clazz, index);
119-
query.setPageable(PageRequest.of(0, Math.max(1, itemCount)));
118+
if (query instanceof SearchTemplateQuery) {
119+
// we cannot get a count here, from and size would be in the template
120120
} else {
121-
query.setPageable(parameterAccessor.getPageable());
121+
if (parameterAccessor.getPageable().isUnpaged()) {
122+
int itemCount = (int) elasticsearchOperations.count(query, clazz, index);
123+
query.setPageable(PageRequest.of(0, Math.max(1, itemCount)));
124+
} else {
125+
query.setPageable(parameterAccessor.getPageable());
126+
}
122127
}
123128
result = elasticsearchOperations.search(query, clazz, index);
124129
} else {
@@ -137,7 +142,8 @@ public Query createQuery(Object[] parameters) {
137142
var query = createQuery(parameterAccessor);
138143
Assert.notNull(query, "unsupported query");
139144

140-
queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter(),
145+
queryMethod.addSpecialMethodParameters(query, parameterAccessor,
146+
elasticsearchOperations.getElasticsearchConverter(),
141147
evaluationContextProvider);
142148

143149
return query;

src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ private Object execute(ElasticsearchParametersParameterAccessor parameterAccesso
105105
var query = createQuery(parameterAccessor);
106106
Assert.notNull(query, "unsupported query");
107107

108-
queryMethod.addMethodParameter(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter(),
108+
queryMethod.addSpecialMethodParameters(query, parameterAccessor, elasticsearchOperations.getElasticsearchConverter(),
109109
evaluationContextProvider);
110110

111111
String indexName = queryMethod.getEntityInformation().getIndexName();

src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java

+4-35
Original file line numberDiff line numberDiff line change
@@ -33,42 +33,11 @@
3333
* @author Rasmus Faber-Espensen
3434
* @author Peter-Josef Meisch
3535
* @author Haibo Liu
36+
* @deprecated since 5.5, use {@link RepositoryPartQuery} instead
3637
*/
37-
public class ElasticsearchPartQuery extends AbstractElasticsearchRepositoryQuery {
38-
39-
private final PartTree tree;
40-
private final MappingContext<?, ElasticsearchPersistentProperty> mappingContext;
41-
42-
public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations,
43-
QueryMethodEvaluationContextProvider evaluationContextProvider) {
38+
@Deprecated(forRemoval = true)
39+
public class ElasticsearchPartQuery extends RepositoryPartQuery {
40+
public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations, QueryMethodEvaluationContextProvider evaluationContextProvider) {
4441
super(method, elasticsearchOperations, evaluationContextProvider);
45-
this.tree = new PartTree(queryMethod.getName(), queryMethod.getResultProcessor().getReturnedType().getDomainType());
46-
this.mappingContext = elasticsearchConverter.getMappingContext();
47-
}
48-
49-
@Override
50-
public boolean isCountQuery() {
51-
return tree.isCountProjection();
52-
}
53-
54-
@Override
55-
protected boolean isDeleteQuery() {
56-
return tree.isDelete();
57-
}
58-
59-
@Override
60-
protected boolean isExistsQuery() {
61-
return tree.isExistsProjection();
62-
}
63-
64-
protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor) {
65-
66-
BaseQuery query = new ElasticsearchQueryCreator(tree, accessor, mappingContext).createQuery();
67-
68-
if (tree.getMaxResults() != null) {
69-
query.setMaxResults(tree.getMaxResults());
70-
}
71-
72-
return query;
7342
}
7443
}

0 commit comments

Comments
 (0)