DATAES-263 - Inner Hits support. (#473)

original PR: #473
This commit is contained in:
Peter-Josef Meisch 2020-06-03 14:25:48 +02:00 committed by GitHub
parent 859b22db8e
commit caebe08cf2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 568 additions and 41 deletions

View File

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

View File

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

View File

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

View File

@ -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="

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {}
}