Skip to content

Add repository method support for search templates. #3049

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/main/antora/modules/ROOT/pages/elasticsearch/misc.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -312,11 +314,13 @@ Repository methods can be defined to have the following return types for returni
* `SearchPage<T>`

[[elasticsearch.query-methods.at-query]]
== Using @Query Annotation
== Using the @Query Annotation

.Declare query on the method using the `@Query` annotation.
====
The arguments passed to the method can be inserted into placeholders in the query string. The placeholders are of the form `?0`, `?1`, `?2` etc. for the first, second, third parameter and so on.
The arguments passed to the method can be inserted into placeholders in the query string.
The placeholders are of the form `?0`, `?1`, `?2` etc. for the first, second, third parameter and so on.

[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
Expand All @@ -341,15 +345,20 @@ It will be sent to Easticsearch as value of the query element; if for example th
}
----
====

.`@Query` annotation on a method taking a Collection argument
====
A repository method such as

[source,java]
----
@Query("{\"ids\": {\"values\": ?0 }}")
List<SampleEntity> getByIds(Collection<String> ids);
----
would make an https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ids-query.html[IDs query] to return all the matching documents. So calling the method with a `List` of `["id1", "id2", "id3"]` would produce the query body

would make an https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ids-query.html[IDs query] to return all the matching documents.
So calling the method with a `List` of `["id1", "id2", "id3"]` would produce the query body

[source,json]
----
{
Expand All @@ -369,7 +378,6 @@ would make an https://www.elastic.co/guide/en/elasticsearch/reference/current/qu
====
https://docs.spring.io/spring-framework/reference/core/expressions.html[SpEL expression] is also supported when defining query in `@Query`.


[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -444,7 +453,9 @@ We can pass `new QueryParameter("John")` as the parameter now, and it will produ

.accessing bean property.
====
https://docs.spring.io/spring-framework/reference/core/expressions/language-ref/bean-references.html[Bean property] is also supported to access. Given that there is a bean named `queryParameter` of type `QueryParameter`, we can access the bean with symbol `@` rather than `#`, and there is no need to declare a parameter of type `QueryParameter` in the query method:
https://docs.spring.io/spring-framework/reference/core/expressions/language-ref/bean-references.html[Bean property] is also supported to access.
Given that there is a bean named `queryParameter` of type `QueryParameter`, we can access the bean with symbol `@` rather than `#`, and there is no need to declare a parameter of type `QueryParameter` in the query method:

[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
Expand Down Expand Up @@ -493,6 +504,7 @@ interface BookRepository extends ElasticsearchRepository<Book, String> {
NOTE: collection values should not be quoted when declaring the elasticsearch json query.

A collection of `names` like `List.of("name1", "name2")` will produce the following terms query:

[source,json]
----
{
Expand Down Expand Up @@ -532,6 +544,7 @@ interface BookRepository extends ElasticsearchRepository<Book, String> {
Page<Book> findByName(Collection<QueryParameter> parameters, Pageable pageable);
}
----

This will extract all the `value` property values as a new `Collection` from `QueryParameter` collection, thus takes the same effect as above.
====

Expand Down Expand Up @@ -560,3 +573,20 @@ interface BookRepository extends ElasticsearchRepository<Book, String> {
----

====

[[elasticsearch.query-methods.at-searchtemplate-query]]
== Using the @SearchTemplateQuery Annotation

When using Elasticsearch search templates - (see xref:elasticsearch/misc.adoc#elasticsearch.misc.searchtemplates [Search Template support]) it is possible to specify that a repository method should use a template by adding the `@SearchTemplateQuery` annotation to that method.

Let's assume that there is a search template stored with the name "book-by-title" and this template need a parameter named "title", then a repository method using that search template can be defined like this:

[source,java]
----
interface BookRepository extends ElasticsearchRepository<Book, String> {
@SearchTemplateQuery(id = "book-by-title")
SearchHits<Book> findByTitle(String title);
}
----

The parameters of the repository method are sent to the seacrh template as key/value pairs where the key is the parameter name and the value is taken from the actual value when the method is invoked.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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 ([email protected])
* @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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,22 @@
*/
package org.springframework.data.elasticsearch.core.query;

import org.springframework.lang.Nullable;

import java.util.Map;

import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;

/**
* @author Peter-Josef Meisch
* @since 5.1
*/
public class SearchTemplateQueryBuilder extends BaseQueryBuilder<SearchTemplateQuery, SearchTemplateQueryBuilder> {

@Nullable
private String id;
@Nullable private String id;
@Nullable String source;

@Nullable
Map<String, Object> params;
@Nullable Map<String, Object> params;

@Nullable
public String getId() {
Expand Down Expand Up @@ -62,6 +62,18 @@ public SearchTemplateQueryBuilder withParams(@Nullable Map<String, Object> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<?, ElasticsearchPersistentProperty> mappingContext;

public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
@Deprecated(forRemoval = true)
public class ElasticsearchPartQuery extends RepositoryPartQuery {
public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations, QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, elasticsearchOperations, evaluationContextProvider);
this.tree = new PartTree(queryMethod.getName(), queryMethod.getResultProcessor().getReturnedType().getDomainType());
this.mappingContext = elasticsearchConverter.getMappingContext();
}

@Override
public boolean isCountQuery() {
return tree.isCountProjection();
}

@Override
protected boolean isDeleteQuery() {
return tree.isDelete();
}

@Override
protected boolean isExistsQuery() {
return tree.isExistsProjection();
}

protected BaseQuery createQuery(ElasticsearchParametersParameterAccessor accessor) {

BaseQuery query = new ElasticsearchQueryCreator(tree, accessor, mappingContext).createQuery();

if (tree.getMaxResults() != null) {
query.setMaxResults(tree.getMaxResults());
}

return query;
}
}
Loading