Implement search by template.

Original Pull Request #2410
Closes #1891
This commit is contained in:
Peter-Josef Meisch 2022-12-30 19:11:17 +01:00 committed by GitHub
parent 4d7d0955f9
commit efd394370a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1306 additions and 52 deletions

1
.gitignore vendored
View File

@ -24,3 +24,4 @@ target
/zap.env
.localdocker-env

View File

@ -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):
====
@ -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]
@ -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:
====
@ -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.
====

View File

@ -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.

View File

@ -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);
}
}

View File

@ -30,6 +30,7 @@ import org.springframework.dao.DataIntegrityViolationException;
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;
/**
@ -77,16 +78,20 @@ public class ElasticsearchExceptionTranslator implements PersistenceExceptionTra
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)) {
if (response.status() == 404) {
// noinspection RegExpRedundantEscape
Pattern pattern = Pattern.compile(".*no such index \\[(.*)\\]");
String index = "";
Matcher matcher = pattern.matcher(errorReason);
if (matcher.matches()) {
index = matcher.group(1);
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);

View File

@ -55,10 +55,12 @@ import org.springframework.data.elasticsearch.core.query.ByQueryResponse;
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;
@ -317,11 +319,21 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
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);
@ -329,6 +341,18 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
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) {
@ -513,6 +537,35 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
// 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

View File

@ -237,6 +237,29 @@ public class ReactiveElasticsearchClient extends ApiClient<ElasticsearchTranspor
return search(fn.apply(new SearchRequest.Builder()).build(), tDocumentClass);
}
/**
* @since 5.1
*/
public <T> Mono<SearchTemplateResponse<T>> searchTemplate(SearchTemplateRequest request, Class<T> tDocumentClass) {
Assert.notNull(request, "request must not be null");
Assert.notNull(tDocumentClass, "tDocumentClass must not be null");
return Mono.fromFuture(transport.performRequestAsync(request,
SearchTemplateRequest.createSearchTemplateEndpoint(this.getDeserializer(tDocumentClass)), transportOptions));
}
/**
* @since 5.1
*/
public <T> Mono<SearchTemplateResponse<T>> searchTemplate(
Function<SearchTemplateRequest.Builder, ObjectBuilder<SearchTemplateRequest>> fn, Class<T> tDocumentClass) {
Assert.notNull(fn, "fn must not be null");
return searchTemplate(fn.apply(new SearchTemplateRequest.Builder()).build(), tDocumentClass);
}
public <T> Mono<ScrollResponse<T>> scroll(ScrollRequest request, Class<T> tDocumentClass) {
Assert.notNull(request, "request must not be null");
@ -320,4 +343,67 @@ public class ReactiveElasticsearchClient extends ApiClient<ElasticsearchTranspor
}
// endregion
// region script api
/**
* @since 5.1
*/
public Mono<PutScriptResponse> putScript(PutScriptRequest request) {
Assert.notNull(request, "request must not be null");
return Mono.fromFuture(transport.performRequestAsync(request, PutScriptRequest._ENDPOINT, transportOptions));
}
/**
* @since 5.1
*/
public Mono<PutScriptResponse> putScript(Function<PutScriptRequest.Builder, ObjectBuilder<PutScriptRequest>> fn) {
Assert.notNull(fn, "fn must not be null");
return putScript(fn.apply(new PutScriptRequest.Builder()).build());
}
/**
* @since 5.1
*/
public Mono<GetScriptResponse> getScript(GetScriptRequest request) {
Assert.notNull(request, "request must not be null");
return Mono.fromFuture(transport.performRequestAsync(request, GetScriptRequest._ENDPOINT, transportOptions));
}
/**
* @since 5.1
*/
public Mono<GetScriptResponse> getScript(Function<GetScriptRequest.Builder, ObjectBuilder<GetScriptRequest>> fn) {
Assert.notNull(fn, "fn must not be null");
return getScript(fn.apply(new GetScriptRequest.Builder()).build());
}
/**
* @since 5.1
*/
public Mono<DeleteScriptResponse> deleteScript(DeleteScriptRequest request) {
Assert.notNull(request, "request must not be null");
return Mono.fromFuture(transport.performRequestAsync(request, DeleteScriptRequest._ENDPOINT, transportOptions));
}
/**
* @since 5.1
*/
public Mono<DeleteScriptResponse> deleteScript(
Function<DeleteScriptRequest.Builder, ObjectBuilder<DeleteScriptRequest>> fn) {
Assert.notNull(fn, "fn must not be null");
return deleteScript(fn.apply(new DeleteScriptRequest.Builder()).build());
}
// endregion
}

View File

