From 0be8bae120020677238ec9806aa12ecb40ee2236 Mon Sep 17 00:00:00 2001 From: Peter-Josef Meisch Date: Sat, 11 Sep 2021 21:08:40 +0200 Subject: [PATCH] Remove Elasticsearch classes from suggest response data. --- ...elasticsearch-migration-guide-4.2-4.3.adoc | 12 + .../reference/elasticsearch-operations.adoc | 27 ++- ...actElasticsearchRestTransportTemplate.java | 21 +- .../core/AbstractElasticsearchTemplate.java | 9 +- .../core/ElasticsearchRestTemplate.java | 14 +- .../core/ElasticsearchTemplate.java | 9 +- .../core/ReactiveElasticsearchTemplate.java | 119 ++++++---- .../core/ReactiveSearchOperations.java | 39 +++- .../elasticsearch/core/RequestFactory.java | 10 +- .../elasticsearch/core/SearchHitMapping.java | 27 ++- .../data/elasticsearch/core/SearchHits.java | 16 ++ .../elasticsearch/core/SearchHitsImpl.java | 29 ++- .../elasticsearch/core/SearchOperations.java | 8 + .../core/document/DocumentAdapters.java | 11 +- .../core/document/SearchDocumentResponse.java | 151 +++++++++++-- ...SimpleElasticsearchPersistentProperty.java | 2 +- .../core/query/NativeSearchQuery.java | 17 ++ .../core/query/NativeSearchQueryBuilder.java | 13 ++ .../{completion => suggest}/Completion.java | 17 +- .../core/suggest/package-info.java | 18 ++ .../response/CompletionSuggestion.java | 80 +++++++ .../suggest/response/PhraseSuggestion.java | 50 +++++ .../core/suggest/response/SortBy.java | 24 ++ .../core/suggest/response/Suggest.java | 139 ++++++++++++ .../core/suggest/response/TermSuggestion.java | 56 +++++ .../response}/package-info.java | 2 +- .../query/ElasticsearchPartQuery.java | 3 +- .../data/elasticsearch/support/ScoreDoc.java | 45 ++++ .../ComposableAnnotationsUnitTest.java | 2 +- .../core/SearchHitSupportTest.java | 2 +- .../elasticsearch/core/StreamQueriesTest.java | 3 +- .../index/MappingBuilderIntegrationTests.java | 2 +- .../core/index/MappingBuilderUnitTests.java | 2 +- .../ElasticsearchTemplateCompletionTests.java | 117 +++++----- ...earchTemplateCompletionTransportTests.java | 2 +- ...chTemplateCompletionWithContextsTests.java | 2 +- ...eCompletionWithContextsTransportTests.java | 2 +- ...searchTemplateSuggestIntegrationTests.java | 212 ++++++++++++++++++ 38 files changed, 1129 insertions(+), 185 deletions(-) rename src/main/java/org/springframework/data/elasticsearch/core/{completion => suggest}/Completion.java (62%) create mode 100644 src/main/java/org/springframework/data/elasticsearch/core/suggest/package-info.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/core/suggest/response/CompletionSuggestion.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/core/suggest/response/PhraseSuggestion.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/core/suggest/response/SortBy.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/core/suggest/response/Suggest.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/core/suggest/response/TermSuggestion.java rename src/main/java/org/springframework/data/elasticsearch/core/{completion => suggest/response}/package-info.java (52%) create mode 100644 src/main/java/org/springframework/data/elasticsearch/support/ScoreDoc.java rename src/test/java/org/springframework/data/elasticsearch/core/{completion => suggest}/ElasticsearchTemplateCompletionTests.java (74%) rename src/test/java/org/springframework/data/elasticsearch/core/{completion => suggest}/ElasticsearchTemplateCompletionTransportTests.java (95%) rename src/test/java/org/springframework/data/elasticsearch/core/{completion => suggest}/ElasticsearchTemplateCompletionWithContextsTests.java (99%) rename src/test/java/org/springframework/data/elasticsearch/core/{completion => suggest}/ElasticsearchTemplateCompletionWithContextsTransportTests.java (95%) create mode 100644 src/test/java/org/springframework/data/elasticsearch/core/suggest/ReactiveElasticsearchTemplateSuggestIntegrationTests.java diff --git a/src/main/asciidoc/reference/elasticsearch-migration-guide-4.2-4.3.adoc b/src/main/asciidoc/reference/elasticsearch-migration-guide-4.2-4.3.adoc index 1da48a342..1b98c5cee 100644 --- a/src/main/asciidoc/reference/elasticsearch-migration-guide-4.2-4.3.adoc +++ b/src/main/asciidoc/reference/elasticsearch-migration-guide-4.2-4.3.adoc @@ -21,6 +21,14 @@ Check the sections on <> and [[elasticsearch-migration-guide-4.2-4.3.deprecations]] == Deprecations +=== suggest methods + +In `SearchOperations`, and so in `ElasticsearchOperations` as well, the `suggest` methods taking a `org.elasticsearch.search.suggest.SuggestBuilder` as argument and returning a `org.elasticsearch.action.search.SearchResponse` have been deprecated. +Use `SearchHits search(Query query, Class clazz)` instead, passing in a `NativeSearchQuery` which can contain a `SuggestBuilder` and read the suggest results from the returned `SearchHit`. + +In `ReactiveSearchOperations` the new `suggest` methods return a `Mono` now. +Here as well the old methods are deprecated. + [[elasticsearch-migration-guide-4.2-4.3.breaking-changes]] == Breaking Changes @@ -59,3 +67,7 @@ Some properties of the `org.springframework.data.elasticsearch.core.query.BulkOp === IndicesOptions change Spring Data Elasticsearch now uses `org.springframework.data.elasticsearch.core.query.IndicesOptions` instead of `org.elasticsearch.action.support.IndicesOptions`. + +=== Completion classes + +The classes from the package `org.springframework.data.elasticsearch.core.completion` have been moved to `org.springframework.data.elasticsearch.core.suggest`. diff --git a/src/main/asciidoc/reference/elasticsearch-operations.adoc b/src/main/asciidoc/reference/elasticsearch-operations.adoc index 1d213604c..3862b79e5 100644 --- a/src/main/asciidoc/reference/elasticsearch-operations.adoc +++ b/src/main/asciidoc/reference/elasticsearch-operations.adoc @@ -20,11 +20,11 @@ The default implementations of the interfaces offer: [NOTE] ==== .Index management and automatic creation of indices and mappings. +The `IndexOperations` interface and the provided implementation which can be obtained from an `ElasticsearchOperations` instance - for example with a call to `operations.indexOps(clazz)`- give the user the ability to create indices, put mappings or store template and alias information in the Elasticsearch cluster. +Details of the index that will be created can be set by using the `@Setting` annotation, refer to <> for further information. -The `IndexOperations` interface and the provided implementation which can be obtained from an `ElasticsearchOperations` instance - for example with a call to `operations.indexOps(clazz)`- give the user the ability to create indices, put mappings or store template and alias information in the Elasticsearch cluster. Details of the index that will be created -can be set by using the `@Setting` annotation, refer to <> for further information. - -**None of these operations are done automatically** by the implementations of `IndexOperations` or `ElasticsearchOperations`. It is the user's responsibility to call the methods. +**None of these operations are done automatically** by the implementations of `IndexOperations` or `ElasticsearchOperations`. +It is the user's responsibility to call the methods. There is support for automatic creation of indices and writing the mappings when using Spring Data Elasticsearch repositories, see <> @@ -58,6 +58,7 @@ public class TransportClientConfig extends ElasticsearchConfigurationSupport { } } ---- + <1> Setting up the <>. Deprecated as of version 4.0. <2> Creating the `ElasticsearchTemplate` bean, offering both names, _elasticsearchOperations_ and _elasticsearchTemplate_. @@ -82,6 +83,7 @@ public class RestClientConfig extends AbstractElasticsearchConfiguration { // no special bean creation needed <2> } ---- + <1> Setting up the <>. <2> The base class `AbstractElasticsearchConfiguration` already provides the `elasticsearchTemplate` bean. ==== @@ -127,6 +129,7 @@ public class TestController { } ---- + <1> Let Spring inject the provided `ElasticsearchOperations` bean in the constructor. <2> Store some entity in the Elasticsearch cluster. <3> Retrieve the entity with a query by id. @@ -164,6 +167,7 @@ Contains the following information: * Maximum score * A list of `SearchHit` objects * Returned aggregations +* Returned suggest results .SearchPage Defines a Spring Data `Page` that contains a `SearchHits` element and can be used for paging access using repository methods. @@ -182,12 +186,12 @@ Almost all of the methods defined in the `SearchOperations` and `ReactiveSearchO [[elasticsearch.operations.criteriaquery]] === CriteriaQuery -`CriteriaQuery` based queries allow the creation of queries to search for data without knowing the syntax or basics of Elasticsearch queries. They allow the user to build queries by simply chaining and combining `Criteria` objects that specifiy the criteria the searched documents must fulfill. +`CriteriaQuery` based queries allow the creation of queries to search for data without knowing the syntax or basics of Elasticsearch queries. +They allow the user to build queries by simply chaining and combining `Criteria` objects that specifiy the criteria the searched documents must fulfill. NOTE: when talking about AND or OR when combining criteria keep in mind, that in Elasticsearch AND are converted to a **must** condition and OR to a **should** -`Criteria` and their usage are best explained by example -(let's assume we have a `Book` entity with a `price` property): +`Criteria` and their usage are best explained by example (let's assume we have a `Book` entity with a `price` property): .Get books with a given price ==== @@ -211,7 +215,7 @@ Query query = new CriteriaQuery(criteria); When chaining `Criteria`, by default a AND logic is used: -.Get all persons with first name _James_ and last name _Miller_: +.Get all persons with first name _James_ and last name _Miller_: ==== [source,java] ---- @@ -219,11 +223,13 @@ Criteria criteria = new Criteria("lastname").is("Miller") <1> .and("firstname").is("James") <2> Query query = new CriteriaQuery(criteria); ---- + <1> the first `Criteria` <2> the and() creates a new `Criteria` and chaines it to the first one. ==== -If you want to create nested queries, you need to use subqueries for this. Let's assume we want to find all persons with a last name of _Miller_ and a first name of either _Jack_ or _John_: +If you want to create nested queries, you need to use subqueries for this. +Let's assume we want to find all persons with a last name of _Miller_ and a first name of either _Jack_ or _John_: .Nested subqueries ==== @@ -236,6 +242,7 @@ Criteria miller = new Criteria("lastName").is("Miller") <.> ); Query query = new CriteriaQuery(criteria); ---- + <.> create a first `Criteria` for the last name <.> this is combined with AND to a subCriteria <.> This sub Criteria is an OR combination for the first name _John_ @@ -281,5 +288,3 @@ Query query = new NativeSearchQueryBuilder() SearchHits searchHits = operations.search(query, Person.class); ---- ==== - - diff --git a/src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchRestTransportTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchRestTransportTemplate.java index 6796706e9..cdaf7c36f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchRestTransportTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchRestTransportTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2021 the original author or authors. + * Copyright 2021 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. @@ -32,6 +32,7 @@ import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.index.query.MoreLikeThisQueryBuilder; import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.search.suggest.SuggestBuilder; import org.springframework.data.elasticsearch.BulkFailureException; import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; @@ -103,11 +104,12 @@ public List> multiSearch(List queries, Class< MultiSearchResponse.Item[] items = getMultiSearchResult(request); + ReadDocumentCallback documentCallback = new ReadDocumentCallback(elasticsearchConverter, clazz, index); SearchDocumentResponseCallback> callback = new ReadSearchDocumentResponseCallback<>(clazz, index); List> res = new ArrayList<>(queries.size()); int c = 0; for (Query query : queries) { - res.add(callback.doWith(SearchDocumentResponse.from(items[c++].getResponse()))); + res.add(callback.doWith(SearchDocumentResponse.from(items[c++].getResponse(), documentCallback::doWith))); } return res; } @@ -134,11 +136,13 @@ public List> multiSearch(List queries, List documentCallback = new ReadDocumentCallback<>(elasticsearchConverter, entityClass, index); SearchDocumentResponseCallback> callback = new ReadSearchDocumentResponseCallback<>(entityClass, - getIndexCoordinatesFor(entityClass)); + index); SearchResponse response = items[c++].getResponse(); - res.add(callback.doWith(SearchDocumentResponse.from(response))); + res.add(callback.doWith(SearchDocumentResponse.from(response, documentCallback::doWith))); } return res; } @@ -166,11 +170,12 @@ public List> multiSearch(List queries, List documentCallback = new ReadDocumentCallback<>(elasticsearchConverter, entityClass, index); SearchDocumentResponseCallback> callback = new ReadSearchDocumentResponseCallback<>(entityClass, index); SearchResponse response = items[c++].getResponse(); - res.add(callback.doWith(SearchDocumentResponse.from(response))); + res.add(callback.doWith(SearchDocumentResponse.from(response, documentCallback::doWith))); } return res; } @@ -204,5 +209,11 @@ protected String getRuntimeLibraryVersion() { return Version.CURRENT.toString(); } + @Override + @Deprecated + public SearchResponse suggest(SuggestBuilder suggestion, Class clazz) { + return suggest(suggestion, getIndexCoordinatesFor(clazz)); + } + // endregion } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchTemplate.java index d3381980c..590fa1613 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchTemplate.java @@ -22,8 +22,6 @@ import java.util.List; import java.util.stream.Collectors; -import org.elasticsearch.action.search.SearchResponse; -import org.elasticsearch.search.suggest.SuggestBuilder; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -432,11 +430,6 @@ protected void searchScrollClear(String scrollId) { */ abstract protected void searchScrollClear(List scrollIds); - @Override - public SearchResponse suggest(SuggestBuilder suggestion, Class clazz) { - return suggest(suggestion, getIndexCoordinatesFor(clazz)); - } - // endregion // region Helper methods @@ -758,6 +751,7 @@ public ReadSearchDocumentResponseCallback(Class type, IndexCoordinates index) this.type = type; } + @NonNull @Override public SearchHits doWith(SearchDocumentResponse response) { List entities = response.getSearchDocuments().stream().map(delegate::doWith).collect(Collectors.toList()); @@ -778,6 +772,7 @@ public ReadSearchScrollDocumentResponseCallback(Class type, IndexCoordinates this.type = type; } + @NonNull @Override public SearchScrollHits doWith(SearchDocumentResponse response) { List entities = response.getSearchDocuments().stream().map(delegate::doWith).collect(Collectors.toList()); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchRestTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchRestTemplate.java index 5656a48ad..7b86aaca6 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchRestTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchRestTemplate.java @@ -317,8 +317,10 @@ public SearchHits search(Query query, Class clazz, IndexCoordinates in SearchRequest searchRequest = requestFactory.searchRequest(query, clazz, index); SearchResponse response = execute(client -> client.search(searchRequest, RequestOptions.DEFAULT)); + ReadDocumentCallback documentCallback = new ReadDocumentCallback(elasticsearchConverter, clazz, index); SearchDocumentResponseCallback> callback = new ReadSearchDocumentResponseCallback<>(clazz, index); - return callback.doWith(SearchDocumentResponse.from(response)); + + return callback.doWith(SearchDocumentResponse.from(response, documentCallback::doWith)); } @Override @@ -332,9 +334,10 @@ public SearchScrollHits searchScrollStart(long scrollTimeInMillis, Query SearchResponse response = execute(client -> client.search(searchRequest, RequestOptions.DEFAULT)); + ReadDocumentCallback documentCallback = new ReadDocumentCallback(elasticsearchConverter, clazz, index); SearchDocumentResponseCallback> callback = new ReadSearchScrollDocumentResponseCallback<>(clazz, index); - return callback.doWith(SearchDocumentResponse.from(response)); + return callback.doWith(SearchDocumentResponse.from(response, documentCallback::doWith)); } @Override @@ -346,9 +349,10 @@ public SearchScrollHits searchScrollContinue(@Nullable String scrollId, l SearchResponse response = execute(client -> client.scroll(request, RequestOptions.DEFAULT)); - SearchDocumentResponseCallback> callback = // - new ReadSearchScrollDocumentResponseCallback<>(clazz, index); - return callback.doWith(SearchDocumentResponse.from(response)); + ReadDocumentCallback documentCallback = new ReadDocumentCallback(elasticsearchConverter, clazz, index); + SearchDocumentResponseCallback> callback = new ReadSearchScrollDocumentResponseCallback<>(clazz, + index); + return callback.doWith(SearchDocumentResponse.from(response, documentCallback::doWith)); } @Override diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java index a58ae90f5..a9070a2dd 100755 --- a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java @@ -353,8 +353,9 @@ public SearchHits search(Query query, Class clazz, IndexCoordinates in SearchRequestBuilder searchRequestBuilder = requestFactory.searchRequestBuilder(client, query, clazz, index); SearchResponse response = getSearchResponse(searchRequestBuilder); + ReadDocumentCallback documentCallback = new ReadDocumentCallback(elasticsearchConverter, clazz, index); SearchDocumentResponseCallback> callback = new ReadSearchDocumentResponseCallback<>(clazz, index); - return callback.doWith(SearchDocumentResponse.from(response)); + return callback.doWith(SearchDocumentResponse.from(response, documentCallback::doWith)); } @Override @@ -369,9 +370,10 @@ public SearchScrollHits searchScrollStart(long scrollTimeInMillis, Query SearchResponse response = getSearchResponseWithTimeout(action); + ReadDocumentCallback documentCallback = new ReadDocumentCallback(elasticsearchConverter, clazz, index); SearchDocumentResponseCallback> callback = new ReadSearchScrollDocumentResponseCallback<>(clazz, index); - return callback.doWith(SearchDocumentResponse.from(response)); + return callback.doWith(SearchDocumentResponse.from(response, documentCallback::doWith)); } @Override @@ -385,9 +387,10 @@ public SearchScrollHits searchScrollContinue(@Nullable String scrollId, l SearchResponse response = getSearchResponseWithTimeout(action); + ReadDocumentCallback documentCallback = new ReadDocumentCallback(elasticsearchConverter, clazz, index); SearchDocumentResponseCallback> callback = new ReadSearchScrollDocumentResponseCallback<>(clazz, index); - return callback.doWith(SearchDocumentResponse.from(response)); + return callback.doWith(SearchDocumentResponse.from(response, documentCallback::doWith)); } @Override diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java index aa14f63fc..2750adb51 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; import org.elasticsearch.Version; @@ -44,7 +45,6 @@ import org.elasticsearch.index.reindex.BulkByScrollResponse; import org.elasticsearch.index.reindex.DeleteByQueryRequest; import org.elasticsearch.index.reindex.UpdateByQueryRequest; -import org.elasticsearch.search.suggest.Suggest; import org.elasticsearch.search.suggest.SuggestBuilder; import org.reactivestreams.Publisher; import org.slf4j.Logger; @@ -84,6 +84,7 @@ 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.suggest.response.Suggest; import org.springframework.data.elasticsearch.support.VersionInfo; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; @@ -375,8 +376,7 @@ protected Mono doIndex(IndexRequest request) { protected Flux doBulkOperation(List queries, BulkOptions bulkOptions, IndexCoordinates index) { BulkRequest bulkRequest = prepareWriteRequest(requestFactory.bulkRequest(queries, bulkOptions, index)); return client.bulk(bulkRequest) // - .onErrorMap( - e -> new UncategorizedElasticsearchException("Error while bulk for request: " + bulkRequest.toString(), e)) // + .onErrorMap(e -> new UncategorizedElasticsearchException("Error while bulk for request: " + bulkRequest, e)) // .flatMap(this::checkForBulkOperationFailure) // .flatMapMany(response -> Flux.fromArray(response.getItems())); } @@ -658,7 +658,7 @@ protected Mono doDeleteBy(DeleteByQueryRequest request) { } /** - * Customization hook to modify a generated {@link DeleteRequest} prior to its execution. Eg. by setting the + * Customization hook to modify a generated {@link DeleteRequest} prior to its execution. E.g. by setting the * {@link WriteRequest#setRefreshPolicy(String) refresh policy} if applicable. * * @param request the generated {@link DeleteRequest}. @@ -669,7 +669,7 @@ protected DeleteRequest prepareDeleteRequest(DeleteRequest request) { } /** - * Customization hook to modify a generated {@link DeleteByQueryRequest} prior to its execution. Eg. by setting the + * Customization hook to modify a generated {@link DeleteByQueryRequest} prior to its execution. E.g. by setting the * {@link WriteRequest#setRefreshPolicy(String) refresh policy} if applicable. * * @param request the generated {@link DeleteByQueryRequest}. @@ -694,7 +694,7 @@ protected DeleteByQueryRequest prepareDeleteByRequest(DeleteByQueryRequest reque } /** - * Customization hook to modify a generated {@link IndexRequest} prior to its execution. Eg. by setting the + * Customization hook to modify a generated {@link IndexRequest} prior to its execution. E.g. by setting the * {@link WriteRequest#setRefreshPolicy(String) refresh policy} if applicable. * * @param source the source object the {@link IndexRequest} was derived from. @@ -706,7 +706,7 @@ protected IndexRequest prepareIndexRequest(Object source, IndexRequest request) } /** - * Pre process the write request before it is sent to the server, eg. by setting the + * Preprocess the write request before it is sent to the server, e.g. by setting the * {@link WriteRequest#setRefreshPolicy(String) refresh policy} if applicable. * * @param request must not be {@literal null}. @@ -777,7 +777,10 @@ private Mono doFindForResponse(Query query, Class cla return Mono.defer(() -> { SearchRequest request = requestFactory.searchRequest(query, clazz, index); request = prepareSearchRequest(request, false); - return doFindForResponse(request); + + SearchDocumentCallback documentCallback = new ReadSearchDocumentCallback<>(clazz, index); + + return doFindForResponse(request, searchDocument -> documentCallback.toEntity(searchDocument).block()); }); } @@ -788,34 +791,73 @@ public Flux> aggregate(Query query, Class entityType) @Override public Flux> aggregate(Query query, Class entityType, IndexCoordinates index) { - return doAggregate(query, entityType, index); + + Assert.notNull(query, "query must not be null"); + Assert.notNull(entityType, "entityType must not be null"); + Assert.notNull(index, "index must not be null"); + + return Flux.defer(() -> { + SearchRequest request = requestFactory.searchRequest(query, entityType, index); + request = prepareSearchRequest(request, false); + return doAggregate(request); + }); + } + + /** + * Customization hook on the actual execution result {@link Publisher}.
+ * + * @param request the already prepared {@link SearchRequest} ready to be executed. + * @return a {@link Flux} emitting the result of the operation. + */ + protected Flux> doAggregate(SearchRequest request) { + + if (QUERY_LOGGER.isDebugEnabled()) { + QUERY_LOGGER.debug("Executing doCount: {}", request); + } + + return Flux.from(execute(client -> client.aggregate(request))) // + .onErrorResume(NoSuchIndexException.class, it -> Flux.empty()).map(ElasticsearchAggregation::new); + } + + @Override + public Mono suggest(Query query, Class entityType) { + return suggest(query, entityType, getIndexCoordinatesFor(entityType)); + } + + @Override + public Mono suggest(Query query, Class entityType, IndexCoordinates index) { + + Assert.notNull(query, "query must not be null"); + Assert.notNull(entityType, "entityType must not be null"); + Assert.notNull(index, "index must not be null"); + + return doFindForResponse(query, entityType, index).mapNotNull(searchDocumentResponse -> { + Suggest suggest = searchDocumentResponse.getSuggest(); + SearchHitMapping.mappingFor(entityType, converter).mapHitsInCompletionSuggestion(suggest); + return suggest; + }); } @Override - public Flux suggest(SuggestBuilder suggestion, Class entityType) { + @Deprecated + public Flux suggest(SuggestBuilder suggestion, Class entityType) { return doSuggest(suggestion, getIndexCoordinatesFor(entityType)); } @Override - public Flux suggest(SuggestBuilder suggestion, IndexCoordinates index) { + @Deprecated + public Flux suggest(SuggestBuilder suggestion, IndexCoordinates index) { return doSuggest(suggestion, index); } - private Flux doSuggest(SuggestBuilder suggestion, IndexCoordinates index) { + @Deprecated + private Flux doSuggest(SuggestBuilder suggestion, IndexCoordinates index) { return Flux.defer(() -> { SearchRequest request = requestFactory.searchRequest(suggestion, index); return Flux.from(execute(client -> client.suggest(request))); }); } - private Flux> doAggregate(Query query, Class entityType, IndexCoordinates index) { - return Flux.defer(() -> { - SearchRequest request = requestFactory.searchRequest(query, entityType, index); - request = prepareSearchRequest(request, false); - return doAggregate(request); - }); - } - @Override public Mono count(Query query, Class entityType) { return count(query, entityType, getIndexCoordinatesFor(entityType)); @@ -855,31 +897,19 @@ protected Flux doFind(SearchRequest request) { * Customization hook on the actual execution result {@link Mono}.
* * @param request the already prepared {@link SearchRequest} ready to be executed. + * @param suggestEntityCreator * @return a {@link Mono} emitting the result of the operation converted to s {@link SearchDocumentResponse}. */ - protected Mono doFindForResponse(SearchRequest request) { + protected Mono doFindForResponse(SearchRequest request, + Function suggestEntityCreator) { if (QUERY_LOGGER.isDebugEnabled()) { QUERY_LOGGER.debug("Executing doFindForResponse: {}", request); } - return Mono.from(execute(client1 -> client1.searchForResponse(request))).map(SearchDocumentResponse::from); - } - - /** - * Customization hook on the actual execution result {@link Publisher}.
- * - * @param request the already prepared {@link SearchRequest} ready to be executed. - * @return a {@link Flux} emitting the result of the operation. - */ - protected Flux> doAggregate(SearchRequest request) { - - if (QUERY_LOGGER.isDebugEnabled()) { - QUERY_LOGGER.debug("Executing doCount: {}", request); - } - - return Flux.from(execute(client -> client.aggregate(request))) // - .onErrorResume(NoSuchIndexException.class, it -> Flux.empty()).map(ElasticsearchAggregation::new); + return Mono.from(execute(client1 -> client1.searchForResponse(request))).map(searchResponse -> { + return SearchDocumentResponse.from(searchResponse, suggestEntityCreator); + }); } /** @@ -915,7 +945,7 @@ protected Flux doScroll(SearchRequest request) { } /** - * Customization hook to modify a generated {@link SearchRequest} prior to its execution. Eg. by setting the + * Customization hook to modify a generated {@link SearchRequest} prior to its execution. E.g. by setting the * {@link SearchRequest#indicesOptions(IndicesOptions) indices options} if applicable. * * @param request the generated {@link SearchRequest}. @@ -941,7 +971,8 @@ protected SearchRequest prepareSearchRequest(SearchRequest request, boolean useS // region Helper methods protected Mono getClusterVersion() { try { - return Mono.from(execute(client -> client.info())).map(mainResponse -> mainResponse.getVersion().toString()); + return Mono.from(execute(ReactiveElasticsearchClient::info)) + .map(mainResponse -> mainResponse.getVersion().toString()); } catch (Exception ignored) {} return Mono.empty(); } @@ -1163,11 +1194,9 @@ public Mono toEntity(@Nullable Document document) { protected interface SearchDocumentCallback { - @NonNull - Mono toEntity(@NonNull SearchDocument response); + Mono toEntity(SearchDocument response); - @NonNull - Mono> toSearchHit(@NonNull SearchDocument response); + Mono> toSearchHit(SearchDocument response); } protected class ReadSearchDocumentCallback implements SearchDocumentCallback { @@ -1210,7 +1239,7 @@ private List indexQueries() { } private T entityAt(long index) { - // it's safe to cast to int because the original indexed colleciton was fitting in memory + // it's safe to cast to int because the original indexed collection was fitting in memory int intIndex = (int) index; return entities.get(intIndex); } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveSearchOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveSearchOperations.java index 6ced92b91..a3cb7b6c3 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveSearchOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveSearchOperations.java @@ -20,11 +20,11 @@ import java.util.List; -import org.elasticsearch.search.suggest.Suggest; import org.elasticsearch.search.suggest.SuggestBuilder; import org.springframework.data.domain.Pageable; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.query.Query; +import org.springframework.data.elasticsearch.core.suggest.response.Suggest; /** * The reactive operations for the @@ -202,18 +202,47 @@ default Mono> searchForPage(Query query, Class entityType, * * @param suggestion the query * @param entityType must not be {@literal null}. - * @return the suggest response + * @return the suggest response (Elasticsearch library classes) + * @deprecated since 4.3, use {@link #suggest(Query, Class)} */ - Flux suggest(SuggestBuilder suggestion, Class entityType); + @Deprecated + Flux suggest(SuggestBuilder suggestion, Class entityType); /** * Does a suggest query * * @param suggestion the query * @param index the index to run the query against - * @return the suggest response + * @return the suggest response (Elasticsearch library classes) + * @deprecated since 4.3, use {@link #suggest(Query, Class, IndexCoordinates)} */ - Flux suggest(SuggestBuilder suggestion, IndexCoordinates index); + @Deprecated + Flux suggest(SuggestBuilder suggestion, IndexCoordinates index); + + /** + * Does a suggest query. + * + * @param query the Query containing the suggest definition. Must be currently a + * {@link org.springframework.data.elasticsearch.core.query.NativeSearchQuery}, must not be {@literal null}. + * @param entityType the type of the entities that might be returned for a completion suggestion, must not be + * {@literal null}. + * @return suggest data + * @since 4.3 + */ + Mono suggest(Query query, Class entityType); + + /** + * Does a suggest query. + * + * @param query the Query containing the suggest definition. Must be currently a + * {@link org.springframework.data.elasticsearch.core.query.NativeSearchQuery}, must not be {@literal null}. + * @param entityType the type of the entities that might be returned for a completion suggestion, must not be + * {@literal null}. + * @param index the index to run the query against, must not be {@literal null}. + * @return suggest data + * @since 4.3 + */ + Mono suggest(Query query, Class entityType, IndexCoordinates index); // region helper /** diff --git a/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java b/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java index a397d34d5..f991b46a0 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java @@ -817,7 +817,8 @@ public HighlightBuilder highlightBuilder(Query query) { if (query instanceof NativeSearchQuery) { NativeSearchQuery searchQuery = (NativeSearchQuery) query; - if (searchQuery.getHighlightFields() != null || searchQuery.getHighlightBuilder() != null) { + if ((searchQuery.getHighlightFields() != null && searchQuery.getHighlightFields().length > 0) + || searchQuery.getHighlightBuilder() != null) { highlightBuilder = searchQuery.getHighlightBuilder(); if (highlightBuilder == null) { @@ -1140,6 +1141,9 @@ private void prepareNativeSearch(NativeSearchQuery query, SearchSourceBuilder so query.getPipelineAggregations().forEach(sourceBuilder::aggregation); } + if (query.getSuggestBuilder() != null) { + sourceBuilder.suggest(query.getSuggestBuilder()); + } } private void prepareNativeSearch(SearchRequestBuilder searchRequestBuilder, NativeSearchQuery nativeSearchQuery) { @@ -1166,6 +1170,10 @@ private void prepareNativeSearch(SearchRequestBuilder searchRequestBuilder, Nati if (!isEmpty(nativeSearchQuery.getPipelineAggregations())) { nativeSearchQuery.getPipelineAggregations().forEach(searchRequestBuilder::addAggregation); } + + if (nativeSearchQuery.getSuggestBuilder() != null) { + searchRequestBuilder.suggest(nativeSearchQuery.getSuggestBuilder()); + } } @SuppressWarnings("rawtypes") diff --git a/src/main/java/org/springframework/data/elasticsearch/core/SearchHitMapping.java b/src/main/java/org/springframework/data/elasticsearch/core/SearchHitMapping.java index 6a3ed4591..77e06a3fe 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/SearchHitMapping.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/SearchHitMapping.java @@ -31,6 +31,8 @@ import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; +import org.springframework.data.elasticsearch.core.suggest.response.CompletionSuggestion; +import org.springframework.data.elasticsearch.core.suggest.response.Suggest; import org.springframework.data.mapping.context.MappingContext; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -97,7 +99,27 @@ private SearchHitsImpl mapHitsFromResponse(SearchDocumentResponse searchDocum AggregationsContainer aggregations = searchDocumentResponse.getAggregations(); TotalHitsRelation totalHitsRelation = TotalHitsRelation.valueOf(searchDocumentResponse.getTotalHitsRelation()); - return new SearchHitsImpl<>(totalHits, totalHitsRelation, maxScore, scrollId, searchHits, aggregations); + Suggest suggest = searchDocumentResponse.getSuggest(); + mapHitsInCompletionSuggestion(suggest); + + return new SearchHitsImpl<>(totalHits, totalHitsRelation, maxScore, scrollId, searchHits, aggregations, suggest); + } + + @SuppressWarnings("unchecked") + public void mapHitsInCompletionSuggestion(@Nullable Suggest suggest) { + if (suggest != null) { + for (Suggest.Suggestion> suggestion : suggest + .getSuggestions()) { + if (suggestion instanceof CompletionSuggestion) { + CompletionSuggestion completionSuggestion = (CompletionSuggestion) suggestion; + for (CompletionSuggestion.Entry entry : completionSuggestion.getEntries()) { + for (CompletionSuggestion.Entry.Option option : entry.getOptions()) { + option.updateSearchHit(this::mapHit); + } + } + } + } + } } SearchHit mapHit(SearchDocument searchDocument, T content) { @@ -213,7 +235,8 @@ private SearchHits mapInnerDocuments(SearchHits searchHits, C searchHits.getMaxScore(), // scrollId, // convertedSearchHits, // - searchHits.getAggregations()); + searchHits.getAggregations(), // + searchHits.getSuggest()); } } catch (Exception e) { LOGGER.warn("Could not map inner_hits", e); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/SearchHits.java b/src/main/java/org/springframework/data/elasticsearch/core/SearchHits.java index bf64203b7..b38d36587 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/SearchHits.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/SearchHits.java @@ -18,6 +18,7 @@ import java.util.Iterator; import java.util.List; +import org.springframework.data.elasticsearch.core.suggest.response.Suggest; import org.springframework.data.util.Streamable; import org.springframework.lang.Nullable; @@ -77,6 +78,21 @@ default boolean hasSearchHits() { return !getSearchHits().isEmpty(); } + /** + * @return the suggest response + * @since 4.3 + */ + @Nullable + Suggest getSuggest(); + + /** + * @return wether the {@link SearchHits} has a suggest response. + * @since 4.3 + */ + default boolean hasSuggest() { + return getSuggest() != null; + } + /** * @return an iterator for {@link SearchHit} */ diff --git a/src/main/java/org/springframework/data/elasticsearch/core/SearchHitsImpl.java b/src/main/java/org/springframework/data/elasticsearch/core/SearchHitsImpl.java index f17e8b3f9..294766fe6 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/SearchHitsImpl.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/SearchHitsImpl.java @@ -18,6 +18,7 @@ import java.util.Collections; import java.util.List; +import org.springframework.data.elasticsearch.core.suggest.response.Suggest; import org.springframework.data.util.Lazy; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -39,6 +40,7 @@ public class SearchHitsImpl implements SearchScrollHits { private final List> searchHits; private final Lazy>> unmodifiableSearchHits; @Nullable private final AggregationsContainer aggregations; + @Nullable private final Suggest suggest; /** * @param totalHits the number of total hits for the search @@ -49,7 +51,8 @@ public class SearchHitsImpl implements SearchScrollHits { * @param aggregations the aggregations if available */ public SearchHitsImpl(long totalHits, TotalHitsRelation totalHitsRelation, float maxScore, @Nullable String scrollId, - List> searchHits, @Nullable AggregationsContainer aggregations) { + List> searchHits, @Nullable AggregationsContainer aggregations, + @Nullable Suggest suggest) { Assert.notNull(searchHits, "searchHits must not be null"); @@ -59,6 +62,7 @@ public SearchHitsImpl(long totalHits, TotalHitsRelation totalHitsRelation, float this.scrollId = scrollId; this.searchHits = searchHits; this.aggregations = aggregations; + this.suggest = suggest; this.unmodifiableSearchHits = Lazy.of(() -> Collections.unmodifiableList(searchHits)); } @@ -88,14 +92,23 @@ public String getScrollId() { public List> getSearchHits() { return unmodifiableSearchHits.get(); } - // endregion - // region SearchHit access @Override public SearchHit getSearchHit(int index) { return searchHits.get(index); } - // endregion + + @Override + @Nullable + public AggregationsContainer getAggregations() { + return aggregations; + } + + @Override + @Nullable + public Suggest getSuggest() { + return suggest; + } @Override public String toString() { @@ -108,12 +121,4 @@ public String toString() { ", aggregations=" + aggregations + // '}'; } - - // region aggregations - @Override - @Nullable - public AggregationsContainer getAggregations() { - return aggregations; - } - // endregion } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/SearchOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/SearchOperations.java index e6cb50a16..f332406ae 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/SearchOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/SearchOperations.java @@ -71,7 +71,11 @@ default long count(Query query, IndexCoordinates index) { * @param clazz the entity class * @return the suggest response * @since 4.1 + * @deprecated since 4.3 use a {@link org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder} with + * {@link org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder#withSuggestBuilder(SuggestBuilder)}, + * call {@link #search(Query, Class)} and get the suggest from {@link SearchHits#getSuggest()} */ + @Deprecated SearchResponse suggest(SuggestBuilder suggestion, Class clazz); /** @@ -80,7 +84,11 @@ default long count(Query query, IndexCoordinates index) { * @param suggestion the query * @param index the index to run the query against * @return the suggest response + * @deprecated since 4.3 use a {@link org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder} with + * {@link org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder#withSuggestBuilder(SuggestBuilder)}, + * call {@link #search(Query, Class)} and get the suggest from {@link SearchHits#getSuggest()} */ + @Deprecated SearchResponse suggest(SuggestBuilder suggestion, IndexCoordinates index); /** diff --git a/src/main/java/org/springframework/data/elasticsearch/core/document/DocumentAdapters.java b/src/main/java/org/springframework/data/elasticsearch/core/document/DocumentAdapters.java index 394836998..f5806af36 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/document/DocumentAdapters.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/document/DocumentAdapters.java @@ -177,8 +177,8 @@ public static SearchDocument from(SearchHit source) { Map sourceInnerHits = source.getInnerHits(); if (sourceInnerHits != null) { - sourceInnerHits - .forEach((name, searchHits) -> innerHits.put(name, SearchDocumentResponse.from(searchHits, null, null))); + sourceInnerHits.forEach((name, searchHits) -> innerHits.put(name, + SearchDocumentResponse.from(searchHits, null, null, null, searchDocument -> null))); } NestedMetaData nestedMetaData = from(source.getNestedIdentity()); @@ -186,12 +186,13 @@ public static SearchDocument from(SearchHit source) { List matchedQueries = from(source.getMatchedQueries()); BytesReference sourceRef = source.getSourceRef(); + Map sourceFields = source.getFields(); if (sourceRef == null || sourceRef.length() == 0) { return new SearchDocumentAdapter( fromDocumentFields(source, source.getIndex(), source.getId(), source.getVersion(), source.getSeqNo(), source.getPrimaryTerm()), - source.getScore(), source.getSortValues(), source.getFields(), highlightFields, innerHits, nestedMetaData, + source.getScore(), source.getSortValues(), sourceFields, highlightFields, innerHits, nestedMetaData, explanation, matchedQueries); } @@ -205,8 +206,8 @@ public static SearchDocument from(SearchHit source) { document.setSeqNo(source.getSeqNo()); document.setPrimaryTerm(source.getPrimaryTerm()); - return new SearchDocumentAdapter(document, source.getScore(), source.getSortValues(), source.getFields(), - highlightFields, innerHits, nestedMetaData, explanation, matchedQueries); + return new SearchDocumentAdapter(document, source.getScore(), source.getSortValues(), sourceFields, highlightFields, + innerHits, nestedMetaData, explanation, matchedQueries); } @Nullable diff --git a/src/main/java/org/springframework/data/elasticsearch/core/document/SearchDocumentResponse.java b/src/main/java/org/springframework/data/elasticsearch/core/document/SearchDocumentResponse.java index b321e848a..f45f425c5 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/document/SearchDocumentResponse.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/document/SearchDocumentResponse.java @@ -17,20 +17,28 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.Function; import org.apache.lucene.search.TotalHits; import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.common.text.Text; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.aggregations.Aggregations; import org.springframework.data.elasticsearch.core.AggregationsContainer; import org.springframework.data.elasticsearch.core.clients.elasticsearch7.ElasticsearchAggregations; +import org.springframework.data.elasticsearch.core.suggest.response.CompletionSuggestion; +import org.springframework.data.elasticsearch.core.suggest.response.PhraseSuggestion; +import org.springframework.data.elasticsearch.core.suggest.response.SortBy; +import org.springframework.data.elasticsearch.core.suggest.response.Suggest; +import org.springframework.data.elasticsearch.core.suggest.response.TermSuggestion; +import org.springframework.data.elasticsearch.support.ScoreDoc; import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** * This represents the complete search response from Elasticsearch, including the returned documents. Instances must be - * created with the {@link #from(SearchResponse)} method. + * created with the {@link #from(SearchResponse,Function)} method. * * @author Peter-Josef Meisch * @since 4.0 @@ -42,16 +50,18 @@ public class SearchDocumentResponse { private final float maxScore; private final String scrollId; private final List searchDocuments; - private final AggregationsContainer aggregations; + @Nullable private final AggregationsContainer aggregations; + @Nullable private final Suggest suggest; private SearchDocumentResponse(long totalHits, String totalHitsRelation, float maxScore, String scrollId, - List searchDocuments, Aggregations aggregations) { + List searchDocuments, @Nullable Aggregations aggregations, @Nullable Suggest suggest) { this.totalHits = totalHits; this.totalHitsRelation = totalHitsRelation; this.maxScore = maxScore; this.scrollId = scrollId; this.searchDocuments = searchDocuments; - this.aggregations = new ElasticsearchAggregations(aggregations); + this.aggregations = aggregations != null ? new ElasticsearchAggregations(aggregations) : null; + this.suggest = suggest; } public long getTotalHits() { @@ -74,40 +84,53 @@ public List getSearchDocuments() { return searchDocuments; } + @Nullable public AggregationsContainer getAggregations() { return aggregations; } + @Nullable + public Suggest getSuggest() { + return suggest; + } + /** * creates a SearchDocumentResponse from the {@link SearchResponse} * * @param searchResponse must not be {@literal null} + * @param suggestEntityCreator function to create an entity from a {@link SearchDocument} + * @param entity type * @return the SearchDocumentResponse */ - public static SearchDocumentResponse from(SearchResponse searchResponse) { + public static SearchDocumentResponse from(SearchResponse searchResponse, + Function suggestEntityCreator) { Assert.notNull(searchResponse, "searchResponse must not be null"); - Aggregations aggregations = searchResponse.getAggregations(); - String scrollId = searchResponse.getScrollId(); - SearchHits searchHits = searchResponse.getHits(); + String scrollId = searchResponse.getScrollId(); + Aggregations aggregations = searchResponse.getAggregations(); + org.elasticsearch.search.suggest.Suggest suggest = searchResponse.getSuggest(); - SearchDocumentResponse searchDocumentResponse = from(searchHits, scrollId, aggregations); - return searchDocumentResponse; + return from(searchHits, scrollId, aggregations, suggest, suggestEntityCreator); } /** - * creates a {@link SearchDocumentResponse} from {@link SearchHits} with the given scrollId and aggregations + * creates a {@link SearchDocumentResponse} from {@link SearchHits} with the given scrollId aggregations and suggest * * @param searchHits the {@link SearchHits} to process * @param scrollId scrollId * @param aggregations aggregations + * @param suggestES the suggestion response from Elasticsearch + * @param suggestEntityCreator function to create an entity from a {@link SearchDocument} + * @param entity type * @return the {@link SearchDocumentResponse} - * @since 4.1 + * @since 4.3 */ - public static SearchDocumentResponse from(SearchHits searchHits, @Nullable String scrollId, - @Nullable Aggregations aggregations) { + public static SearchDocumentResponse from(SearchHits searchHits, @Nullable String scrollId, + @Nullable Aggregations aggregations, @Nullable org.elasticsearch.search.suggest.Suggest suggestES, + Function suggestEntityCreator) { + TotalHits responseTotalHits = searchHits.getTotalHits(); long totalHits; @@ -130,7 +153,105 @@ public static SearchDocumentResponse from(SearchHits searchHits, @Nullable Strin } } - return new SearchDocumentResponse(totalHits, totalHitsRelation, maxScore, scrollId, searchDocuments, aggregations); + Suggest suggest = suggestFrom(suggestES, suggestEntityCreator); + return new SearchDocumentResponse(totalHits, totalHitsRelation, maxScore, scrollId, searchDocuments, aggregations, + suggest); + } + + @Nullable + private static Suggest suggestFrom(@Nullable org.elasticsearch.search.suggest.Suggest suggestES, + Function entityCreator) { + + if (suggestES == null) { + return null; + } + + List>> suggestions = new ArrayList<>(); + + for (org.elasticsearch.search.suggest.Suggest.Suggestion> suggestionES : suggestES) { + + if (suggestionES instanceof org.elasticsearch.search.suggest.term.TermSuggestion) { + org.elasticsearch.search.suggest.term.TermSuggestion termSuggestionES = (org.elasticsearch.search.suggest.term.TermSuggestion) suggestionES; + + List entries = new ArrayList<>(); + for (org.elasticsearch.search.suggest.term.TermSuggestion.Entry entryES : termSuggestionES) { + + List options = new ArrayList<>(); + for (org.elasticsearch.search.suggest.term.TermSuggestion.Entry.Option optionES : entryES) { + options.add(new TermSuggestion.Entry.Option(textToString(optionES.getText()), + textToString(optionES.getHighlighted()), optionES.getScore(), optionES.collateMatch(), + optionES.getFreq())); + } + + entries.add(new TermSuggestion.Entry(textToString(entryES.getText()), entryES.getOffset(), + entryES.getLength(), options)); + } + + suggestions.add(new TermSuggestion(termSuggestionES.getName(), termSuggestionES.getSize(), entries, + suggestFrom(termSuggestionES.getSort()))); + } + + if (suggestionES instanceof org.elasticsearch.search.suggest.phrase.PhraseSuggestion) { + org.elasticsearch.search.suggest.phrase.PhraseSuggestion phraseSuggestionES = (org.elasticsearch.search.suggest.phrase.PhraseSuggestion) suggestionES; + + List entries = new ArrayList<>(); + for (org.elasticsearch.search.suggest.phrase.PhraseSuggestion.Entry entryES : phraseSuggestionES) { + + List options = new ArrayList<>(); + for (org.elasticsearch.search.suggest.phrase.PhraseSuggestion.Entry.Option optionES : entryES) { + options.add(new PhraseSuggestion.Entry.Option(textToString(optionES.getText()), + textToString(optionES.getHighlighted()), optionES.getScore(), optionES.collateMatch())); + } + + entries.add(new PhraseSuggestion.Entry(textToString(entryES.getText()), entryES.getOffset(), + entryES.getLength(), options, entryES.getCutoffScore())); + } + + suggestions.add(new PhraseSuggestion(phraseSuggestionES.getName(), phraseSuggestionES.getSize(), entries)); + } + + if (suggestionES instanceof org.elasticsearch.search.suggest.completion.CompletionSuggestion) { + org.elasticsearch.search.suggest.completion.CompletionSuggestion completionSuggestionES = (org.elasticsearch.search.suggest.completion.CompletionSuggestion) suggestionES; + + List> entries = new ArrayList<>(); + for (org.elasticsearch.search.suggest.completion.CompletionSuggestion.Entry entryES : completionSuggestionES) { + + List> options = new ArrayList<>(); + for (org.elasticsearch.search.suggest.completion.CompletionSuggestion.Entry.Option optionES : entryES) { + SearchDocument searchDocument = optionES.getHit() != null ? DocumentAdapters.from(optionES.getHit()) : null; + T hitEntity = searchDocument != null ? entityCreator.apply(searchDocument) : null; + options.add(new CompletionSuggestion.Entry.Option(textToString(optionES.getText()), + textToString(optionES.getHighlighted()), optionES.getScore(), optionES.collateMatch(), + optionES.getContexts(), scoreDocFrom(optionES.getDoc()), searchDocument, hitEntity)); + } + + entries.add(new CompletionSuggestion.Entry(textToString(entryES.getText()), entryES.getOffset(), + entryES.getLength(), options)); + } + + suggestions.add( + new CompletionSuggestion(completionSuggestionES.getName(), completionSuggestionES.getSize(), entries)); + } + } + + return new Suggest(suggestions, suggestES.hasScoreDocs()); + } + + private static SortBy suggestFrom(org.elasticsearch.search.suggest.SortBy sort) { + return SortBy.valueOf(sort.name().toUpperCase()); + } + + @Nullable + private static ScoreDoc scoreDocFrom(@Nullable org.apache.lucene.search.ScoreDoc scoreDoc) { + + if (scoreDoc == null) { + return null; + } + + return new ScoreDoc(scoreDoc.score, scoreDoc.doc, scoreDoc.shardIndex); } + private static String textToString(@Nullable Text text) { + return text != null ? text.string() : ""; + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java index d13dfc3c1..a482f72d7 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java @@ -30,7 +30,7 @@ import org.springframework.data.elasticsearch.annotations.GeoShapeField; import org.springframework.data.elasticsearch.annotations.MultiField; import org.springframework.data.elasticsearch.core.Range; -import org.springframework.data.elasticsearch.core.completion.Completion; +import org.springframework.data.elasticsearch.core.suggest.Completion; import org.springframework.data.elasticsearch.core.convert.DatePersistentPropertyConverter; import org.springframework.data.elasticsearch.core.convert.DateRangePersistentPropertyConverter; import org.springframework.data.elasticsearch.core.convert.ElasticsearchDateConverter; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQuery.java index 63ec22aff..df5b73078 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQuery.java @@ -26,6 +26,7 @@ import org.elasticsearch.search.collapse.CollapseBuilder; import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; import org.elasticsearch.search.sort.SortBuilder; +import org.elasticsearch.search.suggest.SuggestBuilder; import org.springframework.lang.Nullable; /** @@ -54,6 +55,7 @@ public class NativeSearchQuery extends AbstractQuery { @Nullable private HighlightBuilder.Field[] highlightFields; @Nullable private List indicesBoost; @Nullable private SearchTemplateRequestBuilder searchTemplate; + @Nullable private SuggestBuilder suggestBuilder; public NativeSearchQuery(@Nullable QueryBuilder query) { @@ -184,4 +186,19 @@ public SearchTemplateRequestBuilder getSearchTemplate() { public void setSearchTemplate(@Nullable SearchTemplateRequestBuilder searchTemplate) { this.searchTemplate = searchTemplate; } + + /** + * @since 4.3 + */ + public void setSuggestBuilder(SuggestBuilder suggestBuilder) { + this.suggestBuilder = suggestBuilder; + } + + /** + * @since 4.3 + */ + @Nullable + public SuggestBuilder getSuggestBuilder() { + return suggestBuilder; + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQueryBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQueryBuilder.java index f277db90d..2830a6600 100755 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQueryBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQueryBuilder.java @@ -31,6 +31,7 @@ import org.elasticsearch.search.collapse.CollapseBuilder; import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; import org.elasticsearch.search.sort.SortBuilder; +import org.elasticsearch.search.suggest.SuggestBuilder; import org.springframework.data.domain.Pageable; import org.springframework.lang.Nullable; @@ -76,6 +77,7 @@ public class NativeSearchQueryBuilder { @Nullable private Boolean trackTotalHits; @Nullable private Duration timeout; private final List rescorerQueries = new ArrayList<>(); + @Nullable private SuggestBuilder suggestBuilder; public NativeSearchQueryBuilder withQuery(QueryBuilder queryBuilder) { this.queryBuilder = queryBuilder; @@ -311,6 +313,14 @@ public NativeSearchQueryBuilder withRescorerQuery(RescorerQuery rescorerQuery) { return this; } + /** + * @since 4.3 + */ + public NativeSearchQueryBuilder withSuggestBuilder(SuggestBuilder suggestBuilder) { + this.suggestBuilder = suggestBuilder; + return this; + } + public NativeSearchQuery build() { NativeSearchQuery nativeSearchQuery = new NativeSearchQuery( // @@ -393,6 +403,9 @@ public NativeSearchQuery build() { nativeSearchQuery.setRescorerQueries(rescorerQueries); } + if (suggestBuilder != null) { + nativeSearchQuery.setSuggestBuilder(suggestBuilder); + } return nativeSearchQuery; } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/completion/Completion.java b/src/main/java/org/springframework/data/elasticsearch/core/suggest/Completion.java similarity index 62% rename from src/main/java/org/springframework/data/elasticsearch/core/completion/Completion.java rename to src/main/java/org/springframework/data/elasticsearch/core/suggest/Completion.java index 57cbec193..51e412ce5 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/completion/Completion.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/suggest/Completion.java @@ -1,4 +1,19 @@ -package org.springframework.data.elasticsearch.core.completion; +/* + * Copyright 2021 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.suggest; import java.util.List; import java.util.Map; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/suggest/package-info.java b/src/main/java/org/springframework/data/elasticsearch/core/suggest/package-info.java new file mode 100644 index 000000000..a515c8b00 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/suggest/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2021 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. + */ +@org.springframework.lang.NonNullApi +@org.springframework.lang.NonNullFields +package org.springframework.data.elasticsearch.core.suggest; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/CompletionSuggestion.java b/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/CompletionSuggestion.java new file mode 100644 index 000000000..b3ce08fd3 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/CompletionSuggestion.java @@ -0,0 +1,80 @@ +/* + * Copyright 2021 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.suggest.response; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; + +import org.springframework.data.elasticsearch.core.SearchHit; +import org.springframework.data.elasticsearch.core.document.SearchDocument; +import org.springframework.data.elasticsearch.support.ScoreDoc; +import org.springframework.lang.Nullable; + +/** + * @author Peter-Josef Meisch + * @since 4.3 + */ +public class CompletionSuggestion extends Suggest.Suggestion> { + + public CompletionSuggestion(String name, int size, List> entries) { + super(name, size, entries); + } + + public static class Entry extends Suggest.Suggestion.Entry> { + + public Entry(String text, int offset, int length, List> options) { + super(text, offset, length, options); + } + + public static class Option extends Suggest.Suggestion.Entry.Option { + + private final Map> contexts; + private final ScoreDoc scoreDoc; + @Nullable private final SearchDocument searchDocument; + @Nullable private final T hitEntity; + @Nullable private SearchHit searchHit; + + public Option(String text, String highlighted, float score, Boolean collateMatch, + Map> contexts, ScoreDoc scoreDoc, @Nullable SearchDocument searchDocument, + @Nullable T hitEntity) { + super(text, highlighted, score, collateMatch); + this.contexts = contexts; + this.scoreDoc = scoreDoc; + this.searchDocument = searchDocument; + this.hitEntity = hitEntity; + } + + public Map> getContexts() { + return contexts; + } + + public ScoreDoc getScoreDoc() { + return scoreDoc; + } + + @Nullable + public SearchHit getSearchHit() { + return searchHit; + } + + public void updateSearchHit(BiFunction> mapper) { + searchHit = mapper.apply(searchDocument, hitEntity); + } + } + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/PhraseSuggestion.java b/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/PhraseSuggestion.java new file mode 100644 index 000000000..f1646b618 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/suggest/response/PhraseSuggestion.java @@ -0,0 +1,50 @@ +/* + * Copyright 2021 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.suggest.response; + +import java.util.List; + +/** + * @author Peter-Josef Meisch + * @since 4.3 + */ +public class PhraseSuggestion extends Suggest.Suggestion { + + public PhraseSuggestion(String name, int size, List entries) { + super(name, size, entries); + } + + public static class Entry extends Suggest.Suggestion.Entry { + + private final double cutoffScore; + + public Entry(String text, int offset, int length, List