#6787·quickwit

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

Author: DeviousCardiCreated Sep 8, 2026Updated Sep 8, 2026

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

bash
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.