14
14
import org .apache .lucene .search .ConstantScoreScorer ;
15
15
import org .apache .lucene .search .ConstantScoreWeight ;
16
16
import org .apache .lucene .search .DocIdSetIterator ;
17
+ import org .apache .lucene .search .Explanation ;
17
18
import org .apache .lucene .search .IndexSearcher ;
18
19
import org .apache .lucene .search .LeafCollector ;
19
20
import org .apache .lucene .search .Query ;
22
23
import org .apache .lucene .search .ScoreMode ;
23
24
import org .apache .lucene .search .Scorer ;
24
25
import org .apache .lucene .search .ScorerSupplier ;
26
+ import org .apache .lucene .search .TopDocs ;
25
27
import org .apache .lucene .search .Weight ;
26
28
import org .apache .lucene .util .Bits ;
29
+ import org .apache .lucene .util .CharsRefBuilder ;
27
30
import org .elasticsearch .ElasticsearchException ;
28
31
import org .elasticsearch .TransportVersion ;
29
32
import org .elasticsearch .action .search .SearchRequestBuilder ;
33
36
import org .elasticsearch .core .TimeValue ;
34
37
import org .elasticsearch .index .query .AbstractQueryBuilder ;
35
38
import org .elasticsearch .index .query .QueryBuilder ;
39
+ import org .elasticsearch .index .query .QueryRewriteContext ;
36
40
import org .elasticsearch .index .query .SearchExecutionContext ;
41
+ import org .elasticsearch .index .query .TermQueryBuilder ;
37
42
import org .elasticsearch .plugins .Plugin ;
38
43
import org .elasticsearch .plugins .SearchPlugin ;
39
44
import org .elasticsearch .search .aggregations .bucket .terms .StringTerms ;
40
45
import org .elasticsearch .search .aggregations .bucket .terms .TermsAggregationBuilder ;
41
46
import org .elasticsearch .search .internal .ContextIndexSearcher ;
47
+ import org .elasticsearch .search .rescore .RescoreContext ;
48
+ import org .elasticsearch .search .rescore .Rescorer ;
49
+ import org .elasticsearch .search .rescore .RescorerBuilder ;
50
+ import org .elasticsearch .search .suggest .SortBy ;
51
+ import org .elasticsearch .search .suggest .SuggestBuilder ;
52
+ import org .elasticsearch .search .suggest .Suggester ;
53
+ import org .elasticsearch .search .suggest .SuggestionSearchContext ;
54
+ import org .elasticsearch .search .suggest .term .TermSuggestion ;
55
+ import org .elasticsearch .search .suggest .term .TermSuggestionBuilder ;
42
56
import org .elasticsearch .test .ESIntegTestCase ;
43
57
import org .elasticsearch .test .hamcrest .ElasticsearchAssertions ;
44
58
import org .elasticsearch .xcontent .XContentBuilder ;
@@ -58,7 +72,7 @@ public class SearchTimeoutIT extends ESIntegTestCase {
58
72
59
73
@ Override
60
74
protected Collection <Class <? extends Plugin >> nodePlugins () {
61
- return Collections .singleton (BulkScorerTimeoutQueryPlugin .class );
75
+ return Collections .singleton (SearchTimeoutPlugin .class );
62
76
}
63
77
64
78
@ Override
@@ -72,6 +86,9 @@ protected void setupSuiteScopeCluster() throws Exception {
72
86
indexRandom (true , "test" , randomIntBetween (20 , 50 ));
73
87
}
74
88
89
+ /**
90
+ * Test the scenario where the query times out before starting to collect documents, verify that partial hits are not returned
91
+ */
75
92
public void testTopHitsTimeoutBeforeCollecting () {
76
93
// setting the timeout is necessary only because we check that if a TimeExceededException is thrown, a timeout was set
77
94
SearchRequestBuilder searchRequestBuilder = prepareSearch ("test" ).setTimeout (new TimeValue (10 , TimeUnit .SECONDS ))
@@ -88,6 +105,9 @@ public void testTopHitsTimeoutBeforeCollecting() {
88
105
});
89
106
}
90
107
108
+ /**
109
+ * Test the scenario where the query times out while collecting documents, verify that partial hits results are returned
110
+ */
91
111
public void testTopHitsTimeoutWhileCollecting () {
92
112
// setting the timeout is necessary only because we check that if a TimeExceededException is thrown, a timeout was set
93
113
SearchRequestBuilder searchRequestBuilder = prepareSearch ("test" ).setTimeout (new TimeValue (10 , TimeUnit .SECONDS ))
@@ -103,6 +123,9 @@ public void testTopHitsTimeoutWhileCollecting() {
103
123
});
104
124
}
105
125
126
+ /**
127
+ * Test the scenario where the query times out before starting to collect documents, verify that partial aggs results are not returned
128
+ */
106
129
public void testAggsTimeoutBeforeCollecting () {
107
130
SearchRequestBuilder searchRequestBuilder = prepareSearch ("test" ).setSize (0 )
108
131
// setting the timeout is necessary only because we check that if a TimeExceededException is thrown, a timeout was set
@@ -123,6 +146,9 @@ public void testAggsTimeoutBeforeCollecting() {
123
146
});
124
147
}
125
148
149
+ /**
150
+ * Test the scenario where the query times out while collecting documents, verify that partial aggs results are returned
151
+ */
126
152
public void testAggsTimeoutWhileCollecting () {
127
153
SearchRequestBuilder searchRequestBuilder = prepareSearch ("test" ).setSize (0 )
128
154
// setting the timeout is necessary only because we check that if a TimeExceededException is thrown, a timeout was set
@@ -145,6 +171,56 @@ public void testAggsTimeoutWhileCollecting() {
145
171
});
146
172
}
147
173
174
+ /**
175
+ * Test the scenario where the suggest phase (part of the query phase) times out, yet there are results
176
+ * available coming from executing the query and aggs on each shard.
177
+ */
178
+ public void testSuggestTimeoutWithPartialResults () {
179
+ SuggestBuilder suggestBuilder = new SuggestBuilder ();
180
+ suggestBuilder .setGlobalText ("text" );
181
+ TimeoutSuggestionBuilder timeoutSuggestionBuilder = new TimeoutSuggestionBuilder ();
182
+ suggestBuilder .addSuggestion ("suggest" , timeoutSuggestionBuilder );
183
+ SearchRequestBuilder searchRequestBuilder = prepareSearch ("test" ).suggest (suggestBuilder )
184
+ .addAggregation (new TermsAggregationBuilder ("terms" ).field ("field.keyword" ));
185
+ ElasticsearchAssertions .assertResponse (searchRequestBuilder , searchResponse -> {
186
+ assertThat (searchResponse .isTimedOut (), equalTo (true ));
187
+ assertEquals (0 , searchResponse .getShardFailures ().length );
188
+ assertEquals (0 , searchResponse .getFailedShards ());
189
+ assertThat (searchResponse .getSuccessfulShards (), greaterThan (0 ));
190
+ assertEquals (searchResponse .getSuccessfulShards (), searchResponse .getTotalShards ());
191
+ assertThat (searchResponse .getHits ().getTotalHits ().value (), greaterThan (0L ));
192
+ assertThat (searchResponse .getHits ().getHits ().length , greaterThan (0 ));
193
+ StringTerms terms = searchResponse .getAggregations ().get ("terms" );
194
+ assertEquals (1 , terms .getBuckets ().size ());
195
+ StringTerms .Bucket bucket = terms .getBuckets ().get (0 );
196
+ assertEquals ("value" , bucket .getKeyAsString ());
197
+ assertThat (bucket .getDocCount (), greaterThan (0L ));
198
+ });
199
+ }
200
+
201
+ /**
202
+ * Test the scenario where the rescore phase (part of the query phase) times out, yet there are results
203
+ * available coming from executing the query and aggs on each shard.
204
+ */
205
+ public void testRescoreTimeoutWithPartialResults () {
206
+ SearchRequestBuilder searchRequestBuilder = prepareSearch ("test" ).setRescorer (new TimeoutRescorerBuilder ())
207
+ .addAggregation (new TermsAggregationBuilder ("terms" ).field ("field.keyword" ));
208
+ ElasticsearchAssertions .assertResponse (searchRequestBuilder , searchResponse -> {
209
+ assertThat (searchResponse .isTimedOut (), equalTo (true ));
210
+ assertEquals (0 , searchResponse .getShardFailures ().length );
211
+ assertEquals (0 , searchResponse .getFailedShards ());
212
+ assertThat (searchResponse .getSuccessfulShards (), greaterThan (0 ));
213
+ assertEquals (searchResponse .getSuccessfulShards (), searchResponse .getTotalShards ());
214
+ assertThat (searchResponse .getHits ().getTotalHits ().value (), greaterThan (0L ));
215
+ assertThat (searchResponse .getHits ().getHits ().length , greaterThan (0 ));
216
+ StringTerms terms = searchResponse .getAggregations ().get ("terms" );
217
+ assertEquals (1 , terms .getBuckets ().size ());
218
+ StringTerms .Bucket bucket = terms .getBuckets ().get (0 );
219
+ assertEquals ("value" , bucket .getKeyAsString ());
220
+ assertThat (bucket .getDocCount (), greaterThan (0L ));
221
+ });
222
+ }
223
+
148
224
public void testPartialResultsIntolerantTimeoutBeforeCollecting () {
149
225
ElasticsearchException ex = expectThrows (
150
226
ElasticsearchException .class ,
@@ -171,13 +247,67 @@ public void testPartialResultsIntolerantTimeoutWhileCollecting() {
171
247
assertEquals (429 , ex .status ().getStatus ());
172
248
}
173
249
174
- public static final class BulkScorerTimeoutQueryPlugin extends Plugin implements SearchPlugin {
250
+ public void testPartialResultsIntolerantTimeoutWhileSuggestingOnly () {
251
+ SuggestBuilder suggestBuilder = new SuggestBuilder ();
252
+ suggestBuilder .setGlobalText ("text" );
253
+ TimeoutSuggestionBuilder timeoutSuggestionBuilder = new TimeoutSuggestionBuilder ();
254
+ suggestBuilder .addSuggestion ("suggest" , timeoutSuggestionBuilder );
255
+ ElasticsearchException ex = expectThrows (
256
+ ElasticsearchException .class ,
257
+ prepareSearch ("test" ).suggest (suggestBuilder ).setAllowPartialSearchResults (false ) // this line causes timeouts to report
258
+ // failures
259
+ );
260
+ assertTrue (ex .toString ().contains ("Time exceeded" ));
261
+ assertEquals (429 , ex .status ().getStatus ());
262
+ }
263
+
264
+ public void testPartialResultsIntolerantTimeoutWhileSuggesting () {
265
+ SuggestBuilder suggestBuilder = new SuggestBuilder ();
266
+ suggestBuilder .setGlobalText ("text" );
267
+ TimeoutSuggestionBuilder timeoutSuggestionBuilder = new TimeoutSuggestionBuilder ();
268
+ suggestBuilder .addSuggestion ("suggest" , timeoutSuggestionBuilder );
269
+ ElasticsearchException ex = expectThrows (
270
+ ElasticsearchException .class ,
271
+ prepareSearch ("test" ).setQuery (new TermQueryBuilder ("field" , "value" ))
272
+ .suggest (suggestBuilder )
273
+ .setAllowPartialSearchResults (false ) // this line causes timeouts to report failures
274
+ );
275
+ assertTrue (ex .toString ().contains ("Time exceeded" ));
276
+ assertEquals (429 , ex .status ().getStatus ());
277
+ }
278
+
279
+ public void testPartialResultsIntolerantTimeoutWhileRescoring () {
280
+ ElasticsearchException ex = expectThrows (
281
+ ElasticsearchException .class ,
282
+ prepareSearch ("test" ).setQuery (new TermQueryBuilder ("field" , "value" ))
283
+ .setRescorer (new TimeoutRescorerBuilder ())
284
+ .setAllowPartialSearchResults (false ) // this line causes timeouts to report failures
285
+ );
286
+ assertTrue (ex .toString ().contains ("Time exceeded" ));
287
+ assertEquals (429 , ex .status ().getStatus ());
288
+ }
289
+
290
+ public static final class SearchTimeoutPlugin extends Plugin implements SearchPlugin {
175
291
@ Override
176
292
public List <QuerySpec <?>> getQueries () {
177
293
return Collections .singletonList (new QuerySpec <QueryBuilder >("timeout" , BulkScorerTimeoutQuery ::new , parser -> {
178
294
throw new UnsupportedOperationException ();
179
295
}));
180
296
}
297
+
298
+ @ Override
299
+ public List <SuggesterSpec <?>> getSuggesters () {
300
+ return Collections .singletonList (new SuggesterSpec <>("timeout" , TimeoutSuggestionBuilder ::new , parser -> {
301
+ throw new UnsupportedOperationException ();
302
+ }, TermSuggestion ::new ));
303
+ }
304
+
305
+ @ Override
306
+ public List <RescorerSpec <?>> getRescorers () {
307
+ return Collections .singletonList (new RescorerSpec <>("timeout" , TimeoutRescorerBuilder ::new , parser -> {
308
+ throw new UnsupportedOperationException ();
309
+ }));
310
+ }
181
311
}
182
312
183
313
/**
@@ -315,4 +445,111 @@ public TransportVersion getMinimalSupportedVersion() {
315
445
return null ;
316
446
}
317
447
}
448
+
449
+ /**
450
+ * Suggestion builder that triggers a timeout as part of its execution
451
+ */
452
+ private static final class TimeoutSuggestionBuilder extends TermSuggestionBuilder {
453
+ TimeoutSuggestionBuilder () {
454
+ super ("field" );
455
+ }
456
+
457
+ TimeoutSuggestionBuilder (StreamInput in ) throws IOException {
458
+ super (in );
459
+ }
460
+
461
+ @ Override
462
+ public String getWriteableName () {
463
+ return "timeout" ;
464
+ }
465
+
466
+ @ Override
467
+ public SuggestionSearchContext .SuggestionContext build (SearchExecutionContext context ) {
468
+ return new TimeoutSuggestionContext (new TimeoutSuggester ((ContextIndexSearcher ) context .searcher ()), context );
469
+ }
470
+ }
471
+
472
+ private static final class TimeoutSuggester extends Suggester <TimeoutSuggestionContext > {
473
+ private final ContextIndexSearcher contextIndexSearcher ;
474
+
475
+ TimeoutSuggester (ContextIndexSearcher contextIndexSearcher ) {
476
+ this .contextIndexSearcher = contextIndexSearcher ;
477
+ }
478
+
479
+ @ Override
480
+ protected TermSuggestion innerExecute (
481
+ String name ,
482
+ TimeoutSuggestionContext suggestion ,
483
+ IndexSearcher searcher ,
484
+ CharsRefBuilder spare
485
+ ) {
486
+ contextIndexSearcher .throwTimeExceededException ();
487
+ assert false ;
488
+ return new TermSuggestion (name , suggestion .getSize (), SortBy .SCORE );
489
+ }
490
+
491
+ @ Override
492
+ protected TermSuggestion emptySuggestion (String name , TimeoutSuggestionContext suggestion , CharsRefBuilder spare ) {
493
+ return new TermSuggestion (name , suggestion .getSize (), SortBy .SCORE );
494
+ }
495
+ }
496
+
497
+ private static final class TimeoutSuggestionContext extends SuggestionSearchContext .SuggestionContext {
498
+ TimeoutSuggestionContext (Suggester <?> suggester , SearchExecutionContext searchExecutionContext ) {
499
+ super (suggester , searchExecutionContext );
500
+ }
501
+ }
502
+
503
+ private static final class TimeoutRescorerBuilder extends RescorerBuilder <TimeoutRescorerBuilder > {
504
+ TimeoutRescorerBuilder () {
505
+ super ();
506
+ }
507
+
508
+ TimeoutRescorerBuilder (StreamInput in ) throws IOException {
509
+ super (in );
510
+ }
511
+
512
+ @ Override
513
+ protected void doWriteTo (StreamOutput out ) {}
514
+
515
+ @ Override
516
+ protected void doXContent (XContentBuilder builder , Params params ) {}
517
+
518
+ @ Override
519
+ protected RescoreContext innerBuildContext (int windowSize , SearchExecutionContext context ) throws IOException {
520
+ return new RescoreContext (10 , new Rescorer () {
521
+ @ Override
522
+ public TopDocs rescore (TopDocs topDocs , IndexSearcher searcher , RescoreContext rescoreContext ) {
523
+ ((ContextIndexSearcher ) context .searcher ()).throwTimeExceededException ();
524
+ assert false ;
525
+ return null ;
526
+ }
527
+
528
+ @ Override
529
+ public Explanation explain (
530
+ int topLevelDocId ,
531
+ IndexSearcher searcher ,
532
+ RescoreContext rescoreContext ,
533
+ Explanation sourceExplanation
534
+ ) {
535
+ throw new UnsupportedOperationException ();
536
+ }
537
+ });
538
+ }
539
+
540
+ @ Override
541
+ public String getWriteableName () {
542
+ return "timeout" ;
543
+ }
544
+
545
+ @ Override
546
+ public TransportVersion getMinimalSupportedVersion () {
547
+ return null ;
548
+ }
549
+
550
+ @ Override
551
+ public RescorerBuilder <TimeoutRescorerBuilder > rewrite (QueryRewriteContext ctx ) {
552
+ return this ;
553
+ }
554
+ }
318
555
}
0 commit comments