Skip to content

Commit efd3943

Browse files
authored
Implement search by template.
Original Pull Request #2410 Closes #1891
1 parent 4d7d095 commit efd3943

31 files changed

+1307
-53
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ target
2424

2525

2626
/zap.env
27+
.localdocker-env

src/main/asciidoc/reference/elasticsearch-misc.adoc

+109-5
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ The following arguments are available:
1717
* `refreshIntervall`, defaults to _"1s"_
1818
* `indexStoreType`, defaults to _"fs"_
1919

20-
2120
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):
2221

2322
====
@@ -133,9 +132,7 @@ stream.close();
133132
----
134133
====
135134

136-
There are no methods in the `SearchOperations` API to access the scroll id, if it should be necessary to access this,
137-
the following methods of the `AbstractElasticsearchTemplate` can be used (this is the base implementation for the
138-
different `ElasticsearchOperations` implementations):
135+
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):
139136

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

284-
`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).
281+
`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).
285282
The following code snippet shows how to use this feature with a fictional `Person` class:
286283

287284
====
@@ -310,8 +307,115 @@ SearchHits<Person> searchHits2 = operations.search(query2, Person.class);
310307
operations.closePointInTime(searchHits2.getPointInTimeId()); <.>
311308
312309
----
310+
313311
<.> create a point in time for an index (can be multiple names) and a keep-alive duration and retrieve its id
314312
<.> pass that id into the query to search together with the next keep-alive value
315313
<.> for the next query, use the id returned from the previous search
316314
<.> when done, close the point in time using the last returned id
317315
====
316+
317+
[[elasticsearch.misc.searchtemplates]]
318+
== Search Template support
319+
320+
Use of the search template API is supported.
321+
To use this, it first is necessary to create a stored script.
322+
The `ElasticsearchOperations` interface extends `ScriptOperations` which provides the necessary functions.
323+
The example used here assumes that we have `Person` entity with a property named `firstName`.
324+
A search template script can be saved like this:
325+
326+
====
327+
[source,java]
328+
----
329+
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
330+
import org.springframework.data.elasticsearch.core.script.Script;
331+
332+
operations.putScript( <.>
333+
Script.builder()
334+
.withId("person-firstname") <.>
335+
.withLanguage("mustache") <.>
336+
.withSource(""" <.>
337+
{
338+
"query": {
339+
"bool": {
340+
"must": [
341+
{
342+
"match": {
343+
"firstName": "{{firstName}}" <.>
344+
}
345+
}
346+
]
347+
}
348+
},
349+
"from": "{{from}}", <.>
350+
"size": "{{size}}" <.>
351+
}
352+
""")
353+
.build()
354+
);
355+
----
356+
357+
<.> Use the `putScript()` method to store a search template script
358+
<.> The name / id of the script
359+
<.> Scripts that are used in search templates must be in the _mustache_ language.
360+
<.> The script source
361+
<.> The search parameter in the script
362+
<.> Paging request offset
363+
<.> Paging request size
364+
====
365+
366+
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.
367+
368+
In the following code, we will add a call using a search template query to a custom repository implementation (see
369+
<<repositories.custom-implementations>>) as
370+
an example how this can be integrated into a repository call.
371+
372+
We first define the custom repository fragment interface:
373+
374+
====
375+
[source,java]
376+
----
377+
interface PersonCustomRepository {
378+
SearchPage<Person> findByFirstNameWithSearchTemplate(String firstName, Pageable pageable);
379+
}
380+
----
381+
====
382+
383+
The implementation of this repository fragment looks like this:
384+
385+
====
386+
[source,java]
387+
----
388+
public class PersonCustomRepositoryImpl implements PersonCustomRepository {
389+
390+
private final ElasticsearchOperations operations;
391+
392+
public PersonCustomRepositoryImpl(ElasticsearchOperations operations) {
393+
this.operations = operations;
394+
}
395+
396+
@Override
397+
public SearchPage<Person> findByFirstNameWithSearchTemplate(String firstName, Pageable pageable) {
398+
399+
var query = SearchTemplateQuery.builder() <.>
400+
.withId("person-firstname") <.>
401+
.withParams(
402+
Map.of( <.>
403+
"firstName", firstName,
404+
"from", pageable.getOffset(),
405+
"size", pageable.getPageSize()
406+
)
407+
)
408+
.build();
409+
410+
SearchHits<Person> searchHits = operations.search(query, Person.class); <.>
411+
412+
return SearchHitSupport.searchPageFor(searchHits, pageable);
413+
}
414+
}
415+
----
416+
417+
<.> Create a `SearchTemplateQuery`
418+
<.> Provide the id of the search template
419+
<.> The parameters are passed in a `Map<String,Object>`
420+
<.> Do the search in the same way as with the other query types.
421+
====

src/main/asciidoc/reference/elasticsearch-operations.adoc

+6
Original file line numberDiff line numberDiff line change
@@ -234,3 +234,9 @@ Query query = NativeQuery.builder()
234234
SearchHits<Person> searchHits = operations.search(query, Person.class);
235235
----
236236
====
237+
238+
[[elasticsearch.operations.searchtemplateScOp§query]]
239+
=== SearchTemplateQuery
240+
241+
This is a special implementation of the `Query` interface to be used in combination with a stored search template.
242+
See <<elasticsearch.misc.searchtemplates>> for further information.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2022 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;
17+
18+
import org.springframework.dao.NonTransientDataAccessResourceException;
19+
20+
/**
21+
* @author Peter-Josef Meisch
22+
* @since 5.1
23+
*/
24+
public class ResourceNotFoundException extends NonTransientDataAccessResourceException {
25+
26+
public ResourceNotFoundException(String msg) {
27+
super(msg);
28+
}
29+
}

