Skip to content

Commit 4a5f256

Browse files
Generic Filter in Lookup Join POC
1 parent afd3a42 commit 4a5f256

File tree

17 files changed

+2353
-71
lines changed

17 files changed

+2353
-71
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
9214000
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
batched_response_might_include_reduction_failure,9213000
1+
esql_lookup_join_general_expression,9214000

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ public static void loadDataSetIntoEs(
420420
);
421421
}
422422

423-
private static void loadDataSetIntoEs(
423+
public static void loadDataSetIntoEs(
424424
RestClient client,
425425
boolean supportsIndexModeLookup,
426426
boolean supportsSourceFieldMapping,
@@ -664,7 +664,7 @@ record ColumnHeader(String name, String type) {}
664664
* - multi-values are comma separated
665665
* - commas inside multivalue fields can be escaped with \ (backslash) character
666666
*/
667-
private static void loadCsvData(RestClient client, String indexName, URL resource, boolean allowSubFields, Logger logger)
667+
public static void loadCsvData(RestClient client, String indexName, URL resource, boolean allowSubFields, Logger logger)
668668
throws IOException {
669669

670670
ArrayList<String> failures = new ArrayList<>();
@@ -997,7 +997,7 @@ private Settings readSettingsFile() throws IOException {
997997

998998
public record EnrichConfig(String policyName, String policyFileName) {}
999999

1000-
private interface IndexCreator {
1000+
public interface IndexCreator {
10011001
void createIndex(RestClient client, String indexName, String mapping, Settings indexSettings) throws IOException;
10021002
}
10031003
}

x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join-expression.csv-spec

Lines changed: 664 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 1010 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1427,6 +1427,11 @@ public enum Cap {
14271427
* Bugfix for lookup join with Full Text Function
14281428
*/
14291429
LOOKUP_JOIN_WITH_FULL_TEXT_FUNCTION_BUGFIX,
1430+
1431+
/**
1432+
* Lookup join with General Expression
1433+
*/
1434+
LOOKUP_JOIN_WITH_GENERAL_EXPRESSION,
14301435
/**
14311436
* FORK with remote indices
14321437
*/

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@
188188
import static org.elasticsearch.xpack.esql.core.type.DataType.UNSUPPORTED;
189189
import static org.elasticsearch.xpack.esql.core.type.DataType.VERSION;
190190
import static org.elasticsearch.xpack.esql.core.type.DataType.isTemporalAmount;
191+
import static org.elasticsearch.xpack.esql.parser.ParserUtils.source;
191192
import static org.elasticsearch.xpack.esql.telemetry.FeatureMetric.LIMIT;
192193
import static org.elasticsearch.xpack.esql.telemetry.FeatureMetric.STATS;
193194
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.maybeParseTemporalAmount;
@@ -228,6 +229,10 @@ public class Analyzer extends ParameterizedRuleExecutor<LogicalPlan, AnalyzerCon
228229
"esql_lookup_join_full_text_function"
229230
);
230231

232+
public static final TransportVersion ESQL_LOOKUP_JOIN_GENERAL_EXPRESSION = TransportVersion.fromName(
233+
"esql_lookup_join_general_expression"
234+
);
235+
231236
private final Verifier verifier;
232237

233238
public Analyzer(AnalyzerContext context, Verifier verifier) {
@@ -730,14 +735,17 @@ private Expression resolveJoinFiltersAndSwapIfNeeded(
730735
/**
731736
* This function resolves and orients a single join on condition.
732737
* We support AND of such conditions, here we handle a single child of the AND
733-
* We support the following 2 cases:
738+
* We support the following cases:
734739
* 1) Binary comparisons between a left and a right attribute.
735740
* We resolve all attributes and orient them so that the attribute on the left side of the join
736741
* is on the left side of the binary comparison
737742
* and the attribute from the lookup index is on the right side of the binary comparison
738743
* 2) A Lucene pushable expression containing only attributes from the lookup side of the join
739744
* We resolve all attributes in the expression, verify they are from the right side of the join
740745
* and also verify that the expression is potentially Lucene pushable
746+
* 3) General expressions (when all nodes support ESQL_LOOKUP_JOIN_GENERAL_EXPRESSION) that may reference
747+
* attributes from both sides. We extract all left-side attributes referenced in the expression
748+
* and add them to leftJoinKeysToPopulate to ensure they are sent to the lookup join.
741749
*/
742750
private Expression resolveAndOrientJoinCondition(
743751
Expression condition,
@@ -778,14 +786,35 @@ private Expression resolveAndOrientJoinCondition(
778786
+ condition.sourceText()
779787
);
780788
}
781-
return handleRightOnlyPushableFilter(condition, rightChildOutput);
789+
Expression result = handleRightOnlyPushableFilter(condition, rightChildOutput, context);
790+
// If general expressions are enabled and this is not an error, extract all left-side attributes
791+
// This ensures that fields like 'value' in ABS(value) > 15 are included in leftFields
792+
if (context.minimumVersion().onOrAfter(ESQL_LOOKUP_JOIN_GENERAL_EXPRESSION) && result instanceof UnresolvedAttribute == false) {
793+
// Extract all left-side attributes from the expression and add them to leftJoinKeysToPopulate
794+
// This handles general expressions that reference left-side fields (e.g., ABS(value) > 15)
795+
for (Attribute attr : condition.references()) {
796+
if (leftChildOutput.contains(attr)) {
797+
// Check if we've already added this attribute (by NameId to avoid duplicates)
798+
boolean alreadyAdded = leftJoinKeysToPopulate.stream().anyMatch(a -> a.id().equals(attr.id()));
799+
if (alreadyAdded == false) {
800+
leftJoinKeysToPopulate.add(attr);
801+
}
802+
}
803+
}
804+
}
805+
return result;
782806
}
783807

784-
private Expression handleRightOnlyPushableFilter(Expression condition, AttributeSet rightChildOutput) {
808+
private Expression handleRightOnlyPushableFilter(Expression condition, AttributeSet rightChildOutput, AnalyzerContext context) {
785809
if (isCompletelyRightSideAndTranslatable(condition, rightChildOutput)) {
786810
// The condition is completely on the right side and is translation aware, so it can be (potentially) pushed down
787811
return condition;
788812
} else {
813+
// Check if general expressions are enabled
814+
if (context.minimumVersion().onOrAfter(ESQL_LOOKUP_JOIN_GENERAL_EXPRESSION)) {
815+
// General expressions are enabled, allow the condition
816+
return condition;
817+
}
789818
// The condition cannot be used in the join on clause for now
790819
// It is not a binary comparison between left and right attributes
791820
// It is not using fields from the right side only and translation aware
@@ -801,7 +830,13 @@ private Join resolveLookupJoin(LookupJoin join, AnalyzerContext context) {
801830
JoinConfig config = join.config();
802831
// for now, support only (LEFT) USING clauses
803832
JoinType type = config.type();
804-
833+
if (context.minimumVersion().onOrAfter(ESQL_LOOKUP_JOIN_GENERAL_EXPRESSION) == false
834+
&& (join.config().leftFields().isEmpty() || join.config().rightFields().isEmpty())) {
835+
throw new ParsingException(
836+
Source.EMPTY,
837+
"JOIN ON clause with expressions must contain at least one condition relating the left index and the lookup index"
838+
);
839+
}
805840
// rewrite the join into an equi-join between the field with the same name between left and right
806841
if (type == JoinTypes.LEFT) {
807842
// the lookup cannot be resolved, bail out

0 commit comments

Comments
 (0)