Skip to content

Commit caebe08

Browse files
authored
DATAES-263 - Inner Hits support. (spring-projects#473)
original PR: spring-projects#473
1 parent 859b22d commit caebe08

File tree

11 files changed

+568
-41
lines changed

11 files changed

+568
-41
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ Contains the following information:
134134
* Score
135135
* Sort Values
136136
* Highlight fields
137+
* Inner hits (this is an embedded `SearchHits` object containing eventually returned inner hits)
137138
* The retrieved entity of type <T>
138139

139140
.SearchHits<T>

src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchTemplate.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,7 @@ public ReadSearchDocumentResponseCallback(Class<T> type, IndexCoordinates index)
624624
@Override
625625
public SearchHits<T> doWith(SearchDocumentResponse response) {
626626
List<T> entities = response.getSearchDocuments().stream().map(delegate::doWith).collect(Collectors.toList());
627-
return SearchHitMapping.mappingFor(type, elasticsearchConverter.getMappingContext()).mapHits(response, entities);
627+
return SearchHitMapping.mappingFor(type, elasticsearchConverter).mapHits(response, entities);
628628
}
629629
}
630630

@@ -644,8 +644,7 @@ public ReadSearchScrollDocumentResponseCallback(Class<T> type, IndexCoordinates
644644
@Override
645645
public SearchScrollHits<T> doWith(SearchDocumentResponse response) {
646646
List<T> entities = response.getSearchDocuments().stream().map(delegate::doWith).collect(Collectors.toList());
647-
return SearchHitMapping.mappingFor(type, elasticsearchConverter.getMappingContext()).mapScrollHits(response,
648-
entities);
647+
return SearchHitMapping.mappingFor(type, elasticsearchConverter).mapScrollHits(response, entities);
649648
}
650649
}
651650
// endregion

src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -878,7 +878,7 @@ public ReadSearchDocumentCallback(Class<T> type, IndexCoordinates index) {
878878
@Override
879879
public Mono<SearchHit<T>> doWith(SearchDocument response) {
880880
return delegate.doWith(response)
881-
.map(entity -> SearchHitMapping.mappingFor(type, converter.getMappingContext()).mapHit(response, entity));
881+
.map(entity -> SearchHitMapping.mappingFor(type, converter).mapHit(response, entity));
882882
}
883883
}
884884

src/main/java/org/springframework/data/elasticsearch/core/SearchHit.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Map;
2424
import java.util.stream.Collectors;
2525

26+
import org.springframework.data.elasticsearch.core.document.NestedMetaData;
2627
import org.springframework.lang.Nullable;
2728
import org.springframework.util.Assert;
2829

@@ -41,17 +42,32 @@ public class SearchHit<T> {
4142
private final List<Object> sortValues;
4243
private final T content;
4344
private final Map<String, List<String>> highlightFields = new LinkedHashMap<>();
45+
private final Map<String, SearchHits<?>> innerHits = new LinkedHashMap<>();
46+
@Nullable private final NestedMetaData nestedMetaData;
4447

4548
public SearchHit(@Nullable String index, @Nullable String id, float score, @Nullable Object[] sortValues,
4649
@Nullable Map<String, List<String>> highlightFields, T content) {
50+
this(index, id, score, sortValues, highlightFields, null, null, content);
51+
}
52+
53+
public SearchHit(@Nullable String index, @Nullable String id, float score, @Nullable Object[] sortValues,
54+
@Nullable Map<String, List<String>> highlightFields, @Nullable Map<String, SearchHits<?>> innerHits,
55+
@Nullable NestedMetaData nestedMetaData, T content) {
4756
this.index = index;
4857
this.id = id;
4958
this.score = score;
5059
this.sortValues = (sortValues != null) ? Arrays.asList(sortValues) : new ArrayList<>();
60+
5161
if (highlightFields != null) {
5262
this.highlightFields.putAll(highlightFields);
5363
}
5464

65+
if (innerHits != null) {
66+
this.innerHits.putAll(innerHits);
67+
}
68+
69+
this.nestedMetaData = nestedMetaData;
70+
5571
this.content = content;
5672
}
5773

@@ -90,6 +106,9 @@ public List<Object> getSortValues() {
90106
return Collections.unmodifiableList(sortValues);
91107
}
92108

109+
/**
110+
* @return the map from field names to highlight values, never {@literal null}
111+
*/
93112
public Map<String, List<String>> getHighlightFields() {
94113
return Collections.unmodifiableMap(highlightFields.entrySet().stream()
95114
.collect(Collectors.toMap(Map.Entry::getKey, entry -> Collections.unmodifiableList(entry.getValue()))));
@@ -108,6 +127,39 @@ public List<String> getHighlightField(String field) {
108127
return Collections.unmodifiableList(highlightFields.getOrDefault(field, Collections.emptyList()));
109128
}
110129

130+
/**
131+
* returns the {@link SearchHits} for the inner hits with the given name. If the inner hits could be mapped to a
132+
* nested entity class, the returned data will be of this type, otherwise
133+
* {{@link org.springframework.data.elasticsearch.core.document.SearchDocument}} instances are returned in this
134+
* {@link SearchHits} object.
135+
*
136+
* @param name the inner hits name
137+
* @return {@link SearchHits} if available, otherwise {@literal null}
138+
*/
139+
@Nullable
140+
public SearchHits<?> getInnerHits(String name) {
141+
return innerHits.get(name);
142+
}
143+
144+
/**
145+
* @return the map from inner_hits names to inner hits, in a {@link SearchHits} object, never {@literal null}
146+
* @since 4.1
147+
*/
148+
public Map<String, SearchHits<?>> getInnerHits() {
149+
return innerHits;
150+
}
151+
152+
/**
153+
* If this is a nested inner hit, return the nested metadata information
154+
*
155+
* @return {{@link NestedMetaData}
156+
* @since 4.1
157+
*/
158+
@Nullable
159+
public NestedMetaData getNestedMetaData() {
160+
return nestedMetaData;
161+
}
162+
111163
@Override
112164
public String toString() {
113165
return "SearchHit{" + "id='" + id + '\'' + ", score=" + score + ", sortValues=" + sortValues + ", content="

src/main/java/org/springframework/data/elasticsearch/core/SearchHitMapping.java

Lines changed: 159 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,18 @@
1616
package org.springframework.data.elasticsearch.core;
1717

1818
import java.util.ArrayList;
19+
import java.util.LinkedHashMap;
20+
import java.util.LinkedList;
1921
import java.util.List;
2022
import java.util.Map;
2123
import java.util.stream.Collectors;
2224

2325
import org.elasticsearch.search.aggregations.Aggregations;
26+
import org.slf4j.Logger;
27+
import org.slf4j.LoggerFactory;
28+
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
29+
import org.springframework.data.elasticsearch.core.document.Document;
30+
import org.springframework.data.elasticsearch.core.document.NestedMetaData;
2431
import org.springframework.data.elasticsearch.core.document.SearchDocument;
2532
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
2633
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
@@ -39,36 +46,24 @@
3946
* @since 4.0
4047
*/
4148
class SearchHitMapping<T> {
49+
50+
private static final Logger LOGGER = LoggerFactory.getLogger(SearchHitMapping.class);
51+
4252
private final Class<T> type;
53+
private final ElasticsearchConverter converter;
4354
private final MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext;
4455

45-
private SearchHitMapping(Class<T> type,
46-
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> context) {
47-
56+
private SearchHitMapping(Class<T> type, ElasticsearchConverter converter) {
4857
Assert.notNull(type, "type is null");
49-
Assert.notNull(context, "context is null");
58+
Assert.notNull(converter, "converter is null");
5059

5160
this.type = type;
52-
this.mappingContext = context;
61+
this.converter = converter;
62+
this.mappingContext = converter.getMappingContext();
5363
}
5464

55-
static <T> SearchHitMapping<T> mappingFor(Class<T> entityClass,
56-
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> context) {
57-
return new SearchHitMapping<>(entityClass, context);
58-
}
59-
60-
SearchHit<T> mapHit(SearchDocument searchDocument, T content) {
61-
62-
Assert.notNull(searchDocument, "searchDocument is null");
63-
Assert.notNull(content, "content is null");
64-
65-
String index = searchDocument.getIndex();
66-
String id = searchDocument.hasId() ? searchDocument.getId() : null;
67-
float score = searchDocument.getScore();
68-
Object[] sortValues = searchDocument.getSortValues();
69-
Map<String, List<String>> highlightFields = getHighlightsAndRemapFieldNames(searchDocument);
70-
71-
return new SearchHit<>(index, id, score, sortValues, highlightFields, content);
65+
static <T> SearchHitMapping<T> mappingFor(Class<T> entityClass, ElasticsearchConverter converter) {
66+
return new SearchHitMapping<>(entityClass, converter);
7267
}
7368

7469
SearchHits<T> mapHits(SearchDocumentResponse searchDocumentResponse, List<T> contents) {
@@ -105,6 +100,21 @@ private SearchHitsImpl<T> mapHitsFromResponse(SearchDocumentResponse searchDocum
105100
return new SearchHitsImpl<>(totalHits, totalHitsRelation, maxScore, scrollId, searchHits, aggregations);
106101
}
107102

103+
SearchHit<T> mapHit(SearchDocument searchDocument, T content) {
104+
105+
Assert.notNull(searchDocument, "searchDocument is null");
106+
Assert.notNull(content, "content is null");
107+
108+
return new SearchHit<T>(searchDocument.getIndex(), //
109+
searchDocument.hasId() ? searchDocument.getId() : null, //
110+
searchDocument.getScore(), //
111+
searchDocument.getSortValues(), //
112+
getHighlightsAndRemapFieldNames(searchDocument), //
113+
mapInnerHits(searchDocument), //
114+
searchDocument.getNestedMetaData(), //
115+
content); //
116+
}
117+
108118
@Nullable
109119
private Map<String, List<String>> getHighlightsAndRemapFieldNames(SearchDocument searchDocument) {
110120
Map<String, List<String>> highlightFields = searchDocument.getHighlightFields();
@@ -123,4 +133,131 @@ private Map<String, List<String>> getHighlightsAndRemapFieldNames(SearchDocument
123133
return property != null ? property.getName() : entry.getKey();
124134
}, Map.Entry::getValue));
125135
}
136+
137+
private Map<String, SearchHits<?>> mapInnerHits(SearchDocument searchDocument) {
138+
139+
Map<String, SearchHits<?>> innerHits = new LinkedHashMap<>();
140+
Map<String, SearchDocumentResponse> documentInnerHits = searchDocument.getInnerHits();
141+
142+
if (documentInnerHits != null && documentInnerHits.size() > 0) {
143+
144+
SearchHitMapping<SearchDocument> searchDocumentSearchHitMapping = SearchHitMapping
145+
.mappingFor(SearchDocument.class, converter);
146+
147+
for (Map.Entry<String, SearchDocumentResponse> entry : documentInnerHits.entrySet()) {
148+
SearchDocumentResponse searchDocumentResponse = entry.getValue();
149+
150+
SearchHits<SearchDocument> searchHits = searchDocumentSearchHitMapping
151+
.mapHitsFromResponse(searchDocumentResponse, searchDocumentResponse.getSearchDocuments());
152+
153+
// map Documents to real objects
154+
SearchHits<?> mappedSearchHits = mapInnerDocuments(searchHits, type);
155+
156+
innerHits.put(entry.getKey(), mappedSearchHits);
157+
}
158+
159+
}
160+
return innerHits;
161+
}
162+
163+
/**
164+
* try to convert the SearchDocument instances to instances of the inner property class.
165+
*
166+
* @param searchHits {@link SearchHits} containing {@link Document} instances
167+
* @param type the class of the containing class
168+
* @return a new {@link SearchHits} instance containing the mapped objects or the original inout if any error occurs
169+
*/
170+
private SearchHits<?> mapInnerDocuments(SearchHits<SearchDocument> searchHits, Class<T> type) {
171+
172+
if (searchHits.getTotalHits() == 0) {
173+
return searchHits;
174+
}
175+
176+
try {
177+
NestedMetaData nestedMetaData = searchHits.getSearchHit(0).getContent().getNestedMetaData();
178+
ElasticsearchPersistentEntityWithNestedMetaData persistentEntityWithNestedMetaData = getPersistentEntity(
179+
mappingContext.getPersistentEntity(type), nestedMetaData);
180+
181+
List<SearchHit<Object>> convertedSearchHits = new ArrayList<>();
182+
183+
if (persistentEntityWithNestedMetaData.entity != null) {
184+
Class<?> targetType = persistentEntityWithNestedMetaData.entity.getType();
185+
// convert the list of SearchHit<SearchDocument> to list of SearchHit<Object>
186+
searchHits.getSearchHits().forEach(searchHit -> {
187+
SearchDocument searchDocument = searchHit.getContent();
188+
189+
Object targetObject = converter.read(targetType, searchDocument);
190+
convertedSearchHits.add(new SearchHit<Object>(searchDocument.getIndex(), //
191+
searchDocument.getId(), //
192+
searchDocument.getScore(), //
193+
searchDocument.getSortValues(), //
194+
searchDocument.getHighlightFields(), //
195+
mapInnerHits(searchDocument), //
196+
persistentEntityWithNestedMetaData.nestedMetaData, //
197+
targetObject));
198+
});
199+
200+
String scrollId = null;
201+
if (searchHits instanceof SearchHitsImpl) {
202+
scrollId = ((SearchHitsImpl<?>) searchHits).getScrollId();
203+
}
204+
205+
return new SearchHitsImpl<>(searchHits.getTotalHits(), //
206+
searchHits.getTotalHitsRelation(), //
207+
searchHits.getMaxScore(), //
208+
scrollId, //
209+
convertedSearchHits, //
210+
searchHits.getAggregations());
211+
}
212+
} catch (Exception e) {
213+
LOGGER.warn("Could not map inner_hits", e);
214+
}
215+
216+
return searchHits;
217+
}
218+
219+
/**
220+
* find a {@link ElasticsearchPersistentEntity} following the property chain defined by the nested metadata
221+
*
222+
* @param persistentEntity base entity
223+
* @param nestedMetaData nested metadata
224+
* @return The found entity or null
225+
*/
226+
@Nullable
227+
private ElasticsearchPersistentEntityWithNestedMetaData getPersistentEntity(
228+
@Nullable ElasticsearchPersistentEntity<?> persistentEntity, @Nullable NestedMetaData nestedMetaData) {
229+
230+
NestedMetaData currentMetaData = nestedMetaData;
231+
List<NestedMetaData> mappedNestedMetaDatas = new LinkedList<>();
232+
233+
while (persistentEntity != null && currentMetaData != null) {
234+
ElasticsearchPersistentProperty persistentProperty = persistentEntity
235+
.getPersistentPropertyWithFieldName(currentMetaData.getField());
236+
237+
if (persistentProperty == null) {
238+
persistentEntity = null;
239+
} else {
240+
persistentEntity = mappingContext.getPersistentEntity(persistentProperty.getActualType());
241+
mappedNestedMetaDatas.add(0,
242+
NestedMetaData.of(persistentProperty.getName(), currentMetaData.getOffset(), null));
243+
currentMetaData = currentMetaData.getChild();
244+
}
245+
}
246+
247+
NestedMetaData mappedNestedMetaData = mappedNestedMetaDatas.stream().reduce(null,
248+
(result, nmd) -> NestedMetaData.of(nmd.getField(), nmd.getOffset(), result));
249+
250+
return new ElasticsearchPersistentEntityWithNestedMetaData(persistentEntity, mappedNestedMetaData);
251+
}
252+
253+
private static class ElasticsearchPersistentEntityWithNestedMetaData {
254+
@Nullable private ElasticsearchPersistentEntity<?> entity;
255+
private NestedMetaData nestedMetaData;
256+
257+
public ElasticsearchPersistentEntityWithNestedMetaData(@Nullable ElasticsearchPersistentEntity<?> entity,
258+
NestedMetaData nestedMetaData) {
259+
this.entity = entity;
260+
this.nestedMetaData = nestedMetaData;
261+
}
262+
}
126263
}

0 commit comments

Comments
 (0)