88package org .elasticsearch .xpack .esql .optimizer .rules .physical .local ;
99
1010import org .elasticsearch .index .query .QueryBuilder ;
11+ import org .elasticsearch .xpack .esql .common .Failures ;
1112import org .elasticsearch .xpack .esql .core .expression .Alias ;
1213import org .elasticsearch .xpack .esql .core .expression .Attribute ;
1314import org .elasticsearch .xpack .esql .core .expression .AttributeMap ;
2122import org .elasticsearch .xpack .esql .core .expression .predicate .Range ;
2223import org .elasticsearch .xpack .esql .core .expression .predicate .logical .BinaryLogic ;
2324import org .elasticsearch .xpack .esql .core .expression .predicate .logical .Not ;
25+ import org .elasticsearch .xpack .esql .core .expression .predicate .logical .Or ;
2426import org .elasticsearch .xpack .esql .core .expression .predicate .nulls .IsNotNull ;
2527import org .elasticsearch .xpack .esql .core .expression .predicate .nulls .IsNull ;
2628import org .elasticsearch .xpack .esql .core .expression .predicate .operator .comparison .BinaryComparison ;
2931import org .elasticsearch .xpack .esql .core .querydsl .query .Query ;
3032import org .elasticsearch .xpack .esql .core .type .DataType ;
3133import org .elasticsearch .xpack .esql .core .util .CollectionUtils ;
34+ import org .elasticsearch .xpack .esql .core .util .Holder ;
3235import org .elasticsearch .xpack .esql .core .util .Queries ;
3336import org .elasticsearch .xpack .esql .expression .function .fulltext .FullTextFunction ;
3437import org .elasticsearch .xpack .esql .expression .function .scalar .ip .CIDRMatch ;
@@ -232,7 +235,9 @@ static boolean canPushToSource(Expression exp, LucenePushdownPredicates lucenePu
232235 } else if (exp instanceof InsensitiveBinaryComparison bc ) {
233236 return isAttributePushable (bc .left (), bc , lucenePushdownPredicates ) && bc .right ().foldable ();
234237 } else if (exp instanceof BinaryLogic bl ) {
235- return canPushToSource (bl .left (), lucenePushdownPredicates ) && canPushToSource (bl .right (), lucenePushdownPredicates );
238+ return canPushToSource (bl .left (), lucenePushdownPredicates )
239+ && canPushToSource (bl .right (), lucenePushdownPredicates )
240+ && checkPushableFullTextSearchFunctions (exp );
236241 } else if (exp instanceof In in ) {
237242 return isAttributePushable (in .value (), null , lucenePushdownPredicates ) && Expressions .foldable (in .list ());
238243 } else if (exp instanceof Not not ) {
@@ -252,8 +257,8 @@ static boolean canPushToSource(Expression exp, LucenePushdownPredicates lucenePu
252257 } else if (exp instanceof SpatialRelatesFunction spatial ) {
253258 return canPushSpatialFunctionToSource (spatial , lucenePushdownPredicates );
254259 } else if (exp instanceof FullTextFunction ) {
255- // TODO check for disjunctions
256- return false ;
260+ // In isolation, full text functions are pushable to source. We check if there are no disjunctions on the binary logic check
261+ return true ;
257262 }
258263 return false ;
259264 }
@@ -269,6 +274,49 @@ public static boolean canPushSpatialFunctionToSource(BinarySpatialFunction s, Lu
269274 || isPushableSpatialAttribute (s .right (), lucenePushdownPredicates ) && s .left ().foldable ();
270275 }
271276
277+ /**
278+ * Checks whether a condition contains a full text function that can't be pushed down to source.
279+ * Full text functions can be pushed down as long as they are the only functions in the expression, or there are no disjunctions with
280+ * other non-full text functions conditions.
281+ *
282+ * @param condition condition to check for disjunctions of full text searches
283+ */
284+ private static boolean checkPushableFullTextSearchFunctions (Expression condition ) {
285+ Holder <Boolean > isPushable = new Holder <>(true );
286+ condition .forEachDown (Or .class , or -> {
287+ if (isPushable .get () == false ) {
288+ // Exit early if we already have a failures
289+ return ;
290+ }
291+ boolean hasFullText = or .anyMatch (FullTextFunction .class ::isInstance );
292+ if (hasFullText ) {
293+ boolean hasOnlyFullText = onlyFullTextFunctionsInExpression (or );
294+ if (hasOnlyFullText == false ) {
295+ isPushable .set (false );
296+ }
297+ }
298+ });
299+ return isPushable .get ();
300+ }
301+
302+ /**
303+ * Checks whether an expression contains just full text functions or negations (NOT) and combinations (AND, OR) of full text functions
304+ *
305+ * @param expression expression to check
306+ * @return true if all children are full text functions or negations of full text functions, false otherwise
307+ */
308+ private static boolean onlyFullTextFunctionsInExpression (Expression expression ) {
309+ if (expression instanceof FullTextFunction ) {
310+ return true ;
311+ } else if (expression instanceof Not ) {
312+ return onlyFullTextFunctionsInExpression (expression .children ().get (0 ));
313+ } else if (expression instanceof BinaryLogic binaryLogic ) {
314+ return onlyFullTextFunctionsInExpression (binaryLogic .left ()) && onlyFullTextFunctionsInExpression (binaryLogic .right ());
315+ }
316+
317+ return false ;
318+ }
319+
272320 private static boolean isPushableSpatialAttribute (Expression exp , LucenePushdownPredicates p ) {
273321 return exp instanceof FieldAttribute fa && DataType .isSpatial (fa .dataType ()) && fa .getExactInfo ().hasExact () && p .isIndexed (fa );
274322 }
0 commit comments