ES|QL - Full text functions accept null as field parameter#137430
ES|QL - Full text functions accept null as field parameter#137430carlosdelest merged 30 commits intoelastic:mainfrom
Conversation
… and add the FoldNull rule to LocalLogicalPlanOptimizer
…down to an index that doesn't contain the field
|
Hi @carlosdelest, I've created a changelog YAML for you. |
…tions-accept-null' into bugfix/esql-full-text-functions-accept-null
…-functions-accept-null # Conflicts: # x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizer.java # x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java
| @@ -300,39 +286,7 @@ public final void writeTo(StreamOutput out) throws IOException { | |||
|
|
|||
| @Override | |||
| protected TypeResolution resolveParams() { | |||
| return resolveField().and(resolveQuery()) | |||
There was a problem hiding this comment.
Refactoring - all of these are moved up to SingleFieldFullTextFunction
| } | ||
|
|
||
| @Override | ||
| public boolean foldable() { |
There was a problem hiding this comment.
This is what changed on this PR besides all the refactoring. When the FTF field is null, the FTF is foldable to NULL, and nullable.
| new InferNonNullAggConstraint(), | ||
| new ReplaceDateTruncBucketWithRoundTo() | ||
| new ReplaceDateTruncBucketWithRoundTo(), | ||
| new FoldNull() |
There was a problem hiding this comment.
@alex-spies I added foldNull to local logical optimization, so full text functions can be folded to null when null has been replaced. Does it look OK to you?
There was a problem hiding this comment.
I think this should be beneficial, yes. Not sure why we didn't have it earlier.
Do you think you could please add one/some LocalLogicalPlanOptimizerTests with maybe some "easy" other function(s) (string or math?) to check that they locally now get folded too. This should add a tiny bit of performance to other functions too.
There was a problem hiding this comment.
the FoldNull rule is in the localOperators batch, that is applied after the localRewrites batch. Is it too late to wait for it to be executed in localOperators? I wonder why we need a duplicate of it here?
There was a problem hiding this comment.
Oh, I see - I didn't understand that this rule was already used as part of localOperators(). Removing it, thanks @fang-xing-esql !
| """); | ||
|
|
||
| // Limit[1000[INTEGER],false,false] | ||
| Limit limit = as(plan, Limit.class); |
There was a problem hiding this comment.
@fang-xing-esql , now FTFs can be pushed down - when a field does not exist in the index, it will become NULL instead.
Does this make sense from the union all perspective?
There was a problem hiding this comment.
I found a potential issue here and I'll provide a reproducer.
There was a problem hiding this comment.
The query in the test failed with VerificationException because the full text function does not apply to one of the subquery(branch), the field referenced in the full text function is not an index field, it doesn't throw VerificationException in this PR, however I run into this error when the query is executed. Shall we still throw VerificationException for this kind of situations? Or prune the entire branch when the field does not exist in that branch, a related comment is here.
+ curl -u elastic:password -X POST 'localhost:9200/_query?format=txt&pretty' -H 'Content-Type: application/json' '-d
{
"query": "from sample_data_1, (from books) metadata _score | WHERE author:\"Bradbury\" | sort _score desc, author"
}
'
{
"error" : {
"root_cause" : [
{
"type" : "illegal_argument_exception",
"reason" : "Unrecognized class type class org.elasticsearch.xpack.esql.core.expression.Literal"
}
],
"type" : "illegal_argument_exception",
"reason" : "Unrecognized class type class org.elasticsearch.xpack.esql.core.expression.Literal"
},
"status" : 400
}
The mapping and data a like below.
curl -u elastic:password -X POST "localhost:9200/books/_doc?pretty" -H 'Content-Type: application/json' -d'
{"name": "Snow Crash", "author": "Neal Stephenson", "release_date": "1992-06-01", "page_count": 470, "*name": "*Snow Crash"}
'
curl -u elastic:password -X POST "localhost:9200/_bulk?refresh&pretty" -H 'Content-Type: application/json' -d'
{ "index" : { "_index" : "books" } }
{"name": "Revelation Space", "author": "Alastair Reynolds", "release_date": "2000-03-15", "page_count": 585, "*name": "*Revelation Space"}
{ "index" : { "_index" : "books" } }
{"name": "1984", "author": "George Orwell", "release_date": "1985-06-01", "page_count": 328, "*name": "*1984"}
{ "index" : { "_index" : "books" } }
{"name": "Fahrenheit 451", "author": "Ray Bradbury", "release_date": "1953-10-15", "page_count": 227, "*name": "*Fahrenheit 451"}
{ "index" : { "_index" : "books" } }
{"name": "Brave New World", "author": "Aldous Huxley", "release_date": "1932-06-01", "page_count": 268, "*name": "*Brave New World"}
{ "index" : { "_index" : "books" } }
{"name": "The Handmaids Tale", "author": "Margaret Atwood", "release_date": "1985-06-01", "page_count": 311, "*name": "*The Handmaids Tale"}
'
curl -u elastic:password -X PUT "localhost:9200/sample_data_1?pretty" -H 'Content-Type: application/json' -d'
{
"settings" : {"mode" : "lookup"},
"mappings": {
"properties": {
"client.ip": {"type": "ip"},
"message": {"type": "text"},
"test_date": {"type": "date"},
"event.duration": {"type": "long"},
"mv": {"type": "long"}
}
}
}
'
curl -X PUT "localhost:9200/sample_data_1/_bulk?refresh&pretty" -H 'Content-Type: application/json' -d'
{"index": {}}
{"@timestamp": "2023-10-23T12:15:03.360Z", "client.ip": "172.21.2.162", "message": "Connected to 10.1.0.3", "event.duration": 3450233, "test_date": "1985-11-21T00:00:00Z", "mv": [1.0, 2.0]}
{"index": {}}
{"@timestamp": "2023-10-23T12:27:28.948Z", "client.ip": "172.21.2.113", "message": "Connected to 10.1.0.2", "event.duration": 2764889, "test_date": "1985-11-22T00:00:00Z", "mv": [1.0, 2.0]}
{"index": {}}
{"@timestamp": "2023-10-23T13:33:34.937Z", "client.ip": "172.21.0.5", "message": "Disconnected", "event.duration": 1232382, "test_date": "1985-11-23T00:00:00Z", "mv": [1.0, 2.0]}
{"index": {}}
{"@timestamp": "2023-10-23T13:51:54.732Z", "client.ip": "172.21.3.15", "message": "Connection error", "event.duration": 725448, "test_date": "1985-11-24T00:00:00Z", "mv": [1.0, 2.0]}
{"index": {}}
{"@timestamp": "2023-10-23T13:52:55.015Z", "client.ip": "172.21.3.15", "message": "Connection error", "event.duration": 8268153, "test_date": "1985-11-25T00:00:00Z", "mv": [1.0, 2.0]}
{"index": {}}
{"@timestamp": "2023-10-23T13:53:55.832Z", "client.ip": "172.21.3.15", "message": "Connection error", "event.duration": 5033755, "test_date": "1985-11-26T00:00:00Z", "mv": [1.0, 2.0]}
{"index": {}}
{"@timestamp": "2023-10-23T13:55:01.543Z", "client.ip": "172.21.3.15", "message": "Connected to 10.1.0.1", "event.duration": 1756467, "test_date": "1985-11-27T00:00:00Z", "mv": [1.0, 2.0]}
'
|
Pinging @elastic/es-analytical-engine (Team:Analytics) |
|
Pinging @elastic/es-search-relevance (Team:Search Relevance) |
|
Pinging @elastic/search-relevance (Team:Search - Relevance) |
Co-authored-by: Bogdan Pintea <sig11@mailbox.org>
Co-authored-by: Bogdan Pintea <sig11@mailbox.org>
Co-authored-by: Bogdan Pintea <sig11@mailbox.org>
…tions-accept-null' into bugfix/esql-full-text-functions-accept-null
fang-xing-esql
left a comment
There was a problem hiding this comment.
Thank you @carlosdelest, and for taking into account subqueries as well! Agreed with @bogdan, it will be great to add some additional tests(verify the logical plan and some CsvTests to validate query results) that have these full text functions with null as input field. If the full text functions are used in filters, the PruneFilters rule might be able to replace its children plans with a LocalRelation, that is a sign that the filter on full text function + null fields is applied successfully.
| @Override | ||
| public Object fold(FoldContext ctx) { | ||
| // We only fold when the field is null (it's not present in the mapping), so we return null | ||
| return Literal.NULL; |
There was a problem hiding this comment.
I wonder if we should just return a null here, instead of wrapping a Literal around it? A lot of implementations of fold return a plain null.
There was a problem hiding this comment.
Yes, this is what caused the error you mentioned 🤦
fang-xing-esql
left a comment
There was a problem hiding this comment.
Thank you @carlosdelest, LGTM! A couple of suggestions:
- I came across a similar issue that a field cannot be found on the data node, although it exists in the index mapping once before. It was because there are more than 10 indices on the cluster, and our batch execution model split them into multiple batches, by default 10 indices per batch. When the batch that does not have that index/field is executed,
ReplaceFieldWithConstantOrNullconverts the field tonull. I didn't spend much time to investigate how the original/reported issue related to this PR happened, however this is likely one of the causes, and it will be great that if we have a test that covers it, an example can be found here - the test creates more than 10 indices to trigger the batch executions, and it exercise the code path from a different perspective, it will be nice to have this coverage. subqueryis under snapshot, adding thetest-releaselabel help us ensure the change(relevant tosubquery) is not exposed to a release build.
| import static org.hamcrest.Matchers.startsWith; | ||
|
|
||
| //@TestLogging(value = "org.elasticsearch.xpack.esql:TRACE", reason = "debug") | ||
| @TestLogging(value = "org.elasticsearch.xpack.esql:TRACE", reason = "debug") |
There was a problem hiding this comment.
Don't forget to comment out the debug log before merging.
@fang-xing-esql ah interesting - did a test to exercise that in 7a6538d |
|
@fang-xing-esql I added the |
fang-xing-esql
left a comment
There was a problem hiding this comment.
Thanks for adding the test, @carlosdelest. That failed release test seems to be related to a BWC issue. As long as the release tests for full-text functions and subqueries are passing, we’re good. This also reminds me to double-check the BWC of the new test(Rest).
| assertThat(answer.get("values"), equalTo(List.of(List.of("#"), List.of("foo#bar")))); | ||
| } | ||
|
|
||
| public void testMatchFunctionAcrossMultipleIndicesWithMissingField() throws IOException { |
There was a problem hiding this comment.
Do we need a capability here? Will this test fail on older versions? It also reminds me whether this fix need to be backported to some prior versions.
There was a problem hiding this comment.
IIUC, RestEsqlSpecIT is not a bwc test - even for multi_node, it does not use previous ES versions. Is that correct?
Re backporting - this includes some refactoring for FTFs that I'd like to check to understand the effort needed before commiting.
@fang-xing-esql I believe this is not due to bwc for the feature. All tests are failing for the
That is not related to this change, but to a new data type being added to snapshots that is failing for release tests. |
fang-xing-esql
left a comment
There was a problem hiding this comment.
Thanks for double checking!
BASE=34e3417158280c446b132b753cf6894105f8ade7 HEAD=d42c91bbb6c4cc0ecdba0d2fa9fb08109546018a Branch=main
BASE=34e3417158280c446b132b753cf6894105f8ade7 HEAD=d42c91bbb6c4cc0ecdba0d2fa9fb08109546018a Branch=main
💚 All backports created successfully
Questions ?Please refer to the Backport tool documentation |
Closes #136608
Full text functions that take a field as a parameter should allow null as a field parameter. This is necessary as the field may not be present on an index mapping, and the local optimizer replaces it by null.
We allow null as a field in FullTextFunctions, meaning that they are nullable and will be replaced by
NULL.This PR also refactors full text functions that depend on a single field (
MATCH,MATCH_PHRASE,KNN) to use a common superclass (SingleFieldFullTextFunction) that contains the common field logic.