Skip to content

fix(engine): terms aggregation ignores script, always returns empty buckets - #50

Merged
xerj-org merged 1 commit into
xerj-org:mainfrom
Vinz2168:fix/terms-agg-script-support
Jul 27, 2026
Merged

fix(engine): terms aggregation ignores script, always returns empty buckets#50
xerj-org merged 1 commit into
xerj-org:mainfrom
Vinz2168:fix/terms-agg-script-support

Conversation

@Vinz2168

Copy link
Copy Markdown
Collaborator

Summary

terms aggregation required a top-level field param and bailed out to {"buckets": []} immediately when it was absent — it never checked for script, so any script-based terms aggregation silently returned zero buckets regardless of how many documents matched the query. This breaks any Kibana visualization that buckets by a Painless script rather than a plain field — a common pattern (e.g. a "Heat Map" viz bucketing by an hour-of-day script for its Y-axis).

Root cause

let field = match params.get("field").and_then(Value::as_str) {
    Some(f) => f,
    None => return json!({"buckets": []}),
};

This is the very first thing run_terms does — it bails before touching docs at all if field is missing, with no branch anywhere in the function that looks at params.get("script").

The rest of run_terms (ordering, min_doc_count, include/exclude, partitioning, sub-agg computation, sum_other_doc_count) is agnostic to how a bucket's string key was derived — it just needs Vec<String> per doc — so this only required a key-source abstraction slotted into the field-based call sites, reusing the Painless evaluation engine (crate::painless::eval_painless/PainlessCtx) already proven correct elsewhere in this codebase for script_fields (plain search) and top_hits' runtime_mappings.

Fix

Added a small FieldOrScript enum and terms_agg_bucket_keys() helper:

  • field present → unchanged behavior (extract_field_values)
  • field absent, script present → evaluate the Painless script per-doc via the existing engine, flatten the result the same way a field value would be flattened
  • neither present → unchanged {"buckets": []} fallback (correct ES behavior — a terms agg needs one or the other)

All four call sites inside run_terms that previously hardcoded extract_field_values(doc, field) (main counting loop, min_doc_count: 0 background-fill loop, and both bucket-doc-membership closures used for sub-agg computation and final bucket assembly) now go through this abstraction.

Scope note

This fixes the aggregation-dispatch gap specifically — terms was silently ignoring script entirely. It does not add any new Painless language features; a script's own semantics (e.g. date-value accessor methods) are a separate concern and out of scope here. The same "silently requires field, ignores script" pattern exists in most of the other bucket/metric aggregations in this file (avg, sum, min, max, stats, cardinality, histogram, date_histogram, percentiles) — terms was the one hit in practice and is fixed here; the rest are a known follow-up.

Test plan

  • cargo build --release -p xerj-api -p xerj-engine -p xerj-server
  • cargo fmt --check / cargo clippy --no-deps -- -D warnings — clean
  • Full ES-compat YAML conformance suite: 1360 passed, 0 failed, 3 skipped — no regressions
  • Manual verification: {"terms": {"script": {"source": "return 5;"}}} now produces a real bucket (key: 5.0, doc_count: N) instead of an unconditional empty array

🤖 Generated with Claude Code

… buckets

run_terms required a top-level `field` param and bailed out to
`{"buckets": []}` immediately whenever it was absent -- it never
looked at `script`, so any script-based terms aggregation (e.g.
Kibana's "Heat Map" visualization bucketing by an hour-of-day
Painless script, a common Y-axis dimension) silently returned zero
buckets regardless of how many documents matched the query.

The rest of run_terms (ordering, min_doc_count, include/exclude,
partitioning, sub-agg computation, sum_other_doc_count) is agnostic
to how a bucket's string key was derived, so this only needed a
key-source abstraction (FieldOrScript) slotted into the existing
field-based extract_field_values() call sites. Script evaluation
reuses crate::painless::eval_painless/PainlessCtx, the same engine
already proven correct for script_fields and top_hits scripts.

Note: this fixes the aggregation-dispatch gap specifically. It does
not add any new Painless language features -- a script's own
semantics (e.g. date-value methods) are unrelated to this fix and
out of scope here.

Verified: `{"terms": {"script": {"source": "return 5;"}}}` now
produces real buckets keyed on the script's per-doc result instead
of an unconditional empty array. Full ES-compat YAML conformance
suite: 1360 passed, 0 failed, 3 skipped -- no regressions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016XJaZygeuRfZfUg2B8tPKU
@xerj-org
xerj-org merged commit 038c4f9 into xerj-org:main Jul 27, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants