Skip to content

Commit a0e5e34

Browse files
julian-elasticfzowl
authored andcommitted
ESQL: Add support for Full Text Functions and Lucene Pushable Predicates for Lookup Join (elastic#136104)
• Added Full Text Functions Support in Lookup Join Expressions: Extended lookup join functionality to support full-text functions like MATCH() as part of join conditions, enabling text search capabilities within lookup operations. • Enhanced Lucene Pushable Filters Support. Implemented support for any Lucene pushable filters that contain only fields from the right side of the lookup join, allowing for more complex filtering conditions. At least one condition relating the left and right side of the Lookup Join is still required. This commit may include content that was generated or assisted by GenAI tools Cursor, Gemini CLI and/or Github Copilot
1 parent 2dd184b commit a0e5e34

File tree

28 files changed

+1550
-219
lines changed

28 files changed

+1550
-219
lines changed

docs/changelog/136104.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 136104
2+
summary: Add support for Full Text Functions and Lucene pushable conditions on fields from the Lookup Index for Lookup Join
3+
area: ES|QL
4+
type: enhancement
5+
issues: [ ]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
9201000
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
inference_cached_tokens,9200000
1+
esql_lookup_join_full_text_function,9201000

test/external-modules/esql-heap-attack/src/javaRestTest/java/org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -861,11 +861,22 @@ private Map<String, Object> lookupExplosion(
861861
}
862862
}
863863
if (lookupEntries != lookupEntriesToKeep) {
864-
// add a filter to reduce the number of matches
865-
// we add both a Lucene pushable filter and a non-pushable filter
866-
// this is to make sure that even if there are non-pushable filters the pushable filters is still applied
867-
query.append(" | WHERE ABS(filter_key) > -1 AND filter_key < ").append(lookupEntriesToKeep);
868-
864+
boolean applyAsExpressionJoinFilter = expressionBasedJoin && randomBoolean();
865+
// we randomly add the filter after the join or as part of the join
866+
// in both cases we should have the same amount of results
867+
if (applyAsExpressionJoinFilter == false) {
868+
// add a filter after the join to reduce the number of matches
869+
// we add both a Lucene pushable filter and a non-pushable filter
870+
// this is to make sure that even if there are non-pushable filters the pushable filters is still applied
871+
query.append(" | WHERE ABS(filter_key) > -1 AND filter_key < ").append(lookupEntriesToKeep);
872+
} else {
873+
// apply the filter as part of the join
874+
// then we filter out the rows that do not match the filter after
875+
// so the number of rows is the same as in the field based join case
876+
// and can get the same number of rows for verification purposes
877+
query.append(" AND filter_key < ").append(lookupEntriesToKeep);
878+
query.append(" | WHERE filter_key IS NOT NULL ");
879+
}
869880
}
870881
query.append(" | STATS COUNT(location) | LIMIT 100\"}");
871882
return responseAsMap(query(query.toString(), null));

x-pack/plugin/esql/qa/security/src/javaRestTest/java/org/elasticsearch/xpack/esql/EsqlSecurityIT.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -828,7 +828,7 @@ private void testLookupJoinFieldLevelSecurityHelper(boolean useExpressionJoin) t
828828
ResponseException error = expectThrows(ResponseException.class, () -> runESQLCommand("fls_user4_1", query));
829829
assertThat(error.getResponse().getStatusLine().getStatusCode(), equalTo(HttpStatus.SC_BAD_REQUEST));
830830
if (useExpressionJoin) {
831-
assertThat(error.getMessage(), containsString("Unsupported join filter expression:value_left == value"));
831+
assertThat(error.getMessage(), containsString("Unknown column [value], did you mean [value_left]?"));
832832
} else {
833833
assertThat(error.getMessage(), containsString("Unknown column [value] in right side of join"));
834834
}
@@ -902,7 +902,7 @@ private void testLookupJoinFieldLevelSecurityOnAliasHelper(boolean useExpression
902902
ResponseException error = expectThrows(ResponseException.class, () -> runESQLCommand("fls_user4_1_alias", query));
903903
assertThat(error.getResponse().getStatusLine().getStatusCode(), equalTo(HttpStatus.SC_BAD_REQUEST));
904904
if (useExpressionJoin) {
905-
assertThat(error.getMessage(), containsString("Unsupported join filter expression:value_left == value"));
905+
assertThat(error.getMessage(), containsString("Unknown column [value], did you mean [value_left]?"));
906906
} else {
907907
assertThat(error.getMessage(), containsString("Unknown column [value] in right side of join"));
908908
}

x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,9 @@ public class CsvTestsDataLoader {
149149
private static final TestDataset DATE_NANOS_UNION_TYPES = new TestDataset("date_nanos_union_types");
150150
private static final TestDataset COUNTRIES_BBOX = new TestDataset("countries_bbox");
151151
private static final TestDataset COUNTRIES_BBOX_WEB = new TestDataset("countries_bbox_web");
152-
private static final TestDataset AIRPORT_CITY_BOUNDARIES = new TestDataset("airport_city_boundaries");
152+
private static final TestDataset AIRPORT_CITY_BOUNDARIES = new TestDataset("airport_city_boundaries").withSetting(
153+
"lookup-settings.json"
154+
);
153155
private static final TestDataset CARTESIAN_MULTIPOLYGONS = new TestDataset("cartesian_multipolygons");
154156
private static final TestDataset CARTESIAN_MULTIPOLYGONS_NO_DOC_VALUES = new TestDataset("cartesian_multipolygons_no_doc_values")
155157
.withData("cartesian_multipolygons.csv");

x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@
6262
import org.elasticsearch.xcontent.XContentType;
6363
import org.elasticsearch.xcontent.json.JsonXContent;
6464
import org.elasticsearch.xpack.esql.action.EsqlQueryResponse;
65-
import org.elasticsearch.xpack.esql.analysis.AnalyzerContext;
6665
import org.elasticsearch.xpack.esql.analysis.AnalyzerSettings;
6766
import org.elasticsearch.xpack.esql.analysis.EnrichResolution;
67+
import org.elasticsearch.xpack.esql.analysis.MutableAnalyzerContext;
6868
import org.elasticsearch.xpack.esql.analysis.Verifier;
6969
import org.elasticsearch.xpack.esql.core.expression.Alias;
7070
import org.elasticsearch.xpack.esql.core.expression.Attribute;
@@ -449,7 +449,7 @@ public static TransportVersion randomMinimumVersion() {
449449
}
450450

451451
// TODO: make this even simpler, remove the enrichResolution for tests that do not require it (most tests)
452-
public static AnalyzerContext testAnalyzerContext(
452+
public static MutableAnalyzerContext testAnalyzerContext(
453453
Configuration configuration,
454454
EsqlFunctionRegistry functionRegistry,
455455
Map<IndexPattern, IndexResolution> indexResolutions,
@@ -462,15 +462,15 @@ public static AnalyzerContext testAnalyzerContext(
462462
/**
463463
* Analyzer context for a random (but compatible) minimum transport version.
464464
*/
465-
public static AnalyzerContext testAnalyzerContext(
465+
public static MutableAnalyzerContext testAnalyzerContext(
466466
Configuration configuration,
467467
EsqlFunctionRegistry functionRegistry,
468468
Map<IndexPattern, IndexResolution> indexResolutions,
469469
Map<String, IndexResolution> lookupResolution,
470470
EnrichResolution enrichResolution,
471471
InferenceResolution inferenceResolution
472472
) {
473-
return new AnalyzerContext(
473+
return new MutableAnalyzerContext(
474474
configuration,
475475
functionRegistry,
476476
indexResolutions,
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.esql.analysis;
9+
10+
import org.elasticsearch.TransportVersion;
11+
import org.elasticsearch.test.ESTestCase;
12+
import org.elasticsearch.test.TransportVersionUtils;
13+
import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry;
14+
import org.elasticsearch.xpack.esql.index.IndexResolution;
15+
import org.elasticsearch.xpack.esql.inference.InferenceResolution;
16+
import org.elasticsearch.xpack.esql.plan.IndexPattern;
17+
import org.elasticsearch.xpack.esql.session.Configuration;
18+
19+
import java.util.Map;
20+
21+
/**
22+
* A mutable version of AnalyzerContext that allows temporarily changing the transport version.
23+
* This is useful for testing scenarios where different transport versions need to be tested.
24+
*/
25+
public class MutableAnalyzerContext extends AnalyzerContext {
26+
private TransportVersion currentVersion;
27+
28+
public MutableAnalyzerContext(
29+
Configuration configuration,
30+
EsqlFunctionRegistry functionRegistry,
31+
Map<IndexPattern, IndexResolution> indexResolution,
32+
Map<String, IndexResolution> lookupResolution,
33+
EnrichResolution enrichResolution,
34+
InferenceResolution inferenceResolution,
35+
TransportVersion minimumVersion
36+
) {
37+
super(configuration, functionRegistry, indexResolution, lookupResolution, enrichResolution, inferenceResolution, minimumVersion);
38+
this.currentVersion = minimumVersion;
39+
}
40+
41+
@Override
42+
public TransportVersion minimumVersion() {
43+
return currentVersion;
44+
}
45+
46+
/**
47+
* Temporarily set the transport version to a random version between the passed-in version and the latest,
48+
* and return an AutoCloseable to restore it.
49+
* Usage:
50+
* try (var restore = context.setTemporaryTransportVersionOnOrAfter(minVersion)) {...}
51+
*/
52+
public RestoreTransportVersion setTemporaryTransportVersionOnOrAfter(TransportVersion minVersion) {
53+
TransportVersion oldVersion = this.currentVersion;
54+
// Set to a random version between minVersion and current
55+
this.currentVersion = TransportVersionUtils.randomVersionBetween(ESTestCase.random(), minVersion, TransportVersion.current());
56+
return new RestoreTransportVersion(oldVersion);
57+
}
58+
59+
/**
60+
* AutoCloseable that restores the original transport version when closed.
61+
*/
62+
public class RestoreTransportVersion implements AutoCloseable {
63+
private final TransportVersion originalVersion;
64+
65+
private RestoreTransportVersion(TransportVersion originalVersion) {
66+
this.originalVersion = originalVersion;
67+
}
68+
69+
@Override
70+
public void close() {
71+
MutableAnalyzerContext.this.currentVersion = originalVersion;
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)