src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchExceptionTranslator.java

+14-9
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.springframework.dao.OptimisticLockingFailureException;
3131
import org.springframework.dao.support.PersistenceExceptionTranslator;
3232
import org.springframework.data.elasticsearch.NoSuchIndexException;
33+
import org.springframework.data.elasticsearch.ResourceNotFoundException;
3334
import org.springframework.data.elasticsearch.UncategorizedElasticsearchException;
3435

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

80-
if (response.status() == 404 && "index_not_found_exception".equals(errorType)) {
81-
82-
// noinspection RegExpRedundantEscape
83-
Pattern pattern = Pattern.compile(".*no such index \\[(.*)\\]");
84-
String index = "";
85-
Matcher matcher = pattern.matcher(errorReason);
86-
if (matcher.matches()) {
87-
index = matcher.group(1);
81+
if (response.status() == 404) {
82+
83+
if ("index_not_found_exception".equals(errorType)) {
84+
// noinspection RegExpRedundantEscape
85+
Pattern pattern = Pattern.compile(".*no such index \\[(.*)\\]");
86+
String index = "";
87+
Matcher matcher = pattern.matcher(errorReason);
88+
if (matcher.matches()) {
89+
index = matcher.group(1);
90+
}
91+
return new NoSuchIndexException(index);
8892
}
89-
return new NoSuchIndexException(index);
93+
94+
return new ResourceNotFoundException(errorReason);
9095
}
9196
String body = JsonUtils.toJson(response, jsonpMapper);
9297

src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java

+53
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,12 @@
5555
import org.springframework.data.elasticsearch.core.query.IndexQuery;
5656
import org.springframework.data.elasticsearch.core.query.MoreLikeThisQuery;
5757
import org.springframework.data.elasticsearch.core.query.Query;
58+
import org.springframework.data.elasticsearch.core.query.SearchTemplateQuery;
5859
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
5960
import org.springframework.data.elasticsearch.core.query.UpdateResponse;
6061
import org.springframework.data.elasticsearch.core.reindex.ReindexRequest;
6162
import org.springframework.data.elasticsearch.core.reindex.ReindexResponse;
63+
import org.springframework.data.elasticsearch.core.script.Script;
6264
import org.springframework.lang.Nullable;
6365
import org.springframework.util.Assert;
6466

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

319321
Assert.notNull(query, "query must not be null");
322+
Assert.notNull(clazz, "clazz must not be null");
320323
Assert.notNull(index, "index must not be null");
321324

325+
if (query instanceof SearchTemplateQuery searchTemplateQuery) {
326+
return doSearch(searchTemplateQuery, clazz, index);
327+
} else {
328+
return doSearch(query, clazz, index);
329+
}
330+
}
331+
332+
protected <T> SearchHits<T> doSearch(Query query, Class<T> clazz, IndexCoordinates index) {
322333
SearchRequest searchRequest = requestConverter.searchRequest(query, clazz, index, false);
323334
SearchResponse<EntityAsMap> searchResponse = execute(client -> client.search(searchRequest, EntityAsMap.class));
324335

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

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

344+
protected <T> SearchHits<T> doSearch(SearchTemplateQuery query, Class<T> clazz, IndexCoordinates index) {
345+
var searchTemplateRequest = requestConverter.searchTemplate(query, index);
346+
var searchTemplateResponse = execute(client -> client.searchTemplate(searchTemplateRequest, EntityAsMap.class));
347+
348+
// noinspection DuplicatedCode
349+
ReadDocumentCallback<T> readDocumentCallback = new ReadDocumentCallback<>(elasticsearchConverter, clazz, index);
350+
SearchDocumentResponse.EntityCreator<T> entityCreator = getEntityCreator(readDocumentCallback);
351+
SearchDocumentResponseCallback<SearchHits<T>> callback = new ReadSearchDocumentResponseCallback<>(clazz, index);
352+
353+
return callback.doWith(SearchDocumentResponseBuilder.from(searchTemplateResponse, entityCreator, jsonpMapper));
354+
}
355+
332356
@Override
333357
protected <T> SearchHits<T> doSearch(MoreLikeThisQuery query, Class<T> clazz, IndexCoordinates index) {
334358

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

514538
// endregion
515539

540+
// region script methods
541+
@Override
542+
public boolean putScript(Script script) {
543+
544+
Assert.notNull(script, "script must not be null");
545+
546+
var request = requestConverter.scriptPut(script);
547+
return execute(client -> client.putScript(request)).acknowledged();
548+
}
549+
550+
@Nullable
551+
@Override
552+
public Script getScript(String name) {
553+
554+
Assert.notNull(name, "name must not be null");
555+
556+
var request = requestConverter.scriptGet(name);
557+
return responseConverter.scriptResponse(execute(client -> client.getScript(request)));
558+
}
559+
560+
public boolean deleteScript(String name) {
561+
562+
Assert.notNull(name, "name must not be null");
563+
564+
DeleteScriptRequest request = requestConverter.scriptDelete(name);
565+
return execute(client -> client.deleteScript(request)).acknowledged();
566+
}
567+
// endregion
568+
516569
// region client callback
517570
/**
518571
* Callback interface to be used with {@link #execute(ElasticsearchTemplate.ClientCallback)} for operating directly on

0 commit comments

Comments
 (0)