diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ResultsExtractor.java b/src/main/java/org/springframework/data/elasticsearch/core/ResultsExtractor.java deleted file mode 100644 index a1d1b9563..000000000 --- a/src/main/java/org/springframework/data/elasticsearch/core/ResultsExtractor.java +++ /dev/null @@ -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 extract(SearchResponse response); -} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/SearchHit.java b/src/main/java/org/springframework/data/elasticsearch/core/SearchHit.java index a4188bf0c..7b947030f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/SearchHit.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/SearchHit.java @@ -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 { private final float score; private final List sortValues; private final T content; + private final Map> 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> 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 { 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 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 + '}'; } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java index f42cce959..2664e0f5b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java @@ -181,9 +181,10 @@ public class MappingElasticsearchConverter String id = searchDocument.hasId() ? searchDocument.getId() : null; float score = searchDocument.getScore(); Object[] sortValues = searchDocument.getSortValues(); + Map> highlightFields = searchDocument.getHighlightFields(); T content = mapDocument(searchDocument, type); - return new SearchHit<>(id, score, sortValues, content); + return new SearchHit(id, score, sortValues, highlightFields, content); } @Override diff --git a/src/main/java/org/springframework/data/elasticsearch/core/document/DocumentAdapters.java b/src/main/java/org/springframework/data/elasticsearch/core/document/DocumentAdapters.java index 814009b4d..4e2af31bc 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/document/DocumentAdapters.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/document/DocumentAdapters.java @@ -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> 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> fields = new HashMap<>(); private final Document delegate; + private final Map> highlightFields = new HashMap<>(); + + SearchDocumentAdapter(float score, Object[] sortValues, Map fields, + Map> highlightFields, Document delegate) { - SearchDocumentAdapter(float score, Object[] sortValues, Map 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> 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); } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/document/SearchDocument.java b/src/main/java/org/springframework/data/elasticsearch/core/document/SearchDocument.java index ec66da0a2..6ea162475 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/document/SearchDocument.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/document/SearchDocument.java @@ -63,4 +63,11 @@ public interface SearchDocument extends Document { default Object[] getSortValues() { return null; } + + /** + * @return the highlightFields for the search hit. + */ + @Nullable + default Map> getHighlightFields() { + return null;} } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java index 64036be37..a6c276cdb 100755 --- a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java @@ -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 searchHits = operations.search(query, HighlightEntity.class, index); + + assertThat(searchHits).isNotNull(); + assertThat(searchHits.getSearchHits()).hasSize(1); + + SearchHit searchHit = searchHits.getSearchHit(0); + List highlightField = searchHit.getHighlightField("message"); + assertThat(highlightField).hasSize(2); + assertThat(highlightField.get(0)).contains("message"); + assertThat(highlightField.get(1)).contains("message"); + } + 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; + } }