diff --git a/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java b/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java index 2adfe70eb..33418fdf8 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java @@ -1127,7 +1127,6 @@ class RequestFactory { if (query instanceof NativeSearchQuery) { prepareNativeSearch((NativeSearchQuery) query, sourceBuilder); - } if (query.getTrackTotalHits() != null) { @@ -1147,6 +1146,10 @@ class RequestFactory { sourceBuilder.explain(query.getExplain()); + if (query.getSearchAfter() != null) { + sourceBuilder.searchAfter(query.getSearchAfter().toArray()); + } + request.source(sourceBuilder); return request; } @@ -1229,6 +1232,10 @@ class RequestFactory { searchRequestBuilder.setExplain(query.getExplain()); + if (query.getSearchAfter() != null) { + searchRequestBuilder.searchAfter(query.getSearchAfter().toArray()); + } + return searchRequestBuilder; } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/AbstractQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/AbstractQuery.java index b2d9e0a9d..a00f2dcee 100755 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/AbstractQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/AbstractQuery.java @@ -62,6 +62,7 @@ abstract class AbstractQuery implements Query { @Nullable private Duration scrollTime; @Nullable private TimeValue timeout; private boolean explain = false; + @Nullable private List searchAfter; @Override @Nullable @@ -283,4 +284,15 @@ abstract class AbstractQuery implements Query { public void setExplain(boolean explain) { this.explain = explain; } + + @Override + public void setSearchAfter(@Nullable List searchAfter) { + this.searchAfter = searchAfter; + } + + @Nullable + @Override + public List getSearchAfter() { + return searchAfter; + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java b/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java index 790870cce..4e93d0725 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java @@ -27,6 +27,7 @@ import org.elasticsearch.index.query.QueryBuilders; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.elasticsearch.core.SearchHit; import org.springframework.lang.Nullable; /** @@ -287,10 +288,26 @@ public interface Query { TimeValue getTimeout(); /** - * @return {@literal true} when the query has the eplain parameter set, defaults to {@literal false} + * @return {@literal true} when the query has the explain parameter set, defaults to {@literal false} * @since 4.2 */ default boolean getExplain() { return false; } + + /** + * Sets the setSearchAfter objects for this query. + * + * @param searchAfter the setSearchAfter objects. These are obtained with {@link SearchHit#getSortValues()} from a + * search result. + * @since 4.2 + */ + void setSearchAfter(@Nullable List searchAfter); + + /** + * @return the search_after objects. + * @since 4.2 + */ + @Nullable + List getSearchAfter(); } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/paginating/ReactiveSearchAfterIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/paginating/ReactiveSearchAfterIntegrationTests.java new file mode 100644 index 000000000..43c2ba1ae --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/paginating/ReactiveSearchAfterIntegrationTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2021 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.paginating; + +import static org.assertj.core.api.Assertions.*; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.annotation.Id; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +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.ReactiveElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHit; +import org.springframework.data.elasticsearch.core.query.Query; +import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchRestTemplateConfiguration; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Peter-Josef Meisch + */ +@SpringIntegrationTest +@ContextConfiguration(classes = { ReactiveElasticsearchRestTemplateConfiguration.class }) +public class ReactiveSearchAfterIntegrationTests { + + @Autowired private ReactiveElasticsearchOperations operations; + + @Test // #1143 + @DisplayName("should read pages with search_after") + void shouldReadPagesWithSearchAfter() { + + List entities = IntStream.rangeClosed(1, 10) + .mapToObj(i -> Entity.builder().id((long) i).message("message " + i).build()).collect(Collectors.toList()); + operations.saveAll(Mono.just(entities), Entity.class).blockLast(); + + Query query = Query.findAll(); + query.setPageable(PageRequest.of(0, 3)); + query.addSort(Sort.by(Sort.Direction.ASC, "id")); + + List searchAfter = null; + List foundEntities = new ArrayList<>(); + + int loop = 0; + do { + query.setSearchAfter(searchAfter); + List> searchHits = operations.search(query, Entity.class).collectList().block(); + + if (searchHits.size() == 0) { + break; + } + foundEntities.addAll(searchHits.stream().map(searchHit -> searchHit.getContent()).collect(Collectors.toList())); + searchAfter = searchHits.get((int) (searchHits.size() - 1)).getSortValues(); + + if (++loop > 10) { + fail("loop not terminating"); + } + } while (true); + + assertThat(foundEntities).containsExactlyElementsOf(entities); + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + @Builder + @Document(indexName = "test-search-after") + private static class Entity { + @Id private Long id; + @Field(type = FieldType.Text) private String message; + } + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/paginating/SearchAfterIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/paginating/SearchAfterIntegrationTests.java new file mode 100644 index 000000000..120d2bf54 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/paginating/SearchAfterIntegrationTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2021 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.paginating; + +import static org.assertj.core.api.Assertions.*; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.annotation.Id; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +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.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.query.Query; +import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchRestTemplateConfiguration; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Peter-Josef Meisch + */ +@SpringIntegrationTest +@ContextConfiguration(classes = { ElasticsearchRestTemplateConfiguration.class }) +public class SearchAfterIntegrationTests { + + @Autowired private ElasticsearchOperations operations; + + @Test // #1143 + @DisplayName("should read pages with search_after") + void shouldReadPagesWithSearchAfter() { + + List entities = IntStream.rangeClosed(1, 10) + .mapToObj(i -> Entity.builder().id((long) i).message("message " + i).build()).collect(Collectors.toList()); + operations.save(entities); + + Query query = Query.findAll(); + query.setPageable(PageRequest.of(0, 3)); + query.addSort(Sort.by(Sort.Direction.ASC, "id")); + + List searchAfter = null; + List foundEntities = new ArrayList<>(); + + int loop = 0; + do { + query.setSearchAfter(searchAfter); + SearchHits searchHits = operations.search(query, Entity.class); + + if (searchHits.getSearchHits().size() == 0) { + break; + } + foundEntities.addAll(searchHits.stream().map(searchHit -> searchHit.getContent()).collect(Collectors.toList())); + searchAfter = searchHits.getSearchHit((int) (searchHits.getSearchHits().size() - 1)).getSortValues(); + + if (++loop > 10) { + fail("loop not terminating"); + } + } while (true); + + assertThat(foundEntities).containsExactlyElementsOf(entities); + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + @Builder + @Document(indexName = "test-search-after") + private static class Entity { + @Id private Long id; + @Field(type = FieldType.Text) private String message; + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/paginating/SearchAfterTransportIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/paginating/SearchAfterTransportIntegrationTests.java new file mode 100644 index 000000000..86c311147 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/paginating/SearchAfterTransportIntegrationTests.java @@ -0,0 +1,25 @@ +/* + * Copyright 2021 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.paginating; + +import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Peter-Josef Meisch + */ +@ContextConfiguration(classes = { ElasticsearchTemplateConfiguration.class }) +public class SearchAfterTransportIntegrationTests extends SearchAfterIntegrationTests {} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/paginating/package-info.java b/src/test/java/org/springframework/data/elasticsearch/core/paginating/package-info.java new file mode 100644 index 000000000..d242ad337 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/paginating/package-info.java @@ -0,0 +1,6 @@ +/** + * Test for paginating support with search_after and point_in_time API + */ +@org.springframework.lang.NonNullApi +@org.springframework.lang.NonNullFields +package org.springframework.data.elasticsearch.core.paginating;