Browse Source

KQL: Support boolean operators in field queries (#133737)

Aurélien FOUCRET 1 month ago
parent
commit
36a00d152b

+ 6 - 0
docs/changelog/133737.yaml

@@ -0,0 +1,6 @@
+pr: 133737
+summary: "KQL: Support boolean operators in field queries"
+area: Search
+type: bug
+issues:
+ - 132366

+ 3 - 0
server/src/main/java/org/elasticsearch/rest/action/search/SearchCapabilities.java

@@ -33,6 +33,8 @@ public final class SearchCapabilities {
     private static final String TRANSFORM_RANK_RRF_TO_RETRIEVER = "transform_rank_rrf_to_retriever";
     /** Support kql query. */
     private static final String KQL_QUERY_SUPPORTED = "kql_query";
+    private static final String KQL_QUERY_BOOLEAN_FIELD_QUERY_SUPPORTED = "kql_query_boolean_field_query";
+
     /** Support propagating nested retrievers' inner_hits to top-level compound retrievers . */
     private static final String NESTED_RETRIEVER_INNER_HITS_SUPPORT = "nested_retriever_inner_hits_support";
     /** Fixed the math in {@code moving_fn}'s {@code linearWeightedAvg}. */
@@ -74,6 +76,7 @@ public final class SearchCapabilities {
         capabilities.add(MOVING_FN_RIGHT_MATH);
         capabilities.add(K_DEFAULT_TO_SIZE);
         capabilities.add(KQL_QUERY_SUPPORTED);
+        capabilities.add(KQL_QUERY_BOOLEAN_FIELD_QUERY_SUPPORTED);
         capabilities.add(HIGHLIGHT_MAX_ANALYZED_OFFSET_DEFAULT);
         capabilities.add(INDEX_SELECTOR_SYNTAX);
         capabilities.add(SIGNIFICANT_TERMS_BACKGROUND_FILTER_AS_SUB);

+ 3 - 3
x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/KqlFunctionIT.java

@@ -75,12 +75,12 @@ public class KqlFunctionIT extends AbstractEsqlIntegTestCase {
     public void testInvalidKqlQueryEof() {
         var query = """
             FROM test
-            | WHERE kql("content: ((((dog")
+            | WHERE kql("content: (dog")
             """;
 
         var error = expectThrows(QueryShardException.class, () -> run(query));
-        assertThat(error.getMessage(), containsString("Failed to parse KQL query [content: ((((dog]"));
-        assertThat(error.getRootCause().getMessage(), containsString("line 1:11: mismatched input '('"));
+        assertThat(error.getMessage(), containsString("Failed to parse KQL query [content: (dog]"));
+        assertThat(error.getRootCause().getMessage(), containsString("line 1:14: missing ')' at '<EOF>'"));
     }
 
     public void testInvalidKqlQueryLexicalError() {

+ 13 - 7
x-pack/plugin/kql/src/main/antlr/KqlBase.g4

@@ -27,7 +27,7 @@ topLevelQuery
 
 query
     : <assoc=right> query operator=(AND|OR) query  #booleanQuery
-    | simpleQuery                                  #defaultQuery
+    | simpleQuery                                   #defaultQuery
     ;
 
 simpleQuery
@@ -51,7 +51,7 @@ nestedQuery
 
 nestedSubQuery
     : <assoc=right> nestedSubQuery operator=(AND|OR) nestedSubQuery #booleanNestedQuery
-    | nestedSimpleSubQuery                                          #defaultNestedQuery
+    | nestedSimpleSubQuery                                           #defaultNestedQuery
     ;
 
 nestedSimpleSubQuery
@@ -89,21 +89,27 @@ existsQuery
 
 fieldQuery
     : fieldName COLON fieldQueryValue
-    | fieldName COLON LEFT_PARENTHESIS fieldQueryValue RIGHT_PARENTHESIS
     ;
 
 fieldLessQuery
     : fieldQueryValue
-    | LEFT_PARENTHESIS fieldQueryValue RIGHT_PARENTHESIS
     ;
 
 fieldQueryValue
-    : (AND|OR|NOT)? (UNQUOTED_LITERAL|WILDCARD)+ (NOT|AND|OR)?
-    | (AND|OR) (AND|OR|NOT)?
-    | NOT (AND|OR)?
+    : (UNQUOTED_LITERAL|WILDCARD)+
+    | (UNQUOTED_LITERAL|WILDCARD)? (OR|AND|NOT)+
+    | (AND|OR)+ (UNQUOTED_LITERAL|WILDCARD)?
     | QUOTED_STRING
+    | operator=NOT (fieldQueryValue)?
+    | LEFT_PARENTHESIS booleanFieldQueryValue RIGHT_PARENTHESIS
     ;
 
+booleanFieldQueryValue
+   : booleanFieldQueryValue operator=(AND|OR) fieldQueryValue
+   | LEFT_PARENTHESIS booleanFieldQueryValue RIGHT_PARENTHESIS
+   | fieldQueryValue
+   ;
+
 fieldName
     : value=UNQUOTED_LITERAL
     | value=QUOTED_STRING

+ 67 - 30
x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/parser/KqlAstBuilder.java

@@ -25,6 +25,7 @@ import java.util.List;
 import java.util.Set;
 import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
+import java.util.function.Consumer;
 
 import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
 import static org.elasticsearch.xpack.kql.parser.KqlParsingContext.isDateField;
@@ -207,38 +208,76 @@ class KqlAstBuilder extends KqlBaseBaseVisitor<QueryBuilder> {
 
     @Override
     public QueryBuilder visitFieldQuery(KqlBaseParser.FieldQueryContext ctx) {
+        return parseFieldQuery(ctx.fieldName(), ctx.fieldQueryValue());
+    }
 
-        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery().minimumShouldMatch(1);
-        String queryText = extractText(ctx.fieldQueryValue());
-        boolean hasWildcard = hasWildcard(ctx.fieldQueryValue());
+    public QueryBuilder parseBooleanFieldQuery(
+        KqlBaseParser.FieldNameContext fieldNameCtx,
+        KqlBaseParser.BooleanFieldQueryValueContext booleanFieldQueryValueCtx
+    ) {
+        if (booleanFieldQueryValueCtx.operator != null) {
+            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
+
+            Token operator = booleanFieldQueryValueCtx.operator;
+            Consumer<QueryBuilder> boolClauseConsumer = operator.getType() == KqlBaseParser.AND
+                ? boolQueryBuilder::must
+                : boolQueryBuilder::should;
+            boolClauseConsumer.accept(parseBooleanFieldQuery(fieldNameCtx, booleanFieldQueryValueCtx.booleanFieldQueryValue()));
+            boolClauseConsumer.accept(parseFieldQuery(fieldNameCtx, booleanFieldQueryValueCtx.fieldQueryValue()));
+
+            return operator.getType() == KqlBaseParser.AND
+                ? rewriteConjunctionQuery(boolQueryBuilder)
+                : rewriteDisjunctionQuery(boolQueryBuilder);
+        } else if (booleanFieldQueryValueCtx.booleanFieldQueryValue() != null) {
+            return parseBooleanFieldQuery(fieldNameCtx, booleanFieldQueryValueCtx.booleanFieldQueryValue());
+        } else {
+            assert booleanFieldQueryValueCtx.fieldQueryValue() != null;
+            return parseFieldQuery(fieldNameCtx, booleanFieldQueryValueCtx.fieldQueryValue());
+        }
+    }
 
-        withFields(ctx.fieldName(), (fieldName, mappedFieldType) -> {
-            QueryBuilder fieldQuery = null;
-
-            if (hasWildcard && isKeywordField(mappedFieldType)) {
-                fieldQuery = QueryBuilders.wildcardQuery(fieldName, queryText).caseInsensitive(kqlParsingContext.caseInsensitive());
-            } else if (hasWildcard) {
-                fieldQuery = QueryBuilders.queryStringQuery(escapeLuceneQueryString(queryText, true)).field(fieldName);
-            } else if (isDateField(mappedFieldType)) {
-                RangeQueryBuilder rangeFieldQuery = QueryBuilders.rangeQuery(fieldName).gte(queryText).lte(queryText);
-                if (kqlParsingContext.timeZone() != null) {
-                    rangeFieldQuery.timeZone(kqlParsingContext.timeZone().getId());
+    public QueryBuilder parseFieldQuery(
+        KqlBaseParser.FieldNameContext fieldNameCtx,
+        KqlBaseParser.FieldQueryValueContext fieldQueryValueCtx
+    ) {
+        if (fieldQueryValueCtx.operator != null) {
+            assert fieldQueryValueCtx.fieldQueryValue() != null;
+            return QueryBuilders.boolQuery().mustNot(parseFieldQuery(fieldNameCtx, fieldQueryValueCtx.fieldQueryValue()));
+        } else if (fieldQueryValueCtx.booleanFieldQueryValue() != null) {
+            return parseBooleanFieldQuery(fieldNameCtx, fieldQueryValueCtx.booleanFieldQueryValue());
+        } else {
+            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
+            String queryText = extractText(fieldQueryValueCtx);
+            boolean hasWildcard = hasWildcard(fieldQueryValueCtx);
+
+            withFields(fieldNameCtx, (fieldName, mappedFieldType) -> {
+                QueryBuilder fieldQuery;
+
+                if (hasWildcard && isKeywordField(mappedFieldType)) {
+                    fieldQuery = QueryBuilders.wildcardQuery(fieldName, queryText).caseInsensitive(kqlParsingContext.caseInsensitive());
+                } else if (hasWildcard) {
+                    fieldQuery = QueryBuilders.queryStringQuery(escapeLuceneQueryString(queryText, true)).field(fieldName);
+                } else if (isDateField(mappedFieldType)) {
+                    RangeQueryBuilder rangeFieldQuery = QueryBuilders.rangeQuery(fieldName).gte(queryText).lte(queryText);
+                    if (kqlParsingContext.timeZone() != null) {
+                        rangeFieldQuery.timeZone(kqlParsingContext.timeZone().getId());
+                    }
+                    fieldQuery = rangeFieldQuery;
+                } else if (isKeywordField(mappedFieldType)) {
+                    fieldQuery = QueryBuilders.termQuery(fieldName, queryText).caseInsensitive(kqlParsingContext.caseInsensitive());
+                } else if (fieldQueryValueCtx.QUOTED_STRING() != null) {
+                    fieldQuery = QueryBuilders.matchPhraseQuery(fieldName, queryText);
+                } else {
+                    fieldQuery = QueryBuilders.matchQuery(fieldName, queryText);
                 }
-                fieldQuery = rangeFieldQuery;
-            } else if (isKeywordField(mappedFieldType)) {
-                fieldQuery = QueryBuilders.termQuery(fieldName, queryText).caseInsensitive(kqlParsingContext.caseInsensitive());
-            } else if (ctx.fieldQueryValue().QUOTED_STRING() != null) {
-                fieldQuery = QueryBuilders.matchPhraseQuery(fieldName, queryText);
-            } else {
-                fieldQuery = QueryBuilders.matchQuery(fieldName, queryText);
-            }
 
-            if (fieldQuery != null) {
-                boolQueryBuilder.should(wrapWithNestedQuery(fieldName, fieldQuery));
-            }
-        });
+                if (fieldQuery != null) {
+                    boolQueryBuilder.should(wrapWithNestedQuery(fieldName, fieldQuery));
+                }
+            });
 
-        return rewriteDisjunctionQuery(boolQueryBuilder);
+            return rewriteDisjunctionQuery(boolQueryBuilder);
+        }
     }
 
     private static boolean isAndQuery(ParserRuleContext ctx) {
@@ -269,9 +308,7 @@ class KqlAstBuilder extends KqlBaseBaseVisitor<QueryBuilder> {
             return;
         }
 
-        if (ctx.value.getType() == KqlBaseParser.QUOTED_STRING) {
-            assert fieldNames.size() < 2 : "expecting only one matching field";
-        }
+        assert ctx.value.getType() != KqlBaseParser.QUOTED_STRING || fieldNames.size() < 2 : "expecting only one matching field";
 
         fieldNames.forEach(fieldName -> {
             MappedFieldType fieldType = kqlParsingContext.fieldType(fieldName);

File diff suppressed because it is too large
+ 1 - 0
x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/parser/KqlBase.interp


+ 12 - 0
x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/parser/KqlBaseBaseListener.java

@@ -236,6 +236,18 @@ class KqlBaseBaseListener implements KqlBaseListener {
      * <p>The default implementation does nothing.</p>
      */
     @Override public void exitFieldQueryValue(KqlBaseParser.FieldQueryValueContext ctx) { }
+    /**
+     * {@inheritDoc}
+     *
+     * <p>The default implementation does nothing.</p>
+     */
+    @Override public void enterBooleanFieldQueryValue(KqlBaseParser.BooleanFieldQueryValueContext ctx) { }
+    /**
+     * {@inheritDoc}
+     *
+     * <p>The default implementation does nothing.</p>
+     */
+    @Override public void exitBooleanFieldQueryValue(KqlBaseParser.BooleanFieldQueryValueContext ctx) { }
     /**
      * {@inheritDoc}
      *

+ 7 - 0
x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/parser/KqlBaseBaseVisitor.java

@@ -146,6 +146,13 @@ class KqlBaseBaseVisitor<T> extends AbstractParseTreeVisitor<T> implements KqlBa
      * {@link #visitChildren} on {@code ctx}.</p>
      */
     @Override public T visitFieldQueryValue(KqlBaseParser.FieldQueryValueContext ctx) { return visitChildren(ctx); }
+    /**
+     * {@inheritDoc}
+     *
+     * <p>The default implementation returns the result of calling
+     * {@link #visitChildren} on {@code ctx}.</p>
+     */
+    @Override public T visitBooleanFieldQueryValue(KqlBaseParser.BooleanFieldQueryValueContext ctx) { return visitChildren(ctx); }
     /**
      * {@inheritDoc}
      *

+ 10 - 0
x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/parser/KqlBaseListener.java

@@ -203,6 +203,16 @@ interface KqlBaseListener extends ParseTreeListener {
      * @param ctx the parse tree
      */
     void exitFieldQueryValue(KqlBaseParser.FieldQueryValueContext ctx);
+    /**
+     * Enter a parse tree produced by {@link KqlBaseParser#booleanFieldQueryValue}.
+     * @param ctx the parse tree
+     */
+    void enterBooleanFieldQueryValue(KqlBaseParser.BooleanFieldQueryValueContext ctx);
+    /**
+     * Exit a parse tree produced by {@link KqlBaseParser#booleanFieldQueryValue}.
+     * @param ctx the parse tree
+     */
+    void exitBooleanFieldQueryValue(KqlBaseParser.BooleanFieldQueryValueContext ctx);
     /**
      * Enter a parse tree produced by {@link KqlBaseParser#fieldName}.
      * @param ctx the parse tree

File diff suppressed because it is too large
+ 319 - 207
x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/parser/KqlBaseParser.java


+ 6 - 0
x-pack/plugin/kql/src/main/java/org/elasticsearch/xpack/kql/parser/KqlBaseVisitor.java

@@ -130,6 +130,12 @@ interface KqlBaseVisitor<T> extends ParseTreeVisitor<T> {
      * @return the visitor result
      */
     T visitFieldQueryValue(KqlBaseParser.FieldQueryValueContext ctx);
+    /**
+     * Visit a parse tree produced by {@link KqlBaseParser#booleanFieldQueryValue}.
+     * @param ctx the parse tree
+     * @return the visitor result
+     */
+    T visitBooleanFieldQueryValue(KqlBaseParser.BooleanFieldQueryValueContext ctx);
     /**
      * Visit a parse tree produced by {@link KqlBaseParser#fieldName}.
      * @param ctx the parse tree

+ 75 - 4
x-pack/plugin/kql/src/test/java/org/elasticsearch/xpack/kql/parser/KqlParserFieldQueryTests.java

@@ -10,8 +10,10 @@ package org.elasticsearch.xpack.kql.parser;
 import org.elasticsearch.core.Strings;
 import org.elasticsearch.index.query.BoolQueryBuilder;
 import org.elasticsearch.index.query.MatchNoneQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
 
 import java.util.List;
+import java.util.function.Consumer;
 
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.empty;
@@ -77,7 +79,10 @@ public class KqlParserFieldQueryTests extends AbstractKqlParserTestCase {
         // Leading operators (AND, OR) are terms of the match query
         assertTermQueryBuilder(parseKqlQuery(kqlFieldQuery(KEYWORD_FIELD_NAME, "AND bar")), KEYWORD_FIELD_NAME, "AND bar");
         assertTermQueryBuilder(parseKqlQuery(kqlFieldQuery(KEYWORD_FIELD_NAME, "OR bar")), KEYWORD_FIELD_NAME, "OR bar");
-        assertTermQueryBuilder(parseKqlQuery(kqlFieldQuery(KEYWORD_FIELD_NAME, "NOT bar")), KEYWORD_FIELD_NAME, "NOT bar");
+        assertMustNotQueryBuilder(
+            parseKqlQuery(kqlFieldQuery(KEYWORD_FIELD_NAME, "NOT bar")),
+            (subQuery) -> assertTermQueryBuilder(subQuery, KEYWORD_FIELD_NAME, "bar")
+        );
 
         // Lonely operators (AND, NOT, OR)
         assertTermQueryBuilder(parseKqlQuery(kqlFieldQuery(KEYWORD_FIELD_NAME, "AND AND")), KEYWORD_FIELD_NAME, "AND AND");
@@ -124,6 +129,7 @@ public class KqlParserFieldQueryTests extends AbstractKqlParserTestCase {
 
             // Multiple words
             assertMatchQueryBuilder(parseKqlQuery(kqlFieldQuery(fieldName, "foo bar")), fieldName, "foo bar");
+            assertMatchQueryBuilder(parseKqlQuery(kqlFieldQuery(fieldName, "(foo bar)")), fieldName, "foo bar");
 
             // Escaped keywords
             assertMatchQueryBuilder(parseKqlQuery(kqlFieldQuery(fieldName, "foo \\and bar")), fieldName, "foo and bar");
@@ -151,7 +157,10 @@ public class KqlParserFieldQueryTests extends AbstractKqlParserTestCase {
             // Leading operators (AND, OR) are terms of the match query
             assertMatchQueryBuilder(parseKqlQuery(kqlFieldQuery(fieldName, "AND bar")), fieldName, "AND bar");
             assertMatchQueryBuilder(parseKqlQuery(kqlFieldQuery(fieldName, "OR bar")), fieldName, "OR bar");
-            assertMatchQueryBuilder(parseKqlQuery(kqlFieldQuery(fieldName, "NOT bar")), fieldName, "NOT bar");
+            assertMustNotQueryBuilder(
+                parseKqlQuery(kqlFieldQuery(fieldName, "NOT bar")),
+                (subQuery) -> assertMatchQueryBuilder(subQuery, fieldName, "bar")
+            );
 
             // Lonely operators (AND, NOT, OR)
             assertMatchQueryBuilder(parseKqlQuery(kqlFieldQuery(fieldName, "AND")), fieldName, "AND");
@@ -174,6 +183,66 @@ public class KqlParserFieldQueryTests extends AbstractKqlParserTestCase {
         }
     }
 
+    private void assertMustNotQueryBuilder(QueryBuilder queryBuilder, Consumer<QueryBuilder> clauseVerifier) {
+        BoolQueryBuilder boolQuery = asInstanceOf(BoolQueryBuilder.class, queryBuilder);
+        assertThat(boolQuery.must(), empty());
+        assertThat(boolQuery.should(), empty());
+        assertThat(boolQuery.filter(), empty());
+        assertThat(boolQuery.mustNot(), hasSize(1));
+
+        clauseVerifier.accept(boolQuery.mustNot().get(0));
+    }
+
+    public void testBooleanFieldQueries() {
+        assertShouldQueryBuilder(
+            parseKqlQuery(kqlFieldQuery(KEYWORD_FIELD_NAME, "(foo OR bar)")),
+            (clause) -> assertTermQueryBuilder(clause, KEYWORD_FIELD_NAME, "foo"),
+            (clause) -> assertTermQueryBuilder(clause, KEYWORD_FIELD_NAME, "bar")
+        );
+
+        assertMustQueryBuilder(
+            parseKqlQuery(kqlFieldQuery(KEYWORD_FIELD_NAME, "(foo AND bar)")),
+            (clause) -> assertTermQueryBuilder(clause, KEYWORD_FIELD_NAME, "foo"),
+            (clause) -> assertTermQueryBuilder(clause, KEYWORD_FIELD_NAME, "bar")
+        );
+
+        assertMustQueryBuilder(
+            parseKqlQuery(kqlFieldQuery(KEYWORD_FIELD_NAME, "(foo or bar and baz)")),
+            (clause) -> assertShouldQueryBuilder(
+                clause,
+                (subClause) -> assertTermQueryBuilder(subClause, KEYWORD_FIELD_NAME, "foo"),
+                (subClause) -> assertTermQueryBuilder(subClause, KEYWORD_FIELD_NAME, "bar")
+            ),
+            (clause) -> assertTermQueryBuilder(clause, KEYWORD_FIELD_NAME, "baz")
+        );
+    }
+
+    @SafeVarargs
+    private final void assertShouldQueryBuilder(QueryBuilder queryBuilder, Consumer<QueryBuilder>... clauseVerifiers) {
+        BoolQueryBuilder boolQuery = asInstanceOf(BoolQueryBuilder.class, queryBuilder);
+        assertThat(boolQuery.must(), empty());
+        assertThat(boolQuery.filter(), empty());
+        assertThat(boolQuery.mustNot(), empty());
+        assertThat(boolQuery.should(), hasSize(clauseVerifiers.length));
+
+        for (int i = 0; i < clauseVerifiers.length; i++) {
+            clauseVerifiers[i].accept(boolQuery.should().get(i));
+        }
+    }
+
+    @SafeVarargs
+    private final void assertMustQueryBuilder(QueryBuilder queryBuilder, Consumer<QueryBuilder>... clauseVerifiers) {
+        BoolQueryBuilder boolQuery = asInstanceOf(BoolQueryBuilder.class, queryBuilder);
+        assertThat(boolQuery.should(), empty());
+        assertThat(boolQuery.filter(), empty());
+        assertThat(boolQuery.mustNot(), empty());
+        assertThat(boolQuery.must(), hasSize(clauseVerifiers.length));
+
+        for (int i = 0; i < clauseVerifiers.length; i++) {
+            clauseVerifiers[i].accept(boolQuery.must().get(i));
+        }
+    }
+
     public void testParseQuotedStringKeywordFieldQuery() {
         // Single word
         assertTermQueryBuilder(parseKqlQuery(kqlFieldQuery(KEYWORD_FIELD_NAME, quoteString("foo"))), KEYWORD_FIELD_NAME, "foo");
@@ -278,7 +347,10 @@ public class KqlParserFieldQueryTests extends AbstractKqlParserTestCase {
         // Leading operators (AND, OR) are terms of the match query
         assertWildcardQueryBuilder(parseKqlQuery(kqlFieldQuery(KEYWORD_FIELD_NAME, "AND fo*")), KEYWORD_FIELD_NAME, "AND fo*");
         assertWildcardQueryBuilder(parseKqlQuery(kqlFieldQuery(KEYWORD_FIELD_NAME, "OR fo*")), KEYWORD_FIELD_NAME, "OR fo*");
-        assertWildcardQueryBuilder(parseKqlQuery(kqlFieldQuery(KEYWORD_FIELD_NAME, "NOT fo*")), KEYWORD_FIELD_NAME, "NOT fo*");
+        assertMustNotQueryBuilder(
+            parseKqlQuery(kqlFieldQuery(KEYWORD_FIELD_NAME, "NOT fo*")),
+            (subQuery) -> assertWildcardQueryBuilder(subQuery, KEYWORD_FIELD_NAME, "fo*")
+        );
     }
 
     public void testFieldWildcardFieldQueries() {
@@ -291,7 +363,6 @@ public class KqlParserFieldQueryTests extends AbstractKqlParserTestCase {
                 assertThat(parsedQuery.mustNot(), empty());
                 assertThat(parsedQuery.must(), empty());
                 assertThat(parsedQuery.filter(), empty());
-                assertThat(parsedQuery.minimumShouldMatch(), equalTo("1"));
                 assertThat(parsedQuery.should(), hasSize(searchableFields.size()));
 
                 assertThat(

+ 6 - 5
x-pack/plugin/kql/src/test/java/org/elasticsearch/xpack/kql/parser/KqlParserTests.java

@@ -12,6 +12,7 @@ import org.elasticsearch.index.query.QueryBuilder;
 
 import java.io.IOException;
 
+import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.isA;
 
@@ -38,11 +39,11 @@ public class KqlParserTests extends AbstractKqlParserTestCase {
     }
 
     public void testParenthesizedQuery() throws IOException {
-        for (String baseQuuery : readQueries(SUPPORTED_QUERY_FILE_PATH)) {
+        for (String baseQuery : readQueries(SUPPORTED_QUERY_FILE_PATH)) {
             // For each supported query, wrap it into parentheses and check query remains the same.
             // Adding random whitespaces as well and test they are ignored.
-            String parenthesizedQuery = wrapWithRandomWhitespaces("(") + baseQuuery + wrapWithRandomWhitespaces(")");
-            assertThat(parseKqlQuery(parenthesizedQuery), equalTo(parseKqlQuery(baseQuuery)));
+            String parenthesizedQuery = "(" + baseQuery + ")";
+            assertThat(parseKqlQuery(parenthesizedQuery), equalTo(parseKqlQuery(baseQuery)));
         }
     }
 
@@ -80,8 +81,8 @@ public class KqlParserTests extends AbstractKqlParserTestCase {
         {
             KqlParsingException e = assertThrows(KqlParsingException.class, () -> parseKqlQuery("foo: (bar baz AND qux"));
             assertThat(e.getLineNumber(), equalTo(1));
-            assertThat(e.getColumnNumber(), equalTo(15));
-            assertThat(e.getMessage(), equalTo("line 1:15: missing ')' at 'AND'"));
+            assertThat(e.getColumnNumber(), equalTo(22));
+            assertThat(e.getMessage(), containsString("line 1:22: missing ')' at '<EOF>'"));
         }
     }
 }

+ 0 - 2
x-pack/plugin/kql/src/test/resources/unsupported-queries

@@ -41,8 +41,6 @@ mapped_nested: { mapped_string:foo OR (mapped_string_2:foo bar OR foo bar) }
 
 // Missing escape sequences:
 mapped_string: foo:bar
-mapped_string: (foo and bar)
-mapped_string: (foo or bar)
 mapped_string: foo not bar
 mapped_string: foo { bar }
 mapped_string: foo (bar)

+ 64 - 2
x-pack/plugin/kql/src/yamlRestTest/resources/rest-api-spec/test/kql/10_kql_basic_query.yml

@@ -47,7 +47,7 @@ setup:
           }
   - match: { hits.total: 2 }
 
-  # Using the *:* syntax
+  # Using the *: syntax
   - do:
       search:
         index: test-index
@@ -58,7 +58,7 @@ setup:
           }
   - match: { hits.total: 2 }
 
-  # Using the *:* syntax
+  # Using the *: syntax
   - do:
       search:
         index: test-index
@@ -210,3 +210,65 @@ setup:
   - match: { hits.total: 1 }
   - match: { hits.hits.0._id: "doc-1" }
 
+
+---
+"KQL boolean expressions withing field queries":
+  - requires:
+      capabilities:
+        - method: POST
+          path: /_search
+          capabilities: [ kql_query_boolean_field_query ]
+      test_runner_features: capabilities
+      reason: Support for boolean expression within field query is not available.
+
+  - do:
+      search:
+        index: test-index
+        rest_total_hits_as_int: true
+        body: >
+          {
+            "query": { "kql": { "query": "text_field:(foo AND bar)" } }
+          }
+  - match: { hits.total: 1 }
+  - match: { hits.hits.0._id: "doc-1" }
+
+  - do:
+      search:
+        index: test-index
+        rest_total_hits_as_int: true
+        body: >
+          {
+            "query": { "kql": { "query": "text_field:(bar OR baz)" } }
+          }
+  - match: { hits.total: 2 }
+
+  - do:
+      search:
+        index: test-index
+        rest_total_hits_as_int: true
+        body: >
+          {
+            "query": { "kql": { "query": "text_field:(foo AND NOT baz)" } }
+          }
+  - match: { hits.total: 1 }
+  - match: { hits.hits.0._id: "doc-1" }
+
+  - do:
+      search:
+        index: test-index
+        rest_total_hits_as_int: true
+        body: >
+          {
+            "query": { "kql": { "query": "keyword_field:(\"foo bar\" OR \"foo baz\")" } }
+          }
+  - match: { hits.total: 2 }
+
+  - do:
+      search:
+        index: test-index
+        rest_total_hits_as_int: true
+        body: >
+          {
+            "query": { "kql": { "query": "keyword_field:(\"foo bar\" AND \"foo baz\")" } }
+          }
+  - match: { hits.total: 0 }

Some files were not shown because too many files changed in this diff