|
17 | 17 |
|
18 | 18 | import co.elastic.clients.elasticsearch._types.aggregations.Aggregate;
|
19 | 19 | import co.elastic.clients.elasticsearch.core.SearchResponse;
|
20 |
| -import co.elastic.clients.elasticsearch.core.search.Hit; |
21 |
| -import co.elastic.clients.elasticsearch.core.search.HitsMetadata; |
22 |
| -import co.elastic.clients.elasticsearch.core.search.ResponseBody; |
23 |
| -import co.elastic.clients.elasticsearch.core.search.Suggestion; |
24 |
| -import co.elastic.clients.elasticsearch.core.search.TotalHits; |
| 20 | +import co.elastic.clients.elasticsearch.core.search.*; |
25 | 21 | import co.elastic.clients.json.JsonpMapper;
|
26 | 22 |
|
27 | 23 | import java.util.ArrayList;
|
| 24 | +import java.util.HashMap; |
28 | 25 | import java.util.List;
|
29 | 26 | import java.util.Map;
|
| 27 | +import java.util.Set; |
| 28 | +import java.util.stream.Collectors; |
30 | 29 |
|
| 30 | +import org.apache.commons.logging.Log; |
| 31 | +import org.apache.commons.logging.LogFactory; |
31 | 32 | import org.elasticsearch.search.SearchHits;
|
32 | 33 | import org.springframework.data.elasticsearch.core.TotalHitsRelation;
|
33 | 34 | import org.springframework.data.elasticsearch.core.document.SearchDocument;
|
34 | 35 | import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
|
| 36 | +import org.springframework.data.elasticsearch.core.suggest.response.CompletionSuggestion; |
| 37 | +import org.springframework.data.elasticsearch.core.suggest.response.PhraseSuggestion; |
35 | 38 | import org.springframework.data.elasticsearch.core.suggest.response.Suggest;
|
| 39 | +import org.springframework.data.elasticsearch.core.suggest.response.TermSuggestion; |
| 40 | +import org.springframework.data.elasticsearch.support.ScoreDoc; |
36 | 41 | import org.springframework.lang.Nullable;
|
37 | 42 | import org.springframework.util.Assert;
|
| 43 | +import org.springframework.util.CollectionUtils; |
38 | 44 |
|
39 | 45 | /**
|
40 | 46 | * Factory class to create {@link SearchDocumentResponse} instances.
|
|
43 | 49 | * @since 4.4
|
44 | 50 | */
|
45 | 51 | class SearchDocumentResponseBuilder {
|
| 52 | + |
| 53 | + private static final Log LOGGER = LogFactory.getLog(SearchDocumentResponseBuilder.class); |
| 54 | + |
46 | 55 | /**
|
47 | 56 | * creates a SearchDocumentResponse from the {@link SearchResponse}
|
48 | 57 | *
|
@@ -80,7 +89,7 @@ public static <T> SearchDocumentResponse from(ResponseBody<EntityAsMap> response
|
80 | 89 | * @return the {@link SearchDocumentResponse}
|
81 | 90 | */
|
82 | 91 | public static <T> SearchDocumentResponse from(HitsMetadata<?> hitsMetadata, @Nullable String scrollId,
|
83 |
| - Map<String, Aggregate> aggregations, Map<String, List<Suggestion<EntityAsMap>>> suggestES, |
| 92 | + @Nullable Map<String, Aggregate> aggregations, Map<String, List<Suggestion<EntityAsMap>>> suggestES, |
84 | 93 | SearchDocumentResponse.EntityCreator<T> entityCreator, JsonpMapper jsonpMapper) {
|
85 | 94 |
|
86 | 95 | Assert.notNull(hitsMetadata, "hitsMetadata must not be null");
|
@@ -116,10 +125,116 @@ public static <T> SearchDocumentResponse from(HitsMetadata<?> hitsMetadata, @Nul
|
116 | 125 | ElasticsearchAggregations aggregationsContainer = aggregations != null ? new ElasticsearchAggregations(aggregations)
|
117 | 126 | : null;
|
118 | 127 |
|
119 |
| - // todo #2154 |
120 |
| - Suggest suggest = null; |
| 128 | + Suggest suggest = suggestFrom(suggestES, entityCreator); |
121 | 129 |
|
122 | 130 | return new SearchDocumentResponse(totalHits, totalHitsRelation, maxScore, scrollId, searchDocuments,
|
123 | 131 | aggregationsContainer, suggest);
|
124 | 132 | }
|
| 133 | + |
| 134 | + @Nullable |
| 135 | + private static <T> Suggest suggestFrom(Map<String, List<Suggestion<EntityAsMap>>> suggestES, |
| 136 | + SearchDocumentResponse.EntityCreator<T> entityCreator) { |
| 137 | + |
| 138 | + if (CollectionUtils.isEmpty(suggestES)) { |
| 139 | + return null; |
| 140 | + } |
| 141 | + |
| 142 | + List<Suggest.Suggestion<? extends Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option>>> suggestions = new ArrayList<>(); |
| 143 | + |
| 144 | + suggestES.forEach((name, suggestionsES) -> { |
| 145 | + |
| 146 | + if (!suggestionsES.isEmpty()) { |
| 147 | + // take the type from the first entry |
| 148 | + switch (suggestionsES.get(0)._kind()) { |
| 149 | + case Term: { |
| 150 | + suggestions.add(getTermSuggestion(name, suggestionsES)); |
| 151 | + break; |
| 152 | + } |
| 153 | + case Phrase: { |
| 154 | + suggestions.add(getPhraseSuggestion(name, suggestionsES)); |
| 155 | + break; |
| 156 | + } |
| 157 | + case Completion: { |
| 158 | + suggestions.add(getCompletionSuggestion(name, suggestionsES, entityCreator)); |
| 159 | + break; |
| 160 | + } |
| 161 | + default: |
| 162 | + break; |
| 163 | + } |
| 164 | + } |
| 165 | + }); |
| 166 | + |
| 167 | + // todo: hasScoreDocs checks if any one |
| 168 | + boolean hasScoreDocs = false; |
| 169 | + |
| 170 | + return new Suggest(suggestions, hasScoreDocs); |
| 171 | + } |
| 172 | + |
| 173 | + private static TermSuggestion getTermSuggestion(String name, List<Suggestion<EntityAsMap>> suggestionsES) { |
| 174 | + |
| 175 | + List<TermSuggestion.Entry> entries = new ArrayList<>(); |
| 176 | + suggestionsES.forEach(suggestionES -> { |
| 177 | + TermSuggest termSuggest = suggestionES.term(); |
| 178 | + |
| 179 | + TermSuggestOption optionES = termSuggest.options(); |
| 180 | + List<TermSuggestion.Entry.Option> options = new ArrayList<>(); |
| 181 | + options.add(new TermSuggestion.Entry.Option(optionES.text(), null, optionES.score(), null, |
| 182 | + Math.toIntExact(optionES.freq()))); |
| 183 | + entries.add(new TermSuggestion.Entry(termSuggest.text(), termSuggest.offset(), termSuggest.length(), options)); |
| 184 | + }); |
| 185 | + return new TermSuggestion(name, suggestionsES.size(), entries, null); |
| 186 | + } |
| 187 | + |
| 188 | + private static PhraseSuggestion getPhraseSuggestion(String name, List<Suggestion<EntityAsMap>> suggestionsES) { |
| 189 | + |
| 190 | + List<PhraseSuggestion.Entry> entries = new ArrayList<>(); |
| 191 | + suggestionsES.forEach(suggestionES -> { |
| 192 | + PhraseSuggest phraseSuggest = suggestionES.phrase(); |
| 193 | + PhraseSuggestOption optionES = phraseSuggest.options(); |
| 194 | + List<PhraseSuggestion.Entry.Option> options = new ArrayList<>(); |
| 195 | + options.add(new PhraseSuggestion.Entry.Option(optionES.text(), optionES.highlighted(), null, null)); |
| 196 | + entries.add(new PhraseSuggestion.Entry(phraseSuggest.text(), phraseSuggest.offset(), phraseSuggest.length(), |
| 197 | + options, null)); |
| 198 | + }); |
| 199 | + return new PhraseSuggestion(name, suggestionsES.size(), entries); |
| 200 | + } |
| 201 | + |
| 202 | + private static <T> CompletionSuggestion<T> getCompletionSuggestion(String name, |
| 203 | + List<Suggestion<EntityAsMap>> suggestionsES, SearchDocumentResponse.EntityCreator<T> entityCreator) { |
| 204 | + List<CompletionSuggestion.Entry<T>> entries = new ArrayList<>(); |
| 205 | + suggestionsES.forEach(suggestionES -> { |
| 206 | + CompletionSuggest<EntityAsMap> completionSuggest = suggestionES.completion(); |
| 207 | + List<CompletionSuggestion.Entry.Option<T>> options = new ArrayList<>(); |
| 208 | + List<CompletionSuggestOption<EntityAsMap>> optionsES = completionSuggest.options(); |
| 209 | + optionsES.forEach(optionES -> { |
| 210 | + SearchDocument searchDocument = (optionES.source() != null) ? DocumentAdapters.from(optionES) : null; |
| 211 | + T hitEntity = null; |
| 212 | + |
| 213 | + if (searchDocument != null) { |
| 214 | + try { |
| 215 | + hitEntity = entityCreator.apply(searchDocument).get(); |
| 216 | + } catch (Exception e) { |
| 217 | + if (LOGGER.isWarnEnabled()) { |
| 218 | + LOGGER.warn("Error creating entity from SearchDocument: " + e.getMessage()); |
| 219 | + } |
| 220 | + } |
| 221 | + } |
| 222 | + |
| 223 | + Map<String, Set<String>> contexts = new HashMap<>(); |
| 224 | + optionES.contexts().forEach((key, contextList) -> contexts.put(key, |
| 225 | + contextList.stream().map(context -> context._get().toString()).collect(Collectors.toSet()))); |
| 226 | + |
| 227 | + // response from the new client does not have a doc and shardindex as the ScoreDoc from the old client responses |
| 228 | + |
| 229 | + options.add(new CompletionSuggestion.Entry.Option<>(optionES.text(), null, optionES.score(), |
| 230 | + optionES.collateMatch() != null ? optionES.collateMatch() : false, contexts, |
| 231 | + new ScoreDoc(optionES.score() != null ? optionES.score() : Double.NaN, null, null), searchDocument, |
| 232 | + hitEntity)); |
| 233 | + }); |
| 234 | + |
| 235 | + entries.add(new CompletionSuggestion.Entry<>(completionSuggest.text(), completionSuggest.offset(), |
| 236 | + completionSuggest.length(), options)); |
| 237 | + }); |
| 238 | + return new CompletionSuggestion<>(name, suggestionsES.size(), entries); |
| 239 | + } |
125 | 240 | }
|
0 commit comments