Skip to content

Fix KQL case-sensitivity for keyword fields in ES|QL - #135776

Merged
afoucret merged 3 commits into
elastic:mainfrom
afoucret:esql-kql-case-sensitivity
Oct 1, 2025
Merged

afoucret merged 3 commits into
elastic:mainfrom
afoucret:esql-kql-case-sensitivity

Conversation

@afoucret

@afoucret afoucret commented Oct 1, 2025

Copy link
Copy Markdown
Contributor

This pull request addresses an inconsistency in the KQL implementation used by ES|QL, where queries on keyword fields were incorrectly case-insensitive by default. This behavior contradicted the KQL documentation and the behavior of KQL in Kibana, which both specify exact, case-sensitive matching for keyword fields.

The change adjusts the KQL parsing context to ensure that matching on keyword fields is case-sensitive, aligning the ES|QL implementation with the expected standard.

Fixes #135772

The KQL implementation used by the ES|QL kql() function incorrectly defaulted to case-insensitive matching for keyword fields. This behavior was inconsistent with the KQL specification and the implementation in Kibana, which both perform exact, case-sensitive matches on keyword fields.

This commit corrects the default behavior to enforce case-sensitivity for keyword fields within ES|QL, aligning it with the documented and expected behavior.

Fixes elastic#135772
@elasticsearchmachine elasticsearchmachine added needs:triage Requires assignment of a team area label v9.2.0 labels Oct 1, 2025
@afoucret afoucret added :Search Relevance/Search Catch all for Search Relevance auto-backport Automatically create backport pull requests when merged >bug and removed needs:triage Requires assignment of a team area label labels Oct 1, 2025
@elasticsearchmachine elasticsearchmachine added the Team:Search Relevance Meta label for the Search Relevance team in Elasticsearch label Oct 1, 2025
@elasticsearchmachine

Copy link
Copy Markdown
Collaborator

Hi @afoucret, I've created a changelog YAML for you.

@elasticsearchmachine

Copy link
Copy Markdown
Collaborator

Pinging @elastic/es-search-relevance (Team:Search Relevance)

@afoucret afoucret added v8.19.6 v9.1.6 v9.0.9 v8.18.9 and removed Team:Search Relevance Meta label for the Search Relevance team in Elasticsearch labels Oct 1, 2025
@afoucret
afoucret requested a review from carlosdelest October 1, 2025 13:56
@elasticsearchmachine elasticsearchmachine added the Team:Search Relevance Meta label for the Search Relevance team in Elasticsearch label Oct 1, 2025

