DATAES-715 - Highlight results should be returned in the SearchHits.

Original PR: #368
This commit is contained in:
Peter-Josef Meisch 2019-12-30 17:08:52 +01:00 committed by GitHub
parent d026884c12
commit 90d29994f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 119 additions and 59 deletions

View File

@ -1,24 +0,0 @@
/*
* Copyright 2013-2019 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.elasticsearch.action.search.SearchResponse;
public interface ResultsExtractor<T> {
T extract(SearchResponse response);
}

View File

@ -18,9 +18,12 @@ package org.springframework.data.elasticsearch.core;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Encapsulates the found data with additional information from the search.
@ -35,11 +38,17 @@ public class SearchHit<T> {
private final float score;
private final List<Object> sortValues;
private final T content;
private final Map<String, List<String>> highlightFields = new LinkedHashMap<>();
public SearchHit(@Nullable String id, float score, Object[] sortValues, T content) {
public SearchHit(@Nullable String id, float score, @Nullable Object[] sortValues,
@Nullable Map<String, List<String>> highlightFields, T content) {
this.id = id;
this.score = score;
this.sortValues = (sortValues != null) ? Arrays.asList(sortValues) : new ArrayList<>();
if (highlightFields != null) {
this.highlightFields.putAll(highlightFields);
}
this.content = content;
}
@ -69,9 +78,22 @@ public class SearchHit<T> {
return Collections.unmodifiableList(sortValues);
}
/**
* gets the highlight values for a field.
*
* @param field must not be {@literal null}
* @return possibly empty List, never null
*/
public List<String> getHighlightField(String field) {
Assert.notNull(field, "field must not be null");
return Collections.unmodifiableList(highlightFields.getOrDefault(field, Collections.emptyList()));
}
@Override
public String toString() {
return "SearchHit{" + "id='" + id + '\'' + ", score=" + score + ", sortValues=" + sortValues + ", content="
+ content + '}';
+ content + ", highlightFields=" + highlightFields + '}';
}
}

View File

@ -181,9 +181,10 @@ public class MappingElasticsearchConverter
String id = searchDocument.hasId() ? searchDocument.getId() : null;
float score = searchDocument.getScore();
Object[] sortValues = searchDocument.getSortValues();
Map<String, List<String>> highlightFields = searchDocument.getHighlightFields();
T content = mapDocument(searchDocument, type);
return new SearchHit<>(id, score, sortValues, content);
return new SearchHit<T>(id, score, sortValues, highlightFields, content);
}
@Override

View File

