Skip to content

Commit a87bd7a

Browse files
authored
ESQL - Allow full text functions disjunctions for non-full text functions (#120291)
1 parent f38c3e4 commit a87bd7a

File tree

25 files changed

+461
-164
lines changed

25 files changed

+461
-164
lines changed

docs/changelog/120291.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 120291
2+
summary: ESQL - Allow full text functions disjunctions for non-full text functions
3+
area: ES|QL
4+
type: feature
5+
issues: []

docs/reference/esql/esql-limitations.asciidoc

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ it is necessary to use the search function, like <<esql-match>>, in a <<esql-whe
112112
directly after the <<esql-from>> source command, or close enough to it.
113113
Otherwise, the query will fail with a validation error.
114114
Another limitation is that any <<esql-where>> command containing a full-text search function
115-
cannot also use disjunctions (`OR`) unless all functions used in the OR clauses are full-text functions themselves.
115+
cannot use disjunctions (`OR`), unless:
116+
117+
* All functions used in the OR clauses are full-text functions themselves, or scoring is not used
116118

117119
For example, this query is valid:
118120

@@ -131,19 +133,27 @@ FROM books
131133
| WHERE MATCH(author, "Faulkner")
132134
----
133135

134-
And this query will fail due to the disjunction:
136+
And this query that uses a disjunction will succeed:
135137

136138
[source,esql]
137139
----
138140
FROM books
141+
| WHERE MATCH(author, "Faulkner") OR QSTR("author: Hemingway")
142+
----
143+
144+
However using scoring will fail because it uses a non full text function as part of the disjunction:
145+
146+
[source,esql]
147+
----
148+
FROM books METADATA _score
139149
| WHERE MATCH(author, "Faulkner") OR author LIKE "Hemingway"
140150
----
141151

142-
However this query will succeed because it uses full text functions on both `OR` clauses:
152+
Scoring will work in the following query, as it uses full text functions on both `OR` clauses:
143153

144154
[source,esql]
145155
----
146-
FROM books
156+
FROM books METADATA _score
147157
| WHERE MATCH(author, "Faulkner") OR QSTR("author: Hemingway")
148158
----
149159

x-pack/plugin/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ tasks.named("yamlRestCompatTestTransform").configure({ task ->
101101
task.skipTest("esql/190_lookup_join/alias-repeated-index", "LOOKUP JOIN does not support index aliases for now")
102102
task.skipTest("esql/190_lookup_join/alias-pattern-multiple", "LOOKUP JOIN does not support index aliases for now")
103103
task.skipTest("esql/190_lookup_join/alias-pattern-single", "LOOKUP JOIN does not support index aliases for now")
104-
104+
task.skipTest("esql/180_match_operator/match with disjunctions", "Disjunctions in full text functions work now")
105105
})
106106

107107
tasks.named('yamlRestCompatTest').configure {

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.elasticsearch.compute.data.DocVector;
2626
import org.elasticsearch.compute.data.IntVector;
2727
import org.elasticsearch.compute.data.Page;
28+
import org.elasticsearch.compute.operator.DriverContext;
2829
import org.elasticsearch.compute.operator.EvalOperator;
2930
import org.elasticsearch.core.Releasable;
3031
import org.elasticsearch.core.Releasables;
@@ -44,19 +45,20 @@ public record ShardConfig(Query query, IndexSearcher searcher) {}
4445

4546
private final BlockFactory blockFactory;
4647
private final ShardConfig[] shards;
47-
private final int docChannel;
4848

4949
private ShardState[] perShardState = EMPTY_SHARD_STATES;
5050

51-
public LuceneQueryExpressionEvaluator(BlockFactory blockFactory, ShardConfig[] shards, int docChannel) {
51+
public LuceneQueryExpressionEvaluator(BlockFactory blockFactory, ShardConfig[] shards) {
5252
this.blockFactory = blockFactory;
5353
this.shards = shards;
54-
this.docChannel = docChannel;
5554
}
5655

5756
@Override
5857
public Block eval(Page page) {
59-
DocVector docs = page.<DocBlock>getBlock(docChannel).asVector();
58+
// Lucene based operators retrieve DocVectors as first block
59+
Block block = page.getBlock(0);
60+
assert block instanceof DocBlock : "LuceneQueryExpressionEvaluator expects DocBlock as input";
61+
DocVector docs = (DocVector) block.asVector();
6062
try {
6163
if (docs.singleSegmentNonDecreasing()) {
6264
return evalSingleSegmentNonDecreasing(docs).asBlock();
@@ -341,4 +343,17 @@ public void close() {
341343
Releasables.closeExpectNoException(builder);
342344
}
343345
}
346+
347+
public static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
348+
private final ShardConfig[] shardConfigs;
349+
350+
public Factory(ShardConfig[] shardConfigs) {
351+
this.shardConfigs = shardConfigs;
352+
}
353+
354+
@Override
355+
public EvalOperator.ExpressionEvaluator get(DriverContext context) {
356+
return new LuceneQueryExpressionEvaluator(context.blockFactory(), shardConfigs);
357+
}
358+
}
344359
}

x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/LuceneQueryExpressionEvaluatorTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,8 @@ private List<Page> runQuery(Set<String> values, Query query, boolean shuffleDocs
183183
);
184184
LuceneQueryExpressionEvaluator luceneQueryEvaluator = new LuceneQueryExpressionEvaluator(
185185
blockFactory,
186-
new LuceneQueryExpressionEvaluator.ShardConfig[] { shard },
187-
0
186+
new LuceneQueryExpressionEvaluator.ShardConfig[] { shard }
187+
188188
);
189189

190190
List<Operator> operators = new ArrayList<>();

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,40 @@ emp_no:integer | first_name:keyword | last_name:keyword
152152
10053 | Sanjiv | Zschoche
153153
10069 | Margareta | Bierman
154154
;
155+
156+
testKqlWithNonPushableDisjunctions
157+
required_capability: kql_function
158+
required_capability: full_text_functions_disjunctions_compute_engine
159+
160+
from books
161+
| where kql("title:lord") or length(title) > 130
162+
| keep book_no
163+
;
164+
ignoreOrder: true
165+
166+
book_no:keyword
167+
2675
168+
2714
169+
4023
170+
7140
171+
8678
172+
;
173+
174+
testKqlWithNonPushableDisjunctionsOnComplexExpressions
175+
required_capability: kql_function
176+
required_capability: full_text_functions_disjunctions_compute_engine
177+
178+
from books
179+
| where (kql("title:lord") and ratings > 4.5) or (kql("author:dostoevsky") and length(title) > 50)
180+
| keep book_no
181+
;
182+
ignoreOrder: true
183+
184+
book_no:keyword
185+
2675
186+
2924
187+
4023
188+
1937
189+
7140
190+
2714
191+
;

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,3 +718,40 @@ from books
718718
title:text
719719
The Hobbit or There and Back Again
720720
;
721+
722+
testMatchWithNonPushableDisjunctions
723+
required_capability: match_function
724+
required_capability: full_text_functions_disjunctions_compute_engine
725+
726+
from books
727+
| where match(title, "lord") or length(title) > 130
728+
| keep book_no
729+
;
730+
ignoreOrder: true
731+
732+
book_no:keyword
733+
2675
734+
2714
735+
4023
736+
7140
737+
8678
738+
;
739+
740+
testMatchWithNonPushableDisjunctionsOnComplexExpressions
741+
required_capability: match_function
742+
required_capability: full_text_functions_disjunctions_compute_engine
743+
744+
from books
745+
| where (match(title, "lord") and ratings > 4.5) or (match(author, "dostoevsky") and length(title) > 50)
746+
| keep book_no
747+
;
748+
ignoreOrder: true
749+
750+
book_no:keyword
751+
2675
752+
2924
753+
4023
754+
1937
755+
7140
756+
2714
757+
;

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,3 +684,40 @@ from semantic_text
684684
host:keyword | semantic_text_field:text
685685
"host1" | live long and prosper
686686
;
687+
688+
testMatchWithNonPushableDisjunctions
689+
required_capability: match_operator_colon
690+
required_capability: full_text_functions_disjunctions_compute_engine
691+
692+
from books
693+
| where title:"lord" or length(title) > 130
694+
| keep book_no
695+
;
696+
ignoreOrder: true
697+
698+
book_no:keyword
699+
2675
700+
2714
701+
4023
702+
7140
703+
8678
704+
;
705+
706+
testMatchWithNonPushableDisjunctionsOnComplexExpressions
707+
required_capability: match_operator_colon
708+
required_capability: full_text_functions_disjunctions_compute_engine
709+
710+
from books
711+
| where (title:"lord" and ratings > 4.5) or (author:"dostoevsky" and length(title) > 50)
712+
| keep book_no
713+
;
714+
ignoreOrder: true
715+
716+
book_no:keyword
717+
2675
718+
2924
719+
4023
720+
1937
721+
7140
722+
2714
723+
;

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,40 @@ emp_no:integer | first_name:keyword | last_name:keyword
151151
10053 | Sanjiv | Zschoche
152152
10069 | Margareta | Bierman
153153
;
154+
155+
testQstrWithNonPushableDisjunctions
156+
required_capability: qstr_function
157+
required_capability: full_text_functions_disjunctions_compute_engine
158+
159+
from books
160+
| where qstr("title:lord") or length(title) > 130
161+
| keep book_no
162+
;
163+
ignoreOrder: true
164+
165+
book_no:keyword
166+
2675
167+
2714
168+
4023
169+
7140
170+
8678
171+
;
172+
173+
testQstrWithNonPushableDisjunctionsOnComplexExpressions
174+
required_capability: qstr_function
175+
required_capability: full_text_functions_disjunctions_compute_engine
176+
177+
from books
178+
| where (qstr("title:lord") and ratings > 4.5) or (qstr("author:dostoevsky") and length(title) > 50)
179+
| keep book_no
180+
;
181+
ignoreOrder: true
182+
183+
book_no:keyword
184+
2675
185+
2924
186+
4023
187+
1937
188+
7140
189+
2714
190+
;

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

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
import org.elasticsearch.xpack.esql.VerificationException;
1515
import org.elasticsearch.xpack.esql.action.AbstractEsqlIntegTestCase;
1616
import org.elasticsearch.xpack.esql.action.EsqlCapabilities;
17-
import org.elasticsearch.xpack.esql.action.EsqlQueryRequest;
18-
import org.elasticsearch.xpack.esql.action.EsqlQueryResponse;
1917
import org.junit.Before;
2018

2119
import java.util.List;
@@ -31,12 +29,6 @@ public void setupIndex() {
3129
createAndPopulateIndex();
3230
}
3331

34-
@Override
35-
protected EsqlQueryResponse run(EsqlQueryRequest request) {
36-
assumeTrue("match function capability not available", EsqlCapabilities.Cap.MATCH_FUNCTION.isEnabled());
37-
return super.run(request);
38-
}
39-
4032
public void testSimpleWhereMatch() {
4133
var query = """
4234
FROM test
@@ -230,20 +222,19 @@ public void testWhereMatchAfterStats() {
230222
assertThat(error.getMessage(), containsString("Unknown column [content]"));
231223
}
232224

233-
public void testWhereMatchWithFunctions() {
225+
public void testWhereMatchNotPushedDown() {
234226
var query = """
235227
FROM test
236-
| WHERE match(content, "fox") OR to_upper(content) == "FOX"
228+
| WHERE match(content, "fox") OR length(content) < 20
229+
| KEEP id
230+
| SORT id
237231
""";
238-
var error = expectThrows(ElasticsearchException.class, () -> run(query));
239-
assertThat(
240-
error.getMessage(),
241-
containsString(
242-
"Invalid condition [match(content, \"fox\") OR to_upper(content) == \"FOX\"]. "
243-
+ "Full text functions can be used in an OR condition,"
244-
+ " but only if just full text functions are used in the OR condition"
245-
)
246-
);
232+
233+
try (var resp = run(query)) {
234+
assertColumnNames(resp.columns(), List.of("id"));
235+
assertColumnTypes(resp.columns(), List.of("integer"));
236+
assertValues(resp.values(), List.of(List.of(1), List.of(2), List.of(6)));
237+
}
247238
}
248239

249240
public void testWhereMatchWithRow() {

0 commit comments

Comments
 (0)