fix(engine): term/match_phrase on any .keyword multi-field matched 0 docs - #44
Merged
xerj-org merged 1 commit intoJul 27, 2026
Conversation
…docs
`get_field_value` (index.rs, used by `doc_matches_query`'s brute-force
scan path for `term`/`match_phrase`/etc.) had no fallback for ES
multi-field names. A query on `category.keyword`, `manufacturer.keyword`,
or any other `.keyword` sub-field could never resolve a value, because
`_source` only ever stores the PARENT field's raw value -- there's
never a literal `"category.keyword"` key to walk into.
The identical field works fine in a `terms` aggregation, because
aggregations resolve fields through `get_nested_field` (aggs.rs),
which already strips the trailing multi-field segment and retries
against the parent. The query side (`doc_matches_query`) never got
the same treatment, so `term`/`match_phrase` filters on a `.keyword`
field -- one of the single most common ES/Kibana operations, used by
essentially every dashboard filter and "click cell to filter"
interaction -- silently matched zero documents regardless of value.
Confirmed via real Kibana traffic (docker logs) and direct `curl`
reproduction: `term`/`match_phrase` on `manufacturer.keyword`,
`customer_full_name.keyword`, `category.keyword` all returned 0 hits
before this fix, on both scalar- and array-valued underlying fields,
even though the same field's `terms` aggregation bucketed correctly.
Fix: added the same "strip trailing segment, retry against parent"
fallback to `get_field_value`, guarded so it only applies when the
parent resolves to a LEAF value (scalar, or an array with no objects)
-- a real multi-field's parent is always a leaf text/keyword value. A
first version without this guard caused a real regression (5 YAML
`exists_query`/`flattened` test failures): a genuine nested/object
field that's simply absent on a document must NOT be masked by
returning its parent OBJECT, or `exists: {field: "obj.inner_field"}`
would wrongly report the field present just because the parent object
exists.
Verified: `term` queries on `manufacturer.keyword` /
`customer_full_name.keyword` / `category.keyword` now return the
correct non-zero counts, matching their `terms`-aggregation buckets
exactly (1370, 1, 2024, 1218, 1055, ...). Full ES-compat YAML
conformance suite: 1360 passed, 0 failed, 3 skipped -- no regressions
(the is_leaf guard specifically fixes the exists-query regression the
unguarded version introduced).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016XJaZygeuRfZfUg2B8tPKU
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
term/match_phrase/etc. queries against any.keywordmulti-field (category.keyword,manufacturer.keyword, ...) silently matched zero documents, regardless of value — one of the single most common ES/Kibana operations, used by essentially every dashboard filter and "click cell to filter" interaction. Likely the most impactful fix in this batch.Root cause
get_field_value(index.rs, used bydoc_matches_query's brute-force scan path) had no fallback for ES multi-field names._sourceonly ever stores the parent field's raw value — there's never a literal"category.keyword"key to walk into — so a query on the dotted multi-field name could never resolve a value.The identical field works fine in a
termsaggregation, because aggregations resolve fields throughget_nested_field(aggs.rs), which already strips the trailing multi-field segment and retries against the parent. The query side never got the same treatment.Confirmed via real Kibana traffic (captured in
docker logs) and directcurlreproduction:term/match_phraseonmanufacturer.keyword,customer_full_name.keyword,category.keywordall returned 0 hits, on both scalar- and array-valued underlying fields, even though the identical field'stermsaggregation bucketed correctly.Fix
Added the same "strip trailing segment, retry against parent" fallback to
get_field_value— guarded so it only applies when the parent resolves to a leaf value (scalar, or an array with no objects). A real multi-field's parent is always a leaf text/keyword value.An earlier, unguarded version of this fix caused a real regression (5 YAML
exists_query/flattenedtest failures): a genuine nested/object field that's simply absent on a document must NOT be masked by returning its parent object —exists: {field: "obj.inner_field"}would otherwise wrongly report the field present just because the parent object exists. Theis_leafguard fixes that.Test plan
cargo build --release -p xerj-engine -p xerj-api -p xerj-servercargo fmt --check/cargo clippy --no-deps -- -D warnings— cleancargo test --release -p xerj-engine --lib— 156/156 passedtermonmanufacturer.keyword/customer_full_name.keyword/category.keyword: now return correct non-zero counts (1370, 1, 2024, 1218, 1055, ...), matching theirterms-aggregation buckets exactly — previously all 0exists_query/flattenedcases that an earlier unguarded version of this fix broke)🤖 Generated with Claude Code