19
19
import static org .springframework .data .elasticsearch .client .elc .TypeUtils .*;
20
20
21
21
import co .elastic .clients .elasticsearch ._types .Result ;
22
- import co .elastic .clients .elasticsearch ._types .Time ;
23
22
import co .elastic .clients .elasticsearch .core .*;
24
23
import co .elastic .clients .elasticsearch .core .bulk .BulkResponseItem ;
25
24
import co .elastic .clients .elasticsearch .core .get .GetResult ;
35
34
import java .util .HashMap ;
36
35
import java .util .List ;
37
36
import java .util .Map ;
37
+ import java .util .function .BiFunction ;
38
+ import java .util .function .Function ;
39
+ import java .util .stream .Collectors ;
38
40
39
41
import org .reactivestreams .Publisher ;
42
+ import org .slf4j .Logger ;
43
+ import org .slf4j .LoggerFactory ;
44
+ import org .springframework .data .domain .Sort ;
40
45
import org .springframework .data .elasticsearch .BulkFailureException ;
41
46
import org .springframework .data .elasticsearch .NoSuchIndexException ;
42
47
import org .springframework .data .elasticsearch .UncategorizedElasticsearchException ;
43
48
import org .springframework .data .elasticsearch .client .UnsupportedBackendOperation ;
44
49
import org .springframework .data .elasticsearch .client .erhlc .ReactiveClusterOperations ;
45
- import org .springframework .data .elasticsearch .client .util .ScrollState ;
46
50
import org .springframework .data .elasticsearch .core .AbstractReactiveElasticsearchTemplate ;
47
51
import org .springframework .data .elasticsearch .core .AggregationContainer ;
48
52
import org .springframework .data .elasticsearch .core .IndexedObjectInformation ;
54
58
import org .springframework .data .elasticsearch .core .document .SearchDocument ;
55
59
import org .springframework .data .elasticsearch .core .document .SearchDocumentResponse ;
56
60
import org .springframework .data .elasticsearch .core .mapping .IndexCoordinates ;
61
+ import org .springframework .data .elasticsearch .core .query .BaseQuery ;
57
62
import org .springframework .data .elasticsearch .core .query .BulkOptions ;
58
63
import org .springframework .data .elasticsearch .core .query .ByQueryResponse ;
59
64
import org .springframework .data .elasticsearch .core .query .Query ;
64
69
import org .springframework .lang .Nullable ;
65
70
import org .springframework .util .Assert ;
66
71
import org .springframework .util .CollectionUtils ;
72
+ import org .springframework .util .StringUtils ;
67
73
68
74
/**
69
75
* Implementation of {@link org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations} using the new
74
80
*/
75
81
public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearchTemplate {
76
82
83
+ private static final Logger LOGGER = LoggerFactory .getLogger (ReactiveElasticsearchTemplate .class );
84
+
77
85
private final ReactiveElasticsearchClient client ;
78
86
private final RequestConverter requestConverter ;
79
87
private final ResponseConverter responseConverter ;
@@ -136,6 +144,32 @@ public <T> Flux<T> saveAll(Mono<? extends Collection<? extends T>> entitiesPubli
136
144
});
137
145
}
138
146
147
+ @ Override
148
+ protected Mono <Boolean > doExists (String id , IndexCoordinates index ) {
149
+
150
+ Assert .notNull (id , "id must not be null" );
151
+ Assert .notNull (index , "index must not be null" );
152
+
153
+ GetRequest getRequest = requestConverter .documentGetRequest (id , routingResolver .getRouting (), index , true );
154
+
155
+ return Mono .from (execute (
156
+ ((ClientCallback <Publisher <GetResponse <EntityAsMap >>>) client -> client .get (getRequest , EntityAsMap .class ))))
157
+ .map (GetResult ::found ) //
158
+ .onErrorReturn (NoSuchIndexException .class , false );
159
+ }
160
+
161
+ @ Override
162
+ public Mono <ByQueryResponse > delete (Query query , Class <?> entityType , IndexCoordinates index ) {
163
+
164
+ Assert .notNull (query , "query must not be null" );
165
+
166
+ DeleteByQueryRequest request = requestConverter .documentDeleteByQueryRequest (query , entityType , index ,
167
+ getRefreshPolicy ());
168
+ return Mono
169
+ .from (execute ((ClientCallback <Publisher <DeleteByQueryResponse >>) client -> client .deleteByQuery (request )))
170
+ .map (responseConverter ::byQueryResponse );
171
+ }
172
+
139
173
@ Override
140
174
public <T > Mono <T > get (String id , Class <T > entityType , IndexCoordinates index ) {
141
175
@@ -183,6 +217,29 @@ public Mono<String> submitReindex(ReindexRequest reindexRequest) {
183
217
: Mono .just (response .task ()));
184
218
}
185
219
220
+ @ Override
221
+ public Mono <UpdateResponse > update (UpdateQuery updateQuery , IndexCoordinates index ) {
222
+
223
+ Assert .notNull (updateQuery , "UpdateQuery must not be null" );
224
+ Assert .notNull (index , "Index must not be null" );
225
+
226
+ UpdateRequest <Document , ?> request = requestConverter .documentUpdateRequest (updateQuery , index , getRefreshPolicy (),
227
+ routingResolver .getRouting ());
228
+
229
+ return Mono .from (execute (
230
+ (ClientCallback <Publisher <co .elastic .clients .elasticsearch .core .UpdateResponse <Document >>>) client -> client
231
+ .update (request , Document .class )))
232
+ .flatMap (response -> {
233
+ UpdateResponse .Result result = result (response .result ());
234
+ return result == null ? Mono .empty () : Mono .just (UpdateResponse .of (result ));
235
+ });
236
+ }
237
+
238
+ @ Override
239
+ public Mono <ByQueryResponse > updateByQuery (UpdateQuery updateQuery , IndexCoordinates index ) {
240
+ throw new UnsupportedOperationException ("not implemented" );
241
+ }
242
+
186
243
@ Override
187
244
public Mono <Void > bulkUpdate (List <UpdateQuery > queries , BulkOptions bulkOptions , IndexCoordinates index ) {
188
245
@@ -279,87 +336,108 @@ protected ReactiveElasticsearchTemplate doCopy() {
279
336
return new ReactiveElasticsearchTemplate (client , converter );
280
337
}
281
338
282
- @ Override
283
- protected Mono <Boolean > doExists (String id , IndexCoordinates index ) {
284
-
285
- Assert .notNull (id , "id must not be null" );
286
- Assert .notNull (index , "index must not be null" );
287
-
288
- GetRequest getRequest = requestConverter .documentGetRequest (id , routingResolver .getRouting (), index , true );
289
-
290
- return Mono .from (execute (
291
- ((ClientCallback <Publisher <GetResponse <EntityAsMap >>>) client -> client .get (getRequest , EntityAsMap .class ))))
292
- .map (GetResult ::found ) //
293
- .onErrorReturn (NoSuchIndexException .class , false );
294
- }
295
-
296
- @ Override
297
- public Mono <ByQueryResponse > delete (Query query , Class <?> entityType , IndexCoordinates index ) {
298
-
299
- Assert .notNull (query , "query must not be null" );
300
-
301
- DeleteByQueryRequest request = requestConverter .documentDeleteByQueryRequest (query , entityType , index ,
302
- getRefreshPolicy ());
303
- return Mono
304
- .from (execute ((ClientCallback <Publisher <DeleteByQueryResponse >>) client -> client .deleteByQuery (request )))
305
- .map (responseConverter ::byQueryResponse );
306
- }
307
-
308
339
// region search operations
309
340
310
341
@ Override
311
342
protected Flux <SearchDocument > doFind (Query query , Class <?> clazz , IndexCoordinates index ) {
312
343
313
344
return Flux .defer (() -> {
314
- boolean useScroll = !(query .getPageable ().isPaged () || query .isLimiting ());
315
- SearchRequest searchRequest = requestConverter .searchRequest (query , clazz , index , false , useScroll );
345
+ boolean queryIsUnbounded = !(query .getPageable ().isPaged () || query .isLimiting ());
316
346
317
- if (useScroll ) {
318
- return doScroll (searchRequest );
319
- } else {
320
- return doFind (searchRequest );
321
- }
347
+ return queryIsUnbounded ? doFindUnbounded (query , clazz , index ) : doFindBounded (query , clazz , index );
322
348
});
323
349
324
350
}
325
351
326
- private Flux <SearchDocument > doScroll (SearchRequest searchRequest ) {
352
+ private Flux <SearchDocument > doFindUnbounded (Query query , Class <?> clazz , IndexCoordinates index ) {
353
+
354
+ if (query instanceof BaseQuery baseQuery ) {
355
+ var pitKeepAlive = Duration .ofMinutes (5 );
356
+ // setup functions for Flux.usingWhen()
357
+ Mono <PitSearchAfter > resourceSupplier = openPointInTime (index , pitKeepAlive , true )
358
+ .map (pit -> new PitSearchAfter (baseQuery , pit ));
359
+
360
+ Function <PitSearchAfter , Publisher <?>> asyncComplete = this ::cleanupPit ;
361
+
362
+ BiFunction <PitSearchAfter , Throwable , Publisher <?>> asyncError = (psa , ex ) -> {
363
+ if (LOGGER .isErrorEnabled ()) {
364
+ LOGGER .error (String .format ("Error during pit/search_after" ), ex );
365
+ }
366
+ return cleanupPit (psa );
367
+ };
368
+
369
+ Function <PitSearchAfter , Publisher <?>> asyncCancel = psa -> {
370
+ if (LOGGER .isWarnEnabled ()) {
371
+ LOGGER .warn (String .format ("pit/search_after was cancelled" ));
372
+ }
373
+ return cleanupPit (psa );
374
+ };
327
375
328
- Time scrollTimeout = searchRequest . scroll () != null ? searchRequest . scroll () : Time . of ( t -> t . time ( "1m" ));
376
+ Function < PitSearchAfter , Publisher <? extends ResponseBody < EntityAsMap >>> resourceClosure = psa -> {
329
377
330
- Flux <ResponseBody <EntityAsMap >> searchResponses = Flux .usingWhen (Mono .fromSupplier (ScrollState ::new ), //
331
- state -> Mono
332
- .from (execute ((ClientCallback <Publisher <ResponseBody <EntityAsMap >>>) client -> client .search (searchRequest ,
333
- EntityAsMap .class ))) //
334
- .expand (entityAsMapSearchResponse -> {
378
+ baseQuery .setPointInTime (new Query .PointInTime (psa .getPit (), pitKeepAlive ));
379
+ baseQuery .addSort (Sort .by ("_shard_doc" ));
380
+ SearchRequest firstSearchRequest = requestConverter .searchRequest (baseQuery , clazz , index , false , true );
335
381
336
- state .updateScrollId (entityAsMapSearchResponse .scrollId ());
382
+ return Mono .from (execute ((ClientCallback <Publisher <ResponseBody <EntityAsMap >>>) client -> client
383
+ .search (firstSearchRequest , EntityAsMap .class ))).expand (entityAsMapSearchResponse -> {
337
384
338
- if ( entityAsMapSearchResponse .hits () == null
339
- || CollectionUtils .isEmpty (entityAsMapSearchResponse . hits (). hits () )) {
385
+ var hits = entityAsMapSearchResponse .hits (). hits ();
386
+ if ( CollectionUtils .isEmpty (hits )) {
340
387
return Mono .empty ();
341
388
}
342
389
343
- return Mono . from ( execute (( ClientCallback < Publisher < ScrollResponse < EntityAsMap >>>) client1 -> {
344
- ScrollRequest scrollRequest = ScrollRequest
345
- . of ( sr -> sr . scrollId ( state . getScrollId ()). scroll ( scrollTimeout ) );
346
- return client1 . scroll ( scrollRequest , EntityAsMap . class );
347
- }));
348
- }),
349
- this :: cleanupScroll , ( state , ex ) -> cleanupScroll ( state ), this :: cleanupScroll );
390
+ List < Object > sortOptions = hits . get ( hits . size () - 1 ). sort (). stream (). map ( TypeUtils :: toObject )
391
+ . collect ( Collectors . toList ());
392
+ baseQuery . setSearchAfter ( sortOptions );
393
+ SearchRequest followSearchRequest = requestConverter . searchRequest ( baseQuery , clazz , index , false , true );
394
+ return Mono . from ( execute (( ClientCallback < Publisher < ResponseBody < EntityAsMap >>>) client -> client
395
+ . search ( followSearchRequest , EntityAsMap . class )));
396
+ } );
350
397
351
- return searchResponses .flatMapIterable (entityAsMapSearchResponse -> entityAsMapSearchResponse .hits ().hits ())
352
- .map (entityAsMapHit -> DocumentAdapters .from (entityAsMapHit , jsonpMapper ));
398
+ };
399
+
400
+ Flux <ResponseBody <EntityAsMap >> searchResponses = Flux .usingWhen (resourceSupplier , resourceClosure , asyncComplete ,
401
+ asyncError , asyncCancel );
402
+ return searchResponses .flatMapIterable (entityAsMapSearchResponse -> entityAsMapSearchResponse .hits ().hits ())
403
+ .map (entityAsMapHit -> DocumentAdapters .from (entityAsMapHit , jsonpMapper ));
404
+ } else {
405
+ return Flux .error (new IllegalArgumentException ("Query must be derived from BaseQuery" ));
406
+ }
353
407
}
354
408
355
- private Publisher <?> cleanupScroll (ScrollState state ) {
409
+ private Publisher <?> cleanupPit (PitSearchAfter psa ) {
410
+ var baseQuery = psa .getBaseQuery ();
411
+ baseQuery .setPointInTime (null );
412
+ baseQuery .setSearchAfter (null );
413
+ baseQuery .setSort (psa .getSort ());
414
+ var pit = psa .getPit ();
415
+ return StringUtils .hasText (pit ) ? closePointInTime (pit ) : Mono .empty ();
416
+ }
417
+
418
+ static private class PitSearchAfter {
419
+ private final BaseQuery baseQuery ;
420
+ @ Nullable private final Sort sort ;
421
+ private final String pit ;
422
+
423
+ PitSearchAfter (BaseQuery baseQuery , String pit ) {
424
+ this .baseQuery = baseQuery ;
425
+ this .sort = baseQuery .getSort ();
426
+ this .pit = pit ;
427
+ }
428
+
429
+ public BaseQuery getBaseQuery () {
430
+ return baseQuery ;
431
+ }
356
432
357
- if (state .getScrollIds ().isEmpty ()) {
358
- return Mono .empty ();
433
+ @ Nullable
434
+ public Sort getSort () {
435
+ return sort ;
359
436
}
360
437
361
- return execute ((ClientCallback <Publisher <ClearScrollResponse >>) client -> client
362
- .clearScroll (ClearScrollRequest .of (csr -> csr .scrollId (state .getScrollIds ()))));
438
+ public String getPit () {
439
+ return pit ;
440
+ }
363
441
}
364
442
365
443
@ Override
@@ -368,15 +446,17 @@ protected Mono<Long> doCount(Query query, Class<?> entityType, IndexCoordinates
368
446
Assert .notNull (query , "query must not be null" );
369
447
Assert .notNull (index , "index must not be null" );
370
448
371
- SearchRequest searchRequest = requestConverter .searchRequest (query , entityType , index , true , false );
449
+ SearchRequest searchRequest = requestConverter .searchRequest (query , entityType , index , true );
372
450
373
451
return Mono
374
452
.from (execute ((ClientCallback <Publisher <ResponseBody <EntityAsMap >>>) client -> client .search (searchRequest ,
375
453
EntityAsMap .class )))
376
454
.map (searchResponse -> searchResponse .hits ().total () != null ? searchResponse .hits ().total ().value () : 0L );
377
455
}
378
456
379
- private Flux <SearchDocument > doFind (SearchRequest searchRequest ) {
457
+ private Flux <SearchDocument > doFindBounded (Query query , Class <?> clazz , IndexCoordinates index ) {
458
+
459
+ SearchRequest searchRequest = requestConverter .searchRequest (query , clazz , index , false , false );
380
460
381
461
return Mono
382
462
.from (execute ((ClientCallback <Publisher <ResponseBody <EntityAsMap >>>) client -> client .search (searchRequest ,
@@ -391,7 +471,7 @@ protected <T> Mono<SearchDocumentResponse> doFindForResponse(Query query, Class<
391
471
Assert .notNull (query , "query must not be null" );
392
472
Assert .notNull (index , "index must not be null" );
393
473
394
- SearchRequest searchRequest = requestConverter .searchRequest (query , clazz , index , false , false );
474
+ SearchRequest searchRequest = requestConverter .searchRequest (query , clazz , index , false );
395
475
396
476
// noinspection unchecked
397
477
SearchDocumentCallback <T > callback = new ReadSearchDocumentCallback <>((Class <T >) clazz , index );
@@ -458,29 +538,6 @@ public Mono<String> getClusterVersion() {
458
538
})).map (infoResponse -> infoResponse .version ().number ());
459
539
}
460
540
461
- @ Override
462
- public Mono <UpdateResponse > update (UpdateQuery updateQuery , IndexCoordinates index ) {
463
-
464
- Assert .notNull (updateQuery , "UpdateQuery must not be null" );
465
- Assert .notNull (index , "Index must not be null" );
466
-
467
- UpdateRequest <Document , ?> request = requestConverter .documentUpdateRequest (updateQuery , index , getRefreshPolicy (),
468
- routingResolver .getRouting ());
469
-
470
- return Mono .from (execute (
471
- (ClientCallback <Publisher <co .elastic .clients .elasticsearch .core .UpdateResponse <Document >>>) client -> client
472
- .update (request , Document .class )))
473
- .flatMap (response -> {
474
- UpdateResponse .Result result = result (response .result ());
475
- return result == null ? Mono .empty () : Mono .just (UpdateResponse .of (result ));
476
- });
477
- }
478
-
479
- @ Override
480
- public Mono <ByQueryResponse > updateByQuery (UpdateQuery updateQuery , IndexCoordinates index ) {
481
- throw new UnsupportedOperationException ("not implemented" );
482
- }
483
-
484
541
@ Override
485
542
@ Deprecated
486
543
public <T > Publisher <T > execute (ReactiveElasticsearchOperations .ClientCallback <Publisher <T >> callback ) {
0 commit comments