Scripted metric aggregations
The scripted_metric
aggregation is a multi-value metric aggregation that returns metrics calculated from a specified script. A script has four phases, init
, map
, combine
, and reduce
, which are run in order by each aggregation and allow you to combine results from your documents.
All four scripts share a mutable object called state
that is defined by you. The state
is local to each shard during the init
, map
, and combine
phases. The result is passed into the states
array for the reduce
phase. Therefore, each shard’s state
is independent until the shards are combined in the reduce
step.
Parameters
The scripted_metric
aggregation takes the following parameters.
Parameter | Data type | Required/Optional | Description |
---|---|---|---|
init_script | String | Optional | A script that executes once per shard before any documents are processed. Used to set up an initial state (for example, initialize counters or lists in a state object). If not provided, the state starts as an empty object on each shard. |
map_script | String | Required | A script that executes for each document collected by the aggregation. This script updates the state based on the document’s data. For example, you might check the field’s value and then increment a counter or calculate a running sum in the state . |
combine_script | String | Required | A script that executes once per shard after all documents on that shard have been processed by the map_script . This script aggregates the shard’s state into a single result to be sent back to the coordinating node. This script is used to finalize the computation for one shard (for example, summing up counters or totals stored in the state ). The script should return the consolidated value or structure for its shard. |
reduce_script | String | Required | A script that executes once on the coordinating node after receiving combined results from all shards. This script receives a special variable states , which is an array containing each shard’s output from the combine_script . The reduce_script iterates over states and produces the final aggregation output (for example, adding shard sums or merging maps of counts). The value returned by the reduce_script is the value reported in the aggregation results. |
params | Object | Optional | User-defined parameters accessible from all scripts except reduce_script . |
Allowed return types
Scripts can use any valid operation and object internally. However, the data you store in state
or return from any script must be of one of the allowed types. This restriction exists because the intermediate state
needs to be sent between nodes. The following types are allowed:
- Primitive types:
int
,long
,float
,double
,boolean
- String
- Map (with keys and values of only allowed types: primitive types, string, map, or array)
- Array (containing only allowed types: primitive types, string, map, or array)
The state
can be a number, a string, a map
(object) or an array (list), or a combination of these. For example, you can use a map
to accumulate multiple counters, an array to collect values, or a single number to keep a running sum. If you need to return multiple metrics, you can store them in a map
or array. If you return a map
as the final value from the reduce_script
, the aggregation result contains an object. If you return a single number or string, the result is a single value.
Using parameters in scripts
You can optionally pass custom parameters to your scripts using the params
field. This is a user-defined object whose contents become variables available in your init_script
, map_script
, and combine_script
. The reduce_script
does not directly receive params
because by the reduce
phase, all needed data must be in the states
array. If you need a constant in the reduce
phase, you can include it as part of each shard’s state
or use a stored script. All parameters must be defined inside the global params
object. This ensures that they are shared across the different script phases. If you do not specify any params
, the params
object is empty.
For example, you can supply a threshold
or field
name in params
and then reference params.threshold
or params.field
in your scripts:
"scripted_metric": {
"params": {
"threshold": 100,
"field": "amount"
},
"init_script": "...",
"map_script": "...",
"combine_script": "...",
"reduce_script": "..."
}
Examples
The following examples demonstrate different ways to use scripted_metric
.
Calculating net profit from transactions
The following example demonstrates the use of the scripted_metric
aggregation to compute a custom metric that is not directly supported by built-in aggregations. The dataset represents financial transactions, in which each document is classified as either a sale
(income) or a cost
(expense) and includes an amount
field. The objective is to calculate the total net profit by subtracting the total cost from the total sales across all documents.
Create an index:
PUT transactions
{
"mappings": {
"properties": {
"type": { "type": "keyword" },
"amount": { "type": "double" }
}
}
}
Index four transactions, two sales (amounts 80
and 130
), and two costs (10
and 30
):
PUT transactions/_bulk?refresh=true
{ "index": {} }
{ "type": "sale", "amount": 80 }
{ "index": {} }
{ "type": "cost", "amount": 10 }
{ "index": {} }
{ "type": "cost", "amount": 30 }
{ "index": {} }
{ "type": "sale", "amount": 130 }
To run a search with a scripted_metric
aggregation to calculate the profit, use the following scripts:
- The
init_script
creates an empty list used to store transaction values for each shard. - The
map_script
adds each document’s amount to thestate.transactions
list as a positive number if the type issale
or as a negative number if the type iscost
. By the end of themap
phase, each shard has astate.transactions
list representing its income and expenses. - The
combine_script
processes thestate.transactions
list and computes a singleshardProfit
value for the shard. TheshardProfit
is then returned as the shard’s output. - The
reduce_script
runs on the coordinating node, receiving thestates
array, which holds theshardProfit
value from each shard. It checks for null entries, adds these values to compute the overall profit, and returns the final result.
The following request contains all described scripts:
GET transactions/_search
{
"size": 0,
"aggs": {
"total_profit": {
"scripted_metric": {
"init_script": "state.transactions = []",
"map_script": "state.transactions.add(doc['type'].value == 'sale' ? doc['amount'].value : -1 * doc['amount'].value)",
"combine_script": "double shardProfit = 0; for (t in state.transactions) { shardProfit += t; } return shardProfit;",
"reduce_script": "double totalProfit = 0; for (p in states) { if (p != null) { totalProfit += p; }} return totalProfit;"
}
}
}
}
The response returns the total_profit
:
{
...
"hits": {
"total": {
"value": 4,
"relation": "eq"
},
"max_score": null,
"hits": []
},
"aggregations": {
"total_profit": {
"value": 170
}
}
}
Categorizing HTTP response codes
The following example demonstrates a more advanced use of the scripted_metric
aggregation for returning multiple values within a single aggregation. The dataset consists of web server log entries, each containing an HTTP response code. The goal is to classify the responses into three categories: successful responses (2xx status codes), client or server errors (4xx or 5xx status codes), and other responses (1xx or 3xx status codes). This classification is implemented by maintaining counters within a map-based aggregation state
.
Create a sample index:
PUT logs
{
"mappings": {
"properties": {
"response": { "type": "keyword" }
}
}
}
Add sample documents with a variety of response codes:
PUT logs/_bulk?refresh=true
{ "index": {} }
{ "response": "200" }
{ "index": {} }
{ "response": "201" }
{ "index": {} }
{ "response": "404" }
{ "index": {} }
{ "response": "500" }
{ "index": {} }
{ "response": "304" }
The state
(on each shard) is a map
with three counters: error
, success
, and other
.
To run a scripted metric aggregation that counts the categories, use the following scripts:
- The
init_script
initializes counters forerror
,success
, andother
to0
. - The
map_script
examines each document’s response code and increments the relevant counter based on the response code. - The
combine_script
returns thestate.responses map
for that shard. - The
reduce_script
merges the array of maps (states
) from all shards. Thus, it creates a new combinedmap
and adds theerror
,success
, andother
counts from each shard’smap
. This combinedmap
is returned as the final result.
The following request contains all described scripts:
GET logs/_search
{
"size": 0,
"aggs": {
"responses_by_type": {
"scripted_metric": {
"init_script": "state.responses = new HashMap(); state.responses.put('success', 0); state.responses.put('error', 0); state.responses.put('other', 0);",
"map_script": """
String code = doc['response'].value;
if (code.startsWith("5") || code.startsWith("4")) {
// 4xx or 5xx -> count as error
state.responses.error += 1;
} else if (code.startsWith("2")) {
// 2xx -> count as success
state.responses.success += 1;
} else {
// anything else (e.g., 1xx, 3xx, etc.) -> count as other
state.responses.other += 1;
}
""",
"combine_script": "return state.responses;",
"reduce_script": """
Map combined = new HashMap();
combined.error = 0;
combined.success = 0;
combined.other = 0;
for (state in states) {
if (state != null) {
combined.error += state.error;
combined.success += state.success;
combined.other += state.other;
}
}
return combined;
"""
}
}
}
}
The response returns three values in the value
object, demonstrating how a scripted metric can return multiple metrics at once by using a map
in the state
:
{
...
"hits": {
"total": {
"value": 5,
"relation": "eq"
},
"max_score": null,
"hits": []
},
"aggregations": {
"responses_by_type": {
"value": {
"other": 1,
"success": 2,
"error": 2
}
}
}
}
Managing empty buckets (no documents)
When using a scripted_metric
aggregation as a subaggregation within a bucket aggregation (such as terms
), it is important to account for buckets that contain no documents on certain shards. In such cases, those shards return a null
value for the aggregation state
. During the reduce_script
phase, the states
array may therefore include null
entries corresponding to these shards. To ensure reliable execution, the reduce_script
must be designed to handle null
values gracefully. A common approach is to include a conditional check, such as if (state != null)
, before accessing or operating on each state
. Failure to implement such checks can result in runtime errors when processing empty buckets across shards.
Performance considerations
Because scripted metrics run custom code for every document and therefore potentially accumulate a large in-memory state
, they can be slower than built-in aggregations. The intermediate state
from each shard must be serialized in order to send it to the coordinating node. Therefore if your state
is very large, it can consume a lot of memory and network bandwidth. To keep your searches efficient, make your scripts as lightweight as possible and avoid accumulating unnecessary data in the state
. Use the combine stage to shrink state
data before sending, as demonstrated in Calculating net profit from transactions, and only collect the values that you truly need to produce the final metric.