@ -62,10 +62,12 @@ import org.springframework.data.elasticsearch.core.query.BaseQuery;
import org.springframework.data.elasticsearch.core.query.BulkOptions;
import org.springframework.data.elasticsearch.core.query.ByQueryResponse;
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;
import org.springframework.util.CollectionUtils;
@ -341,12 +343,18 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
@Override
protected Flux<SearchDocument> doFind(Query query, Class<?> clazz, IndexCoordinates index) {
return Flux.defer(() -> {
boolean queryIsUnbounded = !(query.getPageable().isPaged() || query.isLimiting());
return queryIsUnbounded ? doFindUnbounded(query, clazz, index) : doFindBounded(query, clazz, 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 Flux.defer(() -> doSearch(searchTemplateQuery, clazz, index));
} else {
return Flux.defer(() -> {
boolean queryIsUnbounded = !(query.getPageable().isPaged() || query.isLimiting());
return queryIsUnbounded ? doFindUnbounded(query, clazz, index) : doFindBounded(query, clazz, index);
});
}
}
private Flux<SearchDocument> doFindUnbounded(Query query, Class<?> clazz, IndexCoordinates index) {
@ -465,6 +473,17 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
.map(entityAsMapHit -> DocumentAdapters.from(entityAsMapHit, jsonpMapper));
}
private Flux<SearchDocument> doSearch(SearchTemplateQuery query, Class<?> clazz, IndexCoordinates index) {
var request = requestConverter.searchTemplate(query, index);
return Mono
.from(execute((ClientCallback<Publisher<SearchTemplateResponse<EntityAsMap>>>) client -> client
.searchTemplate(request, EntityAsMap.class))) //
.flatMapIterable(entityAsMapSearchResponse -> entityAsMapSearchResponse.hits().hits()) //
.map(entityAsMapHit -> DocumentAdapters.from(entityAsMapHit, jsonpMapper));
}
@Override
protected <T> Mono<SearchDocumentResponse> doFindForResponse(Query query, Class<?> clazz, IndexCoordinates index) {
@ -519,6 +538,37 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
// endregion
// region script operations
@Override
public Mono<Boolean> putScript(Script script) {
Assert.notNull(script, "script must not be null");
var request = requestConverter.scriptPut(script);
return Mono.from(execute((ClientCallback<Publisher<PutScriptResponse>>) client -> client.putScript(request)))
.map(PutScriptResponse::acknowledged);
}
@Override
public Mono<Script> getScript(String name) {
Assert.notNull(name, "name must not be null");
var request = requestConverter.scriptGet(name);
return Mono.from(execute((ClientCallback<Publisher<GetScriptResponse>>) client -> client.getScript(request)))
.mapNotNull(responseConverter::scriptResponse);
}
@Override
public Mono<Boolean> deleteScript(String name) {
Assert.notNull(name, "name must not be null");
var request = requestConverter.scriptDelete(name);
return Mono.from(execute((ClientCallback<Publisher<DeleteScriptResponse>>) client -> client.deleteScript(request)))
.map(DeleteScriptResponse::acknowledged);
}
// endregion
@Override
public Mono<String> getVendor() {
return Mono.just("Elasticsearch");

View File

@ -85,9 +85,11 @@ import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.data.elasticsearch.core.reindex.ReindexRequest;
import org.springframework.data.elasticsearch.core.reindex.Remote;
import org.springframework.data.elasticsearch.core.script.Script;
import org.springframework.data.elasticsearch.support.DefaultStringObjectMap;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
@ -99,6 +101,7 @@ import org.springframework.util.StringUtils;
* @author scoobyzhang
* @since 4.4
*/
@SuppressWarnings("ClassCanBeRecord")
class RequestConverter {
// the default max result window size of Elasticsearch
@ -1163,13 +1166,26 @@ class RequestConverter {
builder //
.version(true) //
.trackScores(query.getTrackScores());
.trackScores(query.getTrackScores()) //
.allowNoIndices(query.getAllowNoIndices()) //
.source(getSourceConfig(query)) //
.searchType(searchType(query.getSearchType())) //
.timeout(timeStringMs(query.getTimeout())) //
.requestCache(query.getRequestCache()) //
;
var pointInTime = query.getPointInTime();
if (pointInTime != null) {
builder.pit(pb -> pb.id(pointInTime.id()).keepAlive(time(pointInTime.keepAlive())));
} else {
builder.index(Arrays.asList(indexNames));
builder //
.index(Arrays.asList(indexNames)) //
;
var expandWildcards = query.getExpandWildcards();
if (expandWildcards != null && !expandWildcards.isEmpty()) {
builder.expandWildcards(expandWildcards(expandWildcards));
}
if (query.getRoute() != null) {
builder.routing(query.getRoute());
@ -1192,8 +1208,6 @@ class RequestConverter {
builder.from(0).size(INDEX_MAX_RESULT_WINDOW);
}
builder.source(getSourceConfig(query));
if (!isEmpty(query.getFields())) {
builder.fields(fb -> {
query.getFields().forEach(fb::field);
@ -1217,8 +1231,6 @@ class RequestConverter {
builder.minScore((double) query.getMinScore());
}
builder.searchType(searchType(query.getSearchType()));
if (query.getSort() != null) {
List<SortOptions> sortOptions = getSortOptions(query.getSort(), persistentEntity);
@ -1241,8 +1253,6 @@ class RequestConverter {
builder.trackTotalHits(th -> th.count(query.getTrackTotalHitsUpTo()));
}
builder.timeout(timeStringMs(query.getTimeout()));
if (query.getExplain()) {
builder.explain(true);
}
@ -1254,8 +1264,6 @@ class RequestConverter {
query.getRescorerQueries().forEach(rescorerQuery -> builder.rescore(getRescore(rescorerQuery)));
builder.requestCache(query.getRequestCache());
if (!query.getRuntimeFields().isEmpty()) {
Map<String, RuntimeField> runtimeMappings = new HashMap<>();
@ -1540,8 +1548,69 @@ class RequestConverter {
return ClosePointInTimeRequest.of(cpit -> cpit.id(pit));
}
public SearchTemplateRequest searchTemplate(SearchTemplateQuery query, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
return SearchTemplateRequest.of(builder -> {
builder //
.allowNoIndices(query.getAllowNoIndices()) //
.explain(query.getExplain()) //
.id(query.getId()) //
.index(Arrays.asList(index.getIndexNames())) //
.preference(query.getPreference()) //
.routing(query.getRoute()) //
.searchType(searchType(query.getSearchType()))
.source(query.getSource()) //
;
var expandWildcards = query.getExpandWildcards();
if (!expandWildcards.isEmpty()) {
builder.expandWildcards(expandWildcards(expandWildcards));
}
if (query.hasScrollTime()) {
builder.scroll(time(query.getScrollTime()));
}
if (!CollectionUtils.isEmpty(query.getParams())) {
Function<Map.Entry<String, Object>, String> keyMapper = Map.Entry::getKey;
Function<Map.Entry<String, Object>, JsonData> valueMapper = entry -> JsonData.of(entry.getValue(), jsonpMapper);
Map<String, JsonData> params = query.getParams().entrySet().stream()
.collect(Collectors.toMap(keyMapper, valueMapper));
builder.params(params);
}
return builder;
});
}
// endregion
public PutScriptRequest scriptPut(Script script) {
Assert.notNull(script, "script must not be null");
return PutScriptRequest.of(b -> b //
.id(script.id()) //
.script(sb -> sb //
.lang(script.language()) //
.source(script.source())));
}
public GetScriptRequest scriptGet(String name) {
Assert.notNull(name, "name must not be null");
return GetScriptRequest.of(b -> b.id(name));
}
public DeleteScriptRequest scriptDelete(String name) {
Assert.notNull(name, "name must not be null");
return DeleteScriptRequest.of(b -> b.id(name));
}
// region helper functions
public <T> T fromJson(String json, JsonpDeserializer<T> deserializer) {

View File

@ -15,7 +15,7 @@
*/
package org.springframework.data.elasticsearch.client.elc;
import static org.springframework.data.elasticsearch.client.elc.JsonUtils.toJson;
import static org.springframework.data.elasticsearch.client.elc.JsonUtils.*;
import co.elastic.clients.elasticsearch._types.BulkIndexByScrollFailure;
import co.elastic.clients.elasticsearch._types.ErrorCause;
@ -23,6 +23,7 @@ import co.elastic.clients.elasticsearch._types.Time;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch.cluster.HealthResponse;
import co.elastic.clients.elasticsearch.core.DeleteByQueryResponse;
import co.elastic.clients.elasticsearch.core.GetScriptResponse;
import co.elastic.clients.elasticsearch.core.UpdateByQueryResponse;
import co.elastic.clients.elasticsearch.core.mget.MultiGetError;
import co.elastic.clients.elasticsearch.core.mget.MultiGetResponseItem;
@ -62,6 +63,7 @@ import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.ByQueryResponse;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.core.reindex.ReindexResponse;
import org.springframework.data.elasticsearch.core.script.Script;
import org.springframework.data.elasticsearch.support.DefaultStringObjectMap;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@ -421,6 +423,22 @@ class ResponseConverter {
}
// endregion
// region script API
@Nullable
public Script scriptResponse(GetScriptResponse response) {
Assert.notNull(response, "response must not be null");
return response.found() //
? Script.builder() //
.withId(response.id()) //
.withLanguage(response.script().lang()) //
.withSource(response.script().source()).build() //
: null;
}
// endregion
// region helper functions
private long timeToLong(Time time) {

View File

@ -17,6 +17,7 @@ package org.springframework.data.elasticsearch.client.elc;
import co.elastic.clients.elasticsearch._types.aggregations.Aggregate;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.SearchTemplateResponse;
import co.elastic.clients.elasticsearch.core.search.CompletionSuggest;
import co.elastic.clients.elasticsearch.core.search.CompletionSuggestOption;
import co.elastic.clients.elasticsearch.core.search.Hit;
@ -70,6 +71,7 @@ class SearchDocumentResponseBuilder {
Assert.notNull(responseBody, "responseBody must not be null");
Assert.notNull(entityCreator, "entityCreator must not be null");
Assert.notNull(jsonpMapper, "jsonpMapper must not be null");
HitsMetadata<EntityAsMap> hitsMetadata = responseBody.hits();
String scrollId = responseBody.scrollId();
@ -80,6 +82,31 @@ class SearchDocumentResponseBuilder {
return from(hitsMetadata, scrollId, pointInTimeId, aggregations, suggest, entityCreator, jsonpMapper);
}
/**
* creates a SearchDocumentResponse from the {@link SearchTemplateResponse}
*
* @param response the Elasticsearch response body
* @param entityCreator function to create an entity from a {@link SearchDocument}
* @param jsonpMapper to map JsonData objects
* @return the SearchDocumentResponse
* @since 5.1
*/
public static <T> SearchDocumentResponse from(SearchTemplateResponse<EntityAsMap> response,
SearchDocumentResponse.EntityCreator<T> entityCreator, JsonpMapper jsonpMapper) {
Assert.notNull(response, "response must not be null");
Assert.notNull(entityCreator, "entityCreator must not be null");
Assert.notNull(jsonpMapper, "jsonpMapper must not be null");
var hitsMetadata = response.hits();
var scrollId = response.scrollId();
var aggregations = response.aggregations();
var suggest = response.suggest();
var pointInTimeId = response.pitId();
return from(hitsMetadata, scrollId, pointInTimeId, aggregations, suggest, entityCreator, jsonpMapper);
}
/**
* creates a {@link SearchDocumentResponseBuilder} from {@link HitsMetadata} with the given scrollId aggregations and
* suggestES

View File

@ -26,10 +26,14 @@ import co.elastic.clients.elasticsearch.core.search.HighlighterType;
import co.elastic.clients.elasticsearch.core.search.ScoreMode;
import java.time.Duration;
import java.util.EnumSet;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.data.elasticsearch.core.RefreshPolicy;
import org.springframework.data.elasticsearch.core.query.GeoDistanceOrder;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.IndicesOptions;
import org.springframework.data.elasticsearch.core.query.Order;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.RescorerQuery;
@ -125,6 +129,7 @@ final class TypeUtils {
default -> throw new IllegalStateException("Unexpected value: " + fieldValue._kind());
}
}
@Nullable
static Object toObject(@Nullable FieldValue fieldValue) {
@ -391,4 +396,14 @@ final class TypeUtils {
static Float toFloat(@Nullable Long value) {
return value != null ? Float.valueOf(value) : null;
}
/**
* @sice 5.1
*/
@Nullable
public static List<ExpandWildcard> expandWildcards(@Nullable EnumSet<IndicesOptions.WildcardStates> wildcardStates) {
return (wildcardStates != null && !wildcardStates.isEmpty()) ? wildcardStates.stream()
.map(wildcardState -> ExpandWildcard.valueOf(wildcardState.name().toLowerCase())).collect(Collectors.toList())
: null;
}
}

View File

@ -228,11 +228,6 @@ public class NativeSearchQueryBuilder extends BaseQueryBuilder<NativeSearchQuery
return this;
}
public NativeSearchQueryBuilder withIndicesOptions(IndicesOptions indicesOptions) {
this.indicesOptions = indicesOptions;
return this;
}
/**
* @since 4.2
*/
@ -306,8 +301,8 @@ public class NativeSearchQueryBuilder extends BaseQueryBuilder<NativeSearchQuery
nativeSearchQuery.setSearchType(Query.SearchType.valueOf(searchType.name()));
}
if (indicesOptions != null) {
nativeSearchQuery.setIndicesOptions(indicesOptions);
if (getIndicesOptions() != null) {
nativeSearchQuery.setIndicesOptions(getIndicesOptions());
}
nativeSearchQuery.setTrackTotalHits(trackTotalHits);

View File

@ -52,6 +52,7 @@ import org.springframework.data.elasticsearch.core.query.UpdateQuery;
import org.springframework.data.elasticsearch.core.query.UpdateResponse;
import org.springframework.data.elasticsearch.core.routing.DefaultRoutingResolver;
import org.springframework.data.elasticsearch.core.routing.RoutingResolver;
import org.springframework.data.elasticsearch.core.script.Script;
import org.springframework.data.elasticsearch.support.VersionInfo;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.callback.EntityCallbacks;
@ -736,6 +737,29 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
}
}
// region script operations
@Override
public boolean putScript(Script script) {
throw new UnsupportedOperationException(
"putScript() operation not implemented by " + getClass().getCanonicalName());
}
@Nullable
@Override
public Script getScript(String name) {
throw new UnsupportedOperationException(
"getScript() operation not implemented by " + getClass().getCanonicalName());
}
@Override
public boolean deleteScript(String name) {
throw new UnsupportedOperationException(
"deleteScript() operation not implemented by " + getClass().getCanonicalName());
}
// endregion
// region Document callbacks
protected interface DocumentCallback<T> {
@Nullable

View File

@ -50,6 +50,7 @@ import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.data.elasticsearch.core.routing.DefaultRoutingResolver;
import org.springframework.data.elasticsearch.core.routing.RoutingResolver;
import org.springframework.data.elasticsearch.core.script.Script;
import org.springframework.data.elasticsearch.core.suggest.response.Suggest;
import org.springframework.data.elasticsearch.support.VersionInfo;
import org.springframework.data.mapping.PersistentPropertyAccessor;
@ -584,12 +585,14 @@ abstract public class AbstractReactiveElasticsearchTemplate
/**
* Callback to convert a {@link SearchDocument} into different other classes
*
* @param <T> the entity type
*/
protected interface SearchDocumentCallback<T> {
/**
* converts a {@link SearchDocument} to an entity
*
* @param searchDocument
* @return the entity in a MOno
*/
@ -597,6 +600,7 @@ abstract public class AbstractReactiveElasticsearchTemplate
/**
* converts a {@link SearchDocument} into a SearchHit
*
* @param searchDocument
* @return
*/
@ -627,6 +631,26 @@ abstract public class AbstractReactiveElasticsearchTemplate
// endregion
// region script operations
@Override
public Mono<Boolean> putScript(Script script) {
throw new UnsupportedOperationException(
"putScript() operation not implemented by " + getClass().getCanonicalName());
}
@Override
public Mono<Script> getScript(String name) {
throw new UnsupportedOperationException(
"getScript() operation not implemented by " + getClass().getCanonicalName());
}
@Override
public Mono<Boolean> deleteScript(String name) {
throw new UnsupportedOperationException(
"deleteScript() operation not implemented by " + getClass().getCanonicalName());
}
// endregion
// region Helper methods
@Override
public IndexCoordinates getIndexCoordinatesFor(Class<?> clazz) {

View File

@ -21,6 +21,7 @@ import org.springframework.data.elasticsearch.core.cluster.ClusterOperations;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.routing.RoutingResolver;
import org.springframework.data.elasticsearch.core.script.ScriptOperations;
import org.springframework.lang.Nullable;
/**
@ -36,7 +37,7 @@ import org.springframework.lang.Nullable;
* @author Dmitriy Yakovlev
* @author Peter-Josef Meisch
*/
public interface ElasticsearchOperations extends DocumentOperations, SearchOperations {
public interface ElasticsearchOperations extends DocumentOperations, SearchOperations, ScriptOperations {
/**
* get an {@link IndexOperations} that is bound to the given class

View File

@ -23,6 +23,7 @@ import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverte
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.routing.RoutingResolver;
import org.springframework.data.elasticsearch.core.script.ReactiveScriptOperations;
import org.springframework.lang.Nullable;
/**
@ -37,7 +38,8 @@ import org.springframework.lang.Nullable;
* @author Peter-Josef Meisch
* @since 3.2
*/
public interface ReactiveElasticsearchOperations extends ReactiveDocumentOperations, ReactiveSearchOperations {
public interface ReactiveElasticsearchOperations
extends ReactiveDocumentOperations, ReactiveSearchOperations, ReactiveScriptOperations {
/**
* Execute within a {@link ClientCallback} managing resources and translating errors.

View File

@ -22,6 +22,7 @@ import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@ -75,9 +76,11 @@ public class BaseQuery implements Query {
protected List<IdWithRouting> idsWithRouting = Collections.emptyList();
protected final List<RuntimeField> runtimeFields = new ArrayList<>();
@Nullable protected PointInTime pointInTime;
private boolean queryIsUpdatedByConverter = false;
@Nullable private Integer reactiveBatchSize = null;
@Nullable private Boolean allowNoIndices = null;
private EnumSet<IndicesOptions.WildcardStates> expandWildcards;
public BaseQuery() {}
@ -109,6 +112,8 @@ public class BaseQuery implements Query {
this.idsWithRouting = builder.getIdsWithRouting();
this.pointInTime = builder.getPointInTime();
this.reactiveBatchSize = builder.getReactiveBatchSize();
this.allowNoIndices = builder.getAllowNoIndices();
this.expandWildcards = builder.getExpandWildcards();
}
/**
@ -509,4 +514,14 @@ public class BaseQuery implements Query {
public void setReactiveBatchSize(Integer reactiveBatchSize) {
this.reactiveBatchSize = reactiveBatchSize;
}
@Nullable
public Boolean getAllowNoIndices() {
return allowNoIndices;
}
@Override
public EnumSet<IndicesOptions.WildcardStates> getExpandWildcards() {
return expandWildcards;
}
}

View File

@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import org.springframework.data.domain.Pageable;
@ -45,26 +46,28 @@ public abstract class BaseQueryBuilder<Q extends BaseQuery, SELF extends BaseQue
private float minScore;
private final Collection<String> ids = new ArrayList<>();
@Nullable private String route;
protected Query.SearchType searchType = Query.SearchType.QUERY_THEN_FETCH;
@Nullable protected IndicesOptions indicesOptions;
private Query.SearchType searchType = Query.SearchType.QUERY_THEN_FETCH;
@Nullable private IndicesOptions indicesOptions;
private boolean trackScores;
@Nullable private String preference;
@Nullable private Integer maxResults;
@Nullable protected HighlightQuery highlightQuery;
@Nullable private HighlightQuery highlightQuery;
@Nullable private Boolean trackTotalHits;
@Nullable protected Integer trackTotalHitsUpTo;
@Nullable protected Duration scrollTime;
@Nullable protected Duration timeout;
@Nullable private Integer trackTotalHitsUpTo;
@Nullable private Duration scrollTime;
@Nullable private Duration timeout;
boolean explain = false;
@Nullable protected List<Object> searchAfter;
@Nullable private List<Object> searchAfter;
@Nullable private List<IndexBoost> indicesBoost;
protected final List<RescorerQuery> rescorerQueries = new ArrayList<>();
@Nullable protected Boolean requestCache;
protected final List<Query.IdWithRouting> idsWithRouting = new ArrayList<>();
protected final List<RuntimeField> runtimeFields = new ArrayList<>();
@Nullable protected Query.PointInTime pointInTime;
@Nullable private Boolean requestCache;
private final List<Query.IdWithRouting> idsWithRouting = new ArrayList<>();
private final List<RuntimeField> runtimeFields = new ArrayList<>();
@Nullable private Query.PointInTime pointInTime;
@Nullable private Boolean allowNoIndices;
private EnumSet<IndicesOptions.WildcardStates> expandWildcards = EnumSet.noneOf(IndicesOptions.WildcardStates.class);
@Nullable Integer reactiveBatchSize;
@ -200,6 +203,21 @@ public abstract class BaseQueryBuilder<Q extends BaseQuery, SELF extends BaseQue
return reactiveBatchSize;
}
/**
* @since 5.1
*/
@Nullable
public Boolean getAllowNoIndices() {
return allowNoIndices;
}
/**
* @since 5.1
*/
public EnumSet<IndicesOptions.WildcardStates> getExpandWildcards() {
return expandWildcards;
}
public SELF withPageable(Pageable pageable) {
this.pageable = pageable;
return self();
@ -392,6 +410,19 @@ public abstract class BaseQueryBuilder<Q extends BaseQuery, SELF extends BaseQue
return self();
}
public SELF witAllowNoIndices(@Nullable Boolean allowNoIndices) {
this.allowNoIndices = allowNoIndices;
return self();
}
public SELF withExpandWildcards(EnumSet<IndicesOptions.WildcardStates> expandWildcards) {
Assert.notNull(expandWildcards, "expandWildcards must not be null");
this.expandWildcards = expandWildcards;
return self();
}
public abstract Q build();
private SELF self() {

View File

@ -80,7 +80,7 @@ public class IndicesOptions {
}
public enum WildcardStates {
OPEN, CLOSED, HIDDEN;
OPEN, CLOSED, HIDDEN, ALL, NONE;
}
public enum Option {

View File

@ -18,6 +18,7 @@ package org.springframework.data.elasticsearch.core.query;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
@ -460,8 +461,19 @@ public interface Query {
}
/**
* @since 4.3
* @since 5.1
*/
@Nullable
Boolean getAllowNoIndices();
/**
* @since 5.1
*/
EnumSet<IndicesOptions.WildcardStates> getExpandWildcards();
/**
* @since 4.3
*/
enum SearchType {
QUERY_THEN_FETCH, DFS_QUERY_THEN_FETCH
}

View File

@ -0,0 +1,62 @@
/*
* 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.core.query;
import java.util.Map;
import org.springframework.lang.Nullable;
/**
* @author Peter-Josef Meisch
* @since 5.1
*/
public class SearchTemplateQuery extends BaseQuery {
@Nullable final private String id;
@Nullable final String source;
@Nullable final Map<String, Object> params;
public static SearchTemplateQueryBuilder builder() {
return new SearchTemplateQueryBuilder();
}
public SearchTemplateQuery(SearchTemplateQueryBuilder builder) {
super(builder);
this.id = builder.getId();
this.source = builder.getSource();
this.params = builder.getParams();
if (id == null && source == null) {
throw new IllegalArgumentException("Either id or source must be set");
}
}
@Nullable
public String getId() {
return id;
}
@Nullable
public String getSource() {
return source;
}
@Nullable
public Map<String, Object> getParams() {
return params;
}
}

View File

@ -0,0 +1,69 @@
/*
* 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.core.query;
import org.springframework.lang.Nullable;
import java.util.Map;
/**
* @author Peter-Josef Meisch
* @since 5.1
*/
public class SearchTemplateQueryBuilder extends BaseQueryBuilder<SearchTemplateQuery, SearchTemplateQueryBuilder> {
@Nullable
private String id;
@Nullable String source;
@Nullable
Map<String, Object> params;
@Nullable
public String getId() {
return id;
}
@Nullable
public String getSource() {
return source;
}
@Nullable
public Map<String, Object> getParams() {
return params;
}
public SearchTemplateQueryBuilder withId(@Nullable String id) {
this.id = id;
return this;
}
public SearchTemplateQueryBuilder withSource(@Nullable String source) {
this.source = source;
return this;
}
public SearchTemplateQueryBuilder withParams(@Nullable Map<String, Object> params) {
this.params = params;
return this;
}
@Override
public SearchTemplateQuery build() {
return new SearchTemplateQuery(this);
}
}

View File

@ -0,0 +1,50 @@
/*
* 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.core.script;
import reactor.core.publisher.Mono;
/**
* This interfaces defines the operations to access the
* <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.5/script-apis.html">Elasticsearch script API</a>.
*
* @author Peter-Josef Meisch
* @since 5.1
*/
public interface ReactiveScriptOperations {
/**
* Stores the given script in the Elasticsearch cluster.
*
* @return {{@literal true} if successful
*/
Mono<Boolean> putScript(Script script);
/**
* Gest the script with the given name.
*
* @param name the name of the script
* @return Script or null when a script with this name does not exist.
*/
Mono<Script> getScript(String name);
/**
* Deletes the script with the given name
*
* @param name the name of the script.
* @return true if the request was acknowledged by the cluster.
*/
Mono<Boolean> deleteScript(String name);
}

View File

@ -0,0 +1,74 @@
/*
* 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.core.script;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* @author Peter-Josef Meisch
* @since 5.1
*/
public record Script(String id, String language, String source) {
public Script {
Assert.notNull(id, "id must not be null");
Assert.notNull(language, "language must not be null");
Assert.notNull(source, "source must not be null");
}
public static ScriptBuilder builder() {
return new ScriptBuilder();
}
public static final class ScriptBuilder {
@Nullable private String id;
@Nullable private String language;
@Nullable private String source;
private ScriptBuilder() {}
public ScriptBuilder withId(String id) {
Assert.notNull(id, "id must not be null");
this.id = id;
return this;
}
public ScriptBuilder withLanguage(String language) {
Assert.notNull(language, "language must not be null");
this.language = language;
return this;
}
public ScriptBuilder withSource(String source) {
Assert.notNull(source, "source must not be null");
this.source = source;
return this;
}
public Script build() {
return new Script(id, language, source);
}
}
}

View File

@ -0,0 +1,51 @@
/*
* 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.core.script;
import org.springframework.lang.Nullable;
/**
* This interfaces defines the operations to access the
* <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.5/script-apis.html">Elasticsearch script API</a>.
*
* @author Peter-Josef Meisch
* @since 5.1
*/
public interface ScriptOperations {
/**
* Stores the given script in the Elasticsearch cluster.
*
* @return {{@literal true} if successful
*/
boolean putScript(Script script);
/**
* Gest the script with the given name.
*
* @param name the name of the script
* @return Script or null when a script with this name does not exist.
*/
@Nullable
Script getScript(String name);
/**
* Deletes the script with the given name
*
* @param name the name of the script.
* @return true if the request was acknowledged by the cluster.
*/
boolean deleteScript(String name);
}

View File

@ -0,0 +1,6 @@
/**
* Classes and interfaces to access to script API of Elasticsearch (https://www.elastic.co/guide/en/elasticsearch/reference/8.5/script-apis.html).
*/
@org.springframework.lang.NonNullApi
@org.springframework.lang.NonNullFields
package org.springframework.data.elasticsearch.core.script;

View File

@ -0,0 +1,40 @@
/*
* 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.core;
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.utils.IndexNameProvider;
import org.springframework.test.context.ContextConfiguration;
/**
* @author Peter-Josef Meisch
* @since 5.1
*/
@ContextConfiguration(classes = ReactiveSearchTemplateELCIntegrationTests.Config.class)
public class ReactiveSearchTemplateELCIntegrationTests extends ReactiveSearchTemplateIntegrationTests {
@Configuration
@Import({ ReactiveElasticsearchTemplateConfiguration.class })
static class Config {
@Bean
IndexNameProvider indexNameProvider() {
return new IndexNameProvider("reactive-searchtemplate");
}
}
}

View File

@ -0,0 +1,151 @@
/*
* 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.core;
import static org.assertj.core.api.Assertions.*;
import static org.skyscreamer.jsonassert.JSONAssert.*;
import reactor.test.StepVerifier;
import java.util.Arrays;
import java.util.Map;
import org.json.JSONException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.ResourceNotFoundException;
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.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.SearchTemplateQuery;
import org.springframework.data.elasticsearch.core.script.Script;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.lang.Nullable;
/**
* Integration tests for the point in time API.
*
* @author Peter-Josef Meisch
* @since 5.1
*/
@SpringIntegrationTest
public abstract class ReactiveSearchTemplateIntegrationTests {
private static final String SCRIPT = """
{
"query": {
"bool": {
"must": [
{
"match": {
"firstName": "{{firstName}}"
}
}
]
}
},
"from": 0,
"size": 100
}
""";
private Script script = Script.builder() //
.withId("testScript") //
.withLanguage("mustache") //
.withSource(SCRIPT) //
.build();
@Autowired ReactiveElasticsearchOperations operations;
@Autowired IndexNameProvider indexNameProvider;
@Nullable ReactiveIndexOperations indexOperations;
@BeforeEach
void setUp() {
indexNameProvider.increment();
indexOperations = operations.indexOps(Person.class);
indexOperations.createWithMapping().block();
}
@Test
@Order(Integer.MAX_VALUE)
void cleanup() {
operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + '*')).delete().block();
}
@Test // #1891
@DisplayName("should store, retrieve and delete template script")
void shouldStoreAndRetrieveAndDeleteTemplateScript() throws JSONException {
// we do all in this test because scripts aren't stored in an index but in the cluster and we need to clenaup.
var success = operations.putScript(script).block();
assertThat(success).isTrue();
var savedScript = operations.getScript(script.id()).block();
assertThat(savedScript).isNotNull();
assertThat(savedScript.id()).isEqualTo(script.id());
assertThat(savedScript.language()).isEqualTo(script.language());
assertEquals(savedScript.source(), script.source(), false);
success = operations.deleteScript(script.id()).block();
assertThat(success).isTrue();
savedScript = operations.getScript(script.id()).block();
assertThat(savedScript).isNull();
operations.deleteScript(script.id()) //
.as(StepVerifier::create) //
.verifyError(ResourceNotFoundException.class);
}
@Test // #1891
@DisplayName("should search with template")
void shouldSearchWithTemplate() {
var success = operations.putScript(script).block();
assertThat(success).isTrue();
operations.saveAll( //
Arrays.asList(new Person("1", "John", "Smith"), //
new Person("2", "Willy", "Smith"), //
new Person("3", "John", "Myers")), //
Person.class).blockLast();
var query = SearchTemplateQuery.builder() //
.withId(script.id()) //
.withParams(Map.of("firstName", "John")) //
.build();
operations.search(query, Person.class)//
.as(StepVerifier::create) //
.expectNextCount(2L) //
.verifyComplete();
success = operations.deleteScript(script.id()).block();
assertThat(success).isTrue();
}
@Document(indexName = "#{@indexNameProvider.indexName()}")
record Person( //
@Nullable @Id String id, //
@Field(type = FieldType.Text) String firstName, //
@Field(type = FieldType.Text) String lastName //
) {
}
}

View File

@ -0,0 +1,39 @@
/*
* 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.core;
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.utils.IndexNameProvider;
import org.springframework.test.context.ContextConfiguration;
/**
* @author Peter-Josef Meisch
*/
@ContextConfiguration(classes = {SearchTemplateELCIntegrationTests.Config.class })
public class SearchTemplateELCIntegrationTests extends SearchTemplateIntegrationTests {
@Configuration
@Import({ElasticsearchTemplateConfiguration.class })
static class Config {
@Bean
IndexNameProvider indexNameProvider() {
return new IndexNameProvider("search-templates");
}
}
}

View File

@ -0,0 +1,145 @@
/*
* 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.core;
import static org.assertj.core.api.Assertions.*;
import static org.skyscreamer.jsonassert.JSONAssert.*;
import java.util.Map;
import org.json.JSONException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.ResourceNotFoundException;
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.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.SearchTemplateQuery;
import org.springframework.data.elasticsearch.core.script.Script;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.lang.Nullable;
/**
* Integration tests search template API.
*
* @author Peter-Josef Meisch
*/
@SpringIntegrationTest
public abstract class SearchTemplateIntegrationTests {
private static final String SCRIPT = """
{
"query": {
"bool": {
"must": [
{
"match": {
"firstName": "{{firstName}}"
}
}
]
}
},
"from": 0,
"size": 100
}
""";
private Script script = Script.builder() //
.withId("testScript") //
.withLanguage("mustache") //
.withSource(SCRIPT) //
.build();
@Autowired ElasticsearchOperations operations;
@Autowired IndexNameProvider indexNameProvider;
@Nullable IndexOperations indexOperations;
@BeforeEach
void setUp() {
indexNameProvider.increment();
indexOperations = operations.indexOps(Person.class);
indexOperations.createWithMapping();
}
@Test
@Order(Integer.MAX_VALUE)
void cleanup() {
operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + '*')).delete();
}
@Test // #1891
@DisplayName("should store, retrieve and delete template script")
void shouldStoreAndRetrieveAndDeleteTemplateScript() throws JSONException {
// we do all in this test because scripts aren't stored in an index but in the cluster and we need to clenaup.
var success = operations.putScript(script);
assertThat(success).isTrue();
var savedScript = operations.getScript(script.id());
assertThat(savedScript).isNotNull();
assertThat(savedScript.id()).isEqualTo(script.id());
assertThat(savedScript.language()).isEqualTo(script.language());
assertEquals(savedScript.source(), script.source(), false);
success = operations.deleteScript(script.id());
assertThat(success).isTrue();
savedScript = operations.getScript(script.id());
assertThat(savedScript).isNull();
assertThatThrownBy(() -> operations.deleteScript(script.id())) //
.isInstanceOf(ResourceNotFoundException.class);
}
@Test // #1891
@DisplayName("should search with template")
void shouldSearchWithTemplate() {
var success = operations.putScript(script);
assertThat(success).isTrue();
operations.save( //
new Person("1", "John", "Smith"), //
new Person("2", "Willy", "Smith"), //
new Person("3", "John", "Myers"));
var query = SearchTemplateQuery.builder() //
.withId(script.id()) //
.withParams(Map.of("firstName", "John")) //
.build();
var searchHits = operations.search(query, Person.class);
assertThat(searchHits.getTotalHits()).isEqualTo(2);
success = operations.deleteScript(script.id());
assertThat(success).isTrue();
}
@Document(indexName = "#{@indexNameProvider.indexName()}")
record Person( //
@Nullable @Id String id, //
@Field(type = FieldType.Text) String firstName, //
@Field(type = FieldType.Text) String lastName //
) {
}
}