fix(engine): term/terms on keyword ARRAYS match only the first element - #40
Merged
Conversation
Root cause of the WordPress reachability miss (structured "who calls X" returned 1 vs grep's 14): a keyword field stores one value per doc, and for an ARRAY only element [0] is kept -- memtable push_field pushes `first_str`, and the segment KeywordColumn.ords is Vec<u32> (one ordinal per doc, structurally single-valued). Aggregations use a separate multi-valued path and FTS joins all elements, so terms-aggs and `match` see every element but exact `term` sees only [0]. This breaks ES multi-valued keyword semantics and causes silent false negatives. Memtable half of the fix: doc_values_term_query / _term_indices / _terms_query bail (return None) for fields in `array_fields`, mirroring the existing keyword_has_whitespace bail, so the caller falls through to the array-aware source scan (json_values_equal already does array membership). Compiles clean. Segment half (design in docs): the flush builder already removes `multi_valued` fields from the columnar doc-values so the segment term reader abandons to the stored-source scan; remaining work is to close every ingest/flush + fused columnar path (or make KeywordColumn multi-valued). Regression test test_term_matches_non_first_array_element added, #[ignore]d until that lands. Token impact (real WP index, one reachability query): scroll-graph-to-client ~482,800 tokens or grep+read ~4,300 -> one fixed `terms` query ~170 tokens (2823x / 25x). Restores composable structured graph queries: audit cost goes from O(index size) to O(matching results). Full write-up + boolean-term follow-up in docs/research/xerj-keyword-array-term-fix.md. Co-Authored-By: Xerj Squad A <noreply@xerj.org>
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.
Problem
A reverse call-graph query — "who calls
wp_safe_remote_get" — returned 1result from XERJ's structured
termindex, whilegrepfound 14 files and XERJfull-text (
match) found 9. A false "not reachable" is the worst error in asecurity audit.
Root cause (proven)
A keyword field stores one value per document, and for an array value XERJ
keeps only element
[0]:DocValues::push_field, theValue::Arrayarm, doeskcol.push(first_str).KeywordColumn.ords: Vec<u32>is one ordinal per doc — structurally single-valued.Aggregations use a separate multi-valued path and FTS joins all elements, so
terms-aggs andmatchsee every element but exacttermsees only[0]. Thisbreaks ES multi-valued keyword semantics.
Reproduced end-to-end (
calls: ["first_fn","second_fn","wp_safe_remote_get"]):Fix
The always-correct source scan already handles arrays (
json_values_equaldoesarray membership), so route
term/termson array fields to it:doc_values_term_query,doc_values_term_indices,and
doc_values_terms_querybail (return None) for fields inarray_fields,mirroring the existing
keyword_has_whitespacebail. Compiles clean; no regressionsin the touched crate.
multi_valuedset covers everyingest path and route the remaining fused-columnar
termpath to the scan (ormake
KeywordColumnmulti-valued). A regression testtest_term_matches_non_first_array_elementis included,#[ignore]d until then.Also found (filed for the series)
Boolean
termmatches as a string:{term:{f:true}}→ 0 while{term:{f:"true"}}matches. JSON booleans must coerce to the stored
"true"/"false"form.Impact (measured, real WP index of 11,990 functions)
One reverse-reachability query:
termsquery → exact callers as facts: ~170 tokens (2,823× / 25×)Restores composable structured graph queries: audit cost goes from O(index size)
to O(matching results), and makes the pre-AST index strictly better than grep for
reachability. Full write-up:
docs/research/xerj-keyword-array-term-fix.md.Files
engine/crates/xerj-engine/src/memtable.rs— array-field bail in 3 fast pathsengine/crates/xerj-engine/tests/integration.rs— reproduction test (#[ignore])docs/research/xerj-keyword-array-term-fix.md— root cause + token math🤖 Generated with Claude Code