Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2710d5a
Migrate scripted metric aggregation scripts to ScriptContext design #…
rationull Apr 2, 2018
0f9ba91
Merge branch 'master' into rationull/29328-metric-agg-script-contexts
rationull May 6, 2018
1f7f477
Merge branch 'master' into rationull/29328-metric-agg-script-contexts
rationull May 9, 2018
2beee07
Rename new script context container class and add clarifying comments…
rationull May 9, 2018
22079c2
Merge branch 'master' into rationull/29328-metric-agg-script-contexts
rationull May 15, 2018
0b98c8a
Merge branch 'master' into rationull/29328-metric-agg-script-contexts
rationull May 20, 2018
3d3c914
Misc cleanup: make mock metric agg script inner classes static
rationull May 20, 2018
c16e700
Move _score to an accessor rather than an arg for scripted metric agg…
rationull May 20, 2018
d28c019
Documentation changes for params._agg -> agg
rationull May 29, 2018
802db8b
Merge branch 'master' into rationull/29328-metric-agg-script-contexts
rationull May 29, 2018
096b136
Migration doc addition for scripted metric aggs _agg object change
rationull May 29, 2018
793e47b
Rename "agg" Scripted Metric Aggregation script context variable to "…
rationull Jun 1, 2018
8b35cf1
Merge branch 'master' into rationull/29328-metric-agg-script-contexts
rationull Jun 1, 2018
46526a1
Rename a private base class from ...Agg to ...State that I missed in …
rationull Jun 10, 2018
c8a9256
Merge branch 'master' into rationull/29328-metric-agg-script-contexts
rationull Jun 10, 2018
1be1660
Merge branch 'master' into rationull/29328-metric-agg-script-contexts
rationull Jun 12, 2018
13986d0
Merge branch 'master' into rationull/29328-metric-agg-script-contexts
rationull Jun 23, 2018
c28eece
Clean up imports after merge
rationull Jun 23, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.elasticsearch.painless;

