mirror of
https://github.com/spring-projects/spring-data-elasticsearch.git
synced 2025-06-24 04:52:12 +00:00
DATAES-715 - Highlight results should be returned in the SearchHits.
Original PR: #368
This commit is contained in:
parent
d026884c12
commit
90d29994f1
@ -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);
|
||||
}
|
@ -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 + '}';
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user