1010import org .apache .lucene .search .BooleanClause ;
1111import org .apache .lucene .search .BooleanQuery ;
1212import org .apache .lucene .search .Query ;
13+ import org .elasticsearch .cluster .service .ClusterService ;
1314import org .elasticsearch .compute .operator .lookup .LookupEnrichQueryGenerator ;
1415import org .elasticsearch .compute .operator .lookup .QueryList ;
15- import org .elasticsearch .index .query .QueryBuilder ;
1616import org .elasticsearch .index .query .SearchExecutionContext ;
17- import org .elasticsearch .xpack .esql .core .expression .Literal ;
17+ import org .elasticsearch .xpack .esql .capabilities .TranslationAware ;
18+ import org .elasticsearch .xpack .esql .optimizer .rules .physical .local .LucenePushdownPredicates ;
19+ import org .elasticsearch .xpack .esql .plan .physical .EsQueryExec ;
20+ import org .elasticsearch .xpack .esql .plan .physical .FilterExec ;
21+ import org .elasticsearch .xpack .esql .plan .physical .PhysicalPlan ;
22+ import org .elasticsearch .xpack .esql .plugin .EsqlFlags ;
23+ import org .elasticsearch .xpack .esql .stats .SearchContextStats ;
1824
1925import java .io .IOException ;
2026import java .util .List ;
2127
28+ import static org .elasticsearch .xpack .esql .planner .TranslatorHandler .TRANSLATOR_HANDLER ;
29+
2230/**
2331 * A {@link LookupEnrichQueryGenerator} that combines multiple {@link QueryList}s into a single query.
2432 * Each query in the resulting query will be a conjunction of all queries from the input lists at the same position.
2533 * In the future we can extend this to support more complex expressions, such as disjunctions or negations.
2634 */
27- public class ExpressionQueryList implements LookupEnrichQueryGenerator {
35+ public class ExpressionQueryList implements LookupEnrichQueryGenerator , PostJoinFilterable {
2836 private final List <QueryList > queryLists ;
29- private final QueryBuilder preJoinFilter ;
37+ private Query preJoinFilter ;
38+ private FilterExec postJoinFilter ;
3039 private final SearchExecutionContext context ;
3140
32- public ExpressionQueryList (List <QueryList > queryLists , SearchExecutionContext context , QueryBuilder preJoinFilter ) {
33- if (queryLists .size () < 2 && Literal .TRUE .equals (preJoinFilter )) {
41+ public ExpressionQueryList (
42+ List <QueryList > queryLists ,
43+ SearchExecutionContext context ,
44+ PhysicalPlan rightPreJoinPlan ,
45+ ClusterService clusterService
46+ ) {
47+ if (queryLists .size () < 2 && rightPreJoinPlan == null ) {
3448 throw new IllegalArgumentException ("ExpressionQueryList must have at least two QueryLists" );
3549 }
3650 this .queryLists = queryLists ;
37- this .preJoinFilter = preJoinFilter ;
3851 this .context = context ;
52+ buildPrePostJoinFilter (rightPreJoinPlan , clusterService );
53+ }
54+
55+ private void buildPrePostJoinFilter (PhysicalPlan rightPreJoinPlan , ClusterService clusterService ) {
56+ // we support a FilterExec as the pre-join filter
57+ // if we filter Exec is not translatable to a QueryBuilder, we will apply it after the join
58+ if (rightPreJoinPlan instanceof FilterExec filterExec ) {
59+ try {
60+ LucenePushdownPredicates lucenePushdownPredicates = LucenePushdownPredicates .from (
61+ SearchContextStats .from (List .of (context )),
62+ new EsqlFlags (clusterService .getClusterSettings ())
63+ );
64+ // If the pre-join filter is a FilterExec, we can convert it to a QueryBuilder
65+ // try to convert it to a QueryBuilder, if not possible apply it after the join
66+ if (filterExec instanceof TranslationAware translationAware ) {
67+ preJoinFilter = translationAware .asQuery (lucenePushdownPredicates , TRANSLATOR_HANDLER )
68+ .toQueryBuilder ()
69+ .toQuery (context );
70+ } else {
71+ // if the filter is not translatable, we will apply it after the join
72+ postJoinFilter = filterExec ;
73+ }
74+ } catch (IOException e ) {
75+ throw new IllegalArgumentException ("Failed to translate pre-join filter: " + filterExec , e );
76+ }
77+
78+ } else if (rightPreJoinPlan instanceof EsQueryExec ) {
79+ // we are good, nothing to do here
80+ } else {
81+ throw new IllegalArgumentException ("Unsupported pre-join filter type: " + preJoinFilter .getClass ().getName ());
82+ }
3983 }
4084
4185 @ Override
@@ -51,42 +95,12 @@ public Query getQuery(int position) throws IOException {
5195 builder .add (q , BooleanClause .Occur .FILTER );
5296 }
5397 // also attach the pre-join filter if it exists
54- /*if (Literal.TRUE.equals(preJoinFilter) == false) {
55- if (preJoinFilter instanceof TranslationAware translationAware) {
56- Query preJoinQuery = tryToGetAsLuceneQuery(translationAware);
57- if (preJoinQuery == null) {
58- preJoinQuery = tryToGetThroughQueryBuilder(translationAware);
59- }
60- if (preJoinQuery == null) {
61- throw new UnsupportedOperationException("Cannot translate pre-join filter to Lucene query: " + preJoinFilter);
62- }
63- builder.add(preJoinQuery, BooleanClause.Occur.FILTER);
64- }
65- }*/
6698 if (preJoinFilter != null ) {
67- // JULIAN TO DO: Can we precompile the query? I don't want to call toQuery for every row
68- builder .add (preJoinFilter .toQuery (context ), BooleanClause .Occur .FILTER );
99+ builder .add (preJoinFilter , BooleanClause .Occur .FILTER );
69100 }
70101 return builder .build ();
71102 }
72103
73- /*private Query tryToGetThroughQueryBuilder(TranslationAware translationAware) {
74- // it seems I might need to pass a QueryBuilder, instead of Expression directly????
75- // can a QueryBuilder support nested complex expressions with AND, OR, NOT?
76- return translationAware.asQuery(WHAT_GOES_HERE, WHAT_GOES_HERE).toQueryBuilder().toQuery(queryLists.get(0).searchExecutionContext);
77- }
78-
79- private Query tryToGetAsLuceneQuery(TranslationAware translationAware) {
80- // attempt to translate directly to a Lucene Query
81- // not sure how to get the field name from the expression
82- MappedFieldType fieldType = context.getFieldType(WHAT_GOES_HERE.fieldName().string());
83- try {
84- return translationAware.asLuceneQuery(fieldType, CONSTANT_SCORE_REWRITE, context);
85- } catch (Exception e) {}
86- // only a few expression types support asLuceneQuery, it is OK to fail here and we will try a different approach
87- return null;
88- }
89- */
90104 @ Override
91105 public int getPositionCount () {
92106 int positionCount = queryLists .get (0 ).getPositionCount ();
@@ -102,4 +116,9 @@ public int getPositionCount() {
102116 }
103117 return positionCount ;
104118 }
119+
120+ @ Override
121+ public FilterExec getPostJoinFilter () {
122+ return postJoinFilter ;
123+ }
105124}
0 commit comments