import org.elasticsearch.painless.spi.Whitelist;
import org.elasticsearch.script.ScriptedMetricAggContexts;
import org.elasticsearch.script.ScriptContext;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ScriptedMetricAggContextsTests extends ScriptTestCase {
@Override
protected Map<ScriptContext<?>, List<Whitelist>> scriptContexts() {
Map<ScriptContext<?>, List<Whitelist>> contexts = new HashMap<>();
contexts.put(ScriptedMetricAggContexts.InitScript.CONTEXT, Whitelist.BASE_WHITELISTS);
contexts.put(ScriptedMetricAggContexts.MapScript.CONTEXT, Whitelist.BASE_WHITELISTS);
contexts.put(ScriptedMetricAggContexts.CombineScript.CONTEXT, Whitelist.BASE_WHITELISTS);
contexts.put(ScriptedMetricAggContexts.ReduceScript.CONTEXT, Whitelist.BASE_WHITELISTS);
return contexts;
}

public void testInitBasic() {
ScriptedMetricAggContexts.InitScript.Factory factory = scriptEngine.compile("test",
"agg.testField = params.initialVal", ScriptedMetricAggContexts.InitScript.CONTEXT, Collections.emptyMap());

Map<String, Object> params = new HashMap<>();
Map<String, Object> agg = new HashMap<>();

params.put("initialVal", 10);

ScriptedMetricAggContexts.InitScript script = factory.newInstance(params, agg);
script.execute();

assert(agg.containsKey("testField"));
assertEquals(10, agg.get("testField"));
}

public void testMapBasic() {
ScriptedMetricAggContexts.MapScript.Factory factory = scriptEngine.compile("test",
"agg.testField = 2*_score", ScriptedMetricAggContexts.MapScript.CONTEXT, Collections.emptyMap());

Map<String, Object> params = new HashMap<>();
Map<String, Object> agg = new HashMap<>();
double _score = 0.5;

ScriptedMetricAggContexts.MapScript.LeafFactory leafFactory = factory.newFactory(params, agg, null);
ScriptedMetricAggContexts.MapScript script = leafFactory.newInstance(null);

script.execute(_score);

assert(agg.containsKey("testField"));
assertEquals(1.0, agg.get("testField"));
}

public void testCombineBasic() {
ScriptedMetricAggContexts.CombineScript.Factory factory = scriptEngine.compile("test",
"agg.testField = params.initialVal; return agg.testField + params.inc", ScriptedMetricAggContexts.CombineScript.CONTEXT,
Collections.emptyMap());

Map<String, Object> params = new HashMap<>();
Map<String, Object> agg = new HashMap<>();

params.put("initialVal", 10);
params.put("inc", 2);

ScriptedMetricAggContexts.CombineScript script = factory.newInstance(params, agg);
Object res = script.execute();

assert(agg.containsKey("testField"));
assertEquals(10, agg.get("testField"));
assertEquals(12, res);
}

public void testReduceBasic() {
ScriptedMetricAggContexts.ReduceScript.Factory factory = scriptEngine.compile("test",
"aggs[0].testField + aggs[1].testField", ScriptedMetricAggContexts.ReduceScript.CONTEXT, Collections.emptyMap());

Map<String, Object> params = new HashMap<>();
List<Object> aggs = new ArrayList<>();

Map<String, Object> agg1 = new HashMap<>(), agg2 = new HashMap<>();
agg1.put("testField", 1);
agg2.put("testField", 2);

aggs.add(agg1);
aggs.add(agg2);

ScriptedMetricAggContexts.ReduceScript script = factory.newInstance(params, aggs);
Object res = script.execute();
assertEquals(3, res);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ public class ScriptModule {
FilterScript.CONTEXT,
SimilarityScript.CONTEXT,
SimilarityWeightScript.CONTEXT,
TemplateScript.CONTEXT
TemplateScript.CONTEXT,
ScriptedMetricAggContexts.InitScript.CONTEXT,
ScriptedMetricAggContexts.MapScript.CONTEXT,
ScriptedMetricAggContexts.CombineScript.CONTEXT,
ScriptedMetricAggContexts.ReduceScript.CONTEXT
).collect(Collectors.toMap(c -> c.name, Function.identity()));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.elasticsearch.script;

import org.apache.lucene.index.LeafReaderContext;
import org.elasticsearch.index.fielddata.ScriptDocValues;
import org.elasticsearch.search.lookup.LeafSearchLookup;
import org.elasticsearch.search.lookup.SearchLookup;

import java.util.List;
import java.util.Map;

public class ScriptedMetricAggContexts {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would name this MetricAggScripts so that the inner classes read more naturally, eg MetricAggScripts.Init.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I had originally, actually. I abandoned e.g. Init in favor of InitScript because naming one of the classes Map ends up requiring fully qualifying java.util.Map in a bunch of spots which is pretty annoying (although really isn't a dealbreaker since it could be limited to this file). And past that, @colings86 suggestion that "metric agg" as a term is more general than this feature made sense to me. Admittedly the current situation makes the outer class kind of unnecessary since references to the scripts end up using long names anyway. This could be replaced with a package (or not even) and names like ScriptedMetricAggInitScript or just MetricAggInitScript which may be more palatable since it avoids a more general name without "script" in it.

TBH I might lean toward the latter; using a class as a small namespace in this context is a little clunky anyway IMO (even if it's the best Java can do). I defer to what you two think makes the most sense in context. I will await comment from @rjernst and @colings86 to settle this one.

Copy link
Contributor Author

@rationull rationull May 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rjernst I'm inclined to leave this as-is because personally I think the MapScript name qualification is more annoying than the ScriptedMetricAggContexts.*Script naming, and on further reflection I do think having them all in one file is better than splitting them apart. Let me know if you are firm in a different opinion on this though.

private abstract static class ParamsAndAggBase {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this base class could be removed by passing the arguments directly to execute()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did have a general question (hidden/outdated now in this PR) about execute() args vs accessors. Is there a general intent in the script context design for which mechanism any given script context variable should use? My read was that "ambient" or earlier-bound context should be via accessors and more execution specific or later-bound context should be via execute() args.

In this case specifically both agg and params are conveniently available at the time the script contexts are created, but less conveniently (in some cases) available when the script is executed, so I opted to use accessors instead. They could be passed down the chain of course if that design is preferable for some reason.

The base class could be removed regardless of that though, IMO just a matter of style taste. I have no love for extraneous classes or deep inheritance so happy to remove it if you feel it just complicates things.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had thought the params were setup next to the execution, but I see now the extra level of indirection in the aggs classes. I'm fine with the way you have it in that case.

More generally, the args and accessors are mostly interchangeable, for the reasons you stated. Args are necessary when the value will change per execution.

private final Map<String, Object> params;
private final Object agg;

ParamsAndAggBase(Map<String, Object> params, Object agg) {
this.params = params;
this.agg = agg;
}

public Map<String, Object> getParams() {
return params;
}

public Object getAgg() {
return agg;
}
}

public abstract static class InitScript extends ParamsAndAggBase {
public InitScript(Map<String, Object> params, Object agg) {
super(params, agg);
}

public abstract void execute();

public interface Factory {
InitScript newInstance(Map<String, Object> params, Object agg);
}

public static String[] PARAMETERS = {};
public static ScriptContext<Factory> CONTEXT = new ScriptContext<>("aggs_init", Factory.class);
}

public abstract static class MapScript extends ParamsAndAggBase {
private final LeafSearchLookup leafLookup;

public MapScript(Map<String, Object> params, Object agg, SearchLookup lookup, LeafReaderContext leafContext) {
super(params, agg);

this.leafLookup = leafContext == null ? null : lookup.getLeafSearchLookup(leafContext);
}

// Return the doc as a map (instead of LeafDocLookup) in order to abide by type whitelisting rules for
// Painless scripts.
public Map<String, ScriptDocValues<?>> getDoc() {
return leafLookup == null ? null : leafLookup.doc();
}

public void setDocument(int docId) {
if (leafLookup != null) {
leafLookup.setDocument(docId);
}
}

public abstract void execute(double _score);

public interface LeafFactory {
MapScript newInstance(LeafReaderContext ctx);
}

public interface Factory {
LeafFactory newFactory(Map<String, Object> params, Object agg, SearchLookup lookup);
}

public static String[] PARAMETERS = new String[] {"_score"};
public static ScriptContext<Factory> CONTEXT = new ScriptContext<>("aggs_map", Factory.class);
}

public abstract static class CombineScript extends ParamsAndAggBase {
public CombineScript(Map<String, Object> params, Object agg) {
super(params, agg);
}

public abstract Object execute();

public interface Factory {
CombineScript newInstance(Map<String, Object> params, Object agg);
}

public static String[] PARAMETERS = {};
public static ScriptContext<Factory> CONTEXT = new ScriptContext<>("aggs_combine", Factory.class);
}

public abstract static class ReduceScript {
private final Map<String, Object> params;
private final List<Object> aggs;

public ReduceScript(Map<String, Object> params, List<Object> aggs) {
this.params = params;
this.aggs = aggs;
}

public Map<String, Object> getParams() {
return params;
}

public List<Object> getAggs() {
return aggs;
}

public abstract Object execute();

public interface Factory {
ReduceScript newInstance(Map<String, Object> params, List<Object> aggs);
}

public static String[] PARAMETERS = {};
public static ScriptContext<Factory> CONTEXT = new ScriptContext<>("aggs_reduce", Factory.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.ScriptedMetricAggContexts;
import org.elasticsearch.script.Script;
import org.elasticsearch.search.aggregations.InternalAggregation;
import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator;
Expand Down Expand Up @@ -89,15 +89,18 @@ public InternalAggregation doReduce(List<InternalAggregation> aggregations, Redu
InternalScriptedMetric firstAggregation = ((InternalScriptedMetric) aggregations.get(0));
List<Object> aggregation;
if (firstAggregation.reduceScript != null && reduceContext.isFinalReduce()) {
Map<String, Object> vars = new HashMap<>();
vars.put("_aggs", aggregationObjects);
Map<String, Object> params = new HashMap<>();
if (firstAggregation.reduceScript.getParams() != null) {
vars.putAll(firstAggregation.reduceScript.getParams());
params.putAll(firstAggregation.reduceScript.getParams());
}
ExecutableScript.Factory factory = reduceContext.scriptService().compile(
firstAggregation.reduceScript, ExecutableScript.AGGS_CONTEXT);
ExecutableScript script = factory.newInstance(vars);
aggregation = Collections.singletonList(script.run());

// Add _aggs to params map for backwards compatibility (redundant with a context variable on the ReduceScript created below).
params.put("_aggs", aggregationObjects);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed? I thought that the reason to use script contexts was to that we then wouldn't need to put the _aggs variable in the params because it is passed directly to the script?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This (along with analogous code for _agg in ScriptedMetricAggregatorFactory.java and some of the integration tests I did not modify) is needed if we want to retain backwards compatibility with the old way for one ES version, letting people migrate to the context variables while both still work.

If a clean break is acceptable then this would be removed and some of the tests could be cleaned up. But I would assume we want some compatibility leniency and possibly some warning messages or other notification (which I have not implemented yet -- item 2 on my list in the first comment on this PR).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, understood. I'm +1 on keeping this for backwards compatibility, thanks for explaining. Maybe we could add a comment just noting that its there for backwards compatibility to avoid someone removing it thinking its just left over?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. I will add a comment here and in other relevant spots.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad you brought this up -- when looking for spots that needed a comment I found a leftover usage of params.get("_agg") in ScriptedMetricAggregator.java that needed to be replaced with a direct usage of the agg object. No functional issue but it would've had to get cleaned up later.


ScriptedMetricAggContexts.ReduceScript.Factory factory = reduceContext.scriptService().compile(
firstAggregation.reduceScript, ScriptedMetricAggContexts.ReduceScript.CONTEXT);
ScriptedMetricAggContexts.ReduceScript script = factory.newInstance(params, aggregationObjects);
aggregation = Collections.singletonList(script.execute());
} else if (reduceContext.isFinalReduce()) {
aggregation = Collections.singletonList(aggregationObjects);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,10 @@
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.query.QueryShardContext;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.ScriptedMetricAggContexts;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.SearchScript;
import org.elasticsearch.search.aggregations.AbstractAggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregatorFactories;
import org.elasticsearch.search.aggregations.AggregatorFactories.Builder;
import org.elasticsearch.search.aggregations.AggregatorFactory;
import org.elasticsearch.search.internal.SearchContext;
Expand Down Expand Up @@ -203,30 +201,32 @@ protected ScriptedMetricAggregatorFactory doBuild(SearchContext context, Aggrega
// Extract params from scripts and pass them along to ScriptedMetricAggregatorFactory, since it won't have
// access to them for the scripts it's given precompiled.

ExecutableScript.Factory executableInitScript;
ScriptedMetricAggContexts.InitScript.Factory compiledInitScript;
Map<String, Object> initScriptParams;
if (initScript != null) {
executableInitScript = queryShardContext.getScriptService().compile(initScript, ExecutableScript.AGGS_CONTEXT);
compiledInitScript = queryShardContext.getScriptService().compile(initScript, ScriptedMetricAggContexts.InitScript.CONTEXT);
initScriptParams = initScript.getParams();
} else {
executableInitScript = p -> null;
compiledInitScript = (p, a) -> null;
initScriptParams = Collections.emptyMap();
}

SearchScript.Factory searchMapScript = queryShardContext.getScriptService().compile(mapScript, SearchScript.AGGS_CONTEXT);
ScriptedMetricAggContexts.MapScript.Factory compiledMapScript = queryShardContext.getScriptService().compile(mapScript,
ScriptedMetricAggContexts.MapScript.CONTEXT);
Map<String, Object> mapScriptParams = mapScript.getParams();

ExecutableScript.Factory executableCombineScript;
ScriptedMetricAggContexts.CombineScript.Factory compiledCombineScript;
Map<String, Object> combineScriptParams;
if (combineScript != null) {
executableCombineScript = queryShardContext.getScriptService().compile(combineScript, ExecutableScript.AGGS_CONTEXT);
compiledCombineScript = queryShardContext.getScriptService().compile(combineScript,
ScriptedMetricAggContexts.CombineScript.CONTEXT);
combineScriptParams = combineScript.getParams();
} else {
executableCombineScript = p -> null;
compiledCombineScript = (p, a) -> null;
combineScriptParams = Collections.emptyMap();
}
return new ScriptedMetricAggregatorFactory(name, searchMapScript, mapScriptParams, executableInitScript, initScriptParams,
executableCombineScript, combineScriptParams, reduceScript,
return new ScriptedMetricAggregatorFactory(name, compiledMapScript, mapScriptParams, compiledInitScript,
initScriptParams, compiledCombineScript, combineScriptParams, reduceScript,
params, queryShardContext.lookup(), context, parent, subfactoriesBuilder, metaData);
}

Expand Down
Loading