From c1a659e1da346eed792118f90ed1b27a9877f1fe Mon Sep 17 00:00:00 2001 From: Luigi Dell'Aquila Date: Thu, 22 May 2025 11:48:55 +0200 Subject: [PATCH] ES|QL: add local optimizations for constant_keyword (#127549) --- docs/changelog/127549.yaml | 5 + .../optimizer/LocalLogicalPlanOptimizer.java | 4 +- ...va => ReplaceFieldWithConstantOrNull.java} | 37 ++- .../xpack/esql/stats/SearchContextStats.java | 35 +++ .../xpack/esql/stats/SearchStats.java | 8 + .../LocalPhysicalPlanOptimizerTests.java | 121 ++++++++ .../rest-api-spec/test/esql/30_types.yml | 265 ++++++++++++++++++ 7 files changed, 467 insertions(+), 8 deletions(-) create mode 100644 docs/changelog/127549.yaml rename x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/{ReplaceMissingFieldWithNull.java => ReplaceFieldWithConstantOrNull.java} (77%) diff --git a/docs/changelog/127549.yaml b/docs/changelog/127549.yaml new file mode 100644 index 0000000000000..5f24111d22689 --- /dev/null +++ b/docs/changelog/127549.yaml @@ -0,0 +1,5 @@ +pr: 127549 +summary: Add local optimizations for `constant_keyword` +area: ES|QL +type: enhancement +issues: [] diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizer.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizer.java index 3da07e9485af7..413cfe1940370 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizer.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizer.java @@ -12,7 +12,7 @@ import org.elasticsearch.xpack.esql.optimizer.rules.logical.local.InferIsNotNull; import org.elasticsearch.xpack.esql.optimizer.rules.logical.local.InferNonNullAggConstraint; import org.elasticsearch.xpack.esql.optimizer.rules.logical.local.LocalPropagateEmptyRelation; -import org.elasticsearch.xpack.esql.optimizer.rules.logical.local.ReplaceMissingFieldWithNull; +import org.elasticsearch.xpack.esql.optimizer.rules.logical.local.ReplaceFieldWithConstantOrNull; import org.elasticsearch.xpack.esql.optimizer.rules.logical.local.ReplaceTopNWithLimitAndSort; import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; import org.elasticsearch.xpack.esql.rule.ParameterizedRuleExecutor; @@ -43,7 +43,7 @@ protected List> batches() { "Local rewrite", Limiter.ONCE, new ReplaceTopNWithLimitAndSort(), - new ReplaceMissingFieldWithNull(), + new ReplaceFieldWithConstantOrNull(), new InferIsNotNull(), new InferNonNullAggConstraint() ); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceMissingFieldWithNull.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceFieldWithConstantOrNull.java similarity index 77% rename from x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceMissingFieldWithNull.java rename to x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceFieldWithConstantOrNull.java index 3e4058d51c176..52cad9865c3d9 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceMissingFieldWithNull.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceFieldWithConstantOrNull.java @@ -12,6 +12,7 @@ import org.elasticsearch.xpack.esql.core.expression.Alias; import org.elasticsearch.xpack.esql.core.expression.Attribute; import org.elasticsearch.xpack.esql.core.expression.AttributeSet; +import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; import org.elasticsearch.xpack.esql.core.expression.Literal; import org.elasticsearch.xpack.esql.core.expression.NamedExpression; @@ -28,22 +29,24 @@ import org.elasticsearch.xpack.esql.rule.ParameterizedRule; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Predicate; /** - * Look for any fields used in the plan that are missing locally and replace them with null. + * Look for any fields used in the plan that are missing and replaces them with null or look for fields that are constant. * This should minimize the plan execution, in the best scenario skipping its execution all together. */ -public class ReplaceMissingFieldWithNull extends ParameterizedRule { +public class ReplaceFieldWithConstantOrNull extends ParameterizedRule { @Override public LogicalPlan apply(LogicalPlan plan, LocalLogicalOptimizerContext localLogicalOptimizerContext) { // Fields from lookup indices don't need to be present on the node, and our search stats don't include them, anyway. Ignore them. var lookupFieldsBuilder = AttributeSet.builder(); + Map attrToConstant = new HashMap<>(); plan.forEachUp(EsRelation.class, esRelation -> { - // Looking only for indices in LOOKUP mode is correct: during parsing, we assign the expected mode and even if a lookup index + // Looking for indices in LOOKUP mode is correct: during parsing, we assign the expected mode and even if a lookup index // is used in the FROM command, it will not be marked with LOOKUP mode there - but STANDARD. // It seems like we could instead just look for JOINs and walk down their right hand side to find lookup fields - but this does // not work as this rule also gets called just on the right hand side of a JOIN, which means that we don't always know that @@ -52,6 +55,18 @@ public LogicalPlan apply(LogicalPlan plan, LocalLogicalOptimizerContext localLog if (esRelation.indexMode() == IndexMode.LOOKUP) { lookupFieldsBuilder.addAll(esRelation.output()); } + // find constant values only in the main indices + else if (esRelation.indexMode() == IndexMode.STANDARD) { + for (Attribute attribute : esRelation.output()) { + if (attribute instanceof FieldAttribute fa) { + // Do not use the attribute name, this can deviate from the field name for union types; use fieldName() instead. + var val = localLogicalOptimizerContext.searchStats().constantValue(fa.fieldName()); + if (val != null) { + attrToConstant.put(attribute, Literal.of(attribute, val)); + } + } + } + } }); // Do not use the attribute name, this can deviate from the field name for union types; use fieldName() instead. @@ -59,10 +74,14 @@ public LogicalPlan apply(LogicalPlan plan, LocalLogicalOptimizerContext localLog Predicate shouldBeRetained = f -> (localLogicalOptimizerContext.searchStats().exists(f.fieldName()) || lookupFieldsBuilder.contains(f)); - return plan.transformUp(p -> missingToNull(p, shouldBeRetained)); + return plan.transformUp(p -> replaceWithNullOrConstant(p, shouldBeRetained, attrToConstant)); } - private LogicalPlan missingToNull(LogicalPlan plan, Predicate shouldBeRetained) { + private LogicalPlan replaceWithNullOrConstant( + LogicalPlan plan, + Predicate shouldBeRetained, + Map attrToConstant + ) { if (plan instanceof EsRelation relation) { // For any missing field, place an Eval right after the EsRelation to assign null values to that attribute (using the same name // id!), thus avoiding that InsertFieldExtrations inserts a field extraction later. @@ -116,7 +135,13 @@ private LogicalPlan missingToNull(LogicalPlan plan, Predicate sh || plan instanceof OrderBy || plan instanceof RegexExtract || plan instanceof TopN) { - return plan.transformExpressionsOnlyUp(FieldAttribute.class, f -> shouldBeRetained.test(f) ? f : Literal.of(f, null)); + return plan.transformExpressionsOnlyUp(FieldAttribute.class, f -> { + if (attrToConstant.containsKey(f)) {// handle constant values field and use the value itself instead + return attrToConstant.get(f); + } else {// handle missing fields and replace them with null + return shouldBeRetained.test(f) ? f : Literal.of(f, null); + } + }); } return plan; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/stats/SearchContextStats.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/stats/SearchContextStats.java index 4d08d6c3342ab..e7705496c0c7c 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/stats/SearchContextStats.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/stats/SearchContextStats.java @@ -305,6 +305,41 @@ public boolean canUseEqualityOnSyntheticSourceDelegate(String name, String value return true; } + public String constantValue(String name) { + String val = null; + for (SearchExecutionContext ctx : contexts) { + MappedFieldType f = ctx.getFieldType(name); + if (f == null) { + return null; + } + if (f instanceof ConstantFieldType cf) { + var fetcher = cf.valueFetcher(ctx, null); + String thisVal = null; + try { + // since the value is a constant, the doc _should_ be irrelevant + List vals = fetcher.fetchValues(null, -1, null); + Object objVal = vals.size() == 1 ? vals.get(0) : null; + // we are considering only string values for now, since this can return "strange" things, + // see IndexModeFieldType + thisVal = objVal instanceof String ? (String) objVal : null; + } catch (IOException iox) {} + + if (thisVal == null) { + // Value not yet set + return null; + } + if (val == null) { + val = thisVal; + } else if (thisVal.equals(val) == false) { + return null; + } + } else { + return null; + } + } + return val; + } + private interface DocCountTester { Boolean test(LeafReader leafReader) throws IOException; } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/stats/SearchStats.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/stats/SearchStats.java index e00de98178832..748ed826836e7 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/stats/SearchStats.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/stats/SearchStats.java @@ -39,6 +39,14 @@ public interface SearchStats { boolean canUseEqualityOnSyntheticSourceDelegate(String name, String value); + /** + * Returns the value for a field if it's a constant (eg. a constant_keyword with only one value for the involved indices). + * NULL if the field is not a constant. + */ + default String constantValue(String name) { + return null; + } + /** * When there are no search stats available, for example when there are no search contexts, we have static results. */ diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java index f8dd7cc0b9b09..5e19c2e32e6b6 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java @@ -55,6 +55,7 @@ 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.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 boolean isSingleValue(String field) { } }; + 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 readScriptSpec() { return settings().stream().map(t -> { @@ -1736,6 +1750,113 @@ public void testMatchFunctionWithPushableDisjunction() { 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); } diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/30_types.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/30_types.yml index 1f9ff72669309..c00a6bc4e320c 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/30_types.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/30_types.yml @@ -29,6 +29,66 @@ constant_keyword: - { "index": { } } - { "color": "red" } + - do: + indices.create: + index: test_2 + body: + mappings: + properties: + kind: + type: constant_keyword + value: a different constant + color: + type: keyword + + - do: + bulk: + index: test_2 + refresh: true + body: + - { "index": { } } + - { "color": "blue" } + + - do: + indices.create: + index: test_3 + body: + mappings: + properties: + kind: + type: keyword + color: + type: keyword + + - do: + bulk: + index: test_3 + refresh: true + body: + - { "index": { } } + - { "kind":"not a constant", "color": "pink" } + - { "index": { } } + - { "kind": "still no constant", "color": "pink" } + + - do: + indices.create: + index: text_test + body: + mappings: + properties: + kind: + type: text + color: + type: keyword + + - do: + bulk: + index: text_test + refresh: true + body: + - { "index": { } } + - { "kind":"a text field", "color": "green" } + - do: allowed_warnings_regex: - "No limit defined, adding default limit of \\[.*\\]" @@ -54,6 +114,211 @@ constant_keyword: - length: {values: 1} - match: {values.0.0: 17} + - do: + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" + esql.query: + body: + query: 'from test | where kind == "wow such constant" | keep color, kind' + - match: {columns.0.name: color} + - match: {columns.0.type: keyword} + - match: { columns.1.name: kind } + - match: { columns.1.type: keyword } + - length: {values: 1} + - match: {values.0.0: red} + - match: {values.0.1: "wow such constant"} + + - do: + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" + esql.query: + body: + query: 'from test | dissect kind "%{one} %{two} %{three}" | keep one, two, three, kind' + - match: {columns.0.name: one} + - match: {columns.0.type: keyword} + - match: {columns.1.name: two } + - match: {columns.1.type: keyword } + - match: {columns.2.name: three } + - match: {columns.2.type: keyword } + - match: {columns.3.name: kind } + - match: {columns.3.type: keyword } + + - length: {values: 1} + - match: {values.0.0: wow} + - match: {values.0.1: such} + - match: {values.0.2: constant} + - match: {values.0.3: wow such constant} + + - do: + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" + esql.query: + body: + query: 'from test | stats x = max(kind)' + - match: {columns.0.name: x} + - match: {columns.0.type: keyword} + + - length: {values: 1} + - match: {values.0.0: wow such constant} + + - do: + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" + esql.query: + body: + query: 'from test,test_2 | where kind == "wow such constant" | keep color, kind' + - match: {columns.0.name: color} + - match: {columns.0.type: keyword} + - match: { columns.1.name: kind } + - match: { columns.1.type: keyword } + - length: {values: 1} + - match: {values.0.0: red} + - match: {values.0.1: "wow such constant"} + + + - do: + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" + esql.query: + body: + query: 'from test* | where kind == "wow such constant" | keep color, kind' + - match: {columns.0.name: color} + - match: {columns.0.type: keyword} + - match: { columns.1.name: kind } + - match: { columns.1.type: keyword } + - length: {values: 1} + - match: {values.0.0: red} + - match: {values.0.1: "wow such constant"} + + - do: + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" + esql.query: + body: + query: 'from test,test_2 | where kind == "wow such constant" | keep color, kind' + - match: {columns.0.name: color} + - match: {columns.0.type: keyword} + - match: { columns.1.name: kind } + - match: { columns.1.type: keyword } + - length: {values: 1} + - match: {values.0.0: red} + - match: {values.0.1: "wow such constant"} + + + - do: + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" + esql.query: + body: + query: 'from test* | where kind == "wow such constant" | keep color, kind' + - match: {columns.0.name: color} + - match: {columns.0.type: keyword} + - match: { columns.1.name: kind } + - match: { columns.1.type: keyword } + - length: {values: 1} + - match: {values.0.0: red} + - match: {values.0.1: "wow such constant"} + + + - do: + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" + esql.query: + body: + query: 'from test* | where kind == "not a constant" | keep color, kind' + - match: {columns.0.name: color} + - match: {columns.0.type: keyword} + - match: { columns.1.name: kind } + - match: { columns.1.type: keyword } + - length: {values: 1} + - match: {values.0.0: pink} + - match: {values.0.1: "not a constant"} + + - do: + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" + esql.query: + body: + query: 'from test* | where kind >= "a" | keep color, kind | sort color, kind' + - match: {columns.0.name: color} + - match: {columns.0.type: keyword} + - match: { columns.1.name: kind } + - match: { columns.1.type: keyword } + - length: {values: 4} + - match: {values.0.0: blue} + - match: {values.0.1: "a different constant"} + - match: {values.1.0: pink} + - match: {values.1.1: "not a constant"} + - match: {values.2.0: pink} + - match: {values.2.1: "still no constant"} + - match: {values.3.0: red} + - match: {values.3.1: "wow such constant"} + + + - do: + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" + esql.query: + body: + query: 'from test* | where kind >= "o" | keep color, kind | sort color, kind' + - match: {columns.0.name: color} + - match: {columns.0.type: keyword} + - match: { columns.1.name: kind } + - match: { columns.1.type: keyword } + - length: {values: 2} + - match: {values.0.0: pink} + - match: {values.0.1: "still no constant"} + - match: {values.1.0: red} + - match: {values.1.1: "wow such constant"} + + + - do: + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" + esql.query: + body: + query: 'from test,text_test | where kind::string == "a text field" | eval kind = kind::string | keep color, kind' + - match: {columns.0.name: color} + - match: {columns.0.type: keyword} + - match: { columns.1.name: kind } + - match: { columns.1.type: keyword } + - length: {values: 1} + - match: {values.0.0: green} + - match: {values.0.1: "a text field"} + + + - do: + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" + esql.query: + body: + query: 'from test,text_test | where kind::string == "wow such constant" | eval kind = kind::string | keep color, kind' + - match: {columns.0.name: color} + - match: {columns.0.type: keyword} + - match: { columns.1.name: kind } + - match: { columns.1.type: keyword } + - length: {values: 1} + - match: {values.0.0: red} + - match: {values.0.1: "wow such constant"} + + + - do: + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" + esql.query: + body: + query: 'from test,text_test | where kind::string >= "a" | eval kind = kind::string | keep color, kind | sort color' + - match: {columns.0.name: color} + - match: {columns.0.type: keyword} + - match: { columns.1.name: kind } + - match: { columns.1.type: keyword } + - length: {values: 2} + - match: {values.0.0: green} + - match: {values.0.1: "a text field"} + - match: {values.1.0: red} + - match: {values.1.1: "wow such constant"} + + --- constant_keyword with null value: - do: