Pipeline scripts #
Three pipeline aggregations evaluate a user-supplied script per bucket:
bucket_script (compute a new metric per bucket),
bucket_selector (keep/drop predicate), and
moving_fn (window statistic). moving_avg is a
moving_fn with a fixed averaging script. The same engine powers
scripted_metric (scripts per document
instead of per bucket) and the
scripted value
sources on the single-value
metric aggs (sum, avg, min, max, value_count, cardinality),
the per-hit
script_fields
of a search request (fetch phase), and the
scripted queries —
script_score, script filters, the _script sort.
Scripts run in a sandboxed Rhai engine — not Painless. Simple ES
arithmetic (params.r / params.n, MovingFunctions.max(values))
compiles unchanged; Java interop, classes and method calls beyond the
helpers below do not.
A guided example #
Revenue per day, a per-bucket ratio, and a rolling 7-day maximum — one request, three scripted and non-scripted aggregations side by side:
A selector with a compound script — statements, a let binding, and a
boolean expression as the result. Per-order revenue must exceed 50 AND
total revenue 1500: t-shirt (30/order) and mug (1350 total) drop, hat
and sticker stay:
The selector itself leaves no trace in the response — it only filters the bucket list of the aggregation that owns it.
Variables #
| Variable | Available in | Meaning |
|---|---|---|
params.<name> | bucket_script, bucket_selector | Sibling metric named by buckets_path ({"r": "revenue"} → params.r). |
<name> | bucket_script, bucket_selector | Same value, bare spelling — r instead of params.r. |
values | moving_fn | Floating-point array of the current window, oldest first. |
Both spellings of a buckets_path variable work; params.<name> is
what stock ES scripts use. Metrics arrive as floats.
The language #
The last expression is the script’s result. Statements available:
let ratio = params.r / params.n; // binding (and // comments)
if ratio > 10 { 1.0 } else { 0.0 } // conditional expression
for v in values { s += v; } // iterate a window
let i = 0; while i < 3 { i += 1; } // loop
switch params.n { 1 => 10.0, _ => 20.0 }
Watch integer division: 5 / 2 is 2, and any int-by-int
expression truncates — values.len() / 2 over a 3-bucket window is
1, not 1.5. When either side is a float the result is a float:
params.r / 2 and 5.0 / 2 are both fine (params.* variables and
values[...] elements are floats). Adding .0 to a literal never
hurts.
Operators: + - * / %, ** (power), comparisons == != < <= > >=,
logical && || !. There is no ?: ternary — use an if/else
expression instead.
Arrays support values.len(), indexing (values[0],
values[values.len() - 1]), iteration, closures, and
values.filter(|v| v > 15) / values.map(|v| v * 2). The whole
expression still must reduce to a number for bucket_script — e.g.
values.filter(|v| v > 100).len() is a valid metric,
values.filter(|v| v > 100) alone is not (a non-numeric result becomes
a null value, not an error).
A few more script shapes that work:
values.max() - values.min() // spread of the window
let total = mf_sum(values); values[values.len() - 1] / total
let s = 0.0; for v in values { s += v; } s / values.len()
Math #
x.sqrt(), x.abs() (also abs(x)), x.floor(), x.round(), and
x ** n for powers. ceil, the pow(x, n) function form, and ?:
are not available.
MovingFunctions helpers #
Elasticsearch moving_fn scripts call helpers through the
MovingFunctions namespace. Those calls work unchanged:
| Helper | Result |
|---|---|
MovingFunctions.max(values) / min(values) | Window maximum / minimum. |
MovingFunctions.sum(values) | Window sum (0 for an empty window). |
MovingFunctions.unweightedAvg(values) | Arithmetic mean. |
MovingFunctions.linearWeightedAvg(values) | Mean with linearly rising weights — the newest value weighs n, the oldest 1. |
MovingFunctions.stdDev(values, avg) | Population standard deviation around the given mean. |
MovingFunctions.ewma(values, alpha) | Exponentially weighted moving average: s = alpha·x + (1-alpha)·s. |
The same functions also work as array methods / plain calls:
values.max(), values.min(), mf_sum(values), mf_ewma(values, 0.5). Other names under MovingFunctions fail to compile.
bucket_selector truthiness #
The predicate result is interpreted as: a boolean, taken as-is; a
number, true when non-zero (0.0 is false); anything else — a string,
an array — defaults to true (keep the bucket). Prefer explicit
comparisons; "enough" is truthy, params.r > 150 says what you mean.
Use cases #
- Derived per-bucket metrics —
bucket_scriptratios and scores: average order value (params.r / params.n), conversion rates, weighted quality scores. - Metric-based bucket filtering —
bucket_selectoras theHAVINGclause of aggregations: keep buckets above a threshold, drop buckets with too little data. - Rolling window statistics —
moving_fnovervalues: trailing max/min envelopes, windowed sums, stability bands fromstdDev. - Conditional logic in one request —
letbindings andif/elseexpressions replace the post-processing pass that would otherwise run client-side over the raw buckets.
Failure and limits #
A script that fails to compile yields a null metric (moving_fn
returns no buckets). A runtime failure — an empty window where a
statistic is undefined, a non-numeric element — nulls that bucket’s
value; bucket_selector keeps the bucket.
Each evaluation runs under sandbox limits: 100,000 operations, expression depth 64 (32 inside function calls), strings up to 4 KB, arrays up to 10,000 elements, maps up to 1,000 entries. Compiled scripts are cached by source text (bounded at 1,024 entries — a flood of unique scripts flushes the cache and recompiles rather than growing memory), so identical scripts compile once.
Related #
- The scripted pipeline aggregations:
bucket_script,bucket_selector,moving_fn— andmoving_avg, its fixed-average specialization - Document-level scripting:
scripted_metric,script_fields,script_score— documents theMovingFunctionshelpers this engine re-implements