diff --git a/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngine.java b/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngine.java index 1cde9c258b4f1..a3a62225b09fb 100644 --- a/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngine.java +++ b/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngine.java @@ -34,10 +34,12 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.fielddata.IndexFieldData; import org.elasticsearch.index.fielddata.IndexNumericFieldData; -import org.elasticsearch.index.mapper.GeoPointFieldMapper.GeoPointFieldType; import org.elasticsearch.index.mapper.DateFieldMapper; +import org.elasticsearch.index.mapper.GeoPointFieldMapper.GeoPointFieldType; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.script.BucketAggregationScript; +import org.elasticsearch.script.BucketAggregationSelectorScript; import org.elasticsearch.script.ClassPermission; import org.elasticsearch.script.ExecutableScript; import org.elasticsearch.script.FilterScript; @@ -54,6 +56,7 @@ import java.security.PrivilegedAction; import java.text.ParseException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -112,6 +115,17 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE } else if (context.instanceClazz.equals(ExecutableScript.class)) { ExecutableScript.Factory factory = (p) -> new ExpressionExecutableScript(expr, p); return context.factoryClazz.cast(factory); + } else if (context.instanceClazz.equals(BucketAggregationScript.class)) { + return context.factoryClazz.cast(newBucketAggregationScriptFactory(expr)); + } else if (context.instanceClazz.equals(BucketAggregationSelectorScript.class)) { + BucketAggregationScript.Factory factory = newBucketAggregationScriptFactory(expr); + BucketAggregationSelectorScript.Factory wrappedFactory = parameters -> new BucketAggregationSelectorScript(parameters) { + @Override + public boolean execute() { + return factory.newInstance(getParams()).execute() == 1.0; + } + }; + return context.factoryClazz.cast(wrappedFactory); } else if (context.instanceClazz.equals(FilterScript.class)) { FilterScript.Factory factory = (p, lookup) -> newFilterScript(expr, lookup, p); return context.factoryClazz.cast(factory); @@ -122,6 +136,37 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE throw new IllegalArgumentException("expression engine does not know how to handle script context [" + context.name + "]"); } + private static BucketAggregationScript.Factory newBucketAggregationScriptFactory(Expression expr) { + return parameters -> { + ReplaceableConstDoubleValues[] functionValuesArray = + new ReplaceableConstDoubleValues[expr.variables.length]; + Map functionValuesMap = new HashMap<>(); + for (int i = 0; i < expr.variables.length; ++i) { + functionValuesArray[i] = new ReplaceableConstDoubleValues(); + functionValuesMap.put(expr.variables[i], functionValuesArray[i]); + } + return new BucketAggregationScript(parameters) { + @Override + public double execute() { + getParams().forEach((name, value) -> { + ReplaceableConstDoubleValues placeholder = functionValuesMap.get(name); + if (placeholder == null) { + throw new IllegalArgumentException("Error using " + expr + ". " + + "The variable [" + name + "] does not exist in the executable expressions script."); + } else if (value instanceof Number == false) { + throw new IllegalArgumentException("Error using " + expr + ". " + + "Executable expressions scripts can only process numbers." + + " The variable [" + name + "] is not a number."); + } else { + placeholder.setValue(((Number) value).doubleValue()); + } + }); + return expr.evaluate(functionValuesArray); + } + }; + }; + } + private SearchScript.LeafFactory newSearchScript(Expression expr, SearchLookup lookup, @Nullable Map vars) { MapperService mapper = lookup.doc().mapperService(); // NOTE: if we need to do anything complicated with bindings in the future, we can just extend Bindings, @@ -267,7 +312,7 @@ public void setDocument(int docid) { }; }; } - + private ScoreScript.LeafFactory newScoreScript(Expression expr, SearchLookup lookup, @Nullable Map vars) { SearchScript.LeafFactory searchLeafFactory = newSearchScript(expr, lookup, vars); return new ScoreScript.LeafFactory() { @@ -284,17 +329,17 @@ public ScoreScript newInstance(LeafReaderContext ctx) throws IOException { public double execute() { return script.runAsDouble(); } - + @Override public void setDocument(int docid) { script.setDocument(docid); } - + @Override public void setScorer(Scorer scorer) { script.setScorer(scorer); } - + @Override public double get_score() { return script.getScore(); diff --git a/server/src/main/java/org/elasticsearch/script/BucketAggregationScript.java b/server/src/main/java/org/elasticsearch/script/BucketAggregationScript.java new file mode 100644 index 0000000000000..5d4a934969424 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/script/BucketAggregationScript.java @@ -0,0 +1,54 @@ +/* + * 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 java.util.Map; + +/** + * A script used in bucket aggregations that returns a {@code double} value. + */ +public abstract class BucketAggregationScript { + + public static final String[] PARAMETERS = {}; + + public static final ScriptContext CONTEXT = new ScriptContext<>("bucket_aggregation", Factory.class); + + /** + * The generic runtime parameters for the script. + */ + private final Map params; + + public BucketAggregationScript(Map params) { + this.params = params; + } + + /** + * Return the parameters for this script. + */ + public Map getParams() { + return params; + } + + public abstract double execute(); + + public interface Factory { + BucketAggregationScript newInstance(Map params); + } +} diff --git a/server/src/main/java/org/elasticsearch/script/BucketAggregationSelectorScript.java b/server/src/main/java/org/elasticsearch/script/BucketAggregationSelectorScript.java new file mode 100644 index 0000000000000..a8e2fad7cdcda --- /dev/null +++ b/server/src/main/java/org/elasticsearch/script/BucketAggregationSelectorScript.java @@ -0,0 +1,54 @@ +/* + * 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 java.util.Map; + +/** + * A script used in bucket aggregations that returns a {@code boolean} value. + */ +public abstract class BucketAggregationSelectorScript { + + public static final String[] PARAMETERS = {}; + + public static final ScriptContext CONTEXT = new ScriptContext<>("aggregation_selector", Factory.class); + + /** + * The generic runtime parameters for the script. + */ + private final Map params; + + public BucketAggregationSelectorScript(Map params) { + this.params = params; + } + + /** + * Return the parameters for this script. + */ + public Map getParams() { + return params; + } + + public abstract boolean execute(); + + public interface Factory { + BucketAggregationSelectorScript newInstance(Map params); + } +} diff --git a/server/src/main/java/org/elasticsearch/script/ExecutableScript.java b/server/src/main/java/org/elasticsearch/script/ExecutableScript.java index 2f7a01c37980d..1bd4c31ebf349 100644 --- a/server/src/main/java/org/elasticsearch/script/ExecutableScript.java +++ b/server/src/main/java/org/elasticsearch/script/ExecutableScript.java @@ -48,6 +48,5 @@ interface Factory { ScriptContext CONTEXT = new ScriptContext<>("executable", Factory.class); // TODO: remove these once each has its own script interface - ScriptContext AGGS_CONTEXT = new ScriptContext<>("aggs_executable", Factory.class); ScriptContext UPDATE_CONTEXT = new ScriptContext<>("update", Factory.class); } diff --git a/server/src/main/java/org/elasticsearch/script/ScriptModule.java b/server/src/main/java/org/elasticsearch/script/ScriptModule.java index 4e4c0a5a3c60f..3eeb26317f9cc 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptModule.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptModule.java @@ -46,7 +46,9 @@ public class ScriptModule { SearchScript.SCRIPT_SORT_CONTEXT, SearchScript.TERMS_SET_QUERY_CONTEXT, ExecutableScript.CONTEXT, - ExecutableScript.AGGS_CONTEXT, + BucketAggregationScript.CONTEXT, + BucketAggregationSelectorScript.CONTEXT, + SignificantTermsHeuristicScoreScript.CONTEXT, ExecutableScript.UPDATE_CONTEXT, IngestScript.CONTEXT, FilterScript.CONTEXT, diff --git a/server/src/main/java/org/elasticsearch/script/SignificantTermsHeuristicScoreScript.java b/server/src/main/java/org/elasticsearch/script/SignificantTermsHeuristicScoreScript.java new file mode 100644 index 0000000000000..0296bc36ce107 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/script/SignificantTermsHeuristicScoreScript.java @@ -0,0 +1,38 @@ +/* + * 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 java.util.Map; + +/** + * A script used in significant terms heuristic scoring. + */ +public abstract class SignificantTermsHeuristicScoreScript { + + public static final String[] PARAMETERS = { "params" }; + + public static final ScriptContext CONTEXT = new ScriptContext<>("script_heuristic", Factory.class); + + public abstract double execute(Map params); + + public interface Factory { + SignificantTermsHeuristicScoreScript newInstance(); + } +} diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/ScriptHeuristic.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/ScriptHeuristic.java index a2cbd49693a27..05415cf7d19a8 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/ScriptHeuristic.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/ScriptHeuristic.java @@ -28,12 +28,14 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.index.query.QueryShardException; -import org.elasticsearch.script.ExecutableScript; import org.elasticsearch.script.Script; +import org.elasticsearch.script.SignificantTermsHeuristicScoreScript; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.internal.SearchContext; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; public class ScriptHeuristic extends SignificanceHeuristic { @@ -48,19 +50,21 @@ static class ExecutableScriptHeuristic extends ScriptHeuristic { private final LongAccessor supersetSizeHolder; private final LongAccessor subsetDfHolder; private final LongAccessor supersetDfHolder; - private final ExecutableScript executableScript; + private final SignificantTermsHeuristicScoreScript executableScript; + private final Map params = new HashMap<>(); - ExecutableScriptHeuristic(Script script, ExecutableScript executableScript){ + ExecutableScriptHeuristic(Script script, SignificantTermsHeuristicScoreScript executableScript) { super(script); subsetSizeHolder = new LongAccessor(); supersetSizeHolder = new LongAccessor(); subsetDfHolder = new LongAccessor(); supersetDfHolder = new LongAccessor(); this.executableScript = executableScript; - executableScript.setNextVar("_subset_freq", subsetDfHolder); - executableScript.setNextVar("_subset_size", subsetSizeHolder); - executableScript.setNextVar("_superset_freq", supersetDfHolder); - executableScript.setNextVar("_superset_size", supersetSizeHolder); + params.putAll(script.getParams()); + params.put("_subset_freq", subsetDfHolder); + params.put("_subset_size", subsetSizeHolder); + params.put("_superset_freq", supersetDfHolder); + params.put("_superset_size", supersetSizeHolder); } @Override @@ -69,7 +73,7 @@ public double getScore(long subsetFreq, long subsetSize, long supersetFreq, long supersetSizeHolder.value = supersetSize; subsetDfHolder.value = subsetFreq; supersetDfHolder.value = supersetFreq; - return ((Number) executableScript.run()).doubleValue(); + return executableScript.execute(params); } } @@ -91,15 +95,15 @@ public void writeTo(StreamOutput out) throws IOException { @Override public SignificanceHeuristic rewrite(InternalAggregation.ReduceContext context) { - ExecutableScript.Factory factory = context.scriptService().compile(script, ExecutableScript.AGGS_CONTEXT); - return new ExecutableScriptHeuristic(script, factory.newInstance(script.getParams())); + SignificantTermsHeuristicScoreScript.Factory factory = context.scriptService().compile(script, SignificantTermsHeuristicScoreScript.CONTEXT); + return new ExecutableScriptHeuristic(script, factory.newInstance()); } @Override public SignificanceHeuristic rewrite(SearchContext context) { QueryShardContext shardContext = context.getQueryShardContext(); - ExecutableScript.Factory compiledScript = shardContext.getScriptService().compile(script, ExecutableScript.AGGS_CONTEXT); - return new ExecutableScriptHeuristic(script, compiledScript.newInstance(script.getParams())); + SignificantTermsHeuristicScoreScript.Factory compiledScript = shardContext.getScriptService().compile(script, SignificantTermsHeuristicScoreScript.CONTEXT); + return new ExecutableScriptHeuristic(script, compiledScript.newInstance()); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketscript/BucketScriptPipelineAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketscript/BucketScriptPipelineAggregator.java index 42337fbce0f98..c8117f9029bb6 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketscript/BucketScriptPipelineAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketscript/BucketScriptPipelineAggregator.java @@ -21,10 +21,9 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.script.ExecutableScript; +import org.elasticsearch.script.BucketAggregationScript; import org.elasticsearch.script.Script; import org.elasticsearch.search.DocValueFormat; -import org.elasticsearch.search.aggregations.AggregationExecutionException; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; import org.elasticsearch.search.aggregations.InternalAggregations; @@ -89,7 +88,8 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext (InternalMultiBucketAggregation) aggregation; List buckets = originalAgg.getBuckets(); - ExecutableScript.Factory factory = reduceContext.scriptService().compile(script, ExecutableScript.AGGS_CONTEXT); + BucketAggregationScript.Factory factory = + reduceContext.scriptService().compile(script, BucketAggregationScript.CONTEXT); List newBuckets = new ArrayList<>(); for (InternalMultiBucketAggregation.InternalBucket bucket : buckets) { Map vars = new HashMap<>(); @@ -110,24 +110,13 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext if (skipBucket) { newBuckets.add(bucket); } else { - ExecutableScript executableScript = factory.newInstance(vars); - Object returned = executableScript.run(); - // no need to check for self references since only numbers are valid - if (returned == null) { - newBuckets.add(bucket); - } else { - if ((returned instanceof Number) == false) { - throw new AggregationExecutionException("series_arithmetic script for reducer [" + name() - + "] must return a Number"); - } - final List aggs = StreamSupport.stream(bucket.getAggregations().spliterator(), false).map( - (p) -> (InternalAggregation) p).collect(Collectors.toList()); - aggs.add(new InternalSimpleValue(name(), ((Number) returned).doubleValue(), formatter, - new ArrayList<>(), metaData())); - InternalMultiBucketAggregation.InternalBucket newBucket = originalAgg.createBucket(new InternalAggregations(aggs), - bucket); - newBuckets.add(newBucket); - } + double returned = factory.newInstance(vars).execute(); + final List aggs = StreamSupport.stream(bucket.getAggregations().spliterator(), false).map( + (p) -> (InternalAggregation) p).collect(Collectors.toList()); + aggs.add(new InternalSimpleValue(name(), returned, formatter, new ArrayList<>(), metaData())); + InternalMultiBucketAggregation.InternalBucket newBucket = originalAgg.createBucket(new InternalAggregations(aggs), + bucket); + newBuckets.add(newBucket); } } return originalAgg.create(newBuckets); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketselector/BucketSelectorPipelineAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketselector/BucketSelectorPipelineAggregator.java index a54ad0ec21f39..06beab04aa605 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketselector/BucketSelectorPipelineAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketselector/BucketSelectorPipelineAggregator.java @@ -22,7 +22,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.script.ExecutableScript; +import org.elasticsearch.script.BucketAggregationSelectorScript; import org.elasticsearch.script.Script; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregation.ReduceContext; @@ -82,7 +82,8 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext (InternalMultiBucketAggregation) aggregation; List buckets = originalAgg.getBuckets(); - ExecutableScript.Factory factory = reduceContext.scriptService().compile(script, ExecutableScript.AGGS_CONTEXT); + BucketAggregationSelectorScript.Factory factory = + reduceContext.scriptService().compile(script, BucketAggregationSelectorScript.CONTEXT); List newBuckets = new ArrayList<>(); for (InternalMultiBucketAggregation.InternalBucket bucket : buckets) { Map vars = new HashMap<>(); @@ -96,17 +97,8 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext vars.put(varName, value); } // TODO: can we use one instance of the script for all buckets? it should be stateless? - ExecutableScript executableScript = factory.newInstance(vars); - Object scriptReturnValue = executableScript.run(); - final boolean keepBucket; - // TODO: WTF!!!!! - if ("expression".equals(script.getLang())) { - double scriptDoubleValue = (double) scriptReturnValue; - keepBucket = scriptDoubleValue == 1.0; - } else { - keepBucket = (boolean) scriptReturnValue; - } - if (keepBucket) { + BucketAggregationSelectorScript executableScript = factory.newInstance(vars); + if (executableScript.execute()) { newBuckets.add(bucket); } } diff --git a/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java b/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java index 8e40e4bcf1468..14dcac926f7b0 100644 --- a/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java +++ b/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java @@ -96,6 +96,30 @@ public void execute(Map ctx) { } }; return context.factoryClazz.cast(factory); + } else if (context.instanceClazz.equals(BucketAggregationScript.class)) { + BucketAggregationScript.Factory factory = parameters -> new BucketAggregationScript(parameters) { + @Override + public double execute() { + return ((Number) script.apply(getParams())).doubleValue(); + } + }; + return context.factoryClazz.cast(factory); + } else if (context.instanceClazz.equals(BucketAggregationSelectorScript.class)) { + BucketAggregationSelectorScript.Factory factory = parameters -> new BucketAggregationSelectorScript(parameters) { + @Override + public boolean execute() { + return (boolean) script.apply(getParams()); + } + }; + return context.factoryClazz.cast(factory); + } else if (context.instanceClazz.equals(SignificantTermsHeuristicScoreScript.class)) { + SignificantTermsHeuristicScoreScript.Factory factory = () -> new SignificantTermsHeuristicScoreScript() { + @Override + public double execute(Map vars) { + return ((Number) script.apply(vars)).doubleValue(); + } + }; + return context.factoryClazz.cast(factory); } else if (context.instanceClazz.equals(TemplateScript.class)) { TemplateScript.Factory factory = vars -> { // TODO: need a better way to implement all these new contexts