|
@@ -55,6 +55,7 @@ import org.elasticsearch.xpack.esql.parser.ParsingException;
|
|
|
import org.elasticsearch.xpack.esql.plan.logical.Enrich;
|
|
|
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
|
|
|
import org.elasticsearch.xpack.esql.plan.physical.AggregateExec;
|
|
|
+import org.elasticsearch.xpack.esql.plan.physical.DissectExec;
|
|
|
import org.elasticsearch.xpack.esql.plan.physical.EsQueryExec;
|
|
|
import org.elasticsearch.xpack.esql.plan.physical.EsStatsQueryExec;
|
|
|
import org.elasticsearch.xpack.esql.plan.physical.EsStatsQueryExec.Stat;
|
|
@@ -65,6 +66,7 @@ import org.elasticsearch.xpack.esql.plan.physical.FieldExtractExec;
|
|
|
import org.elasticsearch.xpack.esql.plan.physical.FilterExec;
|
|
|
import org.elasticsearch.xpack.esql.plan.physical.LimitExec;
|
|
|
import org.elasticsearch.xpack.esql.plan.physical.LocalSourceExec;
|
|
|
+import org.elasticsearch.xpack.esql.plan.physical.MvExpandExec;
|
|
|
import org.elasticsearch.xpack.esql.plan.physical.PhysicalPlan;
|
|
|
import org.elasticsearch.xpack.esql.plan.physical.ProjectExec;
|
|
|
import org.elasticsearch.xpack.esql.plan.physical.TopNExec;
|
|
@@ -141,6 +143,18 @@ public class LocalPhysicalPlanOptimizerTests extends MapperServiceTestCase {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ private final SearchStats CONSTANT_K_STATS = new TestSearchStats() {
|
|
|
+ @Override
|
|
|
+ public boolean isSingleValue(String field) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String constantValue(String name) {
|
|
|
+ return name.startsWith("constant_keyword") ? "foo" : null;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
@ParametersFactory(argumentFormatting = PARAM_FORMATTING)
|
|
|
public static List<Object[]> readScriptSpec() {
|
|
|
return settings().stream().map(t -> {
|
|
@@ -1736,6 +1750,113 @@ public class LocalPhysicalPlanOptimizerTests extends MapperServiceTestCase {
|
|
|
assertThat(esQuery.query().toString(), equalTo(expected.toString()));
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * LimitExec[1000[INTEGER]]
|
|
|
+ * \_ExchangeExec[[!alias_integer, boolean{f}#415, byte{f}#416, constant_keyword-foo{f}#417, date{f}#418, date_nanos{f}#419,
|
|
|
+ * double{f}#420, float{f}#421, half_float{f}#422, integer{f}#424, ip{f}#425, keyword{f}#426, long{f}#427, scaled_float{f}#423,
|
|
|
+ * !semantic_text, short{f}#429, text{f}#430, unsigned_long{f}#428, version{f}#431, wildcard{f}#432], false]
|
|
|
+ * \_ProjectExec[[!alias_integer, boolean{f}#415, byte{f}#416, constant_keyword-foo{f}#417, date{f}#418, date_nanos{f}#419,
|
|
|
+ * double{f}#420, float{f}#421, half_float{f}#422, integer{f}#424, ip{f}#425, keyword{f}#426, long{f}#427, scaled_float{f}#423,
|
|
|
+ * !semantic_text, short{f}#429, text{f}#430, unsigned_long{f}#428, version{f}#431, wildcard{f}#432]]
|
|
|
+ * \_FieldExtractExec[!alias_integer, boolean{f}#415, byte{f}#416, consta..]
|
|
|
+ * \_EsQueryExec[test], indexMode[standard], query[][_doc{f}#434], limit[1000], sort[] estimatedRowSize[412]
|
|
|
+ */
|
|
|
+ public void testConstantKeywordWithMatchingFilter() {
|
|
|
+ String queryText = """
|
|
|
+ from test
|
|
|
+ | where `constant_keyword-foo` == "foo"
|
|
|
+ """;
|
|
|
+ var analyzer = makeAnalyzer("mapping-all-types.json", new EnrichResolution());
|
|
|
+ var plan = plannerOptimizer.plan(queryText, CONSTANT_K_STATS, analyzer);
|
|
|
+
|
|
|
+ var limit = as(plan, LimitExec.class);
|
|
|
+ var exchange = as(limit.child(), ExchangeExec.class);
|
|
|
+ var project = as(exchange.child(), ProjectExec.class);
|
|
|
+ var field = as(project.child(), FieldExtractExec.class);
|
|
|
+ var query = as(field.child(), EsQueryExec.class);
|
|
|
+ assertThat(as(query.limit(), Literal.class).value(), is(1000));
|
|
|
+ assertNull(query.query());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * LimitExec[1000[INTEGER]]
|
|
|
+ * \_ExchangeExec[[!alias_integer, boolean{f}#4, byte{f}#5, constant_keyword-foo{f}#6, date{f}#7, date_nanos{f}#8, double{f}#9,
|
|
|
+ * float{f}#10, half_float{f}#11, integer{f}#13, ip{f}#14, keyword{f}#15, long{f}#16, scaled_float{f}#12, !semantic_text,
|
|
|
+ * short{f}#18, text{f}#19, unsigned_long{f}#17, version{f}#20, wildcard{f}#21], false]
|
|
|
+ * \_LocalSourceExec[[!alias_integer, boolean{f}#4, byte{f}#5, constant_keyword-foo{f}#6, date{f}#7, date_nanos{f}#8, double{f}#9,
|
|
|
+ * float{f}#10, half_float{f}#11, integer{f}#13, ip{f}#14, keyword{f}#15, long{f}#16, scaled_float{f}#12, !semantic_text,
|
|
|
+ * short{f}#18, text{f}#19, unsigned_long{f}#17, version{f}#20, wildcard{f}#21], EMPTY]
|
|
|
+ */
|
|
|
+ public void testConstantKeywordWithNonMatchingFilter() {
|
|
|
+ String queryText = """
|
|
|
+ from test
|
|
|
+ | where `constant_keyword-foo` == "non-matching"
|
|
|
+ """;
|
|
|
+ var analyzer = makeAnalyzer("mapping-all-types.json", new EnrichResolution());
|
|
|
+ var plan = plannerOptimizer.plan(queryText, CONSTANT_K_STATS, analyzer);
|
|
|
+
|
|
|
+ var limit = as(plan, LimitExec.class);
|
|
|
+ var exchange = as(limit.child(), ExchangeExec.class);
|
|
|
+ var source = as(exchange.child(), LocalSourceExec.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * LimitExec[1000[INTEGER]]
|
|
|
+ * \_ExchangeExec[[!alias_integer, boolean{f}#6, byte{f}#7, constant_keyword-foo{r}#25, date{f}#9, date_nanos{f}#10, double{f}#1...
|
|
|
+ * \_ProjectExec[[!alias_integer, boolean{f}#6, byte{f}#7, constant_keyword-foo{r}#25, date{f}#9, date_nanos{f}#10, double{f}#1...
|
|
|
+ * \_FieldExtractExec[!alias_integer, boolean{f}#6, byte{f}#7, date{f}#9,
|
|
|
+ * \_LimitExec[1000[INTEGER]]
|
|
|
+ * \_FilterExec[constant_keyword-foo{r}#25 == [66 6f 6f][KEYWORD]]
|
|
|
+ * \_MvExpandExec[constant_keyword-foo{f}#8,constant_keyword-foo{r}#25]
|
|
|
+ * \_FieldExtractExec[constant_keyword-foo{f}#8]
|
|
|
+ * \_EsQueryExec[test], indexMode[standard], query[][_doc{f}#26], limit[], sort[] estimatedRowSize[412]
|
|
|
+ */
|
|
|
+ public void testConstantKeywordExpandFilter() {
|
|
|
+ String queryText = """
|
|
|
+ from test
|
|
|
+ | mv_expand `constant_keyword-foo`
|
|
|
+ | where `constant_keyword-foo` == "foo"
|
|
|
+ """;
|
|
|
+ var analyzer = makeAnalyzer("mapping-all-types.json", new EnrichResolution());
|
|
|
+ var plan = plannerOptimizer.plan(queryText, CONSTANT_K_STATS, analyzer);
|
|
|
+
|
|
|
+ var limit = as(plan, LimitExec.class);
|
|
|
+ var exchange = as(limit.child(), ExchangeExec.class);
|
|
|
+ var project = as(exchange.child(), ProjectExec.class);
|
|
|
+ var fieldExtract = as(project.child(), FieldExtractExec.class);
|
|
|
+ var limit2 = as(fieldExtract.child(), LimitExec.class);
|
|
|
+ var filter = as(limit2.child(), FilterExec.class);
|
|
|
+ var expand = as(filter.child(), MvExpandExec.class);
|
|
|
+ var field = as(expand.child(), FieldExtractExec.class); // MV_EXPAND is not optimized yet (it doesn't accept literals)
|
|
|
+ as(field.child(), EsQueryExec.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * DissectExec[constant_keyword-foo{f}#8,Parser[pattern=%{bar}, appendSeparator=, ...
|
|
|
+ * \_LimitExec[1000[INTEGER]]
|
|
|
+ * \_ExchangeExec[[!alias_integer, boolean{f}#6, byte{f}#7, constant_keyword-foo{f}#8, date{f}#9, date_nanos{f}#10, double{f}#11...
|
|
|
+ * \_ProjectExec[[!alias_integer, boolean{f}#6, byte{f}#7, constant_keyword-foo{f}#8, date{f}#9, date_nanos{f}#10, double{f}#11...
|
|
|
+ * \_FieldExtractExec[!alias_integer, boolean{f}#6, byte{f}#7, constant_k..]
|
|
|
+ * \_EsQueryExec[test], indexMode[standard], query[][_doc{f}#25], limit[1000], sort[] estimatedRowSize[462]
|
|
|
+ */
|
|
|
+ public void testConstantKeywordDissectFilter() {
|
|
|
+ String queryText = """
|
|
|
+ from test
|
|
|
+ | dissect `constant_keyword-foo` "%{bar}"
|
|
|
+ | where `constant_keyword-foo` == "foo"
|
|
|
+ """;
|
|
|
+ var analyzer = makeAnalyzer("mapping-all-types.json", new EnrichResolution());
|
|
|
+ var plan = plannerOptimizer.plan(queryText, CONSTANT_K_STATS, analyzer);
|
|
|
+
|
|
|
+ var dissect = as(plan, DissectExec.class);
|
|
|
+ var limit = as(dissect.child(), LimitExec.class);
|
|
|
+ var exchange = as(limit.child(), ExchangeExec.class);
|
|
|
+ var project = as(exchange.child(), ProjectExec.class);
|
|
|
+ var field = as(project.child(), FieldExtractExec.class);
|
|
|
+ var query = as(field.child(), EsQueryExec.class);
|
|
|
+ assertNull(query.query());
|
|
|
+ }
|
|
|
+
|
|
|
private QueryBuilder wrapWithSingleQuery(String query, QueryBuilder inner, String fieldName, Source source) {
|
|
|
return FilterTests.singleValueQuery(query, inner, fieldName, source);
|
|
|
}
|