diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index f7f13c6266540..4edeacfa754c5 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -188,6 +188,7 @@ static TransportVersion def(int id) { public static final TransportVersion ESQL_CCS_EXEC_INFO_WITH_FAILURES = def(8_783_00_0); public static final TransportVersion LOGSDB_TELEMETRY = def(8_784_00_0); public static final TransportVersion LOGSDB_TELEMETRY_STATS = def(8_785_00_0); + public static final TransportVersion KQL_QUERY_ADDED = def(8_786_00_0); /* * STOP! READ THIS FIRST! No, really, diff --git a/x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/parser/KqlAstBuilder.java b/x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/parser/KqlAstBuilder.java index a6de28104e313..db34c42ebf323 100644 --- a/x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/parser/KqlAstBuilder.java +++ b/x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/parser/KqlAstBuilder.java @@ -126,7 +126,11 @@ public QueryBuilder visitRangeQuery(KqlBaseParser.RangeQueryContext ctx) { withFields(ctx.fieldName(), (fieldName, mappedFieldType) -> { RangeQueryBuilder rangeQuery = rangeOperation.apply(QueryBuilders.rangeQuery(fieldName), queryText); - // TODO: add timezone for date fields + + if (kqlParserExecutionContext.timeZone() != null) { + rangeQuery.timeZone(kqlParserExecutionContext.timeZone().getId()); + } + boolQueryBuilder.should(rangeQuery); }); @@ -161,16 +165,17 @@ public QueryBuilder visitFieldQuery(KqlBaseParser.FieldQueryContext ctx) { QueryBuilder fieldQuery = null; if (hasWildcard && isKeywordField(mappedFieldType)) { - fieldQuery = QueryBuilders.wildcardQuery(fieldName, queryText) - .caseInsensitive(kqlParserExecutionContext.isCaseSensitive() == false); + fieldQuery = QueryBuilders.wildcardQuery(fieldName, queryText).caseInsensitive(kqlParserExecutionContext.caseInsensitive()); } else if (hasWildcard) { fieldQuery = QueryBuilders.queryStringQuery(escapeLuceneQueryString(queryText, true)).field(fieldName); } else if (isDateField(mappedFieldType)) { - // TODO: add timezone - fieldQuery = QueryBuilders.rangeQuery(fieldName).gte(queryText).lte(queryText); + RangeQueryBuilder rangeFieldQuery = QueryBuilders.rangeQuery(fieldName).gte(queryText).lte(queryText); + if (kqlParserExecutionContext.timeZone() != null) { + rangeFieldQuery.timeZone(kqlParserExecutionContext.timeZone().getId()); + } + fieldQuery = rangeFieldQuery; } else if (isKeywordField(mappedFieldType)) { - fieldQuery = QueryBuilders.termQuery(fieldName, queryText) - .caseInsensitive(kqlParserExecutionContext.isCaseSensitive() == false); + fieldQuery = QueryBuilders.termQuery(fieldName, queryText).caseInsensitive(kqlParserExecutionContext.caseInsensitive()); } else if (ctx.fieldQueryValue().QUOTED_STRING() != null) { fieldQuery = QueryBuilders.matchPhraseQuery(fieldName, queryText); } else { diff --git a/x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/parser/KqlParser.java b/x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/parser/KqlParser.java index 1064f901cacb8..78e4784dc003e 100644 --- a/x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/parser/KqlParser.java +++ b/x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/parser/KqlParser.java @@ -15,7 +15,6 @@ import org.antlr.v4.runtime.Recognizer; import org.antlr.v4.runtime.atn.PredictionMode; import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.Logger; @@ -25,17 +24,12 @@ public class KqlParser { private static final Logger log = LogManager.getLogger(KqlParser.class); - public QueryBuilder parseKqlQuery(String kqlQuery, SearchExecutionContext searchExecutionContext) { + public QueryBuilder parseKqlQuery(String kqlQuery, KqlParserExecutionContext kqlParserContext) { if (log.isDebugEnabled()) { log.debug("Parsing KQL query: {}", kqlQuery); } - return invokeParser( - kqlQuery, - new KqlParserExecutionContext(searchExecutionContext), - KqlBaseParser::topLevelQuery, - KqlAstBuilder::toQueryBuilder - ); + return invokeParser(kqlQuery, kqlParserContext, KqlBaseParser::topLevelQuery, KqlAstBuilder::toQueryBuilder); } private T invokeParser( diff --git a/x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/parser/KqlParserExecutionContext.java b/x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/parser/KqlParserExecutionContext.java index d05c70c6b933f..6e81947a30242 100644 --- a/x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/parser/KqlParserExecutionContext.java +++ b/x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/parser/KqlParserExecutionContext.java @@ -21,7 +21,7 @@ import static org.elasticsearch.core.Tuple.tuple; -class KqlParserExecutionContext extends SearchExecutionContext { +public class KqlParserExecutionContext extends SearchExecutionContext { private static final List IGNORED_METADATA_FIELDS = List.of( "_seq_no", @@ -32,14 +32,25 @@ class KqlParserExecutionContext extends SearchExecutionContext { "_field_names" ); - private static Predicate> searchableFieldFilter = (fieldDef) -> fieldDef.v2().isSearchable(); - - private static Predicate> ignoredFieldFilter = (fieldDef) -> IGNORED_METADATA_FIELDS.contains( + private static final Predicate> searchableFieldFilter = (fieldDef) -> fieldDef.v2().isSearchable(); + private static final Predicate> ignoredFieldFilter = (fieldDef) -> IGNORED_METADATA_FIELDS.contains( fieldDef.v1() ); - KqlParserExecutionContext(SearchExecutionContext source) { + public static Builder builder(SearchExecutionContext searchExecutionContext) { + return new Builder(searchExecutionContext); + } + + private final boolean caseInsensitive; + private final ZoneId timeZone; + + private final String defaultFields; + + public KqlParserExecutionContext(SearchExecutionContext source, boolean caseInsensitive, ZoneId timeZone, String defaultFields) { super(source); + this.caseInsensitive = caseInsensitive; + this.timeZone = timeZone; + this.defaultFields = defaultFields; } public Iterable> resolveFields(KqlBaseParser.FieldNameContext fieldNameContext) { @@ -56,9 +67,8 @@ public Iterable> resolveFields(KqlBaseParser.Fiel .collect(Collectors.toList()); } - public boolean isCaseSensitive() { - // TODO: implementation - return false; + public boolean caseInsensitive() { + return caseInsensitive; } public ZoneId timeZone() { @@ -76,4 +86,34 @@ public static boolean isDateField(MappedFieldType fieldType) { public static boolean isKeywordField(MappedFieldType fieldType) { return fieldType.typeName().equals(KeywordFieldMapper.CONTENT_TYPE); } + + public static class Builder { + private final SearchExecutionContext searchExecutionContext; + private boolean caseInsensitive = true; + private ZoneId timeZone = null; + private String defaultFields = null; + + private Builder(SearchExecutionContext searchExecutionContext) { + this.searchExecutionContext = searchExecutionContext; + } + + public KqlParserExecutionContext build() { + return new KqlParserExecutionContext(searchExecutionContext, caseInsensitive, timeZone, defaultFields); + } + + public Builder caseInsensitive(boolean caseInsensitive) { + this.caseInsensitive = caseInsensitive; + return this; + } + + public Builder timeZone(ZoneId timeZone) { + this.timeZone = timeZone; + return this; + } + + public Builder defaultFields(String defaultFields) { + this.defaultFields = defaultFields; + return this; + } + } } diff --git a/x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/query/KqlQueryBuilder.java b/x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/query/KqlQueryBuilder.java index 090b136afceab..42c30cbe7e660 100644 --- a/x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/query/KqlQueryBuilder.java +++ b/x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/query/KqlQueryBuilder.java @@ -9,6 +9,7 @@ import org.apache.lucene.search.Query; import org.elasticsearch.TransportVersion; +import org.elasticsearch.TransportVersions; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; @@ -23,27 +24,53 @@ import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.kql.parser.KqlParser; +import org.elasticsearch.xpack.kql.parser.KqlParserExecutionContext; import java.io.IOException; +import java.time.ZoneId; import java.util.Objects; import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; public class KqlQueryBuilder extends AbstractQueryBuilder { public static final String NAME = "kql"; public static final ParseField QUERY_FIELD = new ParseField("query"); + private static final ParseField CASE_INSENSITIVE_FIELD = new ParseField("case_insensitive"); + private static final ParseField TIME_ZONE_FIELD = new ParseField("time_zone"); + private static final ParseField DEFAULT_FIELD_FIELD = new ParseField("default_field"); + private static final Logger log = LogManager.getLogger(KqlQueryBuilder.class); - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - NAME, - a -> new KqlQueryBuilder((String) a[0]) - ); + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, a -> { + KqlQueryBuilder kqlQuery = new KqlQueryBuilder((String) a[0]); + + if (a[1] != null) { + kqlQuery.caseInsensitive((Boolean) a[1]); + } + + if (a[2] != null) { + kqlQuery.timeZone((String) a[2]); + } + + if (a[3] != null) { + kqlQuery.defaultFields((String) a[3]); + } + + return kqlQuery; + }); static { PARSER.declareString(constructorArg(), QUERY_FIELD); + PARSER.declareBoolean(optionalConstructorArg(), CASE_INSENSITIVE_FIELD); + PARSER.declareString(optionalConstructorArg(), TIME_ZONE_FIELD); + PARSER.declareString(optionalConstructorArg(), DEFAULT_FIELD_FIELD); declareStandardFields(PARSER); } private final String query; + private boolean caseInsensitive; + private ZoneId timeZone; + private String defaultFields; public KqlQueryBuilder(String query) { this.query = Objects.requireNonNull(query, "query can not be null"); @@ -52,6 +79,9 @@ public KqlQueryBuilder(String query) { public KqlQueryBuilder(StreamInput in) throws IOException { super(in); query = in.readString(); + caseInsensitive = in.readBoolean(); + timeZone = in.readOptionalZoneId(); + defaultFields = in.readOptionalString(); } public static KqlQueryBuilder fromXContent(XContentParser parser) { @@ -62,18 +92,66 @@ public static KqlQueryBuilder fromXContent(XContentParser parser) { } } + @Override + public TransportVersion getMinimalSupportedVersion() { + return TransportVersions.KQL_QUERY_ADDED; + } + + public String queryString() { + return query; + } + + public boolean caseInsensitive() { + return caseInsensitive; + } + + public KqlQueryBuilder caseInsensitive(boolean caseInsensitive) { + this.caseInsensitive = caseInsensitive; + return this; + } + + public ZoneId timeZone() { + return timeZone; + } + + public KqlQueryBuilder timeZone(String timeZone) { + this.timeZone = timeZone != null ? ZoneId.of(timeZone) : null; + return this; + } + + public String defaultFields() { + return defaultFields; + } + + public KqlQueryBuilder defaultFields(String defaultFields) { + this.defaultFields = defaultFields; + return this; + } + @Override protected void doXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(NAME); - builder.field(QUERY_FIELD.getPreferredName(), query); - boostAndQueryNameToXContent(builder); + { + builder.field(QUERY_FIELD.getPreferredName(), query); + builder.field(CASE_INSENSITIVE_FIELD.getPreferredName(), caseInsensitive); + + if (defaultFields != null) { + builder.field(DEFAULT_FIELD_FIELD.getPreferredName(), defaultFields); + } + + if (timeZone != null) { + builder.field(TIME_ZONE_FIELD.getPreferredName(), timeZone.getId()); + } + + boostAndQueryNameToXContent(builder); + } builder.endObject(); } @Override protected QueryBuilder doSearchRewrite(SearchExecutionContext searchExecutionContext) throws IOException { KqlParser parser = new KqlParser(); - QueryBuilder rewrittenQuery = parser.parseKqlQuery(query, searchExecutionContext); + QueryBuilder rewrittenQuery = parser.parseKqlQuery(query, createKqlParserContext(searchExecutionContext)); if (log.isTraceEnabled()) { log.trace("KQL query {} translated to Query DSL: {}", query, Strings.toString(rewrittenQuery)); @@ -89,6 +167,9 @@ protected Query doToQuery(SearchExecutionContext context) throws IOException { protected void doWriteTo(StreamOutput out) throws IOException { out.writeString(query); + out.writeBoolean(caseInsensitive); + out.writeOptionalZoneId(timeZone); + out.writeOptionalString(defaultFields); } @Override @@ -98,21 +179,22 @@ public String getWriteableName() { @Override protected int doHashCode() { - return Objects.hash(query); + return Objects.hash(query, caseInsensitive, timeZone, defaultFields); } @Override protected boolean doEquals(KqlQueryBuilder other) { - return Objects.equals(query, other.query); + return Objects.equals(query, other.query) + && Objects.equals(timeZone, other.timeZone) + && Objects.equals(defaultFields, other.defaultFields) + && caseInsensitive == other.caseInsensitive; } - @Override - public TransportVersion getMinimalSupportedVersion() { - // TODO: Create a transport versions. - return TransportVersion.current(); - } - - public String queryString() { - return query; + private KqlParserExecutionContext createKqlParserContext(SearchExecutionContext searchExecutionContext) { + return KqlParserExecutionContext.builder(searchExecutionContext) + .caseInsensitive(caseInsensitive) + .timeZone(timeZone) + .defaultFields(defaultFields) + .build(); } } diff --git a/x-pack/plugin/kql/src/test/java/org/elasticsearch/xpack/kql/parser/AbstractKqlParserTestCase.java b/x-pack/plugin/kql/src/test/java/org/elasticsearch/xpack/kql/parser/AbstractKqlParserTestCase.java index 88c63e9a2585b..408950ab4f1bd 100644 --- a/x-pack/plugin/kql/src/test/java/org/elasticsearch/xpack/kql/parser/AbstractKqlParserTestCase.java +++ b/x-pack/plugin/kql/src/test/java/org/elasticsearch/xpack/kql/parser/AbstractKqlParserTestCase.java @@ -16,7 +16,6 @@ import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryStringQueryBuilder; import org.elasticsearch.index.query.RangeQueryBuilder; -import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.query.TermQueryBuilder; import org.elasticsearch.index.query.WildcardQueryBuilder; import org.elasticsearch.test.AbstractBuilderTestCase; @@ -111,9 +110,8 @@ protected List searchableFields(String fieldNamePattern) { protected QueryBuilder parseKqlQuery(String kqlQuery) { KqlParser parser = new KqlParser(); - SearchExecutionContext searchExecutionContext = createSearchExecutionContext(); - - return parser.parseKqlQuery(kqlQuery, searchExecutionContext); + KqlParserExecutionContext kqlParserContext = KqlParserExecutionContext.builder(createSearchExecutionContext()).build(); + return parser.parseKqlQuery(kqlQuery, kqlParserContext); } protected static void assertMultiMatchQuery(QueryBuilder query, String expectedValue, MultiMatchQueryBuilder.Type expectedType) { diff --git a/x-pack/plugin/kql/src/test/java/org/elasticsearch/xpack/kql/query/KqlQueryBuilderTests.java b/x-pack/plugin/kql/src/test/java/org/elasticsearch/xpack/kql/query/KqlQueryBuilderTests.java index 5639688862694..6b780a8f1725a 100644 --- a/x-pack/plugin/kql/src/test/java/org/elasticsearch/xpack/kql/query/KqlQueryBuilderTests.java +++ b/x-pack/plugin/kql/src/test/java/org/elasticsearch/xpack/kql/query/KqlQueryBuilderTests.java @@ -30,7 +30,21 @@ protected Collection> getPlugins() { @Override protected KqlQueryBuilder doCreateTestQueryBuilder() { - return new KqlQueryBuilder(generateRandomKqlQuery()); + KqlQueryBuilder kqlQueryBuilder = new KqlQueryBuilder(generateRandomKqlQuery()); + + if (randomBoolean()) { + kqlQueryBuilder.caseInsensitive(randomBoolean()); + } + + if (randomBoolean()) { + kqlQueryBuilder.timeZone(randomTimeZone().getID()); + } + + if (randomBoolean()) { + kqlQueryBuilder.defaultFields(randomFrom("*", "mapped_*", KEYWORD_FIELD_NAME, TEXT_FIELD_NAME)); + } + + return kqlQueryBuilder; } @Override @@ -40,7 +54,44 @@ public KqlQueryBuilder mutateInstance(KqlQueryBuilder instance) throws IOExcepti return super.mutateInstance(instance); } - return new KqlQueryBuilder(randomValueOtherThan(instance.queryString(), this::generateRandomKqlQuery)); + KqlQueryBuilder kqlQueryBuilder = new KqlQueryBuilder(randomValueOtherThan(instance.queryString(), this::generateRandomKqlQuery)) + .caseInsensitive(instance.caseInsensitive()) + .timeZone(instance.timeZone() != null ? instance.timeZone().getId() : null) + .defaultFields(instance.defaultFields()); + + if (kqlQueryBuilder.queryString().equals(instance.queryString()) == false) { + return kqlQueryBuilder; + } + + switch (randomInt() % 3) { + case 0 -> { + kqlQueryBuilder.caseInsensitive(instance.caseInsensitive() == false); + } + case 1 -> { + if (randomBoolean() && instance.defaultFields() != null) { + kqlQueryBuilder.defaultFields(null); + } else { + kqlQueryBuilder.defaultFields( + randomValueOtherThan( + instance.defaultFields(), + () -> randomFrom("*", "mapped_*", KEYWORD_FIELD_NAME, TEXT_FIELD_NAME) + ) + ); + } + } + default -> { + if (randomBoolean() && instance.timeZone() != null) { + kqlQueryBuilder.timeZone(null); + } else if (instance.timeZone() != null) { + kqlQueryBuilder.timeZone(randomValueOtherThan(instance.timeZone().getId(), () -> randomTimeZone().getID())); + } else { + kqlQueryBuilder.timeZone(randomTimeZone().getID()); + } + } + } + ; + + return kqlQueryBuilder; } @Override