mirror of
https://github.com/spring-projects/spring-data-elasticsearch.git
synced 2025-05-31 09:12:11 +00:00
parent
859b22db8e
commit
caebe08cf2
@ -134,6 +134,7 @@ Contains the following information:
|
||||
* Score
|
||||
* Sort Values
|
||||
* Highlight fields
|
||||
* Inner hits (this is an embedded `SearchHits` object containing eventually returned inner hits)
|
||||
* The retrieved entity of type <T>
|
||||
|
||||
.SearchHits<T>
|
||||
|
@ -624,7 +624,7 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
|
||||
@Override
|
||||
public SearchHits<T> doWith(SearchDocumentResponse response) {
|
||||
List<T> entities = response.getSearchDocuments().stream().map(delegate::doWith).collect(Collectors.toList());
|
||||
return SearchHitMapping.mappingFor(type, elasticsearchConverter.getMappingContext()).mapHits(response, entities);
|
||||
return SearchHitMapping.mappingFor(type, elasticsearchConverter).mapHits(response, entities);
|
||||
}
|
||||
}
|
||||
|
||||
@ -644,8 +644,7 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
|
||||
@Override
|
||||
public SearchScrollHits<T> doWith(SearchDocumentResponse response) {
|
||||
List<T> entities = response.getSearchDocuments().stream().map(delegate::doWith).collect(Collectors.toList());
|
||||
return SearchHitMapping.mappingFor(type, elasticsearchConverter.getMappingContext()).mapScrollHits(response,
|
||||
entities);
|
||||
return SearchHitMapping.mappingFor(type, elasticsearchConverter).mapScrollHits(response, entities);
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
@ -878,7 +878,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
|
||||
@Override
|
||||
public Mono<SearchHit<T>> doWith(SearchDocument response) {
|
||||
return delegate.doWith(response)
|
||||
.map(entity -> SearchHitMapping.mappingFor(type, converter.getMappingContext()).mapHit(response, entity));
|
||||
.map(entity -> SearchHitMapping.mappingFor(type, converter).mapHit(response, entity));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.data.elasticsearch.core.document.NestedMetaData;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@ -41,17 +42,32 @@ public class SearchHit<T> {
|
||||
private final List<Object> sortValues;
|
||||
private final T content;
|
||||
private final Map<String, List<String>> highlightFields = new LinkedHashMap<>();
|
||||
private final Map<String, SearchHits<?>> innerHits = new LinkedHashMap<>();
|
||||
@Nullable private final NestedMetaData nestedMetaData;
|
||||
|
||||
public SearchHit(@Nullable String index, @Nullable String id, float score, @Nullable Object[] sortValues,
|
||||
@Nullable Map<String, List<String>> highlightFields, T content) {
|
||||
this(index, id, score, sortValues, highlightFields, null, null, content);
|
||||
}
|
||||
|
||||
public SearchHit(@Nullable String index, @Nullable String id, float score, @Nullable Object[] sortValues,
|
||||
@Nullable Map<String, List<String>> highlightFields, @Nullable Map<String, SearchHits<?>> innerHits,
|
||||
@Nullable NestedMetaData nestedMetaData, T content) {
|
||||
this.index = index;
|
||||
this.id = id;
|
||||
this.score = score;
|
||||
this.sortValues = (sortValues != null) ? Arrays.asList(sortValues) : new ArrayList<>();
|
||||
|
||||
if (highlightFields != null) {
|
||||
this.highlightFields.putAll(highlightFields);
|
||||
}
|
||||
|
||||
if (innerHits != null) {
|
||||
this.innerHits.putAll(innerHits);
|
||||
}
|
||||
|
||||
this.nestedMetaData = nestedMetaData;
|
||||
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
@ -90,6 +106,9 @@ public class SearchHit<T> {
|
||||
return Collections.unmodifiableList(sortValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the map from field names to highlight values, never {@literal null}
|
||||
*/
|
||||
public Map<String, List<String>> getHighlightFields() {
|
||||
return Collections.unmodifiableMap(highlightFields.entrySet().stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, entry -> Collections.unmodifiableList(entry.getValue()))));
|
||||
@ -108,6 +127,39 @@ public class SearchHit<T> {
|
||||
return Collections.unmodifiableList(highlightFields.getOrDefault(field, Collections.emptyList()));
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the {@link SearchHits} for the inner hits with the given name. If the inner hits could be mapped to a
|
||||
* nested entity class, the returned data will be of this type, otherwise
|
||||
* {{@link org.springframework.data.elasticsearch.core.document.SearchDocument}} instances are returned in this
|
||||
* {@link SearchHits} object.
|
||||
*
|
||||
* @param name the inner hits name
|
||||
* @return {@link SearchHits} if available, otherwise {@literal null}
|
||||
*/
|
||||
@Nullable
|
||||
public SearchHits<?> getInnerHits(String name) {
|
||||
return innerHits.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the map from inner_hits names to inner hits, in a {@link SearchHits} object, never {@literal null}
|
||||
* @since 4.1
|
||||
*/
|
||||
public Map<String, SearchHits<?>> getInnerHits() {
|
||||
return innerHits;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is a nested inner hit, return the nested metadata information
|
||||
*
|
||||
* @return {{@link NestedMetaData}
|
||||
* @since 4.1
|
||||
*/
|
||||
@Nullable
|
||||
public NestedMetaData getNestedMetaData() {
|
||||
return nestedMetaData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SearchHit{" + "id='" + id + '\'' + ", score=" + score + ", sortValues=" + sortValues + ", content="
|
||||
|
@ -16,11 +16,18 @@
|
||||
package org.springframework.data.elasticsearch.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.elasticsearch.search.aggregations.Aggregations;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
|
||||
import org.springframework.data.elasticsearch.core.document.Document;
|
||||
import org.springframework.data.elasticsearch.core.document.NestedMetaData;
|
||||
import org.springframework.data.elasticsearch.core.document.SearchDocument;
|
||||
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
|
||||
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
|
||||
@ -39,36 +46,24 @@ import org.springframework.util.Assert;
|
||||
* @since 4.0
|
||||
*/
|
||||
class SearchHitMapping<T> {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SearchHitMapping.class);
|
||||
|
||||
private final Class<T> type;
|
||||
private final ElasticsearchConverter converter;
|
||||
private final MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext;
|
||||
|
||||
private SearchHitMapping(Class<T> type,
|
||||
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> context) {
|
||||
|
||||
private SearchHitMapping(Class<T> type, ElasticsearchConverter converter) {
|
||||
Assert.notNull(type, "type is null");
|
||||
Assert.notNull(context, "context is null");
|
||||
Assert.notNull(converter, "converter is null");
|
||||
|
||||
this.type = type;
|
||||
this.mappingContext = context;
|
||||
this.converter = converter;
|
||||
this.mappingContext = converter.getMappingContext();
|
||||
}
|
||||
|
||||
static <T> SearchHitMapping<T> mappingFor(Class<T> entityClass,
|
||||
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> context) {
|
||||
return new SearchHitMapping<>(entityClass, context);
|
||||
}
|
||||
|
||||
SearchHit<T> mapHit(SearchDocument searchDocument, T content) {
|
||||
|
||||
Assert.notNull(searchDocument, "searchDocument is null");
|
||||
Assert.notNull(content, "content is null");
|
||||
|
||||
String index = searchDocument.getIndex();
|
||||
String id = searchDocument.hasId() ? searchDocument.getId() : null;
|
||||
float score = searchDocument.getScore();
|
||||
Object[] sortValues = searchDocument.getSortValues();
|
||||
Map<String, List<String>> highlightFields = getHighlightsAndRemapFieldNames(searchDocument);
|
||||
|
||||
return new SearchHit<>(index, id, score, sortValues, highlightFields, content);
|
||||
static <T> SearchHitMapping<T> mappingFor(Class<T> entityClass, ElasticsearchConverter converter) {
|
||||
return new SearchHitMapping<>(entityClass, converter);
|
||||
}
|
||||
|
||||
SearchHits<T> mapHits(SearchDocumentResponse searchDocumentResponse, List<T> contents) {
|
||||
@ -105,6 +100,21 @@ class SearchHitMapping<T> {
|
||||
return new SearchHitsImpl<>(totalHits, totalHitsRelation, maxScore, scrollId, searchHits, aggregations);
|
||||
}
|
||||
|
||||
SearchHit<T> mapHit(SearchDocument searchDocument, T content) {
|
||||
|
||||
Assert.notNull(searchDocument, "searchDocument is null");
|
||||
Assert.notNull(content, "content is null");
|
||||
|
||||
return new SearchHit<T>(searchDocument.getIndex(), //
|
||||
searchDocument.hasId() ? searchDocument.getId() : null, //
|
||||
searchDocument.getScore(), //
|
||||
searchDocument.getSortValues(), //
|
||||
getHighlightsAndRemapFieldNames(searchDocument), //
|
||||
mapInnerHits(searchDocument), //
|
||||
searchDocument.getNestedMetaData(), //
|
||||
content); //
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Map<String, List<String>> getHighlightsAndRemapFieldNames(SearchDocument searchDocument) {
|
||||
Map<String, List<String>> highlightFields = searchDocument.getHighlightFields();
|
||||
@ -123,4 +133,131 @@ class SearchHitMapping<T> {
|
||||
return property != null ? property.getName() : entry.getKey();
|
||||
}, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
private Map<String, SearchHits<?>> mapInnerHits(SearchDocument searchDocument) {
|
||||
|
||||
Map<String, SearchHits<?>> innerHits = new LinkedHashMap<>();
|
||||
Map<String, SearchDocumentResponse> documentInnerHits = searchDocument.getInnerHits();
|
||||
|
||||
if (documentInnerHits != null && documentInnerHits.size() > 0) {
|
||||
|
||||
SearchHitMapping<SearchDocument> searchDocumentSearchHitMapping = SearchHitMapping
|
||||
.mappingFor(SearchDocument.class, converter);
|
||||
|
||||
for (Map.Entry<String, SearchDocumentResponse> entry : documentInnerHits.entrySet()) {
|
||||
SearchDocumentResponse searchDocumentResponse = entry.getValue();
|
||||
|
||||
SearchHits<SearchDocument> searchHits = searchDocumentSearchHitMapping
|
||||
.mapHitsFromResponse(searchDocumentResponse, searchDocumentResponse.getSearchDocuments());
|
||||
|
||||
// map Documents to real objects
|
||||
SearchHits<?> mappedSearchHits = mapInnerDocuments(searchHits, type);
|
||||
|
||||
innerHits.put(entry.getKey(), mappedSearchHits);
|
||||
}
|
||||
|
||||
}
|
||||
return innerHits;
|
||||
}
|
||||
|
||||
/**
|
||||
* try to convert the SearchDocument instances to instances of the inner property class.
|
||||
*
|
||||
* @param searchHits {@link SearchHits} containing {@link Document} instances
|
||||
* @param type the class of the containing class
|
||||
* @return a new {@link SearchHits} instance containing the mapped objects or the original inout if any error occurs
|
||||
*/
|
||||
private SearchHits<?> mapInnerDocuments(SearchHits<SearchDocument> searchHits, Class<T> type) {
|
||||
|
||||
if (searchHits.getTotalHits() == 0) {
|
||||
return searchHits;
|
||||
}
|
||||
|
||||
try {
|
||||
NestedMetaData nestedMetaData = searchHits.getSearchHit(0).getContent().getNestedMetaData();
|
||||
ElasticsearchPersistentEntityWithNestedMetaData persistentEntityWithNestedMetaData = getPersistentEntity(
|
||||
mappingContext.getPersistentEntity(type), nestedMetaData);
|
||||
|
||||
List<SearchHit<Object>> convertedSearchHits = new ArrayList<>();
|
||||
|
||||
if (persistentEntityWithNestedMetaData.entity != null) {
|
||||
Class<?> targetType = persistentEntityWithNestedMetaData.entity.getType();
|
||||
// convert the list of SearchHit<SearchDocument> to list of SearchHit<Object>
|
||||
searchHits.getSearchHits().forEach(searchHit -> {
|
||||
SearchDocument searchDocument = searchHit.getContent();
|
||||
|
||||
Object targetObject = converter.read(targetType, searchDocument);
|
||||
convertedSearchHits.add(new SearchHit<Object>(searchDocument.getIndex(), //
|
||||
searchDocument.getId(), //
|
||||
searchDocument.getScore(), //
|
||||
searchDocument.getSortValues(), //
|
||||
searchDocument.getHighlightFields(), //
|
||||
mapInnerHits(searchDocument), //
|
||||
persistentEntityWithNestedMetaData.nestedMetaData, //
|
||||
targetObject));
|
||||
});
|
||||
|
||||
String scrollId = null;
|
||||
if (searchHits instanceof SearchHitsImpl) {
|
||||
scrollId = ((SearchHitsImpl<?>) searchHits).getScrollId();
|
||||
}
|
||||
|
||||
return new SearchHitsImpl<>(searchHits.getTotalHits(), //
|
||||
searchHits.getTotalHitsRelation(), //
|
||||
searchHits.getMaxScore(), //
|
||||
scrollId, //
|
||||
convertedSearchHits, //
|
||||
searchHits.getAggregations());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Could not map inner_hits", e);
|
||||
}
|
||||
|
||||
return searchHits;
|
||||
}
|
||||
|
||||
/**
|
||||
* find a {@link ElasticsearchPersistentEntity} following the property chain defined by the nested metadata
|
||||
*
|
||||
* @param persistentEntity base entity
|
||||
* @param nestedMetaData nested metadata
|
||||
* @return The found entity or null
|
||||
*/
|
||||
@Nullable
|
||||
private ElasticsearchPersistentEntityWithNestedMetaData getPersistentEntity(
|
||||
@Nullable ElasticsearchPersistentEntity<?> persistentEntity, @Nullable NestedMetaData nestedMetaData) {
|
||||
|
||||
NestedMetaData currentMetaData = nestedMetaData;
|
||||
List<NestedMetaData> mappedNestedMetaDatas = new LinkedList<>();
|
||||
|
||||
while (persistentEntity != null && currentMetaData != null) {
|
||||
ElasticsearchPersistentProperty persistentProperty = persistentEntity
|
||||
.getPersistentPropertyWithFieldName(currentMetaData.getField());
|
||||
|
||||
if (persistentProperty == null) {
|
||||
persistentEntity = null;
|
||||
} else {
|
||||
persistentEntity = mappingContext.getPersistentEntity(persistentProperty.getActualType());
|
||||
mappedNestedMetaDatas.add(0,
|
||||
NestedMetaData.of(persistentProperty.getName(), currentMetaData.getOffset(), null));
|
||||
currentMetaData = currentMetaData.getChild();
|
||||
}
|
||||
}
|
||||
|
||||
NestedMetaData mappedNestedMetaData = mappedNestedMetaDatas.stream().reduce(null,
|
||||
(result, nmd) -> NestedMetaData.of(nmd.getField(), nmd.getOffset(), result));
|
||||
|
||||
return new ElasticsearchPersistentEntityWithNestedMetaData(persistentEntity, mappedNestedMetaData);
|
||||
}
|
||||
|
||||
private static class ElasticsearchPersistentEntityWithNestedMetaData {
|
||||
@Nullable private ElasticsearchPersistentEntity<?> entity;
|
||||
private NestedMetaData nestedMetaData;
|
||||
|
||||
public ElasticsearchPersistentEntityWithNestedMetaData(@Nullable ElasticsearchPersistentEntity<?> entity,
|
||||
NestedMetaData nestedMetaData) {
|
||||
this.entity = entity;
|
||||
this.nestedMetaData = nestedMetaData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -37,6 +38,7 @@ import org.elasticsearch.common.document.DocumentField;
|
||||
import org.elasticsearch.common.text.Text;
|
||||
import org.elasticsearch.index.get.GetResult;
|
||||
import org.elasticsearch.search.SearchHit;
|
||||
import org.elasticsearch.search.SearchHits;
|
||||
import org.springframework.data.elasticsearch.ElasticsearchException;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@ -155,12 +157,28 @@ public class DocumentAdapters {
|
||||
.collect(Collectors.toMap(Map.Entry::getKey,
|
||||
entry -> Arrays.stream(entry.getValue().getFragments()).map(Text::string).collect(Collectors.toList()))));
|
||||
|
||||
Map<String, SearchDocumentResponse> innerHits = new LinkedHashMap<>();
|
||||
Map<String, SearchHits> sourceInnerHits = source.getInnerHits();
|
||||
|
||||
if (sourceInnerHits != null) {
|
||||
sourceInnerHits.forEach((name, searchHits) -> {
|
||||
innerHits.put(name, SearchDocumentResponse.from(searchHits, null, null));
|
||||
});
|
||||
}
|
||||
|
||||
NestedMetaData nestedMetaData = null;
|
||||
|
||||
if (source.getNestedIdentity() != null) {
|
||||
nestedMetaData = from(source.getNestedIdentity());
|
||||
}
|
||||
|
||||
BytesReference sourceRef = source.getSourceRef();
|
||||
|
||||
if (sourceRef == null || sourceRef.length() == 0) {
|
||||
return new SearchDocumentAdapter(source.getScore(), source.getSortValues(), source.getFields(), highlightFields,
|
||||
fromDocumentFields(source, source.getIndex(), source.getId(), source.getVersion(), source.getSeqNo(),
|
||||
source.getPrimaryTerm()));
|
||||
return new SearchDocumentAdapter(
|
||||
source.getScore(), source.getSortValues(), source.getFields(), highlightFields, fromDocumentFields(source,
|
||||
source.getIndex(), source.getId(), source.getVersion(), source.getSeqNo(), source.getPrimaryTerm()),
|
||||
innerHits, nestedMetaData);
|
||||
}
|
||||
|
||||
Document document = Document.from(source.getSourceAsMap());
|
||||
@ -174,7 +192,18 @@ public class DocumentAdapters {
|
||||
document.setPrimaryTerm(source.getPrimaryTerm());
|
||||
|
||||
return new SearchDocumentAdapter(source.getScore(), source.getSortValues(), source.getFields(), highlightFields,
|
||||
document);
|
||||
document, innerHits, nestedMetaData);
|
||||
}
|
||||
|
||||
private static NestedMetaData from(SearchHit.NestedIdentity nestedIdentity) {
|
||||
|
||||
NestedMetaData child = null;
|
||||
|
||||
if (nestedIdentity.getChild() != null) {
|
||||
child = from(nestedIdentity.getChild());
|
||||
}
|
||||
|
||||
return NestedMetaData.of(nestedIdentity.getField().string(), nestedIdentity.getOffset(), child);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -427,15 +456,20 @@ public class DocumentAdapters {
|
||||
private final Map<String, List<Object>> fields = new HashMap<>();
|
||||
private final Document delegate;
|
||||
private final Map<String, List<String>> highlightFields = new HashMap<>();
|
||||
private final Map<String, SearchDocumentResponse> innerHits = new HashMap<>();
|
||||
@Nullable private final NestedMetaData nestedMetaData;
|
||||
|
||||
SearchDocumentAdapter(float score, Object[] sortValues, Map<String, DocumentField> fields,
|
||||
Map<String, List<String>> highlightFields, Document delegate) {
|
||||
Map<String, List<String>> highlightFields, Document delegate, Map<String, SearchDocumentResponse> innerHits,
|
||||
@Nullable NestedMetaData nestedMetaData) {
|
||||
|
||||
this.score = score;
|
||||
this.sortValues = sortValues;
|
||||
this.delegate = delegate;
|
||||
fields.forEach((name, documentField) -> this.fields.put(name, documentField.getValues()));
|
||||
this.highlightFields.putAll(highlightFields);
|
||||
this.innerHits.putAll(innerHits);
|
||||
this.nestedMetaData = nestedMetaData;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -530,6 +564,17 @@ public class DocumentAdapters {
|
||||
delegate.setPrimaryTerm(primaryTerm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, SearchDocumentResponse> getInnerHits() {
|
||||
return innerHits;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public NestedMetaData getNestedMetaData() {
|
||||
return nestedMetaData;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public <T> T get(Object key, Class<T> type) {
|
||||
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2020 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.document;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* meta data returned for nested inner hits.
|
||||
*
|
||||
* @author Peter-Josef Meisch
|
||||
*/
|
||||
public class NestedMetaData {
|
||||
|
||||
private final String field;
|
||||
private final int offset;
|
||||
@Nullable private final NestedMetaData child;
|
||||
|
||||
public static NestedMetaData of(String field, int offset, @Nullable NestedMetaData nested) {
|
||||
return new NestedMetaData(field, offset, nested);
|
||||
}
|
||||
|
||||
private NestedMetaData(String field, int offset, @Nullable NestedMetaData child) {
|
||||
this.field = field;
|
||||
this.offset = offset;
|
||||
this.child = child;
|
||||
}
|
||||
|
||||
public String getField() {
|
||||
return field;
|
||||
}
|
||||
|
||||
public int getOffset() {
|
||||
return offset;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public NestedMetaData getChild() {
|
||||
return child;
|
||||
}
|
||||
}
|
@ -69,5 +69,24 @@ public interface SearchDocument extends Document {
|
||||
*/
|
||||
@Nullable
|
||||
default Map<String, List<String>> getHighlightFields() {
|
||||
return null;}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the innerHits for the SearchHit
|
||||
* @since 4.1
|
||||
*/
|
||||
@Nullable
|
||||
default Map<String, SearchDocumentResponse> getInnerHits() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the nested metadata in case this is a nested inner hit.
|
||||
* @since 4.1
|
||||
*/
|
||||
@Nullable
|
||||
default NestedMetaData getNestedMetaData() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,9 @@ import java.util.stream.StreamSupport;
|
||||
|
||||
import org.apache.lucene.search.TotalHits;
|
||||
import org.elasticsearch.action.search.SearchResponse;
|
||||
import org.elasticsearch.search.SearchHits;
|
||||
import org.elasticsearch.search.aggregations.Aggregations;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@ -42,7 +44,7 @@ public class SearchDocumentResponse {
|
||||
private final Aggregations aggregations;
|
||||
|
||||
private SearchDocumentResponse(long totalHits, String totalHitsRelation, float maxScore, String scrollId,
|
||||
List<SearchDocument> searchDocuments, Aggregations aggregations) {
|
||||
List<SearchDocument> searchDocuments, Aggregations aggregations) {
|
||||
this.totalHits = totalHits;
|
||||
this.totalHitsRelation = totalHitsRelation;
|
||||
this.maxScore = maxScore;
|
||||
@ -78,27 +80,45 @@ public class SearchDocumentResponse {
|
||||
/**
|
||||
* creates a SearchDocumentResponse from the {@link SearchResponse}
|
||||
*
|
||||
* @param searchResponse
|
||||
* must not be {@literal null}
|
||||
* @param searchResponse must not be {@literal null}
|
||||
* @return the SearchDocumentResponse
|
||||
*/
|
||||
public static SearchDocumentResponse from(SearchResponse searchResponse) {
|
||||
|
||||
Assert.notNull(searchResponse, "searchResponse must not be null");
|
||||
|
||||
TotalHits responseTotalHits = searchResponse.getHits().getTotalHits();
|
||||
Aggregations aggregations = searchResponse.getAggregations();
|
||||
String scrollId = searchResponse.getScrollId();
|
||||
|
||||
SearchHits searchHits = searchResponse.getHits();
|
||||
|
||||
SearchDocumentResponse searchDocumentResponse = from(searchHits, scrollId, aggregations);
|
||||
return searchDocumentResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a {@link SearchDocumentResponse} from {@link SearchHits} with the given scrollId and aggregations
|
||||
*
|
||||
* @param searchHits the {@link SearchHits} to process
|
||||
* @param scrollId scrollId
|
||||
* @param aggregations aggregations
|
||||
* @return the {@link SearchDocumentResponse}
|
||||
* @since 4.1
|
||||
*/
|
||||
public static SearchDocumentResponse from(SearchHits searchHits, @Nullable String scrollId,
|
||||
@Nullable Aggregations aggregations) {
|
||||
TotalHits responseTotalHits = searchHits.getTotalHits();
|
||||
long totalHits = responseTotalHits.value;
|
||||
String totalHitsRelation = responseTotalHits.relation.name();
|
||||
|
||||
float maxScore = searchResponse.getHits().getMaxScore();
|
||||
String scrollId = searchResponse.getScrollId();
|
||||
float maxScore = searchHits.getMaxScore();
|
||||
|
||||
List<SearchDocument> searchDocuments = StreamSupport.stream(searchResponse.getHits().spliterator(), false) //
|
||||
List<SearchDocument> searchDocuments = StreamSupport.stream(searchHits.spliterator(), false) //
|
||||
.filter(Objects::nonNull) //
|
||||
.map(DocumentAdapters::from) //
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Aggregations aggregations = searchResponse.getAggregations();
|
||||
|
||||
return new SearchDocumentResponse(totalHits, totalHitsRelation, maxScore, scrollId, searchDocuments, aggregations);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Copyright 2020 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.elasticsearch.index.query.QueryBuilders.*;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.lucene.search.join.ScoreMode;
|
||||
import org.assertj.core.api.SoftAssertions;
|
||||
import org.elasticsearch.index.query.InnerHitBuilder;
|
||||
import org.elasticsearch.index.query.NestedQueryBuilder;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.annotation.Id;
|
||||
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.document.NestedMetaData;
|
||||
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
|
||||
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
|
||||
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchRestTemplateConfiguration;
|
||||
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
/**
|
||||
* Testing the querying and parsing of inner_hits.
|
||||
*
|
||||
* @author Peter-Josef Meisch
|
||||
*/
|
||||
@SpringIntegrationTest
|
||||
@ContextConfiguration(classes = { InnerHitsTests.Config.class })
|
||||
public class InnerHitsTests {
|
||||
|
||||
public static final String INDEX_NAME = "tests-inner-hits";
|
||||
|
||||
@Configuration
|
||||
@Import({ ElasticsearchRestTemplateConfiguration.class })
|
||||
static class Config {}
|
||||
|
||||
@Autowired ElasticsearchOperations operations;
|
||||
@Nullable IndexOperations indexOps;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
indexOps = operations.indexOps(City.class);
|
||||
indexOps.create();
|
||||
indexOps.putMapping(City.class);
|
||||
|
||||
Inhabitant john = new Inhabitant("John", "Smith");
|
||||
Inhabitant carla = new Inhabitant("Carla", "Miller");
|
||||
House cornerHouse = new House("Round the corner", "7", Arrays.asList(john, carla));
|
||||
City metropole = new City("Metropole", Arrays.asList(cornerHouse));
|
||||
|
||||
Inhabitant jack = new Inhabitant("Jack", "Wayne");
|
||||
Inhabitant emmy = new Inhabitant("Emmy", "Stone");
|
||||
House mainStreet = new House("Main Street", "42", Arrays.asList(jack, emmy));
|
||||
City village = new City("Village", Arrays.asList(mainStreet));
|
||||
|
||||
operations.save(Arrays.asList(metropole, village));
|
||||
indexOps.refresh();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
indexOps.delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnInnerHits() {
|
||||
String innerHitName = "inner_hit_name";
|
||||
|
||||
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
|
||||
|
||||
NestedQueryBuilder nestedQueryBuilder = nestedQuery("hou-ses.in-habi-tants",
|
||||
matchQuery("hou-ses.in-habi-tants.first-name", "Carla"), ScoreMode.Avg);
|
||||
nestedQueryBuilder.innerHit(new InnerHitBuilder(innerHitName));
|
||||
queryBuilder.withQuery(nestedQueryBuilder);
|
||||
|
||||
NativeSearchQuery query = queryBuilder.build();
|
||||
|
||||
SoftAssertions softly = new SoftAssertions();
|
||||
SearchHits<City> searchHits = operations.search(query, City.class);
|
||||
|
||||
softly.assertThat(searchHits.getTotalHits()).isEqualTo(1);
|
||||
|
||||
SearchHit<City> searchHit = searchHits.getSearchHit(0);
|
||||
softly.assertThat(searchHit.getInnerHits()).hasSize(1);
|
||||
|
||||
SearchHits<?> innerHits = searchHit.getInnerHits(innerHitName);
|
||||
softly.assertThat(innerHits).hasSize(1);
|
||||
|
||||
SearchHit<?> innerHit = innerHits.getSearchHit(0);
|
||||
Object content = innerHit.getContent();
|
||||
assertThat(content).isInstanceOf(Inhabitant.class);
|
||||
Inhabitant inhabitant = (Inhabitant) content;
|
||||
softly.assertThat(inhabitant.getFirstName()).isEqualTo("Carla");
|
||||
softly.assertThat(inhabitant.getLastName()).isEqualTo("Miller");
|
||||
|
||||
NestedMetaData nestedMetaData = innerHit.getNestedMetaData();
|
||||
softly.assertThat(nestedMetaData.getField()).isEqualTo("houses");
|
||||
softly.assertThat(nestedMetaData.getOffset()).isEqualTo(0);
|
||||
softly.assertThat(nestedMetaData.getChild().getField()).isEqualTo("inhabitants");
|
||||
softly.assertThat(nestedMetaData.getChild().getOffset()).isEqualTo(1);
|
||||
|
||||
softly.assertAll();
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@RequiredArgsConstructor
|
||||
@Document(indexName = INDEX_NAME)
|
||||
static class City {
|
||||
|
||||
@Id private String name;
|
||||
|
||||
// NOTE: using a custom names here to cover property name matching
|
||||
@Field(name = "hou-ses", type = FieldType.Nested) private Collection<House> houses = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@RequiredArgsConstructor
|
||||
static class House {
|
||||
|
||||
@Field(type = FieldType.Text) private String street;
|
||||
|
||||
@Field(type = FieldType.Text) private String streetNumber;
|
||||
|
||||
// NOTE: using a custom names here to cover property name matching
|
||||
@Field(name = "in-habi-tants", type = FieldType.Nested) private List<Inhabitant> inhabitants = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@RequiredArgsConstructor
|
||||
static class Inhabitant {
|
||||
// NOTE: using a custom names here to cover property name matching
|
||||
|
||||
@Field(name = "first-name", type = FieldType.Text) private String firstName;
|
||||
|
||||
@Field(name = "last-name", type = FieldType.Text) private String lastName;
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2020 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.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
/**
|
||||
* @author Peter-Josef Meisch
|
||||
*/
|
||||
@ContextConfiguration(classes = { InnerHitsTransportTests.Config.class })
|
||||
public class InnerHitsTransportTests extends InnerHitsTests {
|
||||
@Configuration
|
||||
@Import({ ElasticsearchTemplateConfiguration.class })
|
||||
static class Config {}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user