|
@@ -14,6 +14,7 @@ import org.apache.lucene.search.BulkScorer;
|
|
|
import org.apache.lucene.search.ConstantScoreScorer;
|
|
|
import org.apache.lucene.search.ConstantScoreWeight;
|
|
|
import org.apache.lucene.search.DocIdSetIterator;
|
|
|
+import org.apache.lucene.search.Explanation;
|
|
|
import org.apache.lucene.search.IndexSearcher;
|
|
|
import org.apache.lucene.search.LeafCollector;
|
|
|
import org.apache.lucene.search.Query;
|
|
@@ -22,8 +23,10 @@ import org.apache.lucene.search.Scorable;
|
|
|
import org.apache.lucene.search.ScoreMode;
|
|
|
import org.apache.lucene.search.Scorer;
|
|
|
import org.apache.lucene.search.ScorerSupplier;
|
|
|
+import org.apache.lucene.search.TopDocs;
|
|
|
import org.apache.lucene.search.Weight;
|
|
|
import org.apache.lucene.util.Bits;
|
|
|
+import org.apache.lucene.util.CharsRefBuilder;
|
|
|
import org.elasticsearch.ElasticsearchException;
|
|
|
import org.elasticsearch.TransportVersion;
|
|
|
import org.elasticsearch.action.search.SearchRequestBuilder;
|
|
@@ -33,12 +36,23 @@ import org.elasticsearch.common.settings.Settings;
|
|
|
import org.elasticsearch.core.TimeValue;
|
|
|
import org.elasticsearch.index.query.AbstractQueryBuilder;
|
|
|
import org.elasticsearch.index.query.QueryBuilder;
|
|
|
+import org.elasticsearch.index.query.QueryRewriteContext;
|
|
|
import org.elasticsearch.index.query.SearchExecutionContext;
|
|
|
+import org.elasticsearch.index.query.TermQueryBuilder;
|
|
|
import org.elasticsearch.plugins.Plugin;
|
|
|
import org.elasticsearch.plugins.SearchPlugin;
|
|
|
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
|
|
|
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
|
|
|
import org.elasticsearch.search.internal.ContextIndexSearcher;
|
|
|
+import org.elasticsearch.search.rescore.RescoreContext;
|
|
|
+import org.elasticsearch.search.rescore.Rescorer;
|
|
|
+import org.elasticsearch.search.rescore.RescorerBuilder;
|
|
|
+import org.elasticsearch.search.suggest.SortBy;
|
|
|
+import org.elasticsearch.search.suggest.SuggestBuilder;
|
|
|
+import org.elasticsearch.search.suggest.Suggester;
|
|
|
+import org.elasticsearch.search.suggest.SuggestionSearchContext;
|
|
|
+import org.elasticsearch.search.suggest.term.TermSuggestion;
|
|
|
+import org.elasticsearch.search.suggest.term.TermSuggestionBuilder;
|
|
|
import org.elasticsearch.test.ESIntegTestCase;
|
|
|
import org.elasticsearch.test.hamcrest.ElasticsearchAssertions;
|
|
|
import org.elasticsearch.xcontent.XContentBuilder;
|
|
@@ -58,7 +72,7 @@ public class SearchTimeoutIT extends ESIntegTestCase {
|
|
|
|
|
|
@Override
|
|
|
protected Collection<Class<? extends Plugin>> nodePlugins() {
|
|
|
- return Collections.singleton(BulkScorerTimeoutQueryPlugin.class);
|
|
|
+ return Collections.singleton(SearchTimeoutPlugin.class);
|
|
|
}
|
|
|
|
|
|
@Override
|
|
@@ -72,6 +86,9 @@ public class SearchTimeoutIT extends ESIntegTestCase {
|
|
|
indexRandom(true, "test", randomIntBetween(20, 50));
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Test the scenario where the query times out before starting to collect documents, verify that partial hits are not returned
|
|
|
+ */
|
|
|
public void testTopHitsTimeoutBeforeCollecting() {
|
|
|
// setting the timeout is necessary only because we check that if a TimeExceededException is thrown, a timeout was set
|
|
|
SearchRequestBuilder searchRequestBuilder = prepareSearch("test").setTimeout(new TimeValue(10, TimeUnit.SECONDS))
|
|
@@ -88,6 +105,9 @@ public class SearchTimeoutIT extends ESIntegTestCase {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Test the scenario where the query times out while collecting documents, verify that partial hits results are returned
|
|
|
+ */
|
|
|
public void testTopHitsTimeoutWhileCollecting() {
|
|
|
// setting the timeout is necessary only because we check that if a TimeExceededException is thrown, a timeout was set
|
|
|
SearchRequestBuilder searchRequestBuilder = prepareSearch("test").setTimeout(new TimeValue(10, TimeUnit.SECONDS))
|
|
@@ -103,6 +123,9 @@ public class SearchTimeoutIT extends ESIntegTestCase {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Test the scenario where the query times out before starting to collect documents, verify that partial aggs results are not returned
|
|
|
+ */
|
|
|
public void testAggsTimeoutBeforeCollecting() {
|
|
|
SearchRequestBuilder searchRequestBuilder = prepareSearch("test").setSize(0)
|
|
|
// setting the timeout is necessary only because we check that if a TimeExceededException is thrown, a timeout was set
|
|
@@ -123,6 +146,9 @@ public class SearchTimeoutIT extends ESIntegTestCase {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Test the scenario where the query times out while collecting documents, verify that partial aggs results are returned
|
|
|
+ */
|
|
|
public void testAggsTimeoutWhileCollecting() {
|
|
|
SearchRequestBuilder searchRequestBuilder = prepareSearch("test").setSize(0)
|
|
|
// setting the timeout is necessary only because we check that if a TimeExceededException is thrown, a timeout was set
|
|
@@ -145,6 +171,56 @@ public class SearchTimeoutIT extends ESIntegTestCase {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Test the scenario where the suggest phase (part of the query phase) times out, yet there are results
|
|
|
+ * available coming from executing the query and aggs on each shard.
|
|
|
+ */
|
|
|
+ public void testSuggestTimeoutWithPartialResults() {
|
|
|
+ SuggestBuilder suggestBuilder = new SuggestBuilder();
|
|
|
+ suggestBuilder.setGlobalText("text");
|
|
|
+ TimeoutSuggestionBuilder timeoutSuggestionBuilder = new TimeoutSuggestionBuilder();
|
|
|
+ suggestBuilder.addSuggestion("suggest", timeoutSuggestionBuilder);
|
|
|
+ SearchRequestBuilder searchRequestBuilder = prepareSearch("test").suggest(suggestBuilder)
|
|
|
+ .addAggregation(new TermsAggregationBuilder("terms").field("field.keyword"));
|
|
|
+ ElasticsearchAssertions.assertResponse(searchRequestBuilder, searchResponse -> {
|
|
|
+ assertThat(searchResponse.isTimedOut(), equalTo(true));
|
|
|
+ assertEquals(0, searchResponse.getShardFailures().length);
|
|
|
+ assertEquals(0, searchResponse.getFailedShards());
|
|
|
+ assertThat(searchResponse.getSuccessfulShards(), greaterThan(0));
|
|
|
+ assertEquals(searchResponse.getSuccessfulShards(), searchResponse.getTotalShards());
|
|
|
+ assertThat(searchResponse.getHits().getTotalHits().value(), greaterThan(0L));
|
|
|
+ assertThat(searchResponse.getHits().getHits().length, greaterThan(0));
|
|
|
+ StringTerms terms = searchResponse.getAggregations().get("terms");
|
|
|
+ assertEquals(1, terms.getBuckets().size());
|
|
|
+ StringTerms.Bucket bucket = terms.getBuckets().get(0);
|
|
|
+ assertEquals("value", bucket.getKeyAsString());
|
|
|
+ assertThat(bucket.getDocCount(), greaterThan(0L));
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Test the scenario where the rescore phase (part of the query phase) times out, yet there are results
|
|
|
+ * available coming from executing the query and aggs on each shard.
|
|
|
+ */
|
|
|
+ public void testRescoreTimeoutWithPartialResults() {
|
|
|
+ SearchRequestBuilder searchRequestBuilder = prepareSearch("test").setRescorer(new TimeoutRescorerBuilder())
|
|
|
+ .addAggregation(new TermsAggregationBuilder("terms").field("field.keyword"));
|
|
|
+ ElasticsearchAssertions.assertResponse(searchRequestBuilder, searchResponse -> {
|
|
|
+ assertThat(searchResponse.isTimedOut(), equalTo(true));
|
|
|
+ assertEquals(0, searchResponse.getShardFailures().length);
|
|
|
+ assertEquals(0, searchResponse.getFailedShards());
|
|
|
+ assertThat(searchResponse.getSuccessfulShards(), greaterThan(0));
|
|
|
+ assertEquals(searchResponse.getSuccessfulShards(), searchResponse.getTotalShards());
|
|
|
+ assertThat(searchResponse.getHits().getTotalHits().value(), greaterThan(0L));
|
|
|
+ assertThat(searchResponse.getHits().getHits().length, greaterThan(0));
|
|
|
+ StringTerms terms = searchResponse.getAggregations().get("terms");
|
|
|
+ assertEquals(1, terms.getBuckets().size());
|
|
|
+ StringTerms.Bucket bucket = terms.getBuckets().get(0);
|
|
|
+ assertEquals("value", bucket.getKeyAsString());
|
|
|
+ assertThat(bucket.getDocCount(), greaterThan(0L));
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
public void testPartialResultsIntolerantTimeoutBeforeCollecting() {
|
|
|
ElasticsearchException ex = expectThrows(
|
|
|
ElasticsearchException.class,
|
|
@@ -171,13 +247,67 @@ public class SearchTimeoutIT extends ESIntegTestCase {
|
|
|
assertEquals(429, ex.status().getStatus());
|
|
|
}
|
|
|
|
|
|
- public static final class BulkScorerTimeoutQueryPlugin extends Plugin implements SearchPlugin {
|
|
|
+ public void testPartialResultsIntolerantTimeoutWhileSuggestingOnly() {
|
|
|
+ SuggestBuilder suggestBuilder = new SuggestBuilder();
|
|
|
+ suggestBuilder.setGlobalText("text");
|
|
|
+ TimeoutSuggestionBuilder timeoutSuggestionBuilder = new TimeoutSuggestionBuilder();
|
|
|
+ suggestBuilder.addSuggestion("suggest", timeoutSuggestionBuilder);
|
|
|
+ ElasticsearchException ex = expectThrows(
|
|
|
+ ElasticsearchException.class,
|
|
|
+ prepareSearch("test").suggest(suggestBuilder).setAllowPartialSearchResults(false) // this line causes timeouts to report
|
|
|
+ // failures
|
|
|
+ );
|
|
|
+ assertTrue(ex.toString().contains("Time exceeded"));
|
|
|
+ assertEquals(429, ex.status().getStatus());
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testPartialResultsIntolerantTimeoutWhileSuggesting() {
|
|
|
+ SuggestBuilder suggestBuilder = new SuggestBuilder();
|
|
|
+ suggestBuilder.setGlobalText("text");
|
|
|
+ TimeoutSuggestionBuilder timeoutSuggestionBuilder = new TimeoutSuggestionBuilder();
|
|
|
+ suggestBuilder.addSuggestion("suggest", timeoutSuggestionBuilder);
|
|
|
+ ElasticsearchException ex = expectThrows(
|
|
|
+ ElasticsearchException.class,
|
|
|
+ prepareSearch("test").setQuery(new TermQueryBuilder("field", "value"))
|
|
|
+ .suggest(suggestBuilder)
|
|
|
+ .setAllowPartialSearchResults(false) // this line causes timeouts to report failures
|
|
|
+ );
|
|
|
+ assertTrue(ex.toString().contains("Time exceeded"));
|
|
|
+ assertEquals(429, ex.status().getStatus());
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testPartialResultsIntolerantTimeoutWhileRescoring() {
|
|
|
+ ElasticsearchException ex = expectThrows(
|
|
|
+ ElasticsearchException.class,
|
|
|
+ prepareSearch("test").setQuery(new TermQueryBuilder("field", "value"))
|
|
|
+ .setRescorer(new TimeoutRescorerBuilder())
|
|
|
+ .setAllowPartialSearchResults(false) // this line causes timeouts to report failures
|
|
|
+ );
|
|
|
+ assertTrue(ex.toString().contains("Time exceeded"));
|
|
|
+ assertEquals(429, ex.status().getStatus());
|
|
|
+ }
|
|
|
+
|
|
|
+ public static final class SearchTimeoutPlugin extends Plugin implements SearchPlugin {
|
|
|
@Override
|
|
|
public List<QuerySpec<?>> getQueries() {
|
|
|
return Collections.singletonList(new QuerySpec<QueryBuilder>("timeout", BulkScorerTimeoutQuery::new, parser -> {
|
|
|
throw new UnsupportedOperationException();
|
|
|
}));
|
|
|
}
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public List<SuggesterSpec<?>> getSuggesters() {
|
|
|
+ return Collections.singletonList(new SuggesterSpec<>("timeout", TimeoutSuggestionBuilder::new, parser -> {
|
|
|
+ throw new UnsupportedOperationException();
|
|
|
+ }, TermSuggestion::new));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public List<RescorerSpec<?>> getRescorers() {
|
|
|
+ return Collections.singletonList(new RescorerSpec<>("timeout", TimeoutRescorerBuilder::new, parser -> {
|
|
|
+ throw new UnsupportedOperationException();
|
|
|
+ }));
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -315,4 +445,111 @@ public class SearchTimeoutIT extends ESIntegTestCase {
|
|
|
return null;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Suggestion builder that triggers a timeout as part of its execution
|
|
|
+ */
|
|
|
+ private static final class TimeoutSuggestionBuilder extends TermSuggestionBuilder {
|
|
|
+ TimeoutSuggestionBuilder() {
|
|
|
+ super("field");
|
|
|
+ }
|
|
|
+
|
|
|
+ TimeoutSuggestionBuilder(StreamInput in) throws IOException {
|
|
|
+ super(in);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getWriteableName() {
|
|
|
+ return "timeout";
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public SuggestionSearchContext.SuggestionContext build(SearchExecutionContext context) {
|
|
|
+ return new TimeoutSuggestionContext(new TimeoutSuggester((ContextIndexSearcher) context.searcher()), context);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static final class TimeoutSuggester extends Suggester<TimeoutSuggestionContext> {
|
|
|
+ private final ContextIndexSearcher contextIndexSearcher;
|
|
|
+
|
|
|
+ TimeoutSuggester(ContextIndexSearcher contextIndexSearcher) {
|
|
|
+ this.contextIndexSearcher = contextIndexSearcher;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected TermSuggestion innerExecute(
|
|
|
+ String name,
|
|
|
+ TimeoutSuggestionContext suggestion,
|
|
|
+ IndexSearcher searcher,
|
|
|
+ CharsRefBuilder spare
|
|
|
+ ) {
|
|
|
+ contextIndexSearcher.throwTimeExceededException();
|
|
|
+ assert false;
|
|
|
+ return new TermSuggestion(name, suggestion.getSize(), SortBy.SCORE);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected TermSuggestion emptySuggestion(String name, TimeoutSuggestionContext suggestion, CharsRefBuilder spare) {
|
|
|
+ return new TermSuggestion(name, suggestion.getSize(), SortBy.SCORE);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static final class TimeoutSuggestionContext extends SuggestionSearchContext.SuggestionContext {
|
|
|
+ TimeoutSuggestionContext(Suggester<?> suggester, SearchExecutionContext searchExecutionContext) {
|
|
|
+ super(suggester, searchExecutionContext);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static final class TimeoutRescorerBuilder extends RescorerBuilder<TimeoutRescorerBuilder> {
|
|
|
+ TimeoutRescorerBuilder() {
|
|
|
+ super();
|
|
|
+ }
|
|
|
+
|
|
|
+ TimeoutRescorerBuilder(StreamInput in) throws IOException {
|
|
|
+ super(in);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void doWriteTo(StreamOutput out) {}
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void doXContent(XContentBuilder builder, Params params) {}
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected RescoreContext innerBuildContext(int windowSize, SearchExecutionContext context) throws IOException {
|
|
|
+ return new RescoreContext(10, new Rescorer() {
|
|
|
+ @Override
|
|
|
+ public TopDocs rescore(TopDocs topDocs, IndexSearcher searcher, RescoreContext rescoreContext) {
|
|
|
+ ((ContextIndexSearcher) context.searcher()).throwTimeExceededException();
|
|
|
+ assert false;
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Explanation explain(
|
|
|
+ int topLevelDocId,
|
|
|
+ IndexSearcher searcher,
|
|
|
+ RescoreContext rescoreContext,
|
|
|
+ Explanation sourceExplanation
|
|
|
+ ) {
|
|
|
+ throw new UnsupportedOperationException();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getWriteableName() {
|
|
|
+ return "timeout";
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public TransportVersion getMinimalSupportedVersion() {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public RescorerBuilder<TimeoutRescorerBuilder> rewrite(QueryRewriteContext ctx) {
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|