Skip to content

Commit 622fb78

Browse files
authored
Introduce ability to minimize round-trips in CCS (#37828)
With #37566 we have introduced the ability to merge multiple search responses into one. That makes it possible to expose a new way of executing cross-cluster search requests, that makes CCS much faster whenever there is network latency between the CCS coordinating node and the remote clusters. The coordinating node can now send a single search request to each remote cluster, which gets reduced by each one of them. from + size results are requested to each cluster, and the reduce phase in each cluster is non final (meaning that buckets are not pruned and pipeline aggs are not executed). The CCS coordinating node performs an additional, final reduction, which produces one search response out of the multiple responses received from the different clusters. This new execution path will be activated by default for any CCS request unless a scroll is provided or inner hits are requested as part of field collapsing. The search API accepts now a new parameter called ccs_minimize_roundtrips that allows to opt-out of the default behaviour. Relates to #32125
1 parent ae9f4df commit 622fb78

File tree

37 files changed

+1159
-331
lines changed

37 files changed

+1159
-331
lines changed

client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ private static void addSearchRequestParams(Params params, SearchRequest searchRe
399399
params.withPreference(searchRequest.preference());
400400
params.withIndicesOptions(searchRequest.indicesOptions());
401401
params.putParam("search_type", searchRequest.searchType().name().toLowerCase(Locale.ROOT));
402+
params.putParam("ccs_minimize_roundtrips", Boolean.toString(searchRequest.isCcsMinimizeRoundtrips()));
402403
if (searchRequest.requestCache() != null) {
403404
params.putParam("request_cache", Boolean.toString(searchRequest.requestCache()));
404405
}

client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1239,7 +1239,7 @@ public void testMultiSearch() throws IOException {
12391239
requests.add(searchRequest);
12401240
};
12411241
MultiSearchRequest.readMultiLineFormat(new BytesArray(EntityUtils.toByteArray(request.getEntity())),
1242-
REQUEST_BODY_CONTENT_TYPE.xContent(), consumer, null, multiSearchRequest.indicesOptions(), null, null, null,
1242+
REQUEST_BODY_CONTENT_TYPE.xContent(), consumer, null, multiSearchRequest.indicesOptions(), null, null, null, null,
12431243
xContentRegistry(), true);
12441244
assertEquals(requests, multiSearchRequest.requests());
12451245
}
@@ -1862,6 +1862,10 @@ private static void setRandomSearchParams(SearchRequest searchRequest,
18621862
searchRequest.scroll(randomTimeValue());
18631863
expectedParams.put("scroll", searchRequest.scroll().keepAlive().getStringRep());
18641864
}
1865+
if (randomBoolean()) {
1866+
searchRequest.setCcsMinimizeRoundtrips(randomBoolean());
1867+
}
1868+
expectedParams.put("ccs_minimize_roundtrips", Boolean.toString(searchRequest.isCcsMinimizeRoundtrips()));
18651869
}
18661870

18671871
static void setRandomIndicesOptions(Consumer<IndicesOptions> setter, Supplier<IndicesOptions> getter,

docs/reference/modules/cross-cluster-search.asciidoc

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ GET /cluster_one:twitter/_search
6565
{
6666
"took": 150,
6767
"timed_out": false,
68+
"num_reduce_phases": 2,
6869
"_shards": {
6970
"total": 1,
7071
"successful": 1,
@@ -130,6 +131,7 @@ will be prefixed with their remote cluster name:
130131
{
131132
"took": 150,
132133
"timed_out": false,
134+
"num_reduce_phases": 3,
133135
"_shards": {
134136
"total": 2,
135137
"successful": 2,
@@ -222,6 +224,7 @@ GET /cluster_one:twitter,cluster_two:twitter,twitter/_search <1>
222224
{
223225
"took": 150,
224226
"timed_out": false,
227+
"num_reduce_phases": 3,
225228
"_shards": {
226229
"total": 2,
227230
"successful": 2,
@@ -273,3 +276,43 @@ GET /cluster_one:twitter,cluster_two:twitter,twitter/_search <1>
273276
// TESTRESPONSE[s/"_score": 1/"_score": "$body.hits.hits.0._score"/]
274277
// TESTRESPONSE[s/"_score": 2/"_score": "$body.hits.hits.1._score"/]
275278
<1> The `clusters` section indicates that one cluster was unavailable and got skipped
279+
280+
[float]
281+
[[ccs-reduction]]
282+
=== CCS reduction phase
283+
284+
Cross-cluster search requests can be executed in two ways:
285+
286+
- the CCS coordinating node minimizes network round-trips by sending one search
287+
request to each cluster. Each cluster performs the search independently,
288+
reducing and fetching results. Once the CCS node has received all the
289+
responses, it performs another reduction and returns the relevant results back
290+
to the user. This strategy is beneficial when there is network latency between
291+
the CCS coordinating node and the remote clusters involved, which is typically
292+
the case. A single request is sent to each remote cluster, at the cost of
293+
retrieving `from` + `size` already fetched results. This is the default
294+
strategy, used whenever possible. In case a scroll is provided, or inner hits
295+
are requested as part of field collapsing, this strategy is not supported hence
296+
network round-trips cannot be minimized and the following strategy is used
297+
instead.
298+
299+
- the CCS coordinating node sends a <<search-shards,search shards>> request to
300+
each remote cluster, in order to collect information about their corresponding
301+
remote indices involved in the search request and the shards where their data
302+
is located. Once each cluster has responded to such request, the search
303+
executes as if all shards were part of the same cluster. The coordinating node
304+
sends one request to each shard involved, each shard executes the query and
305+
returns its own results which are then reduced (and fetched, depending on the
306+
<<search-request-search-type, search type>>) by the CCS coordinating node.
307+
This strategy may be beneficial whenever there is very low network latency
308+
between the CCS coordinating node and the remote clusters involved, as it
309+
treats all shards the same, at the cost of sending many requests to each remote
310+
cluster, which is problematic in presence of network latency.
311+
312+
The <<search-request-body, search API>> supports the `ccs_minimize_roundtrips`
313+
parameter, which defaults to `true` and can be set to `false` in case
314+
minimizing network round-trips is not desirable.
315+
316+
Note that all the communication between the nodes, regardless of which cluster
317+
they belong to and the selected reduce mode, happens through the
318+
<<modules-transport,transport layer>>.

docs/reference/search/request-body.asciidoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ And here is a sample response:
113113
reduce the memory overhead per search request if the potential number of
114114
shards in the request can be large.
115115

116+
`ccs_minimize_roundtrips`::
117+
118+
Defaults to `true`. Set to `false` to disable minimizing network round-trips
119+
between the coordinating node and the remote clusters when executing
120+
cross-cluster search requests. See <<ccs-reduction>> for more.
116121

117122

118123
Out of the above, the `search_type`, `request_cache` and the `allow_partial_search_results`

modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MultiSearchTemplateRequest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ public MultiSearchTemplateRequest add(SearchTemplateRequest request) {
6565
return this;
6666
}
6767

68-
6968
/**
7069
* Returns the amount of search requests specified in this multi search requests are allowed to be ran concurrently.
7170
*/

modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/MultiSearchTemplateResponseTests.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ protected MultiSearchTemplateResponse createTestInstance() {
5050
int successfulShards = randomIntBetween(0, totalShards);
5151
int skippedShards = totalShards - successfulShards;
5252
InternalSearchResponse internalSearchResponse = InternalSearchResponse.empty();
53-
SearchResponse.Clusters clusters = new SearchResponse.Clusters(totalShards, successfulShards, skippedShards);
53+
SearchResponse.Clusters clusters = randomClusters();
5454
SearchTemplateResponse searchTemplateResponse = new SearchTemplateResponse();
5555
SearchResponse searchResponse = new SearchResponse(internalSearchResponse, null, totalShards,
5656
successfulShards, skippedShards, tookInMillis, ShardSearchFailure.EMPTY_ARRAY, clusters);
@@ -59,22 +59,27 @@ protected MultiSearchTemplateResponse createTestInstance() {
5959
}
6060
return new MultiSearchTemplateResponse(items, overallTookInMillis);
6161
}
62-
62+
63+
private static SearchResponse.Clusters randomClusters() {
64+
int totalClusters = randomIntBetween(0, 10);
65+
int successfulClusters = randomIntBetween(0, totalClusters);
66+
int skippedClusters = totalClusters - successfulClusters;
67+
return new SearchResponse.Clusters(totalClusters, successfulClusters, skippedClusters);
68+
}
6369

6470
private static MultiSearchTemplateResponse createTestInstanceWithFailures() {
6571
int numItems = randomIntBetween(0, 128);
6672
long overallTookInMillis = randomNonNegativeLong();
6773
MultiSearchTemplateResponse.Item[] items = new MultiSearchTemplateResponse.Item[numItems];
6874
for (int i = 0; i < numItems; i++) {
6975
if (randomBoolean()) {
70-
// Creating a minimal response is OK, because SearchResponse self
71-
// is tested elsewhere.
76+
// Creating a minimal response is OK, because SearchResponse is tested elsewhere.
7277
long tookInMillis = randomNonNegativeLong();
7378
int totalShards = randomIntBetween(1, Integer.MAX_VALUE);
7479
int successfulShards = randomIntBetween(0, totalShards);
7580
int skippedShards = totalShards - successfulShards;
7681
InternalSearchResponse internalSearchResponse = InternalSearchResponse.empty();
77-
SearchResponse.Clusters clusters = new SearchResponse.Clusters(totalShards, successfulShards, skippedShards);
82+
SearchResponse.Clusters clusters = randomClusters();
7883
SearchTemplateResponse searchTemplateResponse = new SearchTemplateResponse();
7984
SearchResponse searchResponse = new SearchResponse(internalSearchResponse, null, totalShards,
8085
successfulShards, skippedShards, tookInMillis, ShardSearchFailure.EMPTY_ARRAY, clusters);
@@ -133,6 +138,5 @@ public void testFromXContentWithFailures() throws IOException {
133138
AbstractXContentTestCase.testFromXContent(NUMBER_OF_TEST_RUNS, instanceSupplier, supportsUnknownFields, Strings.EMPTY_ARRAY,
134139
getRandomFieldsExcludeFilterWhenResultHasErrors(), this::createParser, this::doParseInstance,
135140
this::assertEqualInstances, assertToXContentEquivalence, ToXContent.EMPTY_PARAMS);
136-
}
137-
141+
}
138142
}

qa/ccs-unavailable-clusters/src/test/java/org/elasticsearch/search/CrossClusterSearchUnavailableClusterIT.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.apache.http.HttpEntity;
2323
import org.apache.http.entity.ContentType;
2424
import org.apache.http.nio.entity.NStringEntity;
25+
import org.apache.lucene.search.TotalHits;
2526
import org.elasticsearch.ElasticsearchException;
2627
import org.elasticsearch.Version;
2728
import org.elasticsearch.action.admin.cluster.shards.ClusterSearchShardsAction;
@@ -32,9 +33,11 @@
3233
import org.elasticsearch.action.admin.cluster.state.ClusterStateRequest;
3334
import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse;
3435
import org.elasticsearch.action.index.IndexRequest;
36+
import org.elasticsearch.action.search.SearchAction;
3537
import org.elasticsearch.action.search.SearchRequest;
3638
import org.elasticsearch.action.search.SearchResponse;
3739
import org.elasticsearch.action.search.SearchScrollRequest;
40+
import org.elasticsearch.action.search.ShardSearchFailure;
3841
import org.elasticsearch.client.Request;
3942
import org.elasticsearch.client.RequestOptions;
4043
import org.elasticsearch.client.Response;
@@ -49,6 +52,8 @@
4952
import org.elasticsearch.common.settings.Settings;
5053
import org.elasticsearch.common.xcontent.XContentBuilder;
5154
import org.elasticsearch.common.xcontent.json.JsonXContent;
55+
import org.elasticsearch.search.aggregations.InternalAggregations;
56+
import org.elasticsearch.search.internal.InternalSearchResponse;
5257
import org.elasticsearch.test.rest.ESRestTestCase;
5358
import org.elasticsearch.test.transport.MockTransportService;
5459
import org.elasticsearch.threadpool.TestThreadPool;
@@ -107,6 +112,14 @@ private static MockTransportService startTransport(
107112
channel.sendResponse(new ClusterSearchShardsResponse(new ClusterSearchShardsGroup[0],
108113
knownNodes.toArray(new DiscoveryNode[0]), Collections.emptyMap()));
109114
});
115+
newService.registerRequestHandler(SearchAction.NAME, ThreadPool.Names.SAME, SearchRequest::new,
116+
(request, channel, task) -> {
117+
InternalSearchResponse response = new InternalSearchResponse(new SearchHits(new SearchHit[0],
118+
new TotalHits(0, TotalHits.Relation.EQUAL_TO), Float.NaN), InternalAggregations.EMPTY, null, null, false, null, 1);
119+
SearchResponse searchResponse = new SearchResponse(response, null, 1, 1, 0, 100, ShardSearchFailure.EMPTY_ARRAY,
120+
SearchResponse.Clusters.EMPTY);
121+
channel.sendResponse(searchResponse);
122+
});
110123
newService.registerRequestHandler(ClusterStateAction.NAME, ThreadPool.Names.SAME, ClusterStateRequest::new,
111124
(request, channel, task) -> {
112125
DiscoveryNodes.Builder builder = DiscoveryNodes.builder();

qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/10_basic.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
terms:
3737
field: f1.keyword
3838

39+
- match: {_clusters.total: 2}
40+
- match: {_clusters.successful: 2}
41+
- match: {_clusters.skipped: 0}
42+
- match: {num}
3943
- match: { _shards.total: 5 }
4044
- match: { hits.total: 11 }
4145
- gte: { hits.hits.0._seq_no: 0 }
@@ -59,6 +63,9 @@
5963
terms:
6064
field: f1.keyword
6165

66+
- match: {_clusters.total: 2}
67+
- match: {_clusters.successful: 2}
68+
- match: {_clusters.skipped: 0}
6269
- match: { _shards.total: 5 }
6370
- match: { hits.total: 6}
6471
- match: { hits.hits.0._index: "my_remote_cluster:test_index"}
@@ -76,6 +83,9 @@
7683
terms:
7784
field: f1.keyword
7885

86+
- match: {_clusters.total: 1}
87+
- match: {_clusters.successful: 1}
88+
- match: {_clusters.skipped: 0}
7989
- match: { _shards.total: 3 }
8090
- match: { hits.total: 6}
8191
- match: { hits.hits.0._index: "my_remote_cluster:test_index"}
@@ -93,6 +103,7 @@
93103
terms:
94104
field: f1.keyword
95105

106+
- is_false: _clusters
96107
- match: { _shards.total: 2 }
97108
- match: { hits.total: 5}
98109
- match: { hits.hits.0._index: "test_index"}
@@ -122,6 +133,9 @@
122133
rest_total_hits_as_int: true
123134
index: test_remote_cluster:test_index
124135

136+
- match: {_clusters.total: 1}
137+
- match: {_clusters.successful: 1}
138+
- match: {_clusters.skipped: 0}
125139
- match: { _shards.total: 3 }
126140
- match: { hits.total: 6 }
127141
- match: { hits.hits.0._index: "test_remote_cluster:test_index" }
@@ -148,6 +162,9 @@
148162
rest_total_hits_as_int: true
149163
index: "*:test_index"
150164

165+
- match: {_clusters.total: 2}
166+
- match: {_clusters.successful: 2}
167+
- match: {_clusters.skipped: 0}
151168
- match: { _shards.total: 6 }
152169
- match: { hits.total: 12 }
153170

@@ -159,6 +176,9 @@
159176
rest_total_hits_as_int: true
160177
index: my_remote_cluster:aliased_test_index
161178

179+
- match: {_clusters.total: 1}
180+
- match: {_clusters.successful: 1}
181+
- match: {_clusters.skipped: 0}
162182
- match: { _shards.total: 3 }
163183
- match: { hits.total: 2 }
164184
- match: { hits.hits.0._source.filter_field: 1 }
@@ -172,6 +192,9 @@
172192
rest_total_hits_as_int: true
173193
index: my_remote_cluster:aliased_test_index,my_remote_cluster:field_caps_index_1
174194

195+
- match: {_clusters.total: 1}
196+
- match: {_clusters.successful: 1}
197+
- match: {_clusters.skipped: 0}
175198
- match: { _shards.total: 4 }
176199
- match: { hits.total: 2 }
177200
- match: { hits.hits.0._source.filter_field: 1 }
@@ -185,6 +208,9 @@
185208
rest_total_hits_as_int: true
186209
index: "my_remote_cluster:single_doc_index"
187210

211+
- match: {_clusters.total: 1}
212+
- match: {_clusters.successful: 1}
213+
- match: {_clusters.skipped: 0}
188214
- match: { _shards.total: 1 }
189215
- match: { hits.total: 1 }
190216
- match: { hits.hits.0._index: "my_remote_cluster:single_doc_index"}

qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/70_skip_shards.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@
2929
rest_total_hits_as_int: true
3030
index: "skip_shards_index,my_remote_cluster:single_doc_index"
3131
pre_filter_shard_size: 1
32+
ccs_minimize_roundtrips: false
3233
body: { "size" : 10, "query" : { "range" : { "created_at" : { "gte" : "2016-02-01", "lt": "2018-02-01"} } } }
3334

3435
- match: { hits.total: 1 }
3536
- match: { hits.hits.0._index: "skip_shards_index"}
37+
- is_false: num_reduce_phases
3638
- match: { _shards.total: 2 }
3739
- match: { _shards.successful: 2 }
3840
- match: { _shards.skipped : 1}
@@ -45,10 +47,12 @@
4547
rest_total_hits_as_int: true
4648
index: "skip_shards_index,my_remote_cluster:single_doc_index"
4749
pre_filter_shard_size: 1
50+
ccs_minimize_roundtrips: false
4851
body: { "size" : 10, "query" : { "range" : { "created_at" : { "gte" : "2015-02-01", "lt": "2016-02-01"} } } }
4952

5053
- match: { hits.total: 1 }
5154
- match: { hits.hits.0._index: "my_remote_cluster:single_doc_index"}
55+
- is_false: num_reduce_phases
5256
- match: { _shards.total: 2 }
5357
- match: { _shards.successful: 2 }
5458
- match: { _shards.skipped : 1}

rest-api-spec/src/main/resources/rest-api-spec/api/msearch.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@
4343
"type" : "boolean",
4444
"description" : "Indicates whether hits.total should be rendered as an integer or an object in the rest search response",
4545
"default" : false
46+
},
47+
"ccs_minimize_roundtrips": {
48+
"type" : "boolean",
49+
"description" : "Indicates whether network round-trips should be minimized as part of cross-cluster search requests execution",
50+
"default" : "true"
4651
}
4752
}
4853
},

rest-api-spec/src/main/resources/rest-api-spec/api/msearch_template.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@
3333
"type" : "boolean",
3434
"description" : "Indicates whether hits.total should be rendered as an integer or an object in the rest search response",
3535
"default" : false
36+
},
37+
"ccs_minimize_roundtrips": {
38+
"type" : "boolean",
39+
"description" : "Indicates whether network round-trips should be minimized as part of cross-cluster search requests execution",
40+
"default" : "true"
3641
}
3742
}
3843
},

rest-api-spec/src/main/resources/rest-api-spec/api/search.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
"type" : "boolean",
2525
"description" : "Specify whether wildcard and prefix queries should be analyzed (default: false)"
2626
},
27+
"ccs_minimize_roundtrips": {
28+
"type" : "boolean",
29+
"description" : "Indicates whether network round-trips should be minimized as part of cross-cluster search requests execution",
30+
"default" : "true"
31+
},
2732
"default_operator": {
2833
"type" : "enum",
2934
"options" : ["AND","OR"],

rest-api-spec/src/main/resources/rest-api-spec/api/search_template.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@
6767
"type" : "boolean",
6868
"description" : "Indicates whether hits.total should be rendered as an integer or an object in the rest search response",
6969
"default" : false
70+
},
71+
"ccs_minimize_roundtrips": {
72+
"type" : "boolean",
73+
"description" : "Indicates whether network round-trips should be minimized as part of cross-cluster search requests execution",
74+
"default" : "true"
7075
}
7176
}
7277
},

0 commit comments

Comments
 (0)