Skip to content

Commit 9050c9e

Browse files
committed
Backport can_match endpoint to 5.6 to allow 6.0 to use the optimization in mixed version
6.0 applies some optimization to query rewriting if the number of shards is large. In oder to make use of this optimization this commit adds the internal endpoint to 5.6 such that a 6.0 coordinator node can make use of the feature even in a mixed cluster or via cross cluster search. Relates to #25658
1 parent fd1553c commit 9050c9e

File tree

5 files changed

+144
-2
lines changed

5 files changed

+144
-2
lines changed

core/src/main/java/org/elasticsearch/action/search/SearchTransportService.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public class SearchTransportService extends AbstractComponent {
7676
public static final String QUERY_FETCH_SCROLL_ACTION_NAME = "indices:data/read/search[phase/query+fetch/scroll]";
7777
public static final String FETCH_ID_SCROLL_ACTION_NAME = "indices:data/read/search[phase/fetch/id/scroll]";
7878
public static final String FETCH_ID_ACTION_NAME = "indices:data/read/search[phase/fetch/id]";
79+
public static final String QUERY_CAN_MATCH_NAME = "indices:data/read/search[can_match]";
7980

8081
private final TransportService transportService;
8182

@@ -395,8 +396,45 @@ public void messageReceived(ShardFetchSearchRequest request, TransportChannel ch
395396
}
396397
});
397398
TransportActionProxy.registerProxyAction(transportService, FETCH_ID_ACTION_NAME, FetchSearchResult::new);
399+
400+
// this is super cheap and should not hit thread-pool rejections
401+
transportService.registerRequestHandler(QUERY_CAN_MATCH_NAME, ShardSearchTransportRequest::new, ThreadPool.Names.SEARCH,
402+
false, true, new TaskAwareTransportRequestHandler<ShardSearchTransportRequest>() {
403+
@Override
404+
public void messageReceived(ShardSearchTransportRequest request, TransportChannel channel, Task task) throws Exception {
405+
boolean canMatch = searchService.canMatch(request);
406+
channel.sendResponse(new CanMatchResponse(canMatch));
407+
}
408+
});
409+
TransportActionProxy.registerProxyAction(transportService, QUERY_CAN_MATCH_NAME, CanMatchResponse::new);
398410
}
399411

412+
// this feature is only really used in 6.0 but we added the endpoints to 5.6 to ensure if a user is on 5.6 and they desperately
413+
// need it they can use cross cluster search with a 6.0 CCS Node or can use a 6.0 node as a coordinator to at least test if
414+
// if would help their usecase. it also makes the feature in 6.x BWC with the latest 5.x release.
415+
private static final class CanMatchResponse extends SearchPhaseResult {
416+
private boolean canMatch;
417+
418+
private CanMatchResponse() {}
419+
420+
private CanMatchResponse(boolean canMatch) {
421+
this.canMatch = canMatch;
422+
}
423+
424+
@Override
425+
public void readFrom(StreamInput in) throws IOException {
426+
super.readFrom(in);
427+
canMatch = in.readBoolean();
428+
}
429+
430+
@Override
431+
public void writeTo(StreamOutput out) throws IOException {
432+
super.writeTo(out);
433+
out.writeBoolean(canMatch);
434+
}
435+
}
436+
437+
400438
/**
401439
* Returns a connection to the given node on the provided cluster. If the cluster alias is <code>null</code> the node will be resolved
402440
* against the local cluster.

core/src/main/java/org/elasticsearch/search/SearchService.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.elasticsearch.ExceptionsHelper;
2727
import org.elasticsearch.action.OriginalIndices;
2828
import org.elasticsearch.action.search.SearchTask;
29+
import org.elasticsearch.action.search.SearchType;
2930
import org.elasticsearch.cluster.ClusterState;
3031
import org.elasticsearch.cluster.service.ClusterService;
3132
import org.elasticsearch.common.Nullable;
@@ -42,6 +43,9 @@
4243
import org.elasticsearch.index.IndexSettings;
4344
import org.elasticsearch.index.engine.Engine;
4445
import org.elasticsearch.index.query.InnerHitContextBuilder;
46+
import org.elasticsearch.index.query.MatchAllQueryBuilder;
47+
import org.elasticsearch.index.query.MatchNoneQueryBuilder;
48+
import org.elasticsearch.index.query.QueryBuilder;
4549
import org.elasticsearch.index.query.QueryShardContext;
4650
import org.elasticsearch.index.shard.IndexEventListener;
4751
import org.elasticsearch.index.shard.IndexShard;
@@ -828,4 +832,35 @@ public void run() {
828832
public AliasFilter buildAliasFilter(ClusterState state, String index, String... expressions) {
829833
return indicesService.buildAliasFilter(state, index, expressions);
830834
}
835+
836+
/**
837+
* This method does a very quick rewrite of the query and returns true if the query can potentially match any documents.
838+
* This method can have false positives while if it returns <code>false</code> the query won't match any documents on the current
839+
* shard.
840+
*/
841+
public boolean canMatch(ShardSearchRequest request) throws IOException {
842+
assert request.searchType() == SearchType.QUERY_THEN_FETCH : "unexpected search type: " + request.searchType();
843+
try (DefaultSearchContext context = createSearchContext(request, defaultSearchTimeout, null)) {
844+
SearchSourceBuilder source = context.request().source();
845+
if (canRewriteToMatchNone(source)) {
846+
QueryBuilder queryBuilder = source.query();
847+
return queryBuilder instanceof MatchNoneQueryBuilder == false;
848+
}
849+
return true; // null query means match_all
850+
}
851+
}
852+
853+
static boolean canRewriteToMatchNone(SearchSourceBuilder source) {
854+
if (source == null || source.query() == null || source.query() instanceof MatchAllQueryBuilder) {
855+
return false;
856+
} else {
857+
AggregatorFactories.Builder aggregations = source.aggregations();
858+
if (aggregations != null) {
859+
if (aggregations.mustVisitAllDocs()) {
860+
return false;
861+
}
862+
}
863+
}
864+
return true;
865+
}
831866
}

