Skip to content

Elasticsearch API: a term query on a string field matches only the exact value, and no .keyword subfield exists #6787

Description

@DeviousCardi

Repository: quickwit-oss/quickwit
Version: 0.8.2 (quickwit/quickwit:0.8.2)
Reproduced first-hand: yes, on 2026-09-08, against Elasticsearch 8.15.0 as the reference.

What happens

With a dynamically mapped index, a term query for error on a string field:

returns
Elasticsearch 8.15.0, term on level [lower, mixed, norank]
Quickwit 0.8.2, term on level [lower, norank]
Elasticsearch 8.15.0, term on level.keyword [lower, norank]
Quickwit 0.8.2, term on level.keyword []

mixed is the document whose value is "Error Occurred". Elasticsearch's
dynamic mapping makes a string field text — analyzed, so error matches one
of its tokens — with a keyword subfield that is not analyzed. Quickwit's
dynamic mapping declares tokenizer: "raw", so the field is exact-only and no
.keyword subfield is created.

Both halves change results silently. A query written against Elasticsearch
returns fewer rows on the first, and nothing at all on the second — and
field.keyword is the idiom every Elasticsearch dashboard uses for exact
matching and aggregation.

Reproduction

docker run -d --name qw -p 7280:7280 quickwit/quickwit:0.8.2 run
until curl -sf localhost:7280/health/livez >/dev/null; do sleep 1; done

curl -s -X POST localhost:7280/api/v1/indexes -H 'Content-Type: application/json' \
  -d '{"version":"0.8","index_id":"textdemo","doc_mapping":{"mode":"dynamic"}}'

printf '%s\n' \
 '{"index":{"_index":"textdemo"}}' '{"doc":"lower","level":"error"}' \
 '{"index":{"_index":"textdemo"}}' '{"doc":"mixed","level":"Error Occurred"}' \
 '{"index":{"_index":"textdemo"}}' '{"doc":"norank","level":"error"}' \
 | curl -s -X POST localhost:7280/api/v1/_elastic/textdemo/_bulk \
     -H 'Content-Type: application/x-ndjson' --data-binary @-

# A newly created index defaults to commit_timeout_secs: 60.
sleep 70

for f in level level.keyword; do
  curl -s -X POST localhost:7280/api/v1/_elastic/textdemo/_search \
    -H 'Content-Type: application/json' \
    -d "{\"query\":{\"term\":{\"$f\":\"error\"}}}" \
    | python3 -c 'import json,sys; print([h["_source"]["doc"] for h in json.load(sys.stdin)["hits"]["hits"]])'
done

Is this a bug or a mapping choice?

Genuinely unsure, which is why this is a question as much as a report. A
different tokenizer can be configured, and Quickwit's dynamic default is a
reasonable one for a store that is not trying to be Elasticsearch everywhere.

But the default is what a user gets, and the endpoint is
/api/v1/_elastic/, which sets the expectation that Elasticsearch queries
work. If matching Elasticsearch's dynamic mapping is out of scope, saying so in
the Elasticsearch-compatibility documentation would be enough — the surprise is
what costs people time, not the choice.

Where this came from

SpecMatrix, a conformance corpus for observability backends. The check is
cases/es-bulk/text-vs-keyword-term.yaml, whose expectation was taken by
running the query against Elasticsearch and copying what it returned.


Found by SpecMatrix, a conformance corpus for observability backends. Happy to be told this is configuration or already known — the check will record whichever it turns out to be.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions