Pipeline scripts

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 #

VariableAvailable inMeaning
params.<name>bucket_script, bucket_selectorSibling metric named by buckets_path ({"r": "revenue"} → params.r).
<name>bucket_script, bucket_selectorSame value, bare spelling — r instead of params.r.
valuesmoving_fnFloating-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:

HelperResult
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_script ratios and scores: average order value (params.r / params.n), conversion rates, weighted quality scores.
  • Metric-based bucket filtering — bucket_selector as the HAVING clause of aggregations: keep buckets above a threshold, drop buckets with too little data.
  • Rolling window statistics — moving_fn over values: trailing max/min envelopes, windowed sums, stability bands from stdDev.
  • Conditional logic in one request — let bindings and if/else expressions 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.

Calendar October 7, 2026
Edit Edit this page