Skip to content

Commit 13f3864

Browse files
authored
Fix KQL usage in in STATS .. BY (#128371)
1 parent a78f1f0 commit 13f3864

File tree

7 files changed

+137
-4
lines changed

7 files changed

+137
-4
lines changed

x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneQueryEvaluator.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,10 @@ private class ShardState {
184184
private final List<SegmentState> perSegmentState;
185185

186186
ShardState(ShardConfig config) throws IOException {
187-
weight = config.searcher.createWeight(config.query, scoreMode(), 1.0f);
187+
// At this point, only the QueryBuilder has been rewritten into the query, but not the query itself.
188+
// The query needs to be rewritten before creating the Weight so it can be transformed into the final Query to execute.
189+
Query rewritten = config.searcher.rewrite(config.query);
190+
weight = config.searcher.createWeight(rewritten, scoreMode(), 1.0f);
188191
searcher = config.searcher;
189192
perSegmentState = new ArrayList<>(Collections.nCopies(searcher.getLeafContexts().size(), null));
190193
}

x-pack/plugin/esql/qa/testFixtures/src/main/resources/kql-function.csv-spec

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,3 +268,22 @@ r:double | author: text
268268
4.670000076293945 | Walter Scheps
269269
4.559999942779541 | J.R.R. Tolkien
270270
;
271+
272+
testKqlInStatsWithGroupingBy
273+
required_capability: kql_function
274+
required_capability: lucene_query_evaluator_query_rewrite
275+
FROM airports
276+
| STATS c = COUNT(*) where kql("country: United States") BY scalerank
277+
| SORT scalerank desc
278+
;
279+
280+
c: long | scalerank: long
281+
0 | 9
282+
44 | 8
283+
10 | 7
284+
28 | 6
285+
10 | 5
286+
12 | 4
287+
10 | 3
288+
15 | 2
289+
;

x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-function.csv-spec

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,3 +841,22 @@ r:double | author: text
841841
4.670000076293945 | Walter Scheps
842842
4.559999942779541 | J.R.R. Tolkien
843843
;
844+
845+
testMatchInStatsWithGroupingBy
846+
required_capability: match_function
847+
required_capability: full_text_functions_in_stats_where
848+
FROM airports
849+
| STATS c = COUNT(*) where match(country, "United States") BY scalerank
850+
| SORT scalerank desc
851+
;
852+
853+
c: long | scalerank: long
854+
0 | 9
855+
44 | 8
856+
10 | 7
857+
28 | 6
858+
10 | 5
859+
12 | 4
860+
10 | 3
861+
15 | 2
862+
;

x-pack/plugin/esql/qa/testFixtures/src/main/resources/qstr-function.csv-spec

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,3 +299,22 @@ r:double | author: text
299299
4.670000076293945 | Walter Scheps
300300
4.559999942779541 | J.R.R. Tolkien
301301
;
302+
303+
testQstrInStatsWithGroupingBy
304+
required_capability: qstr_function
305+
required_capability: full_text_functions_in_stats_where
306+
FROM airports
307+
| STATS c = COUNT(*) where qstr("country: \"United States\"") BY scalerank
308+
| SORT scalerank desc
309+
;
310+
311+
c: long | scalerank: long
312+
0 | 9
313+
44 | 8
314+
10 | 7
315+
28 | 6
316+
10 | 5
317+
12 | 4
318+
10 | 3
319+
15 | 2
320+
;

x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/KqlFunctionIT.java

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

88
package org.elasticsearch.xpack.esql.plugin;
99

10+
import org.elasticsearch.ElasticsearchException;
1011
import org.elasticsearch.action.index.IndexRequest;
1112
import org.elasticsearch.action.support.WriteRequest;
1213
import org.elasticsearch.common.settings.Settings;
@@ -16,12 +17,14 @@
1617
import org.elasticsearch.xpack.esql.VerificationException;
1718
import org.elasticsearch.xpack.esql.action.AbstractEsqlIntegTestCase;
1819
import org.elasticsearch.xpack.kql.KqlPlugin;
20+
import org.hamcrest.Matchers;
1921
import org.junit.Before;
2022

2123
import java.util.Collection;
2224
import java.util.List;
2325

2426
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
27+
import static org.elasticsearch.xpack.esql.EsqlTestUtils.getValuesList;
2528
import static org.hamcrest.CoreMatchers.containsString;
2629

2730
public class KqlFunctionIT extends AbstractEsqlIntegTestCase {
@@ -91,6 +94,42 @@ public void testInvalidKqlQueryLexicalError() {
9194
assertThat(error.getRootCause().getMessage(), containsString("line 1:1: extraneous input ':' "));
9295
}
9396

97+
public void testKqlhWithStats() {
98+
var errorQuery = """
99+
FROM test
100+
| STATS c = count(*) BY kql("content: fox")
101+
""";
102+
103+
var error = expectThrows(ElasticsearchException.class, () -> run(errorQuery));
104+
assertThat(error.getMessage(), containsString("[KQL] function is only supported in WHERE and STATS commands"));
105+
106+
var query = """
107+
FROM test
108+
| STATS c = count(*) WHERE kql("content: fox"), d = count(*) WHERE kql("content: dog")
109+
""";
110+
111+
try (var resp = run(query)) {
112+
assertColumnNames(resp.columns(), List.of("c", "d"));
113+
assertColumnTypes(resp.columns(), List.of("long", "long"));
114+
assertValues(resp.values(), List.of(List.of(4L, 4L)));
115+
}
116+
117+
query = """
118+
FROM test METADATA _score
119+
| WHERE kql("content: fox")
120+
| STATS m = max(_score), n = min(_score)
121+
""";
122+
123+
try (var resp = run(query)) {
124+
assertColumnNames(resp.columns(), List.of("m", "n"));
125+
assertColumnTypes(resp.columns(), List.of("double", "double"));
126+
List<List<Object>> valuesList = getValuesList(resp.values());
127+
assertEquals(1, valuesList.size());
128+
assertThat((double) valuesList.get(0).get(0), Matchers.lessThan(1.0));
129+
assertThat((double) valuesList.get(0).get(1), Matchers.greaterThan(0.0));
130+
}
131+
}
132+
94133
private void createAndPopulateIndex() {
95134
var indexName = "test";
96135
var client = client().admin().indices();

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,14 +1126,20 @@ public enum Cap {
11261126
LOOKUP_JOIN_ON_MIXED_NUMERIC_FIELDS,
11271127

11281128
/**
1129-
* Dense vector field type support
1129+
* {@link org.elasticsearch.compute.lucene.LuceneQueryEvaluator} rewrites the query before executing it in Lucene. This
1130+
* provides support for KQL in a STATS ... BY command that uses a KQL query for filter, for example.
11301131
*/
1131-
DENSE_VECTOR_FIELD_TYPE(EsqlCorePlugin.DENSE_VECTOR_FEATURE_FLAG),
1132+
LUCENE_QUERY_EVALUATOR_QUERY_REWRITE,
11321133

11331134
/**
11341135
* Support parameters for LiMIT command.
11351136
*/
1136-
PARAMETER_FOR_LIMIT;
1137+
PARAMETER_FOR_LIMIT,
1138+
1139+
/**
1140+
* Dense vector field type support
1141+
*/
1142+
DENSE_VECTOR_FIELD_TYPE(EsqlCorePlugin.DENSE_VECTOR_FEATURE_FLAG);
11371143

11381144
private final boolean enabled;
11391145

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
1111

1212
import org.apache.lucene.search.IndexSearcher;
13+
import org.apache.lucene.util.BytesRef;
1314
import org.elasticsearch.Build;
1415
import org.elasticsearch.common.network.NetworkAddress;
1516
import org.elasticsearch.common.settings.Settings;
@@ -2217,6 +2218,33 @@ public void testMatchFunctionStatisWithNonPushableCondition() {
22172218
assertNull(esQuery.query());
22182219
}
22192220

2221+
public void testMatchFunctionWithStatsBy() {
2222+
String query = """
2223+
from test
2224+
| stats count(*) where match(job_positions, "Data Scientist") by gender
2225+
""";
2226+
var analyzer = makeAnalyzer("mapping-default.json");
2227+
var plannerOptimizer = new TestPlannerOptimizer(config, analyzer);
2228+
var plan = plannerOptimizer.plan(query);
2229+
2230+
var limit = as(plan, LimitExec.class);
2231+
var agg = as(limit.child(), AggregateExec.class);
2232+
var grouping = as(agg.groupings().get(0), FieldAttribute.class);
2233+
assertEquals("gender", grouping.name());
2234+
var aggregateAlias = as(agg.aggregates().get(0), Alias.class);
2235+
assertEquals("count(*) where match(job_positions, \"Data Scientist\")", aggregateAlias.name());
2236+
var count = as(aggregateAlias.child(), Count.class);
2237+
var countFilter = as(count.filter(), Match.class);
2238+
assertEquals("Data Scientist", ((BytesRef) ((Literal) countFilter.query()).value()).utf8ToString());
2239+
var aggregateFieldAttr = as(agg.aggregates().get(1), FieldAttribute.class);
2240+
assertEquals("gender", aggregateFieldAttr.name());
2241+
var exchange = as(agg.child(), ExchangeExec.class);
2242+
var aggExec = as(exchange.child(), AggregateExec.class);
2243+
var fieldExtract = as(aggExec.child(), FieldExtractExec.class);
2244+
var esQuery = as(fieldExtract.child(), EsQueryExec.class);
2245+
assertNull(esQuery.query());
2246+
}
2247+
22202248
private QueryBuilder wrapWithSingleQuery(String query, QueryBuilder inner, String fieldName, Source source) {
22212249
return FilterTests.singleValueQuery(query, inner, fieldName, source);
22222250
}

0 commit comments

Comments
 (0)