|
7 | 7 |
|
8 | 8 | package org.elasticsearch.xpack.esql.optimizer.rules.physical.local; |
9 | 9 |
|
10 | | -import org.elasticsearch.index.query.MatchAllQueryBuilder; |
11 | | -import org.elasticsearch.index.query.QueryBuilder; |
12 | | -import org.elasticsearch.index.query.functionscore.ScriptScoreQueryBuilder; |
13 | 10 | import org.elasticsearch.script.Script; |
| 11 | +import org.elasticsearch.search.sort.ScriptSortBuilder; |
| 12 | +import org.elasticsearch.search.sort.ScriptSortBuilder.ScriptSortType; |
| 13 | +import org.elasticsearch.search.sort.SortBuilder; |
| 14 | +import org.elasticsearch.search.sort.SortOrder; |
14 | 15 | import org.elasticsearch.xpack.esql.core.expression.Alias; |
15 | | -import org.elasticsearch.xpack.esql.core.expression.MetadataAttribute; |
16 | | -import org.elasticsearch.xpack.esql.core.tree.Source; |
| 16 | +import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; |
17 | 17 | import org.elasticsearch.xpack.esql.core.type.DataType; |
| 18 | +import org.elasticsearch.xpack.esql.core.type.EsField; |
| 19 | +import org.elasticsearch.xpack.esql.expression.Order; |
18 | 20 | import org.elasticsearch.xpack.esql.optimizer.LocalPhysicalOptimizerContext; |
19 | 21 | import org.elasticsearch.xpack.esql.optimizer.PhysicalOptimizerRules; |
20 | 22 | import org.elasticsearch.xpack.esql.plan.physical.EsQueryExec; |
|
23 | 25 |
|
24 | 26 | import java.util.ArrayList; |
25 | 27 | import java.util.List; |
| 28 | +import java.util.Map; |
26 | 29 |
|
27 | 30 | public class PushScriptsToSource extends PhysicalOptimizerRules.ParameterizedOptimizerRule<EvalExec, LocalPhysicalOptimizerContext> { |
28 | 31 |
|
29 | 32 | @Override |
30 | 33 | protected PhysicalPlan rule(EvalExec evalExec, LocalPhysicalOptimizerContext ctx) { |
31 | 34 | PhysicalPlan plan = evalExec; |
32 | | - if (evalExec.child() instanceof EsQueryExec esQueryExec) { |
| 35 | + if (evalExec.child() instanceof EsQueryExec esQueryExec && esQueryExec.canPushSorts()) { |
33 | 36 | List<Alias> aliases = evalExec.fields(); |
34 | 37 | boolean aliasPushed = false; |
35 | 38 | List<Alias> nonPushedAliases = new ArrayList<>(); |
36 | | - EvalExec scoreEval = null; |
| 39 | + EvalExec scriptEval = null; |
37 | 40 | for (Alias alias : aliases) { |
38 | 41 | if (aliasPushed == false && alias.child().isPushable()) { |
39 | | - QueryBuilder queryBuilder = esQueryExec.query(); |
40 | | - if (queryBuilder == null) { |
41 | | - queryBuilder = new MatchAllQueryBuilder(); |
| 42 | + |
| 43 | + if (esQueryExec.sorts() != null |
| 44 | + && esQueryExec.sorts() |
| 45 | + .stream() |
| 46 | + .anyMatch(s -> s instanceof ScriptSort ss && ss.field().name().equals(alias.name() + "_script"))) { |
| 47 | + // Already added as a sort, skip |
| 48 | + nonPushedAliases.add(alias); |
| 49 | + continue; |
42 | 50 | } |
43 | | - Script script = new Script(alias.child().asScript()); |
44 | | - ScriptScoreQueryBuilder scriptScore = new ScriptScoreQueryBuilder(queryBuilder, script); |
45 | | - MetadataAttribute scoreAttr = new MetadataAttribute(Source.EMPTY, MetadataAttribute.SCORE, DataType.DOUBLE, false); |
46 | | - Alias scoreAlias = alias.replaceChild(scoreAttr); |
47 | | - EsQueryExec scriptQueryExec = esQueryExec.withQuery(scriptScore); |
48 | | - scriptQueryExec.attrs().add(scoreAttr); |
49 | | - scoreEval = new EvalExec(evalExec.source(), scriptQueryExec, List.of(scoreAlias)); |
| 51 | + |
| 52 | + // Create a script from the expression |
| 53 | + String scriptText = alias.child().asScript(); |
| 54 | + Script script = new Script(scriptText); |
| 55 | + |
| 56 | + // Create an EsField for the computed value |
| 57 | + DataType dataType = alias.child().dataType(); |
| 58 | + EsField esField = new EsField( |
| 59 | + alias.name(), |
| 60 | + dataType, |
| 61 | + Map.of(), |
| 62 | + false, |
| 63 | + EsField.TimeSeriesFieldType.NONE |
| 64 | + ); |
| 65 | + |
| 66 | + // Create a FieldAttribute for the computed value |
| 67 | + FieldAttribute fieldAttr = new FieldAttribute( |
| 68 | + alias.source(), |
| 69 | + alias.name() + "_script", |
| 70 | + esField |
| 71 | + ); |
| 72 | + |
| 73 | + // Create a script sort that computes the value |
| 74 | + // We use NUMBER type for numeric results - adjust based on actual type if needed |
| 75 | + ScriptSortType sortType = alias.child().dataType().isNumeric() |
| 76 | + ? ScriptSortType.NUMBER |
| 77 | + : ScriptSortType.STRING; |
| 78 | + |
| 79 | + ScriptSort scriptSort = new ScriptSort( |
| 80 | + fieldAttr, |
| 81 | + script, |
| 82 | + sortType, |
| 83 | + Order.OrderDirection.ASC // Direction doesn't matter, we just want the value |
| 84 | + ); |
| 85 | + |
| 86 | + // Add the sort to the query exec |
| 87 | + List<EsQueryExec.Sort> newSorts = new ArrayList<>(); |
| 88 | + newSorts.add(scriptSort); |
| 89 | + EsQueryExec scriptQueryExec = esQueryExec.withSorts(newSorts); |
| 90 | + // TODO Keep previous sorts |
| 91 | + |
| 92 | + // Add the field attribute to attrs so it's available in the output |
| 93 | + scriptQueryExec.attrs().add(fieldAttr); |
| 94 | + |
| 95 | + // Create an alias that references the field |
| 96 | + Alias fieldAlias = alias.replaceChild(fieldAttr); |
| 97 | + |
| 98 | + scriptEval = new EvalExec(evalExec.source(), scriptQueryExec, List.of(fieldAlias)); |
50 | 99 | aliasPushed = true; |
51 | 100 | } else { |
52 | 101 | nonPushedAliases.add(alias); |
53 | 102 | } |
54 | 103 | } |
55 | 104 | if (aliasPushed) { |
56 | 105 | if (nonPushedAliases.isEmpty()) { |
57 | | - plan = scoreEval; |
| 106 | + plan = scriptEval; |
58 | 107 | } else { |
59 | | - plan = new EvalExec(evalExec.source(), scoreEval, nonPushedAliases); |
| 108 | + plan = new EvalExec(evalExec.source(), scriptEval, nonPushedAliases); |
60 | 109 | } |
61 | 110 | } |
62 | 111 | } |
63 | 112 |
|
64 | 113 | return plan; |
65 | 114 | } |
| 115 | + |
| 116 | + /** |
| 117 | + * Custom Sort implementation that uses a script to compute the sort value. |
| 118 | + * The sort value is made available through a FieldAttribute. |
| 119 | + */ |
| 120 | + static class ScriptSort implements EsQueryExec.Sort { |
| 121 | + private final FieldAttribute field; |
| 122 | + private final Script script; |
| 123 | + private final ScriptSortType type; |
| 124 | + private final Order.OrderDirection direction; |
| 125 | + |
| 126 | + ScriptSort(FieldAttribute field, Script script, ScriptSortType type, Order.OrderDirection direction) { |
| 127 | + this.field = field; |
| 128 | + this.script = script; |
| 129 | + this.type = type; |
| 130 | + this.direction = direction; |
| 131 | + } |
| 132 | + |
| 133 | + @Override |
| 134 | + public SortBuilder<?> sortBuilder() { |
| 135 | + ScriptSortBuilder builder = new ScriptSortBuilder(script, type); |
| 136 | + builder.order(direction == Order.OrderDirection.ASC ? SortOrder.ASC : SortOrder.DESC); |
| 137 | + return builder; |
| 138 | + } |
| 139 | + |
| 140 | + @Override |
| 141 | + public Order.OrderDirection direction() { |
| 142 | + return direction; |
| 143 | + } |
| 144 | + |
| 145 | + @Override |
| 146 | + public FieldAttribute field() { |
| 147 | + return field; |
| 148 | + } |
| 149 | + |
| 150 | + @Override |
| 151 | + public DataType resulType() { |
| 152 | + return field.dataType(); |
| 153 | + } |
| 154 | + } |
66 | 155 | } |
0 commit comments