core/src/main/java/org/elasticsearch/search/aggregations/AggregatorFactories.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import org.elasticsearch.common.xcontent.XContentBuilder;
2727
import org.elasticsearch.common.xcontent.XContentParser;
2828
import org.elasticsearch.index.query.QueryParseContext;
29+
import org.elasticsearch.search.aggregations.bucket.global.GlobalAggregationBuilder;
30+
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
2931
import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator;
3032
import org.elasticsearch.search.aggregations.support.AggregationPath;
3133
import org.elasticsearch.search.aggregations.support.AggregationPath.PathElement;
@@ -293,8 +295,18 @@ public void writeTo(StreamOutput out) throws IOException {
293295
}
294296
}
295297

296-
public Builder addAggregators(AggregatorFactories factories) {
297-
throw new UnsupportedOperationException("This needs to be removed");
298+
public boolean mustVisitAllDocs() {
299+
for (AggregationBuilder builder : aggregationBuilders) {
300+
if (builder instanceof GlobalAggregationBuilder) {
301+
return true;
302+
} else if (builder instanceof TermsAggregationBuilder) {
303+
if (((TermsAggregationBuilder) builder).minDocCount() == 0) {
304+
return true;
305+
}
306+
}
307+
308+
}
309+
return false;
298310
}
299311

300312
public Builder addAggregator(AggregationBuilder factory) {

core/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregationBuilder.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,13 @@ public TermsAggregationBuilder minDocCount(long minDocCount) {
177177
return this;
178178
}
179179

180+
/**
181+
* Returns the minimum document count required per term
182+
*/
183+
public long minDocCount() {
184+
return bucketCountThresholds.getMinDocCount();
185+
}
186+
180187
/**
181188
* Set the minimum document count terms should have on the shard in order to
182189
* appear in the response.

core/src/test/java/org/elasticsearch/search/SearchServiceTests.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package org.elasticsearch.search;
2020

2121
import com.carrotsearch.hppc.IntArrayList;
22+
2223
import org.apache.lucene.search.Query;
2324
import org.apache.lucene.store.AlreadyClosedException;
2425
import org.elasticsearch.action.ActionListener;
@@ -36,13 +37,19 @@
3637
import org.elasticsearch.common.xcontent.XContentBuilder;
3738
import org.elasticsearch.index.IndexService;
3839
import org.elasticsearch.index.query.AbstractQueryBuilder;
40+
import org.elasticsearch.index.query.MatchAllQueryBuilder;
41+
import org.elasticsearch.index.query.MatchNoneQueryBuilder;
3942
import org.elasticsearch.index.query.QueryBuilder;
4043
import org.elasticsearch.index.query.QueryRewriteContext;
4144
import org.elasticsearch.index.query.QueryShardContext;
45+
import org.elasticsearch.index.query.TermQueryBuilder;
4246
import org.elasticsearch.index.shard.IndexShard;
4347
import org.elasticsearch.indices.IndicesService;
4448
import org.elasticsearch.plugins.Plugin;
4549
import org.elasticsearch.plugins.SearchPlugin;
50+
import org.elasticsearch.search.aggregations.bucket.global.GlobalAggregationBuilder;
51+
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
52+
import org.elasticsearch.search.aggregations.support.ValueType;
4653
import org.elasticsearch.search.builder.SearchSourceBuilder;
4754
import org.elasticsearch.search.fetch.ShardFetchRequest;
4855
import org.elasticsearch.search.internal.AliasFilter;
@@ -304,4 +311,47 @@ public String getWriteableName() {
304311
return null;
305312
}
306313
}
314+
315+
public void testCanMatch() throws IOException {
316+
createIndex("index");
317+
final SearchService service = getInstanceFromNode(SearchService.class);
318+
final IndicesService indicesService = getInstanceFromNode(IndicesService.class);
319+
final IndexService indexService = indicesService.indexServiceSafe(resolveIndex("index"));
320+
final IndexShard indexShard = indexService.getShard(0);
321+
assertTrue(service.canMatch(new ShardSearchLocalRequest(indexShard.shardId(), 1, SearchType.QUERY_THEN_FETCH, null,
322+
Strings.EMPTY_ARRAY, false, new AliasFilter(null, Strings.EMPTY_ARRAY), 1f)));
323+
324+
assertTrue(service.canMatch(new ShardSearchLocalRequest(indexShard.shardId(), 1, SearchType.QUERY_THEN_FETCH,
325+
new SearchSourceBuilder(), Strings.EMPTY_ARRAY, false, new AliasFilter(null, Strings.EMPTY_ARRAY), 1f)));
326+
327+
assertTrue(service.canMatch(new ShardSearchLocalRequest(indexShard.shardId(), 1, SearchType.QUERY_THEN_FETCH,
328+
new SearchSourceBuilder().query(new MatchAllQueryBuilder()), Strings.EMPTY_ARRAY, false,
329+
new AliasFilter(null, Strings.EMPTY_ARRAY), 1f)));
330+
331+
assertTrue(service.canMatch(new ShardSearchLocalRequest(indexShard.shardId(), 1, SearchType.QUERY_THEN_FETCH,
332+
new SearchSourceBuilder().query(new MatchNoneQueryBuilder())
333+
.aggregation(new TermsAggregationBuilder("test", ValueType.STRING).minDocCount(0)), Strings.EMPTY_ARRAY, false,
334+
new AliasFilter(null, Strings.EMPTY_ARRAY), 1f)));
335+
assertTrue(service.canMatch(new ShardSearchLocalRequest(indexShard.shardId(), 1, SearchType.QUERY_THEN_FETCH,
336+
new SearchSourceBuilder().query(new MatchNoneQueryBuilder())
337+
.aggregation(new GlobalAggregationBuilder("test")), Strings.EMPTY_ARRAY, false,
338+
new AliasFilter(null, Strings.EMPTY_ARRAY), 1f)));
339+
340+
assertFalse(service.canMatch(new ShardSearchLocalRequest(indexShard.shardId(), 1, SearchType.QUERY_THEN_FETCH,
341+
new SearchSourceBuilder().query(new MatchNoneQueryBuilder()), Strings.EMPTY_ARRAY, false,
342+
new AliasFilter(null, Strings.EMPTY_ARRAY), 1f)));
343+
344+
}
345+
346+
public void testCanRewriteToMatchNone() {
347+
assertFalse(SearchService.canRewriteToMatchNone(new SearchSourceBuilder().query(new MatchNoneQueryBuilder())
348+
.aggregation(new GlobalAggregationBuilder("test"))));
349+
assertFalse(SearchService.canRewriteToMatchNone(new SearchSourceBuilder()));
350+
assertFalse(SearchService.canRewriteToMatchNone(null));
351+
assertFalse(SearchService.canRewriteToMatchNone(new SearchSourceBuilder().query(new MatchNoneQueryBuilder())
352+
.aggregation(new TermsAggregationBuilder("test", ValueType.STRING).minDocCount(0))));
353+
assertTrue(SearchService.canRewriteToMatchNone(new SearchSourceBuilder().query(new TermQueryBuilder("foo", "bar"))));
354+
assertTrue(SearchService.canRewriteToMatchNone(new SearchSourceBuilder().query(new MatchNoneQueryBuilder())
355+
.aggregation(new TermsAggregationBuilder("test", ValueType.STRING).minDocCount(1))));
356+
}
307357
}

0 commit comments

Comments
 (0)