Add support for search_after.

Original Pull Request #1691
Closes #1143
This commit is contained in:
Peter-Josef Meisch 2021-02-12 20:35:56 +01:00 committed by GitHub
parent 154c50b3b7
commit ffc2420bcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 267 additions and 2 deletions

View File

@ -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;
}

View File

@ -62,6 +62,7 @@ abstract class AbstractQuery implements Query {
@Nullable private Duration scrollTime;
@Nullable private TimeValue timeout;
private boolean explain = false;
@Nullable private List<Object> 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<Object> searchAfter) {
this.searchAfter = searchAfter;
}
@Nullable
@Override
public List<Object> getSearchAfter() {
return searchAfter;
}
}

View File

@ -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<Object> searchAfter);
/**
* @return the search_after objects.
* @since 4.2
*/
@Nullable
List<Object> getSearchAfter();
}

View File

@ -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<Entity> 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<Object> searchAfter = null;
List<Entity> foundEntities = new ArrayList<>();
int loop = 0;
do {
query.setSearchAfter(searchAfter);
List<SearchHit<Entity>> 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;
}
}

View File

@ -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<Entity> 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<Object> searchAfter = null;
List<Entity> foundEntities = new ArrayList<>();
int loop = 0;
do {
query.setSearchAfter(searchAfter);
SearchHits<Entity> 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;
}
}

View File

@ -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 {}

View File

@ -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;