2525import org .elasticsearch .xpack .esql .capabilities .TranslationAware ;
2626import org .elasticsearch .xpack .esql .core .expression .Attribute ;
2727import org .elasticsearch .xpack .esql .core .expression .Expression ;
28+ import org .elasticsearch .xpack .esql .core .expression .NameId ;
2829import org .elasticsearch .xpack .esql .core .type .DataType ;
2930import org .elasticsearch .xpack .esql .expression .predicate .Predicates ;
3031import org .elasticsearch .xpack .esql .expression .predicate .operator .comparison .Equals ;
@@ -65,6 +66,7 @@ public class ExpressionQueryList implements LookupEnrichQueryGenerator, PostJoin
6566 private final SearchExecutionContext context ;
6667 private final AliasFilter aliasFilter ;
6768 private final LucenePushdownPredicates lucenePushdownPredicates ;
69+ private final Set <NameId > rightSideFieldNameIds ;
6870 private List <Expression > postJoinFilter ;
6971 private int inputPagePositionCount = -1 ;
7072
@@ -82,10 +84,23 @@ private ExpressionQueryList(
8284 SearchContextStats .from (List .of (context )),
8385 new EsqlFlags (clusterService .getClusterSettings ())
8486 );
87+ this .rightSideFieldNameIds = collectRightSideFieldNameIds (rightPreJoinPlan );
8588 postJoinFilter = new ArrayList <>();
8689 buildPreJoinFilter (rightPreJoinPlan , clusterService );
8790 }
8891
92+ private static Set <NameId > collectRightSideFieldNameIds (PhysicalPlan rightPreJoinPlan ) {
93+ Set <NameId > rightSideFieldNameIds = new HashSet <>();
94+ if (rightPreJoinPlan != null ) {
95+ rightPreJoinPlan .forEachDown (EsSourceExec .class , esSourceExec -> {
96+ for (Attribute attr : esSourceExec .output ()) {
97+ rightSideFieldNameIds .add (attr .id ());
98+ }
99+ });
100+ }
101+ return rightSideFieldNameIds ;
102+ }
103+
89104 /**
90105 * Creates a new {@link ExpressionQueryList} for a field-based join.
91106 * A field-based join is a join where the join conditions are based on the equality of fields from the left and right datasets.
@@ -155,17 +170,12 @@ private void buildJoinOnForExpressionJoin(
155170 this .inputPagePositionCount = inputPage .getPositionCount ();
156171 List <Expression > expressions = Predicates .splitAnd (joinOnConditions );
157172
158- // Build set of left-side field names for categorization
159- Set <String > leftSideFieldNames = new HashSet <>();
160- for (MatchConfig matchField : matchFields ) {
161- leftSideFieldNames .add (matchField .fieldName ());
162- }
163-
164173 // Split expressions into left-only, right-only, and mixed
174+ // Anything not in right-side fields is left-side
165175 List <Expression > leftOnlyExpressions = new ArrayList <>();
166176 List <Expression > rightOnlyExpressions = new ArrayList <>();
167177 List <Expression > mixedExpressions = new ArrayList <>();
168- splitExpressionsBySide (expressions , leftSideFieldNames , leftOnlyExpressions , rightOnlyExpressions , mixedExpressions );
178+ splitExpressionsBySide (expressions , rightSideFieldNameIds , leftOnlyExpressions , rightOnlyExpressions , mixedExpressions );
169179
170180 // Process mixed expressions - try as left-right binary comparison first
171181 // If that fails, add to post-join filter
@@ -191,7 +201,7 @@ private void buildJoinOnForExpressionJoin(
191201
192202 private void splitExpressionsBySide (
193203 List <Expression > expressions ,
194- Set <String > leftSideFieldNames ,
204+ Set <NameId > rightSideFieldNameIds ,
195205 List <Expression > leftOnlyExpressions ,
196206 List <Expression > rightOnlyExpressions ,
197207 List <Expression > mixedExpressions
@@ -204,10 +214,11 @@ private void splitExpressionsBySide(
204214 boolean hasRightSide = false ;
205215
206216 for (Attribute attr : allAttributes ) {
207- if (leftSideFieldNames .contains (attr .name ())) {
208- hasLeftSide = true ;
209- } else {
217+ NameId nameId = attr .id ();
218+ if (rightSideFieldNameIds .contains (nameId )) {
210219 hasRightSide = true ;
220+ } else {
221+ hasLeftSide = true ;
211222 }
212223 }
213224
@@ -222,19 +233,13 @@ private void splitExpressionsBySide(
222233 }
223234
224235 private boolean applyAsRightSidePushableFilter (Expression filter , List <MatchConfig > matchFields ) {
225- // First check if this filter only references right-side attributes
226- // Right-side attributes are those NOT in matchFields (which are left-side fields)
227- Set <String > leftSideFieldNames = new HashSet <>();
228- for (MatchConfig matchField : matchFields ) {
229- leftSideFieldNames .add (matchField .fieldName ());
230- }
231236 // Check if any attribute in the filter expression tree is from the left side
232237 // We need to traverse the entire expression tree, not just top-level references,
233238 // because some functions may have attributes nested in their children
234239 List <Attribute > allAttributes = new ArrayList <>();
235240 filter .forEachDown (Attribute .class , allAttributes ::add );
236241 for (Attribute attr : allAttributes ) {
237- if (leftSideFieldNames .contains (attr .name ())) {
242+ if (rightSideFieldNameIds .contains (attr .id ()) == false ) {
238243 // This filter references a left-side attribute, so it cannot be pushed to Lucene
239244 return false ;
240245 }
@@ -257,20 +262,58 @@ private boolean applyAsRightSidePushableFilter(Expression filter, List<MatchConf
257262 return false ;
258263 }
259264
265+ /**
266+ * Reorients a binary comparison so that the left side is from the input page and the right side is from the lookup index.
267+ * Returns the comparison as-is if already correctly oriented, or a swapped version if needed.
268+ * Returns null if both attributes are from the same side (can't be reoriented).
269+ */
270+ private static EsqlBinaryComparison reorientBinaryComparison (EsqlBinaryComparison binaryComparison , Set <NameId > rightSideFieldNameIds ) {
271+ if (binaryComparison .left () instanceof Attribute leftAttr && binaryComparison .right () instanceof Attribute rightAttr ) {
272+ // Determine which attribute is from the right side (lookup index)
273+ boolean leftIsRightSide = rightSideFieldNameIds .contains (leftAttr .id ());
274+ boolean rightIsRightSide = rightSideFieldNameIds .contains (rightAttr .id ());
275+
276+ // We need exactly one attribute from the right side and one from the left side
277+ if (leftIsRightSide == rightIsRightSide ) {
278+ // Both are from the same side, can't process as left-right comparison
279+ return null ;
280+ }
281+
282+ if (rightIsRightSide ) {
283+ // Original orientation is correct: left is from input, right is from lookup
284+ return binaryComparison ;
285+ } else {
286+ // Need to swap: original left is from lookup, original right is from input
287+ // Swap the comparison and flip the operator if needed
288+ return (EsqlBinaryComparison ) binaryComparison .swapLeftAndRight ();
289+ }
290+ }
291+ return null ;
292+ }
293+
260294 private boolean applyAsLeftRightBinaryComparison (
261295 Expression expr ,
262296 List <MatchConfig > matchFields ,
263297 Page inputPage ,
264298 ClusterService clusterService ,
265299 Warnings warnings
266300 ) {
267- if (expr instanceof EsqlBinaryComparison binaryComparison
268- && binaryComparison .left () instanceof Attribute leftAttribute
269- && binaryComparison .right () instanceof Attribute rightAttribute ) {
270- // the left side comes from the page that was sent to the lookup node
271- // the right side is the field from the lookup index
272- // check if the left side is in the matchFields
273- // if it is its corresponding page is the corresponding number in inputPage
301+ if (expr instanceof EsqlBinaryComparison binaryComparison ) {
302+ // Reorient the comparison so that left is from input page and right is from lookup index
303+ EsqlBinaryComparison orientedComparison = reorientBinaryComparison (binaryComparison , rightSideFieldNameIds );
304+ if (orientedComparison == null ) {
305+ // Can't reorient (both attributes from same side)
306+ return false ;
307+ }
308+
309+ // After reorientation, left is from input page and right is from lookup index
310+ Attribute leftAttribute = (Attribute ) orientedComparison .left ();
311+ Attribute rightAttribute = (Attribute ) orientedComparison .right ();
312+
313+ // The left side comes from the page that was sent to the lookup node
314+ // The right side is the field from the lookup index
315+ // Check if the left side is in the matchFields
316+ // If it is its corresponding page is the corresponding number in inputPage
274317 Block block = null ;
275318 DataType dataType = null ;
276319 for (int i = 0 ; i < matchFields .size (); i ++) {
@@ -285,7 +328,7 @@ private boolean applyAsLeftRightBinaryComparison(
285328 // special handle Equals operator
286329 // TermQuery is faster than BinaryComparisonQueryList, as it does less work per row
287330 // so here we reuse the existing logic from field based join to build a termQueryList for Equals
288- if (binaryComparison instanceof Equals ) {
331+ if (orientedComparison instanceof Equals ) {
289332 QueryList termQueryForEquals = termQueryList (rightFieldType , context , aliasFilter , block , dataType ).onlySingleValues (
290333 warnings ,
291334 "LOOKUP JOIN encountered multi-value"
@@ -297,7 +340,7 @@ private boolean applyAsLeftRightBinaryComparison(
297340 rightFieldType ,
298341 context ,
299342 block ,
300- binaryComparison ,
343+ orientedComparison ,
301344 clusterService ,
302345 aliasFilter ,
303346 warnings
0 commit comments