Skip to content

Commit 0efe5bc

Browse files
rationullrjernst
authored andcommitted
Migrate scripted metric aggregation scripts to ScriptContext design (#30111)
* Migrate scripted metric aggregation scripts to ScriptContext design #29328 * Rename new script context container class and add clarifying comments to remaining references to params._agg(s) * Misc cleanup: make mock metric agg script inner classes static * Move _score to an accessor rather than an arg for scripted metric agg scripts This causes the score to be evaluated only when it's used. * Documentation changes for params._agg -> agg * Migration doc addition for scripted metric aggs _agg object change * Rename "agg" Scripted Metric Aggregation script context variable to "state" * Rename a private base class from ...Agg to ...State that I missed in my last commit * Clean up imports after merge
1 parent fffcf93 commit 0efe5bc

File tree

12 files changed

+616
-108
lines changed

12 files changed

+616
-108
lines changed

docs/java-api/aggregations/metrics/scripted-metric-aggregation.asciidoc

+9-9
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ Here is an example on how to create the aggregation request:
1313
--------------------------------------------------
1414
ScriptedMetricAggregationBuilder aggregation = AggregationBuilders
1515
.scriptedMetric("agg")
16-
.initScript(new Script("params._agg.heights = []"))
17-
.mapScript(new Script("params._agg.heights.add(doc.gender.value == 'male' ? doc.height.value : -1.0 * doc.height.value)"));
16+
.initScript(new Script("state.heights = []"))
17+
.mapScript(new Script("state.heights.add(doc.gender.value == 'male' ? doc.height.value : -1.0 * doc.height.value)"));
1818
--------------------------------------------------
1919

2020
You can also specify a `combine` script which will be executed on each shard:
@@ -23,9 +23,9 @@ You can also specify a `combine` script which will be executed on each shard:
2323
--------------------------------------------------
2424
ScriptedMetricAggregationBuilder aggregation = AggregationBuilders
2525
.scriptedMetric("agg")
26-
.initScript(new Script("params._agg.heights = []"))
27-
.mapScript(new Script("params._agg.heights.add(doc.gender.value == 'male' ? doc.height.value : -1.0 * doc.height.value)"))
28-
.combineScript(new Script("double heights_sum = 0.0; for (t in params._agg.heights) { heights_sum += t } return heights_sum"));
26+
.initScript(new Script("state.heights = []"))
27+
.mapScript(new Script("state.heights.add(doc.gender.value == 'male' ? doc.height.value : -1.0 * doc.height.value)"))
28+
.combineScript(new Script("double heights_sum = 0.0; for (t in state.heights) { heights_sum += t } return heights_sum"));
2929
--------------------------------------------------
3030

3131
You can also specify a `reduce` script which will be executed on the node which gets the request:
@@ -34,10 +34,10 @@ You can also specify a `reduce` script which will be executed on the node which
3434
--------------------------------------------------
3535
ScriptedMetricAggregationBuilder aggregation = AggregationBuilders
3636
.scriptedMetric("agg")
37-
.initScript(new Script("params._agg.heights = []"))
38-
.mapScript(new Script("params._agg.heights.add(doc.gender.value == 'male' ? doc.height.value : -1.0 * doc.height.value)"))
39-
.combineScript(new Script("double heights_sum = 0.0; for (t in params._agg.heights) { heights_sum += t } return heights_sum"))
40-
.reduceScript(new Script("double heights_sum = 0.0; for (a in params._aggs) { heights_sum += a } return heights_sum"));
37+
.initScript(new Script("state.heights = []"))
38+
.mapScript(new Script("state.heights.add(doc.gender.value == 'male' ? doc.height.value : -1.0 * doc.height.value)"))
39+
.combineScript(new Script("double heights_sum = 0.0; for (t in state.heights) { heights_sum += t } return heights_sum"))
40+
.reduceScript(new Script("double heights_sum = 0.0; for (a in states) { heights_sum += a } return heights_sum"));
4141
--------------------------------------------------
4242

4343

docs/reference/aggregations/metrics/scripted-metric-aggregation.asciidoc

+25-39
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ POST ledger/_search?size=0
1515
"aggs": {
1616
"profit": {
1717
"scripted_metric": {
18-
"init_script" : "params._agg.transactions = []",
19-
"map_script" : "params._agg.transactions.add(doc.type.value == 'sale' ? doc.amount.value : -1 * doc.amount.value)", <1>
20-
"combine_script" : "double profit = 0; for (t in params._agg.transactions) { profit += t } return profit",
21-
"reduce_script" : "double profit = 0; for (a in params._aggs) { profit += a } return profit"
18+
"init_script" : "state.transactions = []",
19+
"map_script" : "state.transactions.add(doc.type.value == 'sale' ? doc.amount.value : -1 * doc.amount.value)", <1>
20+
"combine_script" : "double profit = 0; for (t in state.transactions) { profit += t } return profit",
21+
"reduce_script" : "double profit = 0; for (a in states) { profit += a } return profit"
2222
}
2323
}
2424
}
@@ -67,8 +67,7 @@ POST ledger/_search?size=0
6767
"id": "my_combine_script"
6868
},
6969
"params": {
70-
"field": "amount", <1>
71-
"_agg": {} <2>
70+
"field": "amount" <1>
7271
},
7372
"reduce_script" : {
7473
"id": "my_reduce_script"
@@ -82,8 +81,7 @@ POST ledger/_search?size=0
8281
// TEST[setup:ledger,stored_scripted_metric_script]
8382

8483
<1> script parameters for `init`, `map` and `combine` scripts must be specified
85-
in a global `params` object so that it can be share between the scripts.
86-
<2> if you specify script parameters then you must specify `"_agg": {}`.
84+
in a global `params` object so that it can be shared between the scripts.
8785

8886
////
8987
Verify this response as well but in a hidden block.
@@ -108,7 +106,7 @@ For more details on specifying scripts see <<modules-scripting, script documenta
108106

109107
==== Allowed return types
110108

111-
Whilst any valid script object can be used within a single script, the scripts must return or store in the `_agg` object only the following types:
109+
Whilst any valid script object can be used within a single script, the scripts must return or store in the `state` object only the following types:
112110

113111
* primitive types
114112
* String
@@ -121,10 +119,10 @@ The scripted metric aggregation uses scripts at 4 stages of its execution:
121119

122120
init_script:: Executed prior to any collection of documents. Allows the aggregation to set up any initial state.
123121
+
124-
In the above example, the `init_script` creates an array `transactions` in the `_agg` object.
122+
In the above example, the `init_script` creates an array `transactions` in the `state` object.
125123

126124
map_script:: Executed once per document collected. This is the only required script. If no combine_script is specified, the resulting state
127-
needs to be stored in an object named `_agg`.
125+
needs to be stored in the `state` object.
128126
+
129127
In the above example, the `map_script` checks the value of the type field. If the value is 'sale' the value of the amount field
130128
is added to the transactions array. If the value of the type field is not 'sale' the negated value of the amount field is added
@@ -137,8 +135,8 @@ In the above example, the `combine_script` iterates through all the stored trans
137135
and finally returns `profit`.
138136

139137
reduce_script:: Executed once on the coordinating node after all shards have returned their results. The script is provided with access to a
140-
variable `_aggs` which is an array of the result of the combine_script on each shard. If a reduce_script is not provided
141-
the reduce phase will return the `_aggs` variable.
138+
variable `states` which is an array of the result of the combine_script on each shard. If a reduce_script is not provided
139+
the reduce phase will return the `states` variable.
142140
+
143141
In the above example, the `reduce_script` iterates through the `profit` returned by each shard summing the values before returning the
144142
final combined profit which will be returned in the response of the aggregation.
@@ -166,13 +164,11 @@ at each stage of the example above.
166164

167165
===== Before init_script
168166

169-
No params object was specified so the default params object is used:
167+
`state` is initialized as a new empty object.
170168

171169
[source,js]
172170
--------------------------------------------------
173-
"params" : {
174-
"_agg" : {}
175-
}
171+
"state" : {}
176172
--------------------------------------------------
177173
// NOTCONSOLE
178174

@@ -184,10 +180,8 @@ Shard A::
184180
+
185181
[source,js]
186182
--------------------------------------------------
187-
"params" : {
188-
"_agg" : {
189-
"transactions" : []
190-
}
183+
"state" : {
184+
"transactions" : []
191185
}
192186
--------------------------------------------------
193187
// NOTCONSOLE
@@ -196,10 +190,8 @@ Shard B::
196190
+
197191
[source,js]
198192
--------------------------------------------------
199-
"params" : {
200-
"_agg" : {
201-
"transactions" : []
202-
}
193+
"state" : {
194+
"transactions" : []
203195
}
204196
--------------------------------------------------
205197
// NOTCONSOLE
@@ -212,10 +204,8 @@ Shard A::
212204
+
213205
[source,js]
214206
--------------------------------------------------
215-
"params" : {
216-
"_agg" : {
217-
"transactions" : [ 80, -30 ]
218-
}
207+
"state" : {
208+
"transactions" : [ 80, -30 ]
219209
}
220210
--------------------------------------------------
221211
// NOTCONSOLE
@@ -224,10 +214,8 @@ Shard B::
224214
+
225215
[source,js]
226216
--------------------------------------------------
227-
"params" : {
228-
"_agg" : {
229-
"transactions" : [ -10, 130 ]
230-
}
217+
"state" : {
218+
"transactions" : [ -10, 130 ]
231219
}
232220
--------------------------------------------------
233221
// NOTCONSOLE
@@ -242,11 +230,11 @@ Shard B:: 120
242230

243231
===== After reduce_script
244232

245-
The reduce_script receives an `_aggs` array containing the result of the combine script for each shard:
233+
The reduce_script receives a `states` array containing the result of the combine script for each shard:
246234

247235
[source,js]
248236
--------------------------------------------------
249-
"_aggs" : [
237+
"states" : [
250238
50,
251239
120
252240
]
@@ -279,14 +267,12 @@ params:: Optional. An object whose contents will be passed as variable
279267
+
280268
[source,js]
281269
--------------------------------------------------
282-
"params" : {
283-
"_agg" : {}
284-
}
270+
"params" : {}
285271
--------------------------------------------------
286272
// NOTCONSOLE
287273

288274
==== Empty Buckets
289275

290276
If a parent bucket of the scripted metric aggregation does not collect any documents an empty aggregation response will be returned from the
291-
shard with a `null` value. In this case the `reduce_script`'s `_aggs` variable will contain `null` as a response from that shard.
277+
shard with a `null` value. In this case the `reduce_script`'s `states` variable will contain `null` as a response from that shard.
292278
`reduce_script`'s should therefore expect and deal with `null` responses from shards.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.painless;
21+
22+
import org.apache.lucene.search.DocIdSetIterator;
23+
import org.apache.lucene.search.Scorer;
24+
import org.elasticsearch.painless.spi.Whitelist;
25+
import org.elasticsearch.script.ScriptedMetricAggContexts;
26+
import org.elasticsearch.script.ScriptContext;
27+
28+
import java.util.ArrayList;
29+
import java.util.Collections;
30+
import java.util.HashMap;
31+
import java.util.List;
32+
import java.util.Map;
33+
34+
public class ScriptedMetricAggContextsTests extends ScriptTestCase {
35+
@Override
36+
protected Map<ScriptContext<?>, List<Whitelist>> scriptContexts() {
37+
Map<ScriptContext<?>, List<Whitelist>> contexts = new HashMap<>();
38+
contexts.put(ScriptedMetricAggContexts.InitScript.CONTEXT, Whitelist.BASE_WHITELISTS);
39+
contexts.put(ScriptedMetricAggContexts.MapScript.CONTEXT, Whitelist.BASE_WHITELISTS);
40+
contexts.put(ScriptedMetricAggContexts.CombineScript.CONTEXT, Whitelist.BASE_WHITELISTS);
41+
contexts.put(ScriptedMetricAggContexts.ReduceScript.CONTEXT, Whitelist.BASE_WHITELISTS);
42+
return contexts;
43+
}
44+
45+
public void testInitBasic() {
46+
ScriptedMetricAggContexts.InitScript.Factory factory = scriptEngine.compile("test",
47+
"state.testField = params.initialVal", ScriptedMetricAggContexts.InitScript.CONTEXT, Collections.emptyMap());
48+
49+
Map<String, Object> params = new HashMap<>();
50+
Map<String, Object> state = new HashMap<>();
51+
52+
params.put("initialVal", 10);
53+
54+
ScriptedMetricAggContexts.InitScript script = factory.newInstance(params, state);
55+
script.execute();
56+
57+
assert(state.containsKey("testField"));
58+
assertEquals(10, state.get("testField"));
59+
}
60+
61+
public void testMapBasic() {
62+
ScriptedMetricAggContexts.MapScript.Factory factory = scriptEngine.compile("test",
63+
"state.testField = 2*_score", ScriptedMetricAggContexts.MapScript.CONTEXT, Collections.emptyMap());
64+
65+
Map<String, Object> params = new HashMap<>();
66+
Map<String, Object> state = new HashMap<>();
67+
68+
Scorer scorer = new Scorer(null) {
69+
@Override
70+
public int docID() { return 0; }
71+
72+
@Override
73+
public float score() { return 0.5f; }
74+
75+
@Override
76+
public DocIdSetIterator iterator() { return null; }
77+
};
78+
79+
ScriptedMetricAggContexts.MapScript.LeafFactory leafFactory = factory.newFactory(params, state, null);
80+
ScriptedMetricAggContexts.MapScript script = leafFactory.newInstance(null);
81+
82+
script.setScorer(scorer);
83+
script.execute();
84+
85+
assert(state.containsKey("testField"));
86+
assertEquals(1.0, state.get("testField"));
87+
}
88+
89+
public void testCombineBasic() {
90+
ScriptedMetricAggContexts.CombineScript.Factory factory = scriptEngine.compile("test",
91+
"state.testField = params.initialVal; return state.testField + params.inc", ScriptedMetricAggContexts.CombineScript.CONTEXT,
92+
Collections.emptyMap());
93+
94+
Map<String, Object> params = new HashMap<>();
95+
Map<String, Object> state = new HashMap<>();
96+
97+
params.put("initialVal", 10);
98+
params.put("inc", 2);
99+
100+
ScriptedMetricAggContexts.CombineScript script = factory.newInstance(params, state);
101+
Object res = script.execute();
102+
103+
assert(state.containsKey("testField"));
104+
assertEquals(10, state.get("testField"));
105+
assertEquals(12, res);
106+
}
107+
108+
public void testReduceBasic() {
109+
ScriptedMetricAggContexts.ReduceScript.Factory factory = scriptEngine.compile("test",
110+
"states[0].testField + states[1].testField", ScriptedMetricAggContexts.ReduceScript.CONTEXT, Collections.emptyMap());
111+
112+
Map<String, Object> params = new HashMap<>();
113+
List<Object> states = new ArrayList<>();
114+
115+
Map<String, Object> state1 = new HashMap<>(), state2 = new HashMap<>();
116+
state1.put("testField", 1);
117+
state2.put("testField", 2);
118+
119+
states.add(state1);
120+
states.add(state2);
121+
122+
ScriptedMetricAggContexts.ReduceScript script = factory.newInstance(params, states);
123+
Object res = script.execute();
124+
assertEquals(3, res);
125+
}
126+
}

server/src/main/java/org/elasticsearch/script/ScriptModule.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ public class ScriptModule {
5353
SimilarityScript.CONTEXT,
5454
SimilarityWeightScript.CONTEXT,
5555
TemplateScript.CONTEXT,
56-
MovingFunctionScript.CONTEXT
56+
MovingFunctionScript.CONTEXT,
57+
ScriptedMetricAggContexts.InitScript.CONTEXT,
58+
ScriptedMetricAggContexts.MapScript.CONTEXT,
59+
ScriptedMetricAggContexts.CombineScript.CONTEXT,
60+
ScriptedMetricAggContexts.ReduceScript.CONTEXT
5761
).collect(Collectors.toMap(c -> c.name, Function.identity()));
5862
}
5963

0 commit comments

Comments
 (0)