|
20 | 20 | import io.netty.handler.ssl.JdkSslContext;
|
21 | 21 | import io.netty.handler.timeout.ReadTimeoutHandler;
|
22 | 22 | import io.netty.handler.timeout.WriteTimeoutHandler;
|
| 23 | +import reactor.core.publisher.EmitterProcessor; |
23 | 24 | import reactor.core.publisher.Flux;
|
| 25 | +import reactor.core.publisher.FluxSink; |
24 | 26 | import reactor.core.publisher.Mono;
|
25 | 27 | import reactor.netty.http.client.HttpClient;
|
26 | 28 | import reactor.netty.tcp.TcpClient;
|
|
31 | 33 | import java.net.InetSocketAddress;
|
32 | 34 | import java.nio.charset.StandardCharsets;
|
33 | 35 | import java.time.Duration;
|
| 36 | +import java.util.ArrayList; |
34 | 37 | import java.util.Collection;
|
| 38 | +import java.util.Collections; |
| 39 | +import java.util.List; |
35 | 40 | import java.util.Map.Entry;
|
36 | 41 | import java.util.Optional;
|
37 | 42 | import java.util.concurrent.TimeUnit;
|
|
53 | 58 | import org.elasticsearch.action.index.IndexResponse;
|
54 | 59 | import org.elasticsearch.action.main.MainRequest;
|
55 | 60 | import org.elasticsearch.action.main.MainResponse;
|
| 61 | +import org.elasticsearch.action.search.ClearScrollRequest; |
| 62 | +import org.elasticsearch.action.search.ClearScrollResponse; |
56 | 63 | import org.elasticsearch.action.search.SearchRequest;
|
57 | 64 | import org.elasticsearch.action.search.SearchResponse;
|
| 65 | +import org.elasticsearch.action.search.SearchScrollRequest; |
58 | 66 | import org.elasticsearch.action.update.UpdateRequest;
|
59 | 67 | import org.elasticsearch.action.update.UpdateResponse;
|
60 | 68 | import org.elasticsearch.client.Request;
|
| 69 | +import org.elasticsearch.common.unit.TimeValue; |
61 | 70 | import org.elasticsearch.common.xcontent.DeprecationHandler;
|
62 | 71 | import org.elasticsearch.common.xcontent.NamedXContentRegistry;
|
63 | 72 | import org.elasticsearch.common.xcontent.XContentParser;
|
|
67 | 76 | import org.elasticsearch.index.reindex.DeleteByQueryRequest;
|
68 | 77 | import org.elasticsearch.rest.BytesRestResponse;
|
69 | 78 | import org.elasticsearch.rest.RestStatus;
|
| 79 | +import org.elasticsearch.search.Scroll; |
70 | 80 | import org.elasticsearch.search.SearchHit;
|
71 | 81 | import org.reactivestreams.Publisher;
|
72 | 82 | import org.springframework.data.elasticsearch.ElasticsearchException;
|
|
85 | 95 | import org.springframework.util.Assert;
|
86 | 96 | import org.springframework.util.ObjectUtils;
|
87 | 97 | import org.springframework.util.ReflectionUtils;
|
| 98 | +import org.springframework.util.StringUtils; |
88 | 99 | import org.springframework.web.client.HttpServerErrorException;
|
89 | 100 | import org.springframework.web.reactive.function.BodyExtractors;
|
90 | 101 | import org.springframework.web.reactive.function.client.ClientRequest;
|
@@ -297,6 +308,74 @@ public Flux<SearchHit> search(HttpHeaders headers, SearchRequest searchRequest)
|
297 | 308 | .flatMap(Flux::fromIterable);
|
298 | 309 | }
|
299 | 310 |
|
| 311 | + /* |
| 312 | + * (non-Javadoc) |
| 313 | + * @see org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient#scroll(org.springframework.http.HttpHeaders, org.elasticsearch.action.search.SearchRequest) |
| 314 | + */ |
| 315 | + @Override |
| 316 | + public Flux<SearchHit> scroll(HttpHeaders headers, SearchRequest searchRequest) { |
| 317 | + |
| 318 | + TimeValue scrollTimeout = searchRequest.scroll() != null ? searchRequest.scroll().keepAlive() |
| 319 | + : TimeValue.timeValueMinutes(1); |
| 320 | + |
| 321 | + if (searchRequest.scroll() == null) { |
| 322 | + searchRequest.scroll(scrollTimeout); |
| 323 | + } |
| 324 | + |
| 325 | + EmitterProcessor<ActionRequest> outbound = EmitterProcessor.create(false); |
| 326 | + FluxSink<ActionRequest> request = outbound.sink(); |
| 327 | + |
| 328 | + EmitterProcessor<SearchResponse> inbound = EmitterProcessor.create(false); |
| 329 | + |
| 330 | + Flux<SearchResponse> exchange = outbound.startWith(searchRequest).flatMap(it -> { |
| 331 | + |
| 332 | + if (it instanceof SearchRequest) { |
| 333 | + return sendRequest((SearchRequest) it, RequestCreator.search(), SearchResponse.class, headers); |
| 334 | + } else if (it instanceof SearchScrollRequest) { |
| 335 | + return sendRequest((SearchScrollRequest) it, RequestCreator.scroll(), SearchResponse.class, headers); |
| 336 | + } else if (it instanceof ClearScrollRequest) { |
| 337 | + return sendRequest((ClearScrollRequest) it, RequestCreator.clearScroll(), ClearScrollResponse.class, headers) |
| 338 | + .flatMap(discard -> Flux.empty()); |
| 339 | + } |
| 340 | + |
| 341 | + throw new IllegalArgumentException( |
| 342 | + String.format("Cannot handle '%s'. Please make sure to use a 'SearchRequest' or 'SearchScrollRequest'.")); |
| 343 | + }); |
| 344 | + |
| 345 | + ScrollState state = new ScrollState(); |
| 346 | + |
| 347 | + Flux<SearchHit> searchHits = inbound.doOnNext(searchResponse -> { |
| 348 | + state.updateScrollId(searchResponse.getScrollId()); |
| 349 | + }).<SearchResponse> handle((searchResponse, sink) -> { |
| 350 | + |
| 351 | + if (searchResponse.getHits() != null && searchResponse.getHits().getHits() != null |
| 352 | + && searchResponse.getHits().getHits().length == 0) { |
| 353 | + |
| 354 | + inbound.onComplete(); |
| 355 | + outbound.onComplete(); |
| 356 | + |
| 357 | + } else { |
| 358 | + |
| 359 | + sink.next(searchResponse); |
| 360 | + |
| 361 | + SearchScrollRequest searchScrollRequest = new SearchScrollRequest(state.getScrollId()).scroll(scrollTimeout); |
| 362 | + request.next(searchScrollRequest); |
| 363 | + } |
| 364 | + |
| 365 | + }).map(SearchResponse::getHits) // |
| 366 | + .flatMap(Flux::fromIterable) // |
| 367 | + .doOnComplete(() -> { |
| 368 | + |
| 369 | + ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); |
| 370 | + clearScrollRequest.scrollIds(state.getScrollIds()); |
| 371 | + |
| 372 | + // just send the request, resources get cleaned up anyways after scrollTimeout has been reached. |
| 373 | + sendRequest(clearScrollRequest, RequestCreator.clearScroll(), ClearScrollResponse.class, headers).subscribe(); |
| 374 | + }); |
| 375 | + |
| 376 | + return searchHits.doOnSubscribe(ignore -> exchange.subscribe(inbound)); |
| 377 | + } |
| 378 | + |
300 | 379 | /*
|
301 | 380 | * (non-Javadoc)
|
302 | 381 | * @see org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient#ping(org.springframework.http.HttpHeaders, org.elasticsearch.index.reindex.DeleteByQueryRequest)
|
@@ -482,6 +561,14 @@ static Function<SearchRequest, Request> search() {
|
482 | 561 | return RequestConverters::search;
|
483 | 562 | }
|
484 | 563 |
|
| 564 | + static Function<SearchScrollRequest, Request> scroll() { |
| 565 | + return RequestConverters::searchScroll; |
| 566 | + } |
| 567 | + |
| 568 | + static Function<ClearScrollRequest, Request> clearScroll() { |
| 569 | + return RequestConverters::clearScroll; |
| 570 | + } |
| 571 | + |
485 | 572 | static Function<IndexRequest, Request> index() {
|
486 | 573 | return RequestConverters::index;
|
487 | 574 | }
|
@@ -549,4 +636,39 @@ public Collection<ElasticsearchHost> hosts() {
|
549 | 636 | return connectedHosts;
|
550 | 637 | }
|
551 | 638 | }
|
| 639 | + |
| 640 | + /** |
| 641 | + * Mutable state object holding scrollId to be used for {@link SearchScrollRequest#scroll(Scroll)} |
| 642 | + * |
| 643 | + * @author Christoph Strobl |
| 644 | + * @since 4.0 |
| 645 | + */ |
| 646 | + private static class ScrollState { |
| 647 | + |
| 648 | + private Object lock = new Object(); |
| 649 | + |
| 650 | + private String scrollId; |
| 651 | + private List<String> pastIds = new ArrayList<>(1); |
| 652 | + |
| 653 | + String getScrollId() { |
| 654 | + return scrollId; |
| 655 | + } |
| 656 | + |
| 657 | + List<String> getScrollIds() { |
| 658 | + return Collections.unmodifiableList(pastIds); |
| 659 | + } |
| 660 | + |
| 661 | + void updateScrollId(String scrollId) { |
| 662 | + |
| 663 | + if (StringUtils.hasText(scrollId)) { |
| 664 | + |
| 665 | + synchronized (lock) { |
| 666 | + |
| 667 | + this.scrollId = scrollId; |
| 668 | + pastIds.add(scrollId); |
| 669 | + } |
| 670 | + } |
| 671 | + } |
| 672 | + |
| 673 | + } |
552 | 674 | }
|
0 commit comments