Skip to content

Implement search by template. #2410

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 1 commit into from
Dec 30, 2022
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ target


/zap.env
.localdocker-env
114 changes: 109 additions & 5 deletions src/main/asciidoc/reference/elasticsearch-misc.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ The following arguments are available:
* `refreshIntervall`, defaults to _"1s"_
* `indexStoreType`, defaults to _"fs"_


It is as well possible to define https://www.elastic.co/guide/en/elasticsearch/reference/7.11/index-modules-index-sorting.html[index sorting] (check the linked Elasticsearch documentation for the possible field types and values):

====
Expand Down Expand Up @@ -133,9 +132,7 @@ stream.close();
----
====

There are no methods in the `SearchOperations` API to access the scroll id, if it should be necessary to access this,
the following methods of the `AbstractElasticsearchTemplate` can be used (this is the base implementation for the
different `ElasticsearchOperations` implementations):
There are no methods in the `SearchOperations` API to access the scroll id, if it should be necessary to access this, the following methods of the `AbstractElasticsearchTemplate` can be used (this is the base implementation for the different `ElasticsearchOperations` implementations):

====
[source,java]
Expand Down Expand Up @@ -281,7 +278,7 @@ This works with every implementation of the `Query` interface.
[[elasticsearch.misc.point-in-time]]
== Point In Time (PIT) API

`ElasticsearchOperations` supports the point in time API of Elasticsearch (see https://www.elastic.co/guide/en/elasticsearch/reference/8.3/point-in-time-api.html).
`ElasticsearchOperations` supports the point in time API of Elasticsearch (see https://www.elastic.co/guide/en/elasticsearch/reference/8.3/point-in-time-api.html).
The following code snippet shows how to use this feature with a fictional `Person` class:

====
Expand Down Expand Up @@ -310,8 +307,115 @@ SearchHits<Person> searchHits2 = operations.search(query2, Person.class);
operations.closePointInTime(searchHits2.getPointInTimeId()); <.>

----

<.> create a point in time for an index (can be multiple names) and a keep-alive duration and retrieve its id
<.> pass that id into the query to search together with the next keep-alive value
<.> for the next query, use the id returned from the previous search
<.> when done, close the point in time using the last returned id
====

[[elasticsearch.misc.searchtemplates]]
== Search Template support

Use of the search template API is supported.
To use this, it first is necessary to create a stored script.
The `ElasticsearchOperations` interface extends `ScriptOperations` which provides the necessary functions.
The example used here assumes that we have `Person` entity with a property named `firstName`.
A search template script can be saved like this:

====
[source,java]
----
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.script.Script;

operations.putScript( <.>
Script.builder()
.withId("person-firstname") <.>
.withLanguage("mustache") <.>
.withSource(""" <.>
{
"query": {
"bool": {
"must": [
{
"match": {
"firstName": "{{firstName}}" <.>
}
}
]
}
},
"from": "{{from}}", <.>
"size": "{{size}}" <.>
}
""")
.build()
);
----

<.> Use the `putScript()` method to store a search template script
<.> The name / id of the script
<.> Scripts that are used in search templates must be in the _mustache_ language.
<.> The script source
<.> The search parameter in the script
<.> Paging request offset
<.> Paging request size
====

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.

In the following code, we will add a call using a search template query to a custom repository implementation (see
<<repositories.custom-implementations>>) as
an example how this can be integrated into a repository call.

We first define the custom repository fragment interface:

====
[source,java]
----
interface PersonCustomRepository {
SearchPage<Person> findByFirstNameWithSearchTemplate(String firstName, Pageable pageable);
}
----
====

The implementation of this repository fragment looks like this:

====
[source,java]
----
public class PersonCustomRepositoryImpl implements PersonCustomRepository {

private final ElasticsearchOperations operations;

public PersonCustomRepositoryImpl(ElasticsearchOperations operations) {
this.operations = operations;
}

@Override
public SearchPage<Person> findByFirstNameWithSearchTemplate(String firstName, Pageable pageable) {

var query = SearchTemplateQuery.builder() <.>
.withId("person-firstname") <.>
.withParams(
Map.of( <.>
"firstName", firstName,
"from", pageable.getOffset(),
"size", pageable.getPageSize()
)
)
.build();

SearchHits<Person> searchHits = operations.search(query, Person.class); <.>

return SearchHitSupport.searchPageFor(searchHits, pageable);
}
}
----

<.> Create a `SearchTemplateQuery`
<.> Provide the id of the search template
<.> The parameters are passed in a `Map<String,Object>`
<.> Do the search in the same way as with the other query types.
====
6 changes: 6 additions & 0 deletions src/main/asciidoc/reference/elasticsearch-operations.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,9 @@ Query query = NativeQuery.builder()
SearchHits<Person> searchHits = operations.search(query, Person.class);
----
====

[[elasticsearch.operations.searchtemplateScOp§query]]
=== SearchTemplateQuery

This is a special implementation of the `Query` interface to be used in combination with a stored search template.
See <<elasticsearch.misc.searchtemplates>> for further information.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2022 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;

import org.springframework.dao.NonTransientDataAccessResourceException;

/**
* @author Peter-Josef Meisch
* @since 5.1
*/
public class ResourceNotFoundException extends NonTransientDataAccessResourceException {

public ResourceNotFoundException(String msg) {
super(msg);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.elasticsearch.NoSuchIndexException;
import org.springframework.data.elasticsearch.ResourceNotFoundException;
import org.springframework.data.elasticsearch.UncategorizedElasticsearchException;

/**
Expand Down Expand Up @@ -77,16 +78,20 @@ public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
var errorType = response.error().type();
var errorReason = response.error().reason() != null ? response.error().reason() : "undefined reason";

if (response.status() == 404 && "index_not_found_exception".equals(errorType)) {

// noinspection RegExpRedundantEscape
Pattern pattern = Pattern.compile(".*no such index \\[(.*)\\]");
String index = "";
Matcher matcher = pattern.matcher(errorReason);
if (matcher.matches()) {
index = matcher.group(1);
if (response.status() == 404) {

if ("index_not_found_exception".equals(errorType)) {
// noinspection RegExpRedundantEscape
Pattern pattern = Pattern.compile(".*no such index \\[(.*)\\]");
String index = "";
Matcher matcher = pattern.matcher(errorReason);
if (matcher.matches()) {
index = matcher.group(1);
}
return new NoSuchIndexException(index);
}
return new NoSuchIndexException(index);

return new ResourceNotFoundException(errorReason);
}
String body = JsonUtils.toJson(response, jsonpMapper);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,12 @@
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.MoreLikeThisQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.SearchTemplateQuery;
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
import org.springframework.data.elasticsearch.core.query.UpdateResponse;
import org.springframework.data.elasticsearch.core.reindex.ReindexRequest;
import org.springframework.data.elasticsearch.core.reindex.ReindexResponse;
import org.springframework.data.elasticsearch.core.script.Script;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

Expand Down Expand Up @@ -317,18 +319,40 @@ public long count(Query query, @Nullable Class<?> clazz, IndexCoordinates index)
public <T> SearchHits<T> search(Query query, Class<T> clazz, IndexCoordinates index) {

Assert.notNull(query, "query must not be null");
Assert.notNull(clazz, "clazz must not be null");
Assert.notNull(index, "index must not be null");

if (query instanceof SearchTemplateQuery searchTemplateQuery) {
return doSearch(searchTemplateQuery, clazz, index);
} else {
return doSearch(query, clazz, index);
}
}

protected <T> SearchHits<T> doSearch(Query query, Class<T> clazz, IndexCoordinates index) {
SearchRequest searchRequest = requestConverter.searchRequest(query, clazz, index, false);
SearchResponse<EntityAsMap> searchResponse = execute(client -> client.search(searchRequest, EntityAsMap.class));

// noinspection DuplicatedCode
ReadDocumentCallback<T> readDocumentCallback = new ReadDocumentCallback<>(elasticsearchConverter, clazz, index);
SearchDocumentResponse.EntityCreator<T> entityCreator = getEntityCreator(readDocumentCallback);
SearchDocumentResponseCallback<SearchHits<T>> callback = new ReadSearchDocumentResponseCallback<>(clazz, index);

return callback.doWith(SearchDocumentResponseBuilder.from(searchResponse, entityCreator, jsonpMapper));
}

protected <T> SearchHits<T> doSearch(SearchTemplateQuery query, Class<T> clazz, IndexCoordinates index) {
var searchTemplateRequest = requestConverter.searchTemplate(query, index);
var searchTemplateResponse = execute(client -> client.searchTemplate(searchTemplateRequest, EntityAsMap.class));

// noinspection DuplicatedCode
ReadDocumentCallback<T> readDocumentCallback = new ReadDocumentCallback<>(elasticsearchConverter, clazz, index);
SearchDocumentResponse.EntityCreator<T> entityCreator = getEntityCreator(readDocumentCallback);
SearchDocumentResponseCallback<SearchHits<T>> callback = new ReadSearchDocumentResponseCallback<>(clazz, index);

return callback.doWith(SearchDocumentResponseBuilder.from(searchTemplateResponse, entityCreator, jsonpMapper));
}

@Override
protected <T> SearchHits<T> doSearch(MoreLikeThisQuery query, Class<T> clazz, IndexCoordinates index) {

Expand Down Expand Up @@ -513,6 +537,35 @@ public Boolean closePointInTime(String pit) {

// endregion

// region script methods
@Override
public boolean putScript(Script script) {

Assert.notNull(script, "script must not be null");

var request = requestConverter.scriptPut(script);
return execute(client -> client.putScript(request)).acknowledged();
}

@Nullable
@Override
public Script getScript(String name) {

Assert.notNull(name, "name must not be null");

var request = requestConverter.scriptGet(name);
return responseConverter.scriptResponse(execute(client -> client.getScript(request)));
}

public boolean deleteScript(String name) {

Assert.notNull(name, "name must not be null");

DeleteScriptRequest request = requestConverter.scriptDelete(name);
return execute(client -> client.deleteScript(request)).acknowledged();
}
// endregion

// region client callback
/**
* Callback interface to be used with {@link #execute(ElasticsearchTemplate.ClientCallback)} for operating directly on
Expand Down
Loading