@carlosdelest carlosdelest left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@@ -170,7 +170,7 @@ public void testCaseInsensitiveWildcardQuery() throws IOException {
for (boolean caseInsensitive : List.of(true, false)) {
KqlQueryBuilder kqlQuery = new KqlQueryBuilder(KEYWORD_FIELD_NAME + ": foo*");
// Check case case_insensitive is true by default

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Check case case_insensitive is true by default
// Check case case_insensitive is false by default

@@ -190,7 +190,7 @@ public void testCaseInsensitiveTermQuery() throws IOException {
for (boolean caseInsensitive : List.of(true, false)) {
KqlQueryBuilder kqlQuery = new KqlQueryBuilder(KEYWORD_FIELD_NAME + ": foo");
// Check case case_insensitive is true by default

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Check case case_insensitive is true by default
// Check case case_insensitive is false by default

@@ -170,7 +170,7 @@ public void testCaseInsensitiveWildcardQuery() throws IOException {
for (boolean caseInsensitive : List.of(true, false)) {
KqlQueryBuilder kqlQuery = new KqlQueryBuilder(KEYWORD_FIELD_NAME + ": foo*");
// Check case case_insensitive is true by default

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Check case case_insensitive is true by default
// Check case case_insensitive is false by default

@@ -190,7 +190,7 @@ public void testCaseInsensitiveTermQuery() throws IOException {
for (boolean caseInsensitive : List.of(true, false)) {
KqlQueryBuilder kqlQuery = new KqlQueryBuilder(KEYWORD_FIELD_NAME + ": foo");
// Check case case_insensitive is true by default

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Check case case_insensitive is true by default
// Check case case_insensitive is false by default

@elasticsearchmachine

Copy link
Copy Markdown
Collaborator

💚 Backport successful

Status Branch Result
✅ 8.19
✅ 9.1
✅ 9.0
✅ 8.18

afoucret added a commit to afoucret/elasticsearch that referenced this pull request Oct 1, 2025
afoucret added a commit to afoucret/elasticsearch that referenced this pull request Oct 1, 2025
@swg0101

swg0101 commented Oct 1, 2025 •

Copy link
Copy Markdown

Despite this being an inconsistent behavior, we actually have been exploiting this behavior in ES|QL to do case-insensitive searches on keywords where we ordinarily would have to resort to other hacks, like doing a regex DSL with case-insensitive set to true, or doing a to_lower()/upper() in ES|QL. That being said, those functions are much slower than the KQL() function.

It may be wise for there to be a toggle on this so users can choose the behavior they want to see. There has also been a long standing ticket asking for a similar behavior, like this: elastic/kibana#55378

@afoucret

afoucret commented Oct 2, 2025

Copy link
Copy Markdown
Contributor Author

@swg0101 got your point.

I think:

  • the default behavior should be consistent
  • we should expose the feature as an option of the KQL function

I opened an issue that describe exactly this: #135823.

@swg0101

swg0101 commented Oct 2, 2025

Copy link
Copy Markdown

That sounds great. Perhaps the result can be slightly more user-friendly, especially if there are no other variables that would need to be passed. Perhaps naming the function KQLi/kqli with and passing the case_insensitive: true parameter would be easier?

Just like using the DSL like this would be cumbersome:

{
  "kql": {
    "query": "user.name: student01",
    "case_insensitive": true
  }
}

Perhaps adding a toggle in Classic Discover would make it more friendly, although it got closed as not planned under: elastic/kibana#55378. Also, out of scope of this ticket... :)

@afoucret

afoucret commented Oct 2, 2025

Copy link
Copy Markdown
Contributor Author

@swg0101 There are probably some improvements that can be done in the UX but I am very reluctant to introduce new functions for things that can accessible through an optional parameter.

My plan for KQL function is to expose the case_insensitive but also to introduce other params that are supported by the query DSL:

  • default field: list of field to use when no field is specified
  • timezone: the timezone to use for date / time queries

Last but mot least, our plan with the KQL query is to allow users to migrate to ES|QL more easily. We do not want to add new features to the KQL language and would prefer that people rewrite their queries using native ES|QL syntax / functions.
If there are gaps, it would be better that we fix it in ES|QL directly instead of calling KQL to the rescue and call a query language inside a query language. My gut feeling is that we won't add new features to KQL once the above named params are implemented.

@swg0101

swg0101 commented Oct 2, 2025 •

Copy link
Copy Markdown

That makes sense. I am guessing that as long as autocomplete works well, then it probably wouldn't be that much of a problem.
To your point about migrating from KQL --> ES|QL, I think the top few things that drag me back into KQL are as follows:

  1. Native ES|QL is still relatively weak when dealing with array values. For example, seeing if a particular field, like related.ip contains a value, regardless if it's in a scalar form or a multivalue (array) form. KQL couldn't care less about what type the field is in, and works like a "generic contains" match, versus ES|QL being much more literal in its matching (e.g., == vs : in KQL). This specific limitation also makes matching IPs difficult, such as something like related.ip: 192.168.0.0/16, where related.ip could contain either one IP or multiple IPs, depending on the document(s) being searched.

  2. The absolute imposition of a strict limit with no pagination support, as described in ESQL: Support pagination #100000.

  3. I am guessing this very discussion about case insensitivity would be one as well, as == is strictly case sensitive, and there is no option to change this on the fly without falling back to KQL().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-backport Automatically create backport pull requests when merged >bug :Search Relevance/Search Catch all for Search Relevance Team:Search Relevance Meta label for the Search Relevance team in Elasticsearch v8.18.9 v8.19.6 v9.0.9 v9.1.6 v9.2.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ES|QL] KQL defaults to case-insensitive matching on keyword fields (inconsistent with docs & Kibana behavior)

4 participants