Skip to content

Commit ffc2420

Browse files
authored
Add support for search_after.
Original Pull Request #1691 Closes #1143
1 parent 154c50b commit ffc2420

File tree

7 files changed

+267
-2
lines changed

7 files changed

+267
-2
lines changed

Diff for: src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -1127,7 +1127,6 @@ private SearchRequest prepareSearchRequest(Query query, @Nullable Class<?> clazz
11271127

11281128
if (query instanceof NativeSearchQuery) {
11291129
prepareNativeSearch((NativeSearchQuery) query, sourceBuilder);
1130-
11311130
}
11321131

11331132
if (query.getTrackTotalHits() != null) {
@@ -1147,6 +1146,10 @@ private SearchRequest prepareSearchRequest(Query query, @Nullable Class<?> clazz
11471146

11481147
sourceBuilder.explain(query.getExplain());
11491148

1149+
if (query.getSearchAfter() != null) {
1150+
sourceBuilder.searchAfter(query.getSearchAfter().toArray());
1151+
}
1152+
11501153
request.source(sourceBuilder);
11511154
return request;
11521155
}
@@ -1229,6 +1232,10 @@ private SearchRequestBuilder prepareSearchRequestBuilder(Query query, Client cli
12291232

12301233
searchRequestBuilder.setExplain(query.getExplain());
12311234

1235+
if (query.getSearchAfter() != null) {
1236+
searchRequestBuilder.searchAfter(query.getSearchAfter().toArray());
1237+
}
1238+
12321239
return searchRequestBuilder;
12331240
}
12341241

Diff for: src/main/java/org/springframework/data/elasticsearch/core/query/AbstractQuery.java

+12
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ abstract class AbstractQuery implements Query {
6262
@Nullable private Duration scrollTime;
6363
@Nullable private TimeValue timeout;
6464
private boolean explain = false;
65+
@Nullable private List<Object> searchAfter;
6566

6667
@Override
6768
@Nullable
@@ -283,4 +284,15 @@ public boolean getExplain() {
283284
public void setExplain(boolean explain) {
284285
this.explain = explain;
285286
}
287+
288+
@Override
289+
public void setSearchAfter(@Nullable List<Object> searchAfter) {
290+
this.searchAfter = searchAfter;
291+
}
292+
293+
@Nullable
294+
@Override
295+
public List<Object> getSearchAfter() {
296+
return searchAfter;
297+
}
286298
}

Diff for: src/main/java/org/springframework/data/elasticsearch/core/query/Query.java

+18-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.data.domain.PageRequest;
2828
import org.springframework.data.domain.Pageable;
2929
import org.springframework.data.domain.Sort;
30+
import org.springframework.data.elasticsearch.core.SearchHit;
3031
import org.springframework.lang.Nullable;
3132

3233
/**
@@ -287,10 +288,26 @@ default boolean hasScrollTime() {
287288
TimeValue getTimeout();
288289

289290
/**
290-
* @return {@literal true} when the query has the eplain parameter set, defaults to {@literal false}
291+
* @return {@literal true} when the query has the explain parameter set, defaults to {@literal false}
291292
* @since 4.2
292293
*/
293294
default boolean getExplain() {
294295
return false;
295296
}
297+
298+
/**
299+
* Sets the setSearchAfter objects for this query.
300+
*
301+
* @param searchAfter the setSearchAfter objects. These are obtained with {@link SearchHit#getSortValues()} from a
302+
* search result.
303+
* @since 4.2
304+
*/
305+
void setSearchAfter(@Nullable List<Object> searchAfter);
306+
307+
/**
308+
* @return the search_after objects.
309+
* @since 4.2
310+
*/
311+
@Nullable
312+
List<Object> getSearchAfter();
296313
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.elasticsearch.core.paginating;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import lombok.AllArgsConstructor;
21+
import lombok.Builder;
22+
import lombok.Data;
23+
import lombok.NoArgsConstructor;
24+
import reactor.core.publisher.Mono;
25+
26+
import java.util.ArrayList;
27+
import java.util.List;
28+
import java.util.stream.Collectors;
29+
import java.util.stream.IntStream;
30+
31+
import org.junit.jupiter.api.DisplayName;
32+
import org.junit.jupiter.api.Test;
33+
import org.springframework.beans.factory.annotation.Autowired;
34+
import org.springframework.data.annotation.Id;
35+
import org.springframework.data.domain.PageRequest;
36+
import org.springframework.data.domain.Sort;
37+
import org.springframework.data.elasticsearch.annotations.Document;
38+
import org.springframework.data.elasticsearch.annotations.Field;
39+
import org.springframework.data.elasticsearch.annotations.FieldType;
40+
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
41+
import org.springframework.data.elasticsearch.core.SearchHit;
42+
import org.springframework.data.elasticsearch.core.query.Query;
43+
import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchRestTemplateConfiguration;
44+
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
45+
import org.springframework.test.context.ContextConfiguration;
46+
47+
/**
48+
* @author Peter-Josef Meisch
49+
*/
50+
@SpringIntegrationTest
51+
@ContextConfiguration(classes = { ReactiveElasticsearchRestTemplateConfiguration.class })
52+
public class ReactiveSearchAfterIntegrationTests {
53+
54+
@Autowired private ReactiveElasticsearchOperations operations;
55+
56+
@Test // #1143
57+
@DisplayName("should read pages with search_after")
58+
void shouldReadPagesWithSearchAfter() {
59+
60+
List<Entity> entities = IntStream.rangeClosed(1, 10)
61+
.mapToObj(i -> Entity.builder().id((long) i).message("message " + i).build()).collect(Collectors.toList());
62+
operations.saveAll(Mono.just(entities), Entity.class).blockLast();
63+
64+
Query query = Query.findAll();
65+
query.setPageable(PageRequest.of(0, 3));
66+
query.addSort(Sort.by(Sort.Direction.ASC, "id"));
67+
68+
List<Object> searchAfter = null;
69+
List<Entity> foundEntities = new ArrayList<>();
70+
71+
int loop = 0;
72+
do {
73+
query.setSearchAfter(searchAfter);
74+
List<SearchHit<Entity>> searchHits = operations.search(query, Entity.class).collectList().block();
75+
76+
if (searchHits.size() == 0) {
77+
break;
78+
}
79+
foundEntities.addAll(searchHits.stream().map(searchHit -> searchHit.getContent()).collect(Collectors.toList()));
80+
searchAfter = searchHits.get((int) (searchHits.size() - 1)).getSortValues();
81+
82+
if (++loop > 10) {
83+
fail("loop not terminating");
84+
}
85+
} while (true);
86+
87+
assertThat(foundEntities).containsExactlyElementsOf(entities);
88+
}
89+
90+
@Data
91+
@AllArgsConstructor
92+
@NoArgsConstructor
93+
@Builder
94+
@Document(indexName = "test-search-after")
95+
private static class Entity {
96+
@Id private Long id;
97+
@Field(type = FieldType.Text) private String message;
98+
}
99+
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.elasticsearch.core.paginating;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import lombok.AllArgsConstructor;
21+
import lombok.Builder;
22+
import lombok.Data;
23+
import lombok.NoArgsConstructor;
24+
25+
import java.util.ArrayList;
26+
import java.util.List;
27+
import java.util.stream.Collectors;
28+
import java.util.stream.IntStream;
29+
30+
import org.junit.jupiter.api.DisplayName;
31+
import org.junit.jupiter.api.Test;
32+
import org.springframework.beans.factory.annotation.Autowired;
33+
import org.springframework.data.annotation.Id;
34+
import org.springframework.data.domain.PageRequest;
35+
import org.springframework.data.domain.Sort;
36+
import org.springframework.data.elasticsearch.annotations.Document;
37+
import org.springframework.data.elasticsearch.annotations.Field;
38+
import org.springframework.data.elasticsearch.annotations.FieldType;
39+
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
40+
import org.springframework.data.elasticsearch.core.SearchHits;
41+
import org.springframework.data.elasticsearch.core.query.Query;
42+
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchRestTemplateConfiguration;
43+
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
44+
import org.springframework.test.context.ContextConfiguration;
45+
46+
/**
47+
* @author Peter-Josef Meisch
48+
*/
49+
@SpringIntegrationTest
50+
@ContextConfiguration(classes = { ElasticsearchRestTemplateConfiguration.class })
51+
public class SearchAfterIntegrationTests {
52+
53+
@Autowired private ElasticsearchOperations operations;
54+
55+
@Test // #1143
56+
@DisplayName("should read pages with search_after")
57+
void shouldReadPagesWithSearchAfter() {
58+
59+
List<Entity> entities = IntStream.rangeClosed(1, 10)
60+
.mapToObj(i -> Entity.builder().id((long) i).message("message " + i).build()).collect(Collectors.toList());
61+
operations.save(entities);
62+
63+
Query query = Query.findAll();
64+
query.setPageable(PageRequest.of(0, 3));
65+
query.addSort(Sort.by(Sort.Direction.ASC, "id"));
66+
67+
List<Object> searchAfter = null;
68+
List<Entity> foundEntities = new ArrayList<>();
69+
70+
int loop = 0;
71+
do {
72+
query.setSearchAfter(searchAfter);
73+
SearchHits<Entity> searchHits = operations.search(query, Entity.class);
74+
75+
if (searchHits.getSearchHits().size() == 0) {
76+
break;
77+
}
78+
foundEntities.addAll(searchHits.stream().map(searchHit -> searchHit.getContent()).collect(Collectors.toList()));
79+
searchAfter = searchHits.getSearchHit((int) (searchHits.getSearchHits().size() - 1)).getSortValues();
80+
81+
if (++loop > 10) {
82+
fail("loop not terminating");
83+
}
84+
} while (true);
85+
86+
assertThat(foundEntities).containsExactlyElementsOf(entities);
87+
}
88+
89+
@Data
90+
@AllArgsConstructor
91+
@NoArgsConstructor
92+
@Builder
93+
@Document(indexName = "test-search-after")
94+
private static class Entity {
95+
@Id private Long id;
96+
@Field(type = FieldType.Text) private String message;
97+
}
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.elasticsearch.core.paginating;
17+
18+
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
19+
import org.springframework.test.context.ContextConfiguration;
20+
21+
/**
22+
* @author Peter-Josef Meisch
23+
*/
24+
@ContextConfiguration(classes = { ElasticsearchTemplateConfiguration.class })
25+
public class SearchAfterTransportIntegrationTests extends SearchAfterIntegrationTests {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* Test for paginating support with search_after and point_in_time API
3+
*/
4+
@org.springframework.lang.NonNullApi
5+
@org.springframework.lang.NonNullFields
6+
package org.springframework.data.elasticsearch.core.paginating;

0 commit comments

Comments
 (0)