Skip to content

Commit 915d7cc

Browse files
authored
CCS metadata is opt-in in ESQL JSON responses (elastic#114437) (elastic#114688)
Since Kibana only needs CCS metadata in ESQL responses from certain well-defined locations, we are making CCS metadata opt-in. This feature is patterned after ESQL profiling, where you specify "profile": true in the ESQL body and if you asked for it will be present in the response always (it will be written to the .async-search index and you can’t turn it off in later async-search requests against this particular query ID) and if you didn’t ask for it at the beginning it will never be present (it will NOT be written to the .async-search index when it is persisted). The new option is "include_ccs_metadata": true/false. (cherry picked from commit fd9d733)
1 parent 9348d9f commit 915d7cc

File tree

22 files changed

+415
-137
lines changed

22 files changed

+415
-137
lines changed

docs/reference/esql/esql-across-clusters.asciidoc

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,10 @@ FROM *:my-index-000001
188188
[[ccq-cluster-details]]
189189
==== Cross-cluster metadata
190190

191-
ES|QL {ccs} responses include metadata about the search on each cluster when the response format is JSON.
191+
Using the `"include_ccs_metadata": true` option, users can request that
192+
ES|QL {ccs} responses include metadata about the search on each cluster (when the response format is JSON).
192193
Here we show an example using the async search endpoint. {ccs-cap} metadata is also present in the synchronous
193-
search endpoint.
194+
search endpoint response when requested.
194195

195196
[source,console]
196197
----
@@ -200,7 +201,8 @@ POST /_query/async?format=json
200201
FROM my-index-000001,cluster_one:my-index-000001,cluster_two:my-index*
201202
| STATS COUNT(http.response.status_code) BY user.id
202203
| LIMIT 2
203-
"""
204+
""",
205+
"include_ccs_metadata": true
204206
}
205207
----
206208
// TEST[setup:my_index]
@@ -238,7 +240,7 @@ Which returns:
238240
"(local)": { <4>
239241
"status": "successful",
240242
"indices": "blogs",
241-
"took": 36, <5>
243+
"took": 41, <5>
242244
"_shards": { <6>
243245
"total": 13,
244246
"successful": 13,
@@ -260,7 +262,7 @@ Which returns:
260262
"cluster_two": {
261263
"status": "successful",
262264
"indices": "cluster_two:my-index*",
263-
"took": 41,
265+
"took": 40,
264266
"_shards": {
265267
"total": 18,
266268
"successful": 18,
@@ -286,17 +288,14 @@ it is identified as "(local)".
286288
<5> How long (in milliseconds) the search took on each cluster. This can be useful to determine
287289
which clusters have slower response times than others.
288290
<6> The shard details for the search on that cluster, including a count of shards that were
289-
skipped due to the can-match phase. Shards are skipped when they cannot have any matching data
291+
skipped due to the can-match phase results. Shards are skipped when they cannot have any matching data
290292
and therefore are not included in the full ES|QL query.
291293

292294

293295
The cross-cluster metadata can be used to determine whether any data came back from a cluster.
294296
For instance, in the query below, the wildcard expression for `cluster-two` did not resolve
295297
to a concrete index (or indices). The cluster is, therefore, marked as 'skipped' and the total
296298
number of shards searched is set to zero.
297-
Since the other cluster did have a matching index, the search did not return an error, but
298-
instead returned all the matching data it could find.
299-
300299

301300
[source,console]
302301
----
@@ -306,7 +305,8 @@ POST /_query/async?format=json
306305
FROM cluster_one:my-index*,cluster_two:logs*
307306
| STATS COUNT(http.response.status_code) BY user.id
308307
| LIMIT 2
309-
"""
308+
""",
309+
"include_ccs_metadata": true
310310
}
311311
----
312312
// TEST[continued]

docs/reference/esql/esql-query-api.asciidoc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ precedence.
6767
`false`. The API only supports this parameter for CBOR, JSON, SMILE, and YAML
6868
responses. See <<esql-rest-columnar>>.
6969

70+
`include_ccs_metadata`::
71+
(Optional, boolean) If `true`, cross-cluster searches will include metadata about the query
72+
on each cluster. Defaults to `false`. The API only supports this parameter for CBOR, JSON, SMILE,
73+
and YAML responses. See <<ccq-cluster-details>>.
74+
7075
`locale`::
7176
(Optional, string) Returns results (especially dates) formatted per the conventions of the locale.
7277
For syntax, refer to <<esql-locale-param>>.
@@ -85,6 +90,7 @@ https://en.wikipedia.org/wiki/Query_plan[EXPLAIN PLAN].
8590
`query`::
8691
(Required, string) {esql} query to run. For syntax, refer to <<esql-syntax>>.
8792

93+
8894
ifeval::["{release-state}"=="unreleased"]
8995
`table`::
9096
(Optional, object) Named "table" parameters that can be referenced by the <<esql-lookup>> command.
@@ -108,6 +114,13 @@ returned if `drop_null_columns` is sent with the request.
108114
(array of arrays)
109115
Values for the search results.
110116

117+
`_clusters`::
118+
(object)
119+
Metadata about clusters involved in the execution of a cross-cluster query. Only returned (1) for
120+
cross-cluster searches and (2) when `include_ccs_metadata` is sent in the body and set to `true`
121+
and (3) when `format` of the response is set to JSON (the default), CBOR, SMILE, or YAML.
122+
See <<ccq-cluster-details>> for more information.
123+
111124
`profile`::
112125
(object)
113126
Profile describing the execution of the query. Only returned if `profile` was sent in the body.

server/src/main/java/org/elasticsearch/TransportVersions.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ static TransportVersion def(int id) {
240240
public static final TransportVersion SIMULATE_INDEX_TEMPLATES_SUBSTITUTIONS = def(8_764_00_0);
241241
public static final TransportVersion RETRIEVERS_TELEMETRY_ADDED = def(8_765_00_0);
242242
public static final TransportVersion ESQL_CACHED_STRING_SERIALIZATION = def(8_766_00_0);
243+
public static final TransportVersion CHUNK_SENTENCE_OVERLAP_SETTING_ADDE1D = def(8_767_00_0);
244+
public static final TransportVersion OPT_IN_ESQL_CCS_EXECUTION_INFO = def(8_768_00_0);
243245

244246
/*
245247
* STOP! READ THIS FIRST! No, really,

x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClustersIT.java

Lines changed: 81 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,18 @@ void indexDocs(RestClient client, String index, List<Doc> docs) throws IOExcepti
127127
refresh(client, index);
128128
}
129129

130-
private Map<String, Object> run(String query) throws IOException {
131-
Map<String, Object> resp = runEsql(new RestEsqlTestCase.RequestObjectBuilder().query(query).build());
130+
private Map<String, Object> run(String query, boolean includeCCSMetadata) throws IOException {
131+
Map<String, Object> resp = runEsql(
132+
new RestEsqlTestCase.RequestObjectBuilder().query(query).includeCCSMetadata(includeCCSMetadata).build()
133+
);
134+
logger.info("--> query {} response {}", query, resp);
135+
return resp;
136+
}
137+
138+
private Map<String, Object> runWithColumnarAndIncludeCCSMetadata(String query) throws IOException {
139+
Map<String, Object> resp = runEsql(
140+
new RestEsqlTestCase.RequestObjectBuilder().query(query).includeCCSMetadata(true).columnar(true).build()
141+
);
132142
logger.info("--> query {} response {}", query, resp);
133143
return resp;
134144
}
@@ -147,62 +157,77 @@ private Map<String, Object> runEsql(RestEsqlTestCase.RequestObjectBuilder reques
147157

148158
public void testCount() throws Exception {
149159
{
150-
Map<String, Object> result = run("FROM test-local-index,*:test-remote-index | STATS c = COUNT(*)");
160+
boolean includeCCSMetadata = randomBoolean();
161+
Map<String, Object> result = run("FROM test-local-index,*:test-remote-index | STATS c = COUNT(*)", includeCCSMetadata);
151162
var columns = List.of(Map.of("name", "c", "type", "long"));
152163
var values = List.of(List.of(localDocs.size() + remoteDocs.size()));
153164

154165
MapMatcher mapMatcher = matchesMap();
155-
assertMap(
156-
result,
157-
mapMatcher.entry("columns", columns)
158-
.entry("values", values)
159-
.entry("took", greaterThanOrEqualTo(0))
160-
.entry("_clusters", any(Map.class))
161-
);
162-
assertClusterDetailsMap(result, false);
166+
if (includeCCSMetadata) {
167+
mapMatcher = mapMatcher.entry("_clusters", any(Map.class));
168+
}
169+
assertMap(result, mapMatcher.entry("columns", columns).entry("values", values).entry("took", greaterThanOrEqualTo(0)));
170+
if (includeCCSMetadata) {
171+
assertClusterDetailsMap(result, false);
172+
}
163173
}
164174
{
165-
Map<String, Object> result = run("FROM *:test-remote-index | STATS c = COUNT(*)");
175+
boolean includeCCSMetadata = randomBoolean();
176+
Map<String, Object> result = run("FROM *:test-remote-index | STATS c = COUNT(*)", includeCCSMetadata);
166177
var columns = List.of(Map.of("name", "c", "type", "long"));
167178
var values = List.of(List.of(remoteDocs.size()));
168179

169180
MapMatcher mapMatcher = matchesMap();
170-
assertMap(
171-
result,
172-
mapMatcher.entry("columns", columns)
173-
.entry("values", values)
174-
.entry("took", greaterThanOrEqualTo(0))
175-
.entry("_clusters", any(Map.class))
176-
);
177-
assertClusterDetailsMap(result, true);
181+
if (includeCCSMetadata) {
182+
mapMatcher = mapMatcher.entry("_clusters", any(Map.class));
183+
}
184+
assertMap(result, mapMatcher.entry("columns", columns).entry("values", values).entry("took", greaterThanOrEqualTo(0)));
185+
if (includeCCSMetadata) {
186+
assertClusterDetailsMap(result, true);
187+
}
178188
}
179189
}
180190

181191
public void testUngroupedAggs() throws Exception {
182192
{
183-
Map<String, Object> result = run("FROM test-local-index,*:test-remote-index | STATS total = SUM(data)");
193+
boolean includeCCSMetadata = randomBoolean();
194+
Map<String, Object> result = run("FROM test-local-index,*:test-remote-index | STATS total = SUM(data)", includeCCSMetadata);
184195
var columns = List.of(Map.of("name", "total", "type", "long"));
185196
long sum = Stream.concat(localDocs.stream(), remoteDocs.stream()).mapToLong(d -> d.data).sum();
186197
var values = List.of(List.of(Math.toIntExact(sum)));
187198

188199
// check all sections of map except _cluster/details
189200
MapMatcher mapMatcher = matchesMap();
190-
assertMap(
191-
result,
192-
mapMatcher.entry("columns", columns)
193-
.entry("values", values)
194-
.entry("took", greaterThanOrEqualTo(0))
195-
.entry("_clusters", any(Map.class))
196-
);
197-
assertClusterDetailsMap(result, false);
201+
if (includeCCSMetadata) {
202+
mapMatcher = mapMatcher.entry("_clusters", any(Map.class));
203+
}
204+
assertMap(result, mapMatcher.entry("columns", columns).entry("values", values).entry("took", greaterThanOrEqualTo(0)));
205+
if (includeCCSMetadata) {
206+
assertClusterDetailsMap(result, false);
207+
}
198208
}
199209
{
200-
Map<String, Object> result = run("FROM *:test-remote-index | STATS total = SUM(data)");
210+
boolean includeCCSMetadata = randomBoolean();
211+
Map<String, Object> result = run("FROM *:test-remote-index | STATS total = SUM(data)", includeCCSMetadata);
212+
var columns = List.of(Map.of("name", "total", "type", "long"));
213+
long sum = remoteDocs.stream().mapToLong(d -> d.data).sum();
214+
var values = List.of(List.of(Math.toIntExact(sum)));
215+
216+
MapMatcher mapMatcher = matchesMap();
217+
if (includeCCSMetadata) {
218+
mapMatcher = mapMatcher.entry("_clusters", any(Map.class));
219+
}
220+
assertMap(result, mapMatcher.entry("columns", columns).entry("values", values).entry("took", greaterThanOrEqualTo(0)));
221+
if (includeCCSMetadata) {
222+
assertClusterDetailsMap(result, true);
223+
}
224+
}
225+
{
226+
Map<String, Object> result = runWithColumnarAndIncludeCCSMetadata("FROM *:test-remote-index | STATS total = SUM(data)");
201227
var columns = List.of(Map.of("name", "total", "type", "long"));
202228
long sum = remoteDocs.stream().mapToLong(d -> d.data).sum();
203229
var values = List.of(List.of(Math.toIntExact(sum)));
204230

205-
// check all sections of map except _cluster/details
206231
MapMatcher mapMatcher = matchesMap();
207232
assertMap(
208233
result,
@@ -269,7 +294,11 @@ private void assertClusterDetailsMap(Map<String, Object> result, boolean remoteO
269294

270295
public void testGroupedAggs() throws Exception {
271296
{
272-
Map<String, Object> result = run("FROM test-local-index,*:test-remote-index | STATS total = SUM(data) BY color | SORT color");
297+
boolean includeCCSMetadata = randomBoolean();
298+
Map<String, Object> result = run(
299+
"FROM test-local-index,*:test-remote-index | STATS total = SUM(data) BY color | SORT color",
300+
includeCCSMetadata
301+
);
273302
var columns = List.of(Map.of("name", "total", "type", "long"), Map.of("name", "color", "type", "keyword"));
274303
var values = Stream.concat(localDocs.stream(), remoteDocs.stream())
275304
.collect(Collectors.toMap(d -> d.color, Doc::data, Long::sum))
@@ -280,17 +309,20 @@ public void testGroupedAggs() throws Exception {
280309
.toList();
281310

282311
MapMatcher mapMatcher = matchesMap();
283-
assertMap(
284-
result,
285-
mapMatcher.entry("columns", columns)
286-
.entry("values", values)
287-
.entry("took", greaterThanOrEqualTo(0))
288-
.entry("_clusters", any(Map.class))
289-
);
290-
assertClusterDetailsMap(result, false);
312+
if (includeCCSMetadata) {
313+
mapMatcher = mapMatcher.entry("_clusters", any(Map.class));
314+
}
315+
assertMap(result, mapMatcher.entry("columns", columns).entry("values", values).entry("took", greaterThanOrEqualTo(0)));
316+
if (includeCCSMetadata) {
317+
assertClusterDetailsMap(result, false);
318+
}
291319
}
292320
{
293-
Map<String, Object> result = run("FROM *:test-remote-index | STATS total = SUM(data) by color | SORT color");
321+
boolean includeCCSMetadata = randomBoolean();
322+
Map<String, Object> result = run(
323+
"FROM *:test-remote-index | STATS total = SUM(data) by color | SORT color",
324+
includeCCSMetadata
325+
);
294326
var columns = List.of(Map.of("name", "total", "type", "long"), Map.of("name", "color", "type", "keyword"));
295327
var values = remoteDocs.stream()
296328
.collect(Collectors.toMap(d -> d.color, Doc::data, Long::sum))
@@ -300,16 +332,15 @@ public void testGroupedAggs() throws Exception {
300332
.map(e -> List.of(Math.toIntExact(e.getValue()), e.getKey()))
301333
.toList();
302334

303-
// check all sections of map except _cluster/details
335+
// check all sections of map except _clusters/details
304336
MapMatcher mapMatcher = matchesMap();
305-
assertMap(
306-
result,
307-
mapMatcher.entry("columns", columns)
308-
.entry("values", values)
309-
.entry("took", greaterThanOrEqualTo(0))
310-
.entry("_clusters", any(Map.class))
311-
);
312-
assertClusterDetailsMap(result, true);
337+
if (includeCCSMetadata) {
338+
mapMatcher = mapMatcher.entry("_clusters", any(Map.class));
339+
}
340+
assertMap(result, mapMatcher.entry("columns", columns).entry("values", values).entry("took", greaterThanOrEqualTo(0)));
341+
if (includeCCSMetadata) {
342+
assertClusterDetailsMap(result, true);
343+
}
313344
}
314345
}
315346

x-pack/plugin/esql/qa/server/single-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/single_node/RestEsqlIT.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public void testBasicEsql() throws IOException {
7676
indexTimestampData(1);
7777

7878
RequestObjectBuilder builder = requestObjectBuilder().query(fromIndex() + " | stats avg(value)");
79+
requestObjectBuilder().includeCCSMetadata(randomBoolean());
7980
if (Build.current().isSnapshot()) {
8081
builder.pragmas(Settings.builder().put("data_partitioning", "shard").build());
8182
}

x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ public static class RequestObjectBuilder {
128128
private Boolean keepOnCompletion = null;
129129

130130
private Boolean profile = null;
131+
private Boolean includeCCSMetadata = null;
131132

132133
private CheckedConsumer<XContentBuilder, IOException> filter;
133134

@@ -197,6 +198,11 @@ public RequestObjectBuilder profile(boolean profile) {
197198
return this;
198199
}
199200

201+
public RequestObjectBuilder includeCCSMetadata(boolean includeCCSMetadata) {
202+
this.includeCCSMetadata = includeCCSMetadata;
203+
return this;
204+
}
205+
200206
public RequestObjectBuilder filter(CheckedConsumer<XContentBuilder, IOException> filter) {
201207
this.filter = filter;
202208
return this;
@@ -220,6 +226,9 @@ public RequestObjectBuilder build() throws IOException {
220226
if (profile != null) {
221227
builder.field("profile", profile);
222228
}
229+
if (includeCCSMetadata != null) {
230+
builder.field("include_ccs_metadata", includeCCSMetadata);
231+
}
223232
if (filter != null) {
224233
builder.startObject("filter");
225234
filter.accept(builder);

0 commit comments

Comments
 (0)