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 b84b5843f..a4188bf0c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/SearchHit.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/SearchHit.java @@ -15,6 +15,11 @@ */ package org.springframework.data.elasticsearch.core; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + import org.springframework.lang.Nullable; /** @@ -28,11 +33,13 @@ public class SearchHit { private final String id; private final float score; + private final List sortValues; private final T content; - public SearchHit(@Nullable String id, float score, T content) { + public SearchHit(@Nullable String id, float score, Object[] sortValues, T content) { this.id = id; this.score = score; + this.sortValues = (sortValues != null) ? Arrays.asList(sortValues) : new ArrayList<>(); this.content = content; } @@ -55,12 +62,16 @@ public class SearchHit { return content; } + /** + * @return the sort values if the query had a sort criterion. + */ + public List getSortValues() { + return Collections.unmodifiableList(sortValues); + } + @Override public String toString() { - return "SearchHit{" + - "id='" + id + '\'' + - ", score=" + score + - ", content=" + content + - '}'; + return "SearchHit{" + "id='" + id + '\'' + ", score=" + score + ", sortValues=" + sortValues + ", content=" + + content + '}'; } } 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 180034ba0..d209077dc 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 @@ -601,9 +601,10 @@ public class MappingElasticsearchConverter public SearchHit read(Class type, SearchDocument searchDocument) { String id = searchDocument.hasId() ? searchDocument.getId() : null; float score = searchDocument.getScore(); + Object[] sortValues = searchDocument.getSortValues(); T content = mapDocument(searchDocument, type); - return new SearchHit(id, score, content); + return new SearchHit(id, score, sortValues, 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 bb2799c7b..28dd733d9 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 @@ -142,7 +142,7 @@ public class DocumentAdapters { BytesReference sourceRef = source.getSourceRef(); if (sourceRef == null || sourceRef.length() == 0) { - return new SearchDocumentAdapter(source.getScore(), source.getFields(), + return new SearchDocumentAdapter(source.getScore(), source.getSortValues(), source.getFields(), fromDocumentFields(source, source.getId(), source.getVersion())); } @@ -153,7 +153,7 @@ public class DocumentAdapters { document.setVersion(source.getVersion()); } - return new SearchDocumentAdapter(source.getScore(), source.getFields(), document); + return new SearchDocumentAdapter(source.getScore(), source.getSortValues(), source.getFields(), document); } /** @@ -438,11 +438,13 @@ public class DocumentAdapters { static class SearchDocumentAdapter implements SearchDocument { private final float score; + private final Object[] sortValues; private final Map> fields = new HashMap<>(); private final Document delegate; - SearchDocumentAdapter(float score, Map fields, 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())); } @@ -476,6 +478,15 @@ public class DocumentAdapters { return fields; } + /* + * (non-Javadoc) + * @see org.springframework.data.elasticsearch.core.document.SearchDocument#getSortValues() + */ + @Override + public Object[] getSortValues() { + return sortValues; + } + /* * (non-Javadoc) * @see org.springframework.data.elasticsearch.core.document.Document#hasId() 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 8195015af..ec66da0a2 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 @@ -18,6 +18,8 @@ package org.springframework.data.elasticsearch.core.document; import java.util.List; import java.util.Map; +import org.springframework.lang.Nullable; + /** * Extension to {@link Document} exposing a search response related data. * @@ -45,6 +47,7 @@ public interface SearchDocument extends Document { * * @param name the field name */ + @Nullable default V getFieldValue(final String name) { List values = getFields().get(name); if (values == null || values.isEmpty()) { @@ -52,4 +55,12 @@ public interface SearchDocument extends Document { } return (V) values.get(0); } + + /** + * @return the sort values for the search hit + */ + @Nullable + default Object[] getSortValues() { + 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 437808c91..aab98f596 100755 --- a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java @@ -128,6 +128,9 @@ public abstract class ElasticsearchTemplateTests { indexOperations.createIndex(SampleEntityUUIDKeyed.class); indexOperations.putMapping(SampleEntityUUIDKeyed.class); + + indexOperations.createIndex(SearchHitsEntity.class); + indexOperations.putMapping(SearchHitsEntity.class); } @AfterEach @@ -146,6 +149,7 @@ public abstract class ElasticsearchTemplateTests { indexOperations.deleteIndex(INDEX_1_NAME); indexOperations.deleteIndex(INDEX_2_NAME); indexOperations.deleteIndex(INDEX_3_NAME); + indexOperations.deleteIndex(SearchHitsEntity.class); } @Test // DATAES-106 @@ -2856,6 +2860,41 @@ public abstract class ElasticsearchTemplateTests { assertThat(map).containsKey("index.max_result_window"); } + @Test // DATAES-714 + void shouldReturnSortFieldsInSearchHits() { + IndexCoordinates index = IndexCoordinates.of("test-index-searchhits-entity-template"); + SearchHitsEntity entity = SearchHitsEntity.builder().id("1").number(1000L).keyword("thousands").build(); + IndexQuery indexQuery = new IndexQueryBuilder().withId(entity.getId()).withObject(entity).build(); + operations.index(indexQuery, index); + indexOperations.refresh(index); + + NativeSearchQuery query = new NativeSearchQueryBuilder() // + .withQuery(matchAllQuery()) // + .withSort(new FieldSortBuilder("keyword").order(SortOrder.ASC)) + .withSort(new FieldSortBuilder("number").order(SortOrder.DESC)).build(); + + SearchHits searchHits = operations.search(query, SearchHitsEntity.class, index); + + assertThat(searchHits).isNotNull(); + assertThat(searchHits.getSearchHits()).hasSize(1); + + SearchHit searchHit = searchHits.getSearchHit(0); + List sortValues = searchHit.getSortValues(); + assertThat(sortValues).hasSize(2); + assertThat(sortValues.get(0)).isInstanceOf(String.class).isEqualTo("thousands"); + // transport client returns Long, rest client Integer + java.lang.Object o = sortValues.get(1); + if (o instanceof Integer) { + Integer i = (Integer) o; + assertThat(o).isInstanceOf(Integer.class).isEqualTo(1000); + } else if (o instanceof Long) { + Long l = (Long) o; + assertThat(o).isInstanceOf(Long.class).isEqualTo(1000L); + } else { + fail("unexpected object type " + o); + } + } + protected RequestFactory getRequestFactory() { return ((AbstractElasticsearchTemplate) operations).getRequestFactory(); } @@ -3007,4 +3046,14 @@ public abstract class ElasticsearchTemplateTests { } } + @Data + @AllArgsConstructor + @Builder + @Document(indexName = "test-index-searchhits-entity-template") + static class SearchHitsEntity { + @Id private String id; + @Field(type = FieldType.Long) Long number; + @Field(type = FieldType.Keyword) String keyword; + } + } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplateTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplateTests.java index 41c04d0d3..d85e25e4b 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplateTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplateTests.java @@ -40,6 +40,8 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.search.sort.FieldSortBuilder; +import org.elasticsearch.search.sort.SortOrder; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -693,6 +695,27 @@ public class ReactiveElasticsearchTemplateTests { .verifyComplete(); } + @Test + void shouldReturnSortFields() { + SampleEntity entity = randomEntity("test message"); + entity.rate = 42; + index(entity); + + NativeSearchQuery query = new NativeSearchQueryBuilder() // + .withQuery(matchAllQuery()) // + .withSort(new FieldSortBuilder("rate").order(SortOrder.DESC)) // + .build(); + + template.search(query, SampleEntity.class) // + .as(StepVerifier::create) // + .consumeNextWith(it -> { + List sortValues = it.getSortValues(); + assertThat(sortValues).hasSize(1); + assertThat(sortValues.get(0)).isEqualTo(42); + }) // + .verifyComplete(); + } + @Data @Document(indexName = "marvel", type = "characters") static class Person {