@ -34,11 +34,13 @@ import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.get.MultiGetResponse;
import org.elasticsearch.common.bytes.BytesReference;
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.springframework.data.elasticsearch.ElasticsearchException;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonFactory;
@ -139,10 +141,14 @@ public class DocumentAdapters {
Assert.notNull(source, "SearchHit must not be null");
Map<String, List<String>> highlightFields = new HashMap<>(source.getHighlightFields().entrySet().stream() //
.collect(Collectors.toMap(Map.Entry::getKey,
entry -> Arrays.stream(entry.getValue().getFragments()).map(Text::string).collect(Collectors.toList()))));
BytesReference sourceRef = source.getSourceRef();
if (sourceRef == null || sourceRef.length() == 0) {
return new SearchDocumentAdapter(source.getScore(), source.getSortValues(), source.getFields(),
return new SearchDocumentAdapter(source.getScore(), source.getSortValues(), source.getFields(), highlightFields,
fromDocumentFields(source, source.getId(), source.getVersion()));
}
@ -153,7 +159,8 @@ public class DocumentAdapters {
document.setVersion(source.getVersion());
}
return new SearchDocumentAdapter(source.getScore(), source.getSortValues(), source.getFields(), document);
return new SearchDocumentAdapter(source.getScore(), source.getSortValues(), source.getFields(), highlightFields,
document);
}
/**
@ -195,7 +202,7 @@ public class DocumentAdapters {
*/
@Override
public boolean hasId() {
return id != null;
return StringUtils.hasLength(id);
}
/*
@ -287,16 +294,14 @@ public class DocumentAdapters {
* @see java.util.Map#get(java.lang.Object)
*/
@Override
@Nullable
public Object get(Object key) {
return documentFields.stream() //
.filter(documentField -> documentField.getName().equals(key)) //
.map(DocumentField::getValue)
.findFirst() //
.orElse(null); //
for (DocumentField documentField : documentFields) {
if (documentField.getName().equals(key)) {
return getValue(documentField);
}
}
return null;
}
/*
@ -439,12 +444,16 @@ public class DocumentAdapters {
private final Object[] sortValues;
private final Map<String, List<Object>> fields = new HashMap<>();
private final Document delegate;
private final Map<String, List<String>> highlightFields = new HashMap<>();
SearchDocumentAdapter(float score, Object[] sortValues, Map<String, DocumentField> fields,
Map<String, List<String>> highlightFields, Document delegate) {
SearchDocumentAdapter(float score, Object[] sortValues, Map<String, DocumentField> fields, Document delegate) {
this.score = score;
this.sortValues = sortValues;
this.delegate = delegate;
fields.forEach((name, documentField) -> this.fields.put(name, documentField.getValues()));
this.highlightFields.putAll(highlightFields);
}
/*
@ -485,6 +494,15 @@ public class DocumentAdapters {
return sortValues;
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.core.document.SearchDocument#getHighlightFields()
*/
@Override
public Map<String, List<String>> getHighlightFields() {
return highlightFields;
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.core.document.Document#hasId()
@ -672,10 +690,12 @@ public class DocumentAdapters {
*/
@Override
public boolean equals(Object o) {
if (this == o)
if (this == o) {
return true;
if (!(o instanceof SearchDocumentAdapter))
}
if (!(o instanceof SearchDocumentAdapter)) {
return false;
}
SearchDocumentAdapter that = (SearchDocumentAdapter) o;
return Float.compare(that.score, score) == 0 && delegate.equals(that.delegate);
}

View File

@ -63,4 +63,11 @@ public interface SearchDocument extends Document {
default Object[] getSortValues() {
return null;
}
/**
* @return the highlightFields for the search hit.
*/
@Nullable
default Map<String, List<String>> getHighlightFields() {
return null;}
}

View File

@ -27,7 +27,6 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.io.IOException;
import java.lang.Double;
import java.lang.Integer;
import java.lang.Long;
@ -51,6 +50,7 @@ import org.elasticsearch.index.VersionType;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
@ -148,6 +148,7 @@ public abstract class ElasticsearchTemplateTests {
indexOperations.deleteIndex(INDEX_2_NAME);
indexOperations.deleteIndex(INDEX_3_NAME);
indexOperations.deleteIndex(SearchHitsEntity.class);
indexOperations.deleteIndex(HighlightEntity.class);
}
@Test // DATAES-106
@ -332,7 +333,8 @@ public abstract class ElasticsearchTemplateTests {
.withPreference("_only_nodes:oops").build();
// when
assertThatThrownBy(() -> operations.search(searchQueryWithInvalidPreference, SampleEntity.class, index)).isInstanceOf(Exception.class);
assertThatThrownBy(() -> operations.search(searchQueryWithInvalidPreference, SampleEntity.class, index))
.isInstanceOf(Exception.class);
}
@Test // DATAES-422 - Add support for IndicesOptions in search queries
@ -1854,7 +1856,8 @@ public abstract class ElasticsearchTemplateTests {
indexOperations.refresh(IndexCoordinates.of(INDEX_NAME_SAMPLE_ENTITY));
// reindex with version one below
assertThatThrownBy(() -> operations.index(indexQueryBuilder.withVersion(entity.getVersion() - 1).build(), index)).hasMessageContaining("version").hasMessageContaining("conflict");
assertThatThrownBy(() -> operations.index(indexQueryBuilder.withVersion(entity.getVersion() - 1).build(), index))
.hasMessageContaining("version").hasMessageContaining("conflict");
}
@Test
@ -2134,7 +2137,8 @@ public abstract class ElasticsearchTemplateTests {
CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria());
// when
assertThatThrownBy(() -> operations.count(criteriaQuery, (IndexCoordinates) null)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> operations.count(criteriaQuery, (IndexCoordinates) null))
.isInstanceOf(IllegalArgumentException.class);
}
@Test // DATAES-67
@ -2151,7 +2155,8 @@ public abstract class ElasticsearchTemplateTests {
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()).build();
// when
assertThatThrownBy(() -> operations.count(searchQuery, (IndexCoordinates) null)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> operations.count(searchQuery, (IndexCoordinates) null))
.isInstanceOf(IllegalArgumentException.class);
}
@Test // DATAES-71
@ -2881,6 +2886,34 @@ public abstract class ElasticsearchTemplateTests {
}
}
@Test // DATAES-715
void shouldReturnHighlightFieldsInSearchHit() {
IndexCoordinates index = IndexCoordinates.of("test-index-highlight-entity-template");
HighlightEntity entity = HighlightEntity.builder().id("1")
.message("This message is a long text which contains the word to search for "
+ "in two places, the first being near the beginning and the second near the end of the message")
.build();
IndexQuery indexQuery = new IndexQueryBuilder().withId(entity.getId()).withObject(entity).build();
operations.index(indexQuery, index);
indexOperations.refresh(index);
NativeSearchQuery query = new NativeSearchQueryBuilder() //
.withQuery(termQuery("message", "message")) //
.withHighlightFields(new HighlightBuilder.Field("message")) //
.build();
SearchHits<HighlightEntity> searchHits = operations.search(query, HighlightEntity.class, index);
assertThat(searchHits).isNotNull();
assertThat(searchHits.getSearchHits()).hasSize(1);
SearchHit<HighlightEntity> searchHit = searchHits.getSearchHit(0);
List<String> highlightField = searchHit.getHighlightField("message");
assertThat(highlightField).hasSize(2);
assertThat(highlightField.get(0)).contains("<em>message</em>");
assertThat(highlightField.get(1)).contains("<em>message</em>");
}
protected RequestFactory getRequestFactory() {
return ((AbstractElasticsearchTemplate) operations).getRequestFactory();
}
@ -2899,7 +2932,6 @@ public abstract class ElasticsearchTemplateTests {
private int rate;
@ScriptedField private Double scriptedRate;
private boolean available;
private String highlightedMessage;
private GeoPoint location;
@Version private Long version;
@Score private float score;
@ -2913,8 +2945,7 @@ public abstract class ElasticsearchTemplateTests {
@Data
@AllArgsConstructor
@Builder
@Document(indexName = "test-index-uuid-keyed-core-template", replicas = 0,
refreshInterval = "-1")
@Document(indexName = "test-index-uuid-keyed-core-template", replicas = 0, refreshInterval = "-1")
private static class SampleEntityUUIDKeyed {
@Id private UUID id;
@ -2923,10 +2954,7 @@ public abstract class ElasticsearchTemplateTests {
private int rate;
@ScriptedField private Long scriptedRate;
private boolean available;
private String highlightedMessage;
private GeoPoint location;
@Version private Long version;
}
@ -2935,8 +2963,7 @@ public abstract class ElasticsearchTemplateTests {
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = "test-index-book-core-template", replicas = 0,
refreshInterval = "-1")
@Document(indexName = "test-index-book-core-template", replicas = 0, refreshInterval = "-1")
static class Book {
@Id private String id;
@ -2950,7 +2977,6 @@ public abstract class ElasticsearchTemplateTests {
@Data
static class Author {
private String id;
private String name;
}
@ -2959,8 +2985,8 @@ public abstract class ElasticsearchTemplateTests {
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = "test-index-version-core-template", replicas = 0,
refreshInterval = "-1", versionType = VersionType.EXTERNAL_GTE)
@Document(indexName = "test-index-version-core-template", replicas = 0, refreshInterval = "-1",
versionType = VersionType.EXTERNAL_GTE)
private static class GTEVersionEntity {
@Version private Long version;
@ -3001,8 +3027,8 @@ public abstract class ElasticsearchTemplateTests {
}
@Data
@Document(indexName = "test-index-server-configuration", useServerConfiguration = true,
shards = 10, replicas = 10, refreshInterval = "-1")
@Document(indexName = "test-index-server-configuration", useServerConfiguration = true, shards = 10, replicas = 10,
refreshInterval = "-1")
private static class UseServerConfigurationEntity {
@Id private String id;
@ -3042,4 +3068,12 @@ public abstract class ElasticsearchTemplateTests {
@Field(type = FieldType.Keyword) String keyword;
}
@Data
@AllArgsConstructor
@Builder
@Document(indexName = "test-index-highlight-entity-template")
static class HighlightEntity {
@Id private String id;
private String message;
}
}