Skip to content

Commit 3d52caa

Browse files
replace query with queryBuilderAndTags in EsQueryExec
1 parent 7e7ebd8 commit 3d52caa

File tree

9 files changed

+116
-65
lines changed

9 files changed

+116
-65
lines changed

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ protected static List<Batch<PhysicalPlan>> rules(boolean optimizeForEsSource) {
8181
}
8282
// execute the SubstituteRoundToWithQueryAndTags rule once after all the other pushdown rules are applied, as this rule generate
8383
// multiple QueryBuilders according the number of RoundTo points, it should be applied after all the other eligible pushdowns are
84-
// done.
84+
// done, and it should be executed only once.
8585
@SuppressWarnings("unchecked")
8686
var substituteRoundToWithQueryAndTags = new Batch<PhysicalPlan>(
8787
"Substitute RoundTo with QueryAndTags",

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/physical/local/PushFiltersToSource.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,10 @@ private static PhysicalPlan rewrite(
126126
queryExec.indexMode(),
127127
queryExec.indexNameWithModes(),
128128
queryExec.output(),
129-
query,
130129
queryExec.limit(),
131130
queryExec.sorts(),
132-
queryExec.estimatedRowSize()
131+
queryExec.estimatedRowSize(),
132+
List.of(new EsQueryExec.QueryBuilderAndTags(query, List.of()))
133133
);
134134
// If the eval contains other aliases, not just field attributes, we need to keep them in the plan
135135
PhysicalPlan plan = evalFields.isEmpty() ? queryExec : new EvalExec(filterExec.source(), queryExec, evalFields);

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/physical/local/ReplaceRoundToWithQueryAndTags.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,6 @@ private static PhysicalPlan planRoundTo(RoundTo roundTo, EvalExec evalExec, EsQu
426426
queryExec.indexMode(),
427427
queryExec.indexNameWithModes(),
428428
newAttributes,
429-
queryExec.query(),
430429
queryExec.limit(),
431430
queryExec.sorts(),
432431
queryExec.estimatedRowSize(),

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/EsQueryExec.java

Lines changed: 80 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
package org.elasticsearch.xpack.esql.plan.physical;
99

10-
import org.elasticsearch.common.Strings;
1110
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
1211
import org.elasticsearch.common.io.stream.StreamInput;
1312
import org.elasticsearch.common.io.stream.StreamOutput;
@@ -53,7 +52,6 @@ public class EsQueryExec extends LeafExec implements EstimatesRowSize {
5352
private final IndexMode indexMode;
5453
private final Map<String, IndexMode> indexNameWithModes;
5554
private final List<Attribute> attrs;
56-
private final QueryBuilder query;
5755
private final Expression limit;
5856
private final List<Sort> sorts;
5957

@@ -64,8 +62,12 @@ public class EsQueryExec extends LeafExec implements EstimatesRowSize {
6462
private final Integer estimatedRowSize;
6563

6664
/**
67-
* queryBuilderAndTags is build on data node from the {@code RoundTo} function by {@code PushRoundToToSource} rule.
68-
* It will be used by {@code sourcePhysicalOperation} to create {@code LuceneSliceQueue.QueryAndTags}
65+
* queryBuilderAndTags may contain one or multiple {@code QueryBuilder}s, it is built on data node.
66+
* If there is one {@code RoundTo} function in the query plan, {@code ReplaceRoundToWithQueryAndTags} rule will build multiple
67+
* {@code QueryBuilder}s in the list, otherwise it is expected to contain only one {@code QueryBuilder} and its tag is
68+
* null.
69+
* It will be used by {@code EsPhysicalOperationProviders}.{@code sourcePhysicalOperation} to create
70+
* {@code LuceneSliceQueue.QueryAndTags}
6971
*/
7072
private final List<QueryBuilderAndTags> queryBuilderAndTags;
7173

@@ -88,7 +90,7 @@ public SortBuilder<?> sortBuilder() {
8890
}
8991

9092
private static FieldSort readFrom(StreamInput in) throws IOException {
91-
return new EsQueryExec.FieldSort(
93+
return new FieldSort(
9294
FieldAttribute.readFrom(in),
9395
in.readEnum(Order.OrderDirection.class),
9496
in.readEnum(Order.NullsPosition.class)
@@ -118,7 +120,12 @@ public FieldAttribute field() {
118120
}
119121
}
120122

121-
public record QueryBuilderAndTags(QueryBuilder query, List<Object> tags) {};
123+
public record QueryBuilderAndTags(QueryBuilder query, List<Object> tags) {
124+
@Override
125+
public String toString() {
126+
return "QueryBuilderAndTags{" + "queryBuilder=[" + query + "], tags=" + tags.toString() + "}";
127+
}
128+
};
122129

123130
public EsQueryExec(
124131
Source source,
@@ -128,21 +135,17 @@ public EsQueryExec(
128135
List<Attribute> attributes,
129136
QueryBuilder query
130137
) {
131-
this(source, indexPattern, indexMode, indexNameWithModes, attributes, query, null, null, null, null);
132-
}
133-
134-
public EsQueryExec(
135-
Source source,
136-
String indexPattern,
137-
IndexMode indexMode,
138-
Map<String, IndexMode> indexNameWithModes,
139-
List<Attribute> attrs,
140-
QueryBuilder query,
141-
Expression limit,
142-
List<Sort> sorts,
143-
Integer estimatedRowSize
144-
) {
145-
this(source, indexPattern, indexMode, indexNameWithModes, attrs, query, limit, sorts, estimatedRowSize, null);
138+
this(
139+
source,
140+
indexPattern,
141+
indexMode,
142+
indexNameWithModes,
143+
attributes,
144+
null,
145+
null,
146+
null,
147+
List.of(new QueryBuilderAndTags(query, List.of()))
148+
);
146149
}
147150

148151
public EsQueryExec(
@@ -151,7 +154,6 @@ public EsQueryExec(
151154
IndexMode indexMode,
152155
Map<String, IndexMode> indexNameWithModes,
153156
List<Attribute> attrs,
154-
QueryBuilder query,
155157
Expression limit,
156158
List<Sort> sorts,
157159
Integer estimatedRowSize,
@@ -162,10 +164,10 @@ public EsQueryExec(
162164
this.indexMode = indexMode;
163165
this.indexNameWithModes = indexNameWithModes;
164166
this.attrs = attrs;
165-
this.query = query;
166167
this.limit = limit;
167168
this.sorts = sorts;
168169
this.estimatedRowSize = estimatedRowSize;
170+
// cannot keep the ctor with QueryBuilder as it has the same number of arguments as this ctor, EsqlNodeSubclassTests will fail
169171
this.queryBuilderAndTags = queryBuilderAndTags;
170172
}
171173

@@ -193,7 +195,17 @@ private static EsQueryExec readFrom(StreamInput in) throws IOException {
193195
var rowSize = in.readOptionalVInt();
194196
// Ignore sorts from the old serialization format
195197
// queryBuilderAndTags is built on data node, serialization is not needed
196-
return new EsQueryExec(source, indexPattern, indexMode, indexNameWithModes, attrs, query, limit, NO_SORTS, rowSize, null);
198+
return new EsQueryExec(
199+
source,
200+
indexPattern,
201+
indexMode,
202+
indexNameWithModes,
203+
attrs,
204+
limit,
205+
NO_SORTS,
206+
rowSize,
207+
List.of(new QueryBuilderAndTags(query, List.of()))
208+
);
197209
}
198210

199211
private static Sort readSort(StreamInput in) throws IOException {
@@ -215,7 +227,7 @@ public void writeTo(StreamOutput out) throws IOException {
215227
}
216228
EsRelation.writeIndexMode(out, indexMode());
217229
out.writeNamedWriteableCollection(output());
218-
out.writeOptionalNamedWriteable(query());
230+
out.writeOptionalNamedWriteable(query()); // TODO Do we ever serialize EsQueryExec or query() ?
219231
out.writeOptionalNamedWriteable(limit());
220232
out.writeOptionalCollection(NO_SORTS, EsQueryExec::writeSort);
221233
out.writeOptionalVInt(estimatedRowSize());
@@ -249,7 +261,6 @@ protected NodeInfo<EsQueryExec> info() {
249261
indexMode,
250262
indexNameWithModes,
251263
attrs,
252-
query,
253264
limit,
254265
sorts,
255266
estimatedRowSize,
@@ -269,8 +280,13 @@ public Map<String, IndexMode> indexNameWithModes() {
269280
return indexNameWithModes;
270281
}
271282

283+
/**
284+
* query is merged into queryBuilderAndTags, keep this method as it is called by too many places.
285+
* If this method is called, the caller looks for the original queryBuilder, before {@code ReplaceRoundToWithQueryAndTags} converts it
286+
* to multiple queries with tags.
287+
*/
272288
public QueryBuilder query() {
273-
return query;
289+
return queryWithoutTag();
274290
}
275291

276292
@Override
@@ -312,7 +328,7 @@ public PhysicalPlan estimateRowSize(State state) {
312328
}
313329
return Objects.equals(this.estimatedRowSize, size)
314330
? this
315-
: new EsQueryExec(source(), indexPattern, indexMode, indexNameWithModes, attrs, query, limit, sorts, size, queryBuilderAndTags);
331+
: new EsQueryExec(source(), indexPattern, indexMode, indexNameWithModes, attrs, limit, sorts, size, queryBuilderAndTags);
316332
}
317333

318334
public EsQueryExec withLimit(Expression limit) {
@@ -324,7 +340,6 @@ public EsQueryExec withLimit(Expression limit) {
324340
indexMode,
325341
indexNameWithModes,
326342
attrs,
327-
query,
328343
limit,
329344
sorts,
330345
estimatedRowSize,
@@ -349,43 +364,73 @@ public EsQueryExec withSorts(List<Sort> sorts) {
349364
indexMode,
350365
indexNameWithModes,
351366
attrs,
352-
query,
353367
limit,
354368
sorts,
355369
estimatedRowSize,
356370
queryBuilderAndTags
357371
);
358372
}
359373

374+
/**
375+
* query is merged into queryBuilderAndTags, keep this method as it is called by too many places.
376+
* If this method is called, the caller looks for the original queryBuilder, before {@code ReplaceRoundToWithQueryAndTags} converts it
377+
* to multiple queries with tags.
378+
*/
360379
public EsQueryExec withQuery(QueryBuilder query) {
361-
return Objects.equals(this.query, query)
380+
QueryBuilder thisQuery = queryWithoutTag();
381+
return Objects.equals(thisQuery, query)
362382
? this
363383
: new EsQueryExec(
364384
source(),
365385
indexPattern,
366386
indexMode,
367387
indexNameWithModes,
368388
attrs,
369-
query,
370389
limit,
371390
sorts,
372391
estimatedRowSize,
373-
queryBuilderAndTags
392+
List.of(new QueryBuilderAndTags(query, List.of()))
374393
);
375394
}
376395

377396
public List<QueryBuilderAndTags> queryBuilderAndTags() {
378397
return queryBuilderAndTags;
379398
}
380399

400+
/**
401+
* Returns the original queryBuilder before {@code ReplaceRoundToWithQueryAndTags} converts it to multiple queryBuilder with tags.
402+
* If we reach here, the caller is looking for the original query before the rule converts it. If there are multiple queries in
403+
* queryBuilderAndTags or if the single query in queryBuilderAndTags already has a tag, that means
404+
* {@code ReplaceRoundToWithQueryAndTags} already applied to the original query, the original query cannot be retrieved any more,
405+
* exception will be thrown.
406+
*/
407+
private QueryBuilder queryWithoutTag() {
408+
QueryBuilder queryWithoutTag;
409+
if (queryBuilderAndTags == null || queryBuilderAndTags.isEmpty()) {
410+
return null;
411+
} else if (queryBuilderAndTags.size() == 1) {
412+
QueryBuilderAndTags firstQuery = this.queryBuilderAndTags.get(0);
413+
if (firstQuery.tags().isEmpty()) {
414+
queryWithoutTag = firstQuery.query();
415+
} else {
416+
throw new UnsupportedOperationException("query is converted to query with tags: " + "[" + firstQuery + "]");
417+
}
418+
} else {
419+
throw new UnsupportedOperationException(
420+
"query is converted to multiple queries and tags: " + "[" + this.queryBuilderAndTags + "]"
421+
);
422+
}
423+
return queryWithoutTag;
424+
}
425+
381426
public boolean canSubstituteRoundToWithQueryBuilderAndTags() {
382427
// TimeSeriesSourceOperator and LuceneTopNSourceOperator do not support QueryAndTags
383428
return indexMode != IndexMode.TIME_SERIES && (sorts == null || sorts.isEmpty());
384429
}
385430

386431
@Override
387432
public int hashCode() {
388-
return Objects.hash(indexPattern, indexMode, indexNameWithModes, attrs, query, limit, sorts, queryBuilderAndTags);
433+
return Objects.hash(indexPattern, indexMode, indexNameWithModes, attrs, limit, sorts, queryBuilderAndTags);
389434
}
390435

391436
@Override
@@ -403,7 +448,6 @@ public boolean equals(Object obj) {
403448
&& Objects.equals(indexMode, other.indexMode)
404449
&& Objects.equals(indexNameWithModes, other.indexNameWithModes)
405450
&& Objects.equals(attrs, other.attrs)
406-
&& Objects.equals(query, other.query)
407451
&& Objects.equals(limit, other.limit)
408452
&& Objects.equals(sorts, other.sorts)
409453
&& Objects.equals(estimatedRowSize, other.estimatedRowSize)
@@ -419,9 +463,6 @@ public String nodeString() {
419463
+ "indexMode["
420464
+ indexMode
421465
+ "], "
422-
+ "query["
423-
+ (query != null ? Strings.toString(query, false, true) : "")
424-
+ "]"
425466
+ NodeUtils.limitedToString(attrs)
426467
+ ", limit["
427468
+ (limit != null ? limit.toString() : "")
@@ -430,7 +471,7 @@ public String nodeString() {
430471
+ "] estimatedRowSize["
431472
+ estimatedRowSize
432473
+ "] queryBuilderAndTags ["
433-
+ (queryBuilderAndTags != null ? queryBuilderAndTags.toString() : "")
474+
+ (queryBuilderAndTags != null ? queryBuilderAndTags : "")
434475
+ "]";
435476
}
436477

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsPhysicalOperationProviders.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,8 @@ public final PhysicalOperation sourcePhysicalOperation(EsQueryExec esQueryExec,
289289
for (Sort sort : sorts) {
290290
sortBuilders.add(sort.sortBuilder());
291291
}
292-
// LuceneTopNSourceOperator does not support QueryAndTags
292+
// LuceneTopNSourceOperator does not support QueryAndTags, check if there are multiple queries or if the single query has tags,
293+
// UnsupportedOperationException will be thrown
293294
luceneFactory = new LuceneTopNSourceOperator.Factory(
294295
shardContexts,
295296
querySupplier(esQueryExec.query()),
@@ -303,9 +304,7 @@ public final PhysicalOperation sourcePhysicalOperation(EsQueryExec esQueryExec,
303304
} else {
304305
luceneFactory = new LuceneSourceOperator.Factory(
305306
shardContexts,
306-
esQueryExec.queryBuilderAndTags() != null && esQueryExec.queryBuilderAndTags().isEmpty() == false
307-
? querySupplier(esQueryExec.queryBuilderAndTags())
308-
: querySupplier(esQueryExec.query()),
307+
querySupplier(esQueryExec.queryBuilderAndTags()),
309308
context.queryPragmas().dataPartitioning(physicalSettings.defaultDataPartitioning()),
310309
context.queryPragmas().taskConcurrency(),
311310
context.pageSize(rowEstimatedSize),

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/physical/local/PushTopNToSourceTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,7 @@ public TestPhysicalPlanBuilder limit(int limit) {
582582

583583
public TopNExec build() {
584584
List<Attribute> attributes = new ArrayList<>(fields.values());
585-
PhysicalPlan child = new EsQueryExec(Source.EMPTY, this.index, indexMode, Map.of(), attributes, null, null, List.of(), 0);
585+
PhysicalPlan child = new EsQueryExec(Source.EMPTY, this.index, indexMode, Map.of(), attributes, null, List.of(), 0, null);
586586
if (aliases.isEmpty() == false) {
587587
child = new EvalExec(Source.EMPTY, child, aliases);
588588
}

0 commit comments

Comments
 (0)