1414import org .apache .lucene .search .ConstantScoreScorer ;
1515import org .apache .lucene .search .ConstantScoreWeight ;
1616import org .apache .lucene .search .DocIdSetIterator ;
17+ import org .apache .lucene .search .Explanation ;
1718import org .apache .lucene .search .IndexSearcher ;
1819import org .apache .lucene .search .LeafCollector ;
1920import org .apache .lucene .search .Query ;
2223import org .apache .lucene .search .ScoreMode ;
2324import org .apache .lucene .search .Scorer ;
2425import org .apache .lucene .search .ScorerSupplier ;
26+ import org .apache .lucene .search .TopDocs ;
2527import org .apache .lucene .search .Weight ;
2628import org .apache .lucene .util .Bits ;
29+ import org .apache .lucene .util .CharsRefBuilder ;
2730import org .elasticsearch .ElasticsearchException ;
2831import org .elasticsearch .TransportVersion ;
2932import org .elasticsearch .action .search .SearchRequestBuilder ;
3336import org .elasticsearch .core .TimeValue ;
3437import org .elasticsearch .index .query .AbstractQueryBuilder ;
3538import org .elasticsearch .index .query .QueryBuilder ;
39+ import org .elasticsearch .index .query .QueryRewriteContext ;
3640import org .elasticsearch .index .query .SearchExecutionContext ;
41+ import org .elasticsearch .index .query .TermQueryBuilder ;
3742import org .elasticsearch .plugins .Plugin ;
3843import org .elasticsearch .plugins .SearchPlugin ;
3944import org .elasticsearch .search .aggregations .bucket .terms .StringTerms ;
4045import org .elasticsearch .search .aggregations .bucket .terms .TermsAggregationBuilder ;
4146import 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 ;
4256import org .elasticsearch .test .ESIntegTestCase ;
4357import org .elasticsearch .test .hamcrest .ElasticsearchAssertions ;
4458import org .elasticsearch .xcontent .XContentBuilder ;
@@ -58,7 +72,7 @@ public class SearchTimeoutIT extends ESIntegTestCase {
5872
5973 @ Override
6074 protected Collection <Class <? extends Plugin >> nodePlugins () {
61- return Collections .singleton (BulkScorerTimeoutQueryPlugin .class );
75+ return Collections .singleton (SearchTimeoutPlugin .class );
6276 }
6377
6478 @ Override
@@ -72,6 +86,9 @@ protected void setupSuiteScopeCluster() throws Exception {
7286 indexRandom (true , "test" , randomIntBetween (20 , 50 ));
7387 }
7488
89+ /**
90+ * Test the scenario where the query times out before starting to collect documents, verify that partial hits are not returned
91+ */
7592 public void testTopHitsTimeoutBeforeCollecting () {
7693 // setting the timeout is necessary only because we check that if a TimeExceededException is thrown, a timeout was set
7794 SearchRequestBuilder searchRequestBuilder = prepareSearch ("test" ).setTimeout (new TimeValue (10 , TimeUnit .SECONDS ))
@@ -88,6 +105,9 @@ public void testTopHitsTimeoutBeforeCollecting() {
88105 });
89106 }
90107
108+ /**
109+ * Test the scenario where the query times out while collecting documents, verify that partial hits results are returned
110+ */
91111 public void testTopHitsTimeoutWhileCollecting () {
92112 // setting the timeout is necessary only because we check that if a TimeExceededException is thrown, a timeout was set
93113 SearchRequestBuilder searchRequestBuilder = prepareSearch ("test" ).setTimeout (new TimeValue (10 , TimeUnit .SECONDS ))
@@ -103,6 +123,9 @@ public void testTopHitsTimeoutWhileCollecting() {
103123 });
104124 }
105125
126+ /**
127+ * Test the scenario where the query times out before starting to collect documents, verify that partial aggs results are not returned
128+ */
106129 public void testAggsTimeoutBeforeCollecting () {
107130 SearchRequestBuilder searchRequestBuilder = prepareSearch ("test" ).setSize (0 )
108131 // 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() {
123146 });
124147 }
125148
149+ /**
150+ * Test the scenario where the query times out while collecting documents, verify that partial aggs results are returned
151+ */
126152 public void testAggsTimeoutWhileCollecting () {
127153 SearchRequestBuilder searchRequestBuilder = prepareSearch ("test" ).setSize (0 )
128154 // 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() {
145171 });
146172 }
147173
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+
148224 public void testPartialResultsIntolerantTimeoutBeforeCollecting () {
149225 ElasticsearchException ex = expectThrows (
150226 ElasticsearchException .class ,
@@ -171,13 +247,67 @@ public void testPartialResultsIntolerantTimeoutWhileCollecting() {
171247 assertEquals (429 , ex .status ().getStatus ());
172248 }
173249
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 {
175291 @ Override
176292 public List <QuerySpec <?>> getQueries () {
177293 return Collections .singletonList (new QuerySpec <QueryBuilder >("timeout" , BulkScorerTimeoutQuery ::new , parser -> {
178294 throw new UnsupportedOperationException ();
179295 }));
180296 }
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+ }
181311 }
182312
183313 /**
@@ -315,4 +445,111 @@ public TransportVersion getMinimalSupportedVersion() {
315445 return null ;
316446 }
317447 }
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+ }
318555}
0 commit comments