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 ;
2021import org .apache .lucene .search .QueryVisitor ;
2122import org .apache .lucene .search .Scorable ;
2223import org .apache .lucene .search .ScoreMode ;
2324import org .apache .lucene .search .Scorer ;
25+ import org .apache .lucene .search .TopDocs ;
2426import org .apache .lucene .search .Weight ;
2527import org .apache .lucene .util .Bits ;
28+ import org .apache .lucene .util .CharsRefBuilder ;
2629import org .elasticsearch .ElasticsearchException ;
2730import org .elasticsearch .TransportVersion ;
2831import org .elasticsearch .action .search .SearchRequestBuilder ;
3235import org .elasticsearch .core .TimeValue ;
3336import org .elasticsearch .index .query .AbstractQueryBuilder ;
3437import org .elasticsearch .index .query .QueryBuilder ;
38+ import org .elasticsearch .index .query .QueryRewriteContext ;
3539import org .elasticsearch .index .query .SearchExecutionContext ;
40+ import org .elasticsearch .index .query .TermQueryBuilder ;
3641import org .elasticsearch .plugins .Plugin ;
3742import org .elasticsearch .plugins .SearchPlugin ;
3843import org .elasticsearch .search .aggregations .bucket .terms .StringTerms ;
3944import org .elasticsearch .search .aggregations .bucket .terms .TermsAggregationBuilder ;
4045import org .elasticsearch .search .internal .ContextIndexSearcher ;
46+ import org .elasticsearch .search .rescore .RescoreContext ;
47+ import org .elasticsearch .search .rescore .Rescorer ;
48+ import org .elasticsearch .search .rescore .RescorerBuilder ;
49+ import org .elasticsearch .search .suggest .SortBy ;
50+ import org .elasticsearch .search .suggest .SuggestBuilder ;
51+ import org .elasticsearch .search .suggest .Suggester ;
52+ import org .elasticsearch .search .suggest .SuggestionSearchContext ;
53+ import org .elasticsearch .search .suggest .term .TermSuggestion ;
54+ import org .elasticsearch .search .suggest .term .TermSuggestionBuilder ;
4155import org .elasticsearch .test .ESIntegTestCase ;
4256import org .elasticsearch .test .hamcrest .ElasticsearchAssertions ;
4357import org .elasticsearch .xcontent .XContentBuilder ;
@@ -57,7 +71,7 @@ public class SearchTimeoutIT extends ESIntegTestCase {
5771
5872 @ Override
5973 protected Collection <Class <? extends Plugin >> nodePlugins () {
60- return Collections .singleton (BulkScorerTimeoutQueryPlugin .class );
74+ return Collections .singleton (SearchTimeoutPlugin .class );
6175 }
6276
6377 @ Override
@@ -71,6 +85,9 @@ protected void setupSuiteScopeCluster() throws Exception {
7185 indexRandom (true , "test" , randomIntBetween (20 , 50 ));
7286 }
7387
88+ /**
89+ * Test the scenario where the query times out before starting to collect documents, verify that partial hits are not returned
90+ */
7491 public void testTopHitsTimeoutBeforeCollecting () {
7592 // setting the timeout is necessary only because we check that if a TimeExceededException is thrown, a timeout was set
7693 SearchRequestBuilder searchRequestBuilder = prepareSearch ("test" ).setTimeout (new TimeValue (10 , TimeUnit .SECONDS ))
@@ -87,6 +104,9 @@ public void testTopHitsTimeoutBeforeCollecting() {
87104 });
88105 }
89106
107+ /**
108+ * Test the scenario where the query times out while collecting documents, verify that partial hits results are returned
109+ */
90110 public void testTopHitsTimeoutWhileCollecting () {
91111 // setting the timeout is necessary only because we check that if a TimeExceededException is thrown, a timeout was set
92112 SearchRequestBuilder searchRequestBuilder = prepareSearch ("test" ).setTimeout (new TimeValue (10 , TimeUnit .SECONDS ))
@@ -102,6 +122,9 @@ public void testTopHitsTimeoutWhileCollecting() {
102122 });
103123 }
104124
125+ /**
126+ * Test the scenario where the query times out before starting to collect documents, verify that partial aggs results are not returned
127+ */
105128 public void testAggsTimeoutBeforeCollecting () {
106129 SearchRequestBuilder searchRequestBuilder = prepareSearch ("test" ).setSize (0 )
107130 // setting the timeout is necessary only because we check that if a TimeExceededException is thrown, a timeout was set
@@ -122,6 +145,9 @@ public void testAggsTimeoutBeforeCollecting() {
122145 });
123146 }
124147
148+ /**
149+ * Test the scenario where the query times out while collecting documents, verify that partial aggs results are returned
150+ */
125151 public void testAggsTimeoutWhileCollecting () {
126152 SearchRequestBuilder searchRequestBuilder = prepareSearch ("test" ).setSize (0 )
127153 // setting the timeout is necessary only because we check that if a TimeExceededException is thrown, a timeout was set
@@ -144,6 +170,56 @@ public void testAggsTimeoutWhileCollecting() {
144170 });
145171 }
146172
173+ /**
174+ * Test the scenario where the suggest phase (part of the query phase) times out, yet there are results
175+ * available coming from executing the query and aggs on each shard.
176+ */
177+ public void testSuggestTimeoutWithPartialResults () {
178+ SuggestBuilder suggestBuilder = new SuggestBuilder ();
179+ suggestBuilder .setGlobalText ("text" );
180+ TimeoutSuggestionBuilder timeoutSuggestionBuilder = new TimeoutSuggestionBuilder ();
181+ suggestBuilder .addSuggestion ("suggest" , timeoutSuggestionBuilder );
182+ SearchRequestBuilder searchRequestBuilder = prepareSearch ("test" ).suggest (suggestBuilder )
183+ .addAggregation (new TermsAggregationBuilder ("terms" ).field ("field.keyword" ));
184+ ElasticsearchAssertions .assertResponse (searchRequestBuilder , searchResponse -> {
185+ assertThat (searchResponse .isTimedOut (), equalTo (true ));
186+ assertEquals (0 , searchResponse .getShardFailures ().length );
187+ assertEquals (0 , searchResponse .getFailedShards ());
188+ assertThat (searchResponse .getSuccessfulShards (), greaterThan (0 ));
189+ assertEquals (searchResponse .getSuccessfulShards (), searchResponse .getTotalShards ());
190+ assertThat (searchResponse .getHits ().getTotalHits ().value , greaterThan (0L ));
191+ assertThat (searchResponse .getHits ().getHits ().length , greaterThan (0 ));
192+ StringTerms terms = searchResponse .getAggregations ().get ("terms" );
193+ assertEquals (1 , terms .getBuckets ().size ());
194+ StringTerms .Bucket bucket = terms .getBuckets ().get (0 );
195+ assertEquals ("value" , bucket .getKeyAsString ());
196+ assertThat (bucket .getDocCount (), greaterThan (0L ));
197+ });
198+ }
199+
200+ /**
201+ * Test the scenario where the rescore phase (part of the query phase) times out, yet there are results
202+ * available coming from executing the query and aggs on each shard.
203+ */
204+ public void testRescoreTimeoutWithPartialResults () {
205+ SearchRequestBuilder searchRequestBuilder = prepareSearch ("test" ).setRescorer (new TimeoutRescorerBuilder ())
206+ .addAggregation (new TermsAggregationBuilder ("terms" ).field ("field.keyword" ));
207+ ElasticsearchAssertions .assertResponse (searchRequestBuilder , searchResponse -> {
208+ assertThat (searchResponse .isTimedOut (), equalTo (true ));
209+ assertEquals (0 , searchResponse .getShardFailures ().length );
210+ assertEquals (0 , searchResponse .getFailedShards ());
211+ assertThat (searchResponse .getSuccessfulShards (), greaterThan (0 ));
212+ assertEquals (searchResponse .getSuccessfulShards (), searchResponse .getTotalShards ());
213+ assertThat (searchResponse .getHits ().getTotalHits ().value , greaterThan (0L ));
214+ assertThat (searchResponse .getHits ().getHits ().length , greaterThan (0 ));
215+ StringTerms terms = searchResponse .getAggregations ().get ("terms" );
216+ assertEquals (1 , terms .getBuckets ().size ());
217+ StringTerms .Bucket bucket = terms .getBuckets ().get (0 );
218+ assertEquals ("value" , bucket .getKeyAsString ());
219+ assertThat (bucket .getDocCount (), greaterThan (0L ));
220+ });
221+ }
222+
147223 public void testPartialResultsIntolerantTimeoutBeforeCollecting () {
148224 ElasticsearchException ex = expectThrows (
149225 ElasticsearchException .class ,
@@ -170,13 +246,67 @@ public void testPartialResultsIntolerantTimeoutWhileCollecting() {
170246 assertEquals (504 , ex .status ().getStatus ());
171247 }
172248
173- public static final class BulkScorerTimeoutQueryPlugin extends Plugin implements SearchPlugin {
249+ public void testPartialResultsIntolerantTimeoutWhileSuggestingOnly () {
250+ SuggestBuilder suggestBuilder = new SuggestBuilder ();
251+ suggestBuilder .setGlobalText ("text" );
252+ TimeoutSuggestionBuilder timeoutSuggestionBuilder = new TimeoutSuggestionBuilder ();
253+ suggestBuilder .addSuggestion ("suggest" , timeoutSuggestionBuilder );
254+ ElasticsearchException ex = expectThrows (
255+ ElasticsearchException .class ,
256+ prepareSearch ("test" ).suggest (suggestBuilder ).setAllowPartialSearchResults (false ) // this line causes timeouts to report
257+ // failures
258+ );
259+ assertTrue (ex .toString ().contains ("Time exceeded" ));
260+ assertEquals (504 , ex .status ().getStatus ());
261+ }
262+
263+ public void testPartialResultsIntolerantTimeoutWhileSuggesting () {
264+ SuggestBuilder suggestBuilder = new SuggestBuilder ();
265+ suggestBuilder .setGlobalText ("text" );
266+ TimeoutSuggestionBuilder timeoutSuggestionBuilder = new TimeoutSuggestionBuilder ();
267+ suggestBuilder .addSuggestion ("suggest" , timeoutSuggestionBuilder );
268+ ElasticsearchException ex = expectThrows (
269+ ElasticsearchException .class ,
270+ prepareSearch ("test" ).setQuery (new TermQueryBuilder ("field" , "value" ))
271+ .suggest (suggestBuilder )
272+ .setAllowPartialSearchResults (false ) // this line causes timeouts to report failures
273+ );
274+ assertTrue (ex .toString ().contains ("Time exceeded" ));
275+ assertEquals (504 , ex .status ().getStatus ());
276+ }
277+
278+ public void testPartialResultsIntolerantTimeoutWhileRescoring () {
279+ ElasticsearchException ex = expectThrows (
280+ ElasticsearchException .class ,
281+ prepareSearch ("test" ).setQuery (new TermQueryBuilder ("field" , "value" ))
282+ .setRescorer (new TimeoutRescorerBuilder ())
283+ .setAllowPartialSearchResults (false ) // this line causes timeouts to report failures
284+ );
285+ assertTrue (ex .toString ().contains ("Time exceeded" ));
286+ assertEquals (504 , ex .status ().getStatus ());
287+ }
288+
289+ public static final class SearchTimeoutPlugin extends Plugin implements SearchPlugin {
174290 @ Override
175291 public List <QuerySpec <?>> getQueries () {
176292 return Collections .singletonList (new QuerySpec <QueryBuilder >("timeout" , BulkScorerTimeoutQuery ::new , parser -> {
177293 throw new UnsupportedOperationException ();
178294 }));
179295 }
296+
297+ @ Override
298+ public List <SuggesterSpec <?>> getSuggesters () {
299+ return Collections .singletonList (new SuggesterSpec <>("timeout" , TimeoutSuggestionBuilder ::new , parser -> {
300+ throw new UnsupportedOperationException ();
301+ }, TermSuggestion ::new ));
302+ }
303+
304+ @ Override
305+ public List <RescorerSpec <?>> getRescorers () {
306+ return Collections .singletonList (new RescorerSpec <>("timeout" , TimeoutRescorerBuilder ::new , parser -> {
307+ throw new UnsupportedOperationException ();
308+ }));
309+ }
180310 }
181311
182312 /**
@@ -314,4 +444,111 @@ public TransportVersion getMinimalSupportedVersion() {
314444 return null ;
315445 }
316446 }
447+
448+ /**
449+ * Suggestion builder that triggers a timeout as part of its execution
450+ */
451+ private static final class TimeoutSuggestionBuilder extends TermSuggestionBuilder {
452+ TimeoutSuggestionBuilder () {
453+ super ("field" );
454+ }
455+
456+ TimeoutSuggestionBuilder (StreamInput in ) throws IOException {
457+ super (in );
458+ }
459+
460+ @ Override
461+ public String getWriteableName () {
462+ return "timeout" ;
463+ }
464+
465+ @ Override
466+ public SuggestionSearchContext .SuggestionContext build (SearchExecutionContext context ) {
467+ return new TimeoutSuggestionContext (new TimeoutSuggester ((ContextIndexSearcher ) context .searcher ()), context );
468+ }
469+ }
470+
471+ private static final class TimeoutSuggester extends Suggester <TimeoutSuggestionContext > {
472+ private final ContextIndexSearcher contextIndexSearcher ;
473+
474+ TimeoutSuggester (ContextIndexSearcher contextIndexSearcher ) {
475+ this .contextIndexSearcher = contextIndexSearcher ;
476+ }
477+
478+ @ Override
479+ protected TermSuggestion innerExecute (
480+ String name ,
481+ TimeoutSuggestionContext suggestion ,
482+ IndexSearcher searcher ,
483+ CharsRefBuilder spare
484+ ) {
485+ contextIndexSearcher .throwTimeExceededException ();
486+ assert false ;
487+ return new TermSuggestion (name , suggestion .getSize (), SortBy .SCORE );
488+ }
489+
490+ @ Override
491+ protected TermSuggestion emptySuggestion (String name , TimeoutSuggestionContext suggestion , CharsRefBuilder spare ) {
492+ return new TermSuggestion (name , suggestion .getSize (), SortBy .SCORE );
493+ }
494+ }
495+
496+ private static final class TimeoutSuggestionContext extends SuggestionSearchContext .SuggestionContext {
497+ TimeoutSuggestionContext (Suggester <?> suggester , SearchExecutionContext searchExecutionContext ) {
498+ super (suggester , searchExecutionContext );
499+ }
500+ }
501+
502+ private static final class TimeoutRescorerBuilder extends RescorerBuilder <TimeoutRescorerBuilder > {
503+ TimeoutRescorerBuilder () {
504+ super ();
505+ }
506+
507+ TimeoutRescorerBuilder (StreamInput in ) throws IOException {
508+ super (in );
509+ }
510+
511+ @ Override
512+ protected void doWriteTo (StreamOutput out ) {}
513+
514+ @ Override
515+ protected void doXContent (XContentBuilder builder , Params params ) {}
516+
517+ @ Override
518+ protected RescoreContext innerBuildContext (int windowSize , SearchExecutionContext context ) throws IOException {
519+ return new RescoreContext (10 , new Rescorer () {
520+ @ Override
521+ public TopDocs rescore (TopDocs topDocs , IndexSearcher searcher , RescoreContext rescoreContext ) {
522+ ((ContextIndexSearcher ) context .searcher ()).throwTimeExceededException ();
523+ assert false ;
524+ return null ;
525+ }
526+
527+ @ Override
528+ public Explanation explain (
529+ int topLevelDocId ,
530+ IndexSearcher searcher ,
531+ RescoreContext rescoreContext ,
532+ Explanation sourceExplanation
533+ ) {
534+ throw new UnsupportedOperationException ();
535+ }
536+ });
537+ }
538+
539+ @ Override
540+ public String getWriteableName () {
541+ return "timeout" ;
542+ }
543+
544+ @ Override
545+ public TransportVersion getMinimalSupportedVersion () {
546+ return null ;
547+ }
548+
549+ @ Override
550+ public RescorerBuilder <TimeoutRescorerBuilder > rewrite (QueryRewriteContext ctx ) {
551+ return this ;
552+ }
553+ }
317554}
0 commit comments