Skip to content

Commit

Permalink
Introducing new options for the kql query (case_insnsitive, time_zone…
Browse files Browse the repository at this point in the history
…, and default_fields)
  • Loading branch information
afoucret committed Nov 6, 2024
1 parent 58f3141 commit a164f24
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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> T invokeParser(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

import static org.elasticsearch.core.Tuple.tuple;

class KqlParserExecutionContext extends SearchExecutionContext {
public class KqlParserExecutionContext extends SearchExecutionContext {

private static final List<String> IGNORED_METADATA_FIELDS = List.of(
"_seq_no",
Expand All @@ -32,14 +32,25 @@ class KqlParserExecutionContext extends SearchExecutionContext {
"_field_names"
);

private static Predicate<Tuple<String, MappedFieldType>> searchableFieldFilter = (fieldDef) -> fieldDef.v2().isSearchable();

private static Predicate<Tuple<String, MappedFieldType>> ignoredFieldFilter = (fieldDef) -> IGNORED_METADATA_FIELDS.contains(
private static final Predicate<Tuple<String, MappedFieldType>> searchableFieldFilter = (fieldDef) -> fieldDef.v2().isSearchable();
private static final Predicate<Tuple<String, MappedFieldType>> 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<Tuple<String, MappedFieldType>> resolveFields(KqlBaseParser.FieldNameContext fieldNameContext) {
Expand All @@ -56,9 +67,8 @@ public Iterable<Tuple<String, MappedFieldType>> resolveFields(KqlBaseParser.Fiel
.collect(Collectors.toList());
}

public boolean isCaseSensitive() {
// TODO: implementation
return false;
public boolean caseInsensitive() {
return caseInsensitive;
}

public ZoneId timeZone() {
Expand All @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<KqlQueryBuilder> {
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<KqlQueryBuilder, Void> PARSER = new ConstructingObjectParser<>(
NAME,
a -> new KqlQueryBuilder((String) a[0])
);
private static final ConstructingObjectParser<KqlQueryBuilder, Void> 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");
Expand All @@ -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) {
Expand All @@ -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));
Expand All @@ -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
Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -111,9 +110,8 @@ protected List<String> 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) {
Expand Down
Loading

0 comments on commit a164f24

Please sign in to comment.