Scripting: Add script engine for lucene expressions.
These are javascript expressions, which can only access numeric fielddata, parameters, and _score. They can only be used for searches (not document updates). closes #6818
This commit is contained in:
parent
1464bea00f
commit
64ab22816c
|
@ -17,8 +17,8 @@ different languages. Currently supported plugins are `lang-javascript`
|
|||
for JavaScript, `lang-mvel` for Mvel, and `lang-python` for Python.
|
||||
All places where a `script` parameter can be used, a `lang` parameter
|
||||
(on the same level) can be provided to define the language of the
|
||||
script. The `lang` options are `groovy`, `js`, `mvel`, `python`, and
|
||||
`native`.
|
||||
script. The `lang` options are `groovy`, `js`, `mvel`, `python`,
|
||||
`expression` and `native`.
|
||||
|
||||
added[1.2.0, Dynamic scripting is disabled for non-sandboxed languages by default since version 1.2.0]
|
||||
|
||||
|
@ -184,14 +184,43 @@ the name of the script as the `script`.
|
|||
|
||||
Note, the scripts need to be in the classpath of elasticsearch. One
|
||||
simple way to do it is to create a directory under plugins (choose a
|
||||
descriptive name), and place the jar / classes files there, they will be
|
||||
descriptive name), and place the jar / classes files there. They will be
|
||||
automatically loaded.
|
||||
|
||||
[float]
|
||||
=== Lucene Expressions Scripts
|
||||
|
||||
[WARNING]
|
||||
========================
|
||||
This feature is *experimental* and subject to change in future versions.
|
||||
========================
|
||||
|
||||
Lucene's expressions module provides a mechanism to compile a
|
||||
`javascript` expression to bytecode. This allows very fast execution,
|
||||
as if you had written a `native` script. Expression scripts can be
|
||||
used in `script_score`, `script_fields`, sort scripts and numeric aggregation scripts.
|
||||
|
||||
See the link:http://lucene.apache.org/core/4_9_0/expressions/index.html?org/apache/lucene/expressions/js/package-summary.html[expressions module documentation]
|
||||
for details on what operators and functions are available.
|
||||
|
||||
Variables in `expression` scripts are available to access:
|
||||
|
||||
* Single valued document fields, e.g. `doc['myfield'].value`
|
||||
* Parameters passed into the script, e.g. `mymodifier`
|
||||
* The current document's score, `_score` (only available when used in a `script_score`)
|
||||
|
||||
There are a few limitations relative to other script languages:
|
||||
|
||||
* Only numeric fields may be accessed
|
||||
* Stored fields are not available
|
||||
* If a field is sparse (only some documents contain a value), documents missing the field will have a value of `0`
|
||||
|
||||
[float]
|
||||
=== Score
|
||||
|
||||
In all scripts that can be used in facets, allow to access the current
|
||||
doc score using `doc.score`.
|
||||
In all scripts that can be used in facets, the current
|
||||
document's score is accessible in `doc.score`. When using a `script_score`,
|
||||
the current score is available in `_score`.
|
||||
|
||||
[float]
|
||||
=== Computing scores based on terms in scripts
|
||||
|
@ -402,3 +431,4 @@ integer with the value of `8`, the result is `0` even though you were
|
|||
expecting it to be `0.125`. You may need to enforce precision by
|
||||
explicitly using a double like `1.0/num` in order to get the expected
|
||||
result.
|
||||
|
||||
|
|
7
pom.xml
7
pom.xml
|
@ -149,6 +149,13 @@
|
|||
<version>${lucene.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.lucene</groupId>
|
||||
<artifactId>lucene-expressions</artifactId>
|
||||
<version>${lucene.version}</version>
|
||||
<scope>compile</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.spatial4j</groupId>
|
||||
<artifactId>spatial4j</artifactId>
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
package org.apache.lucene.expressions;
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF 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.
|
||||
*/
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.lucene.queries.function.ValueSource;
|
||||
import org.apache.lucene.queries.function.valuesource.DoubleFieldSource;
|
||||
import org.apache.lucene.queries.function.valuesource.FloatFieldSource;
|
||||
import org.apache.lucene.queries.function.valuesource.IntFieldSource;
|
||||
import org.apache.lucene.queries.function.valuesource.LongFieldSource;
|
||||
import org.apache.lucene.search.SortField;
|
||||
|
||||
/**
|
||||
* Simple class that binds expression variable names to {@link SortField}s
|
||||
* or other {@link Expression}s.
|
||||
* <p>
|
||||
* Example usage:
|
||||
* <pre class="prettyprint">
|
||||
* XSimpleBindings bindings = new XSimpleBindings();
|
||||
* // document's text relevance score
|
||||
* bindings.add(new SortField("_score", SortField.Type.SCORE));
|
||||
* // integer NumericDocValues field (or from FieldCache)
|
||||
* bindings.add(new SortField("popularity", SortField.Type.INT));
|
||||
* // another expression
|
||||
* bindings.add("recency", myRecencyExpression);
|
||||
*
|
||||
* // create a sort field in reverse order
|
||||
* Sort sort = new Sort(expr.getSortField(bindings, true));
|
||||
* </pre>
|
||||
*
|
||||
* @lucene.experimental
|
||||
*/
|
||||
public final class XSimpleBindings extends Bindings {
|
||||
|
||||
static {
|
||||
assert org.elasticsearch.Version.CURRENT.luceneVersion == org.apache.lucene.util.Version.LUCENE_4_9: "Remove this code once we upgrade to Lucene 4.10 (LUCENE-5806)";
|
||||
}
|
||||
|
||||
final Map<String,Object> map = new HashMap<>();
|
||||
|
||||
/** Creates a new empty Bindings */
|
||||
public XSimpleBindings() {}
|
||||
|
||||
/**
|
||||
* Adds a SortField to the bindings.
|
||||
* <p>
|
||||
* This can be used to reference a DocValuesField, a field from
|
||||
* FieldCache, the document's score, etc.
|
||||
*/
|
||||
public void add(SortField sortField) {
|
||||
map.put(sortField.getField(), sortField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a {@link ValueSource} directly to the given name.
|
||||
*/
|
||||
public void add(String name, ValueSource source) { map.put(name, source); }
|
||||
|
||||
/**
|
||||
* Adds an Expression to the bindings.
|
||||
* <p>
|
||||
* This can be used to reference expressions from other expressions.
|
||||
*/
|
||||
public void add(String name, Expression expression) {
|
||||
map.put(name, expression);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValueSource getValueSource(String name) {
|
||||
Object o = map.get(name);
|
||||
if (o == null) {
|
||||
throw new IllegalArgumentException("Invalid reference '" + name + "'");
|
||||
} else if (o instanceof Expression) {
|
||||
return ((Expression)o).getValueSource(this);
|
||||
} else if (o instanceof ValueSource) {
|
||||
return ((ValueSource)o);
|
||||
}
|
||||
SortField field = (SortField) o;
|
||||
switch(field.getType()) {
|
||||
case INT:
|
||||
return new IntFieldSource(field.getField());
|
||||
case LONG:
|
||||
return new LongFieldSource(field.getField());
|
||||
case FLOAT:
|
||||
return new FloatFieldSource(field.getField());
|
||||
case DOUBLE:
|
||||
return new DoubleFieldSource(field.getField());
|
||||
case SCORE:
|
||||
return getScoreValueSource();
|
||||
default:
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses the graph of bindings, checking there are no cycles or missing references
|
||||
* @throws IllegalArgumentException if the bindings is inconsistent
|
||||
*/
|
||||
public void validate() {
|
||||
for (Object o : map.values()) {
|
||||
if (o instanceof Expression) {
|
||||
Expression expr = (Expression) o;
|
||||
try {
|
||||
expr.getValueSource(this);
|
||||
} catch (StackOverflowError e) {
|
||||
throw new IllegalArgumentException("Recursion Error: Cycle detected originating in (" + expr.sourceText + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,614 @@
|
|||
package org.apache.lucene.expressions.js;
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF 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.
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.ParseException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.antlr.runtime.ANTLRStringStream;
|
||||
import org.antlr.runtime.CharStream;
|
||||
import org.antlr.runtime.CommonTokenStream;
|
||||
import org.antlr.runtime.RecognitionException;
|
||||
import org.antlr.runtime.tree.Tree;
|
||||
import org.apache.lucene.expressions.Expression;
|
||||
import org.apache.lucene.queries.function.FunctionValues;
|
||||
import org.apache.lucene.util.IOUtils;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.Label;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.commons.GeneratorAdapter;
|
||||
|
||||
/**
|
||||
* An expression compiler for javascript expressions.
|
||||
* <p>
|
||||
* Example:
|
||||
* <pre class="prettyprint">
|
||||
* Expression foo = XJavascriptCompiler.compile("((0.3*popularity)/10.0)+(0.7*score)");
|
||||
* </pre>
|
||||
* <p>
|
||||
* See the {@link org.apache.lucene.expressions.js package documentation} for
|
||||
* the supported syntax and default functions.
|
||||
* <p>
|
||||
* You can compile with an alternate set of functions via {@link #compile(String, Map, ClassLoader)}.
|
||||
* For example:
|
||||
* <pre class="prettyprint">
|
||||
* Map<String,Method> functions = new HashMap<>();
|
||||
* // add all the default functions
|
||||
* functions.putAll(XJavascriptCompiler.DEFAULT_FUNCTIONS);
|
||||
* // add cbrt()
|
||||
* functions.put("cbrt", Math.class.getMethod("cbrt", double.class));
|
||||
* // call compile with customized function map
|
||||
* Expression foo = XJavascriptCompiler.compile("cbrt(score)+ln(popularity)",
|
||||
* functions,
|
||||
* getClass().getClassLoader());
|
||||
* </pre>
|
||||
*
|
||||
* @lucene.experimental
|
||||
*/
|
||||
public class XJavascriptCompiler {
|
||||
|
||||
static {
|
||||
assert org.elasticsearch.Version.CURRENT.luceneVersion == org.apache.lucene.util.Version.LUCENE_4_9: "Remove this code once we upgrade to Lucene 4.10 (LUCENE-5806)";
|
||||
}
|
||||
|
||||
static final class Loader extends ClassLoader {
|
||||
Loader(ClassLoader parent) {
|
||||
super(parent);
|
||||
}
|
||||
|
||||
public Class<? extends Expression> define(String className, byte[] bytecode) {
|
||||
return defineClass(className, bytecode, 0, bytecode.length).asSubclass(Expression.class);
|
||||
}
|
||||
}
|
||||
|
||||
private static final int CLASSFILE_VERSION = Opcodes.V1_7;
|
||||
|
||||
// We use the same class name for all generated classes as they all have their own class loader.
|
||||
// The source code is displayed as "source file name" in stack trace.
|
||||
private static final String COMPILED_EXPRESSION_CLASS = XJavascriptCompiler.class.getName() + "$CompiledExpression";
|
||||
private static final String COMPILED_EXPRESSION_INTERNAL = COMPILED_EXPRESSION_CLASS.replace('.', '/');
|
||||
|
||||
private static final Type EXPRESSION_TYPE = Type.getType(Expression.class);
|
||||
private static final Type FUNCTION_VALUES_TYPE = Type.getType(FunctionValues.class);
|
||||
|
||||
private static final org.objectweb.asm.commons.Method
|
||||
EXPRESSION_CTOR = getMethod("void <init>(String, String[])"),
|
||||
EVALUATE_METHOD = getMethod("double evaluate(int, " + FunctionValues.class.getName() + "[])"),
|
||||
DOUBLE_VAL_METHOD = getMethod("double doubleVal(int)");
|
||||
|
||||
// to work around import clash:
|
||||
private static org.objectweb.asm.commons.Method getMethod(String method) {
|
||||
return org.objectweb.asm.commons.Method.getMethod(method);
|
||||
}
|
||||
|
||||
// This maximum length is theoretically 65535 bytes, but as its CESU-8 encoded we dont know how large it is in bytes, so be safe
|
||||
// rcmuir: "If your ranking function is that large you need to check yourself into a mental institution!"
|
||||
private static final int MAX_SOURCE_LENGTH = 16384;
|
||||
|
||||
private final String sourceText;
|
||||
private final Map<String, Integer> externalsMap = new LinkedHashMap<>();
|
||||
private final ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
|
||||
private GeneratorAdapter gen;
|
||||
|
||||
private final Map<String,Method> functions;
|
||||
|
||||
/**
|
||||
* Compiles the given expression.
|
||||
*
|
||||
* @param sourceText The expression to compile
|
||||
* @return A new compiled expression
|
||||
* @throws ParseException on failure to compile
|
||||
*/
|
||||
public static Expression compile(String sourceText) throws ParseException {
|
||||
return new XJavascriptCompiler(sourceText).compileExpression(XJavascriptCompiler.class.getClassLoader());
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles the given expression with the supplied custom functions.
|
||||
* <p>
|
||||
* Functions must be {@code public static}, return {@code double} and
|
||||
* can take from zero to 256 {@code double} parameters.
|
||||
*
|
||||
* @param sourceText The expression to compile
|
||||
* @param functions map of String names to functions
|
||||
* @param parent a {@code ClassLoader} that should be used as the parent of the loaded class.
|
||||
* It must contain all classes referred to by the given {@code functions}.
|
||||
* @return A new compiled expression
|
||||
* @throws ParseException on failure to compile
|
||||
*/
|
||||
public static Expression compile(String sourceText, Map<String,Method> functions, ClassLoader parent) throws ParseException {
|
||||
if (parent == null) {
|
||||
throw new NullPointerException("A parent ClassLoader must be given.");
|
||||
}
|
||||
for (Method m : functions.values()) {
|
||||
checkFunction(m, parent);
|
||||
}
|
||||
return new XJavascriptCompiler(sourceText, functions).compileExpression(parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is unused, it is just here to make sure that the function signatures don't change.
|
||||
* If this method fails to compile, you also have to change the byte code generator to correctly
|
||||
* use the FunctionValues class.
|
||||
*/
|
||||
@SuppressWarnings({"unused", "null"})
|
||||
private static void unusedTestCompile() {
|
||||
FunctionValues f = null;
|
||||
double ret = f.doubleVal(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a compiler for expressions.
|
||||
* @param sourceText The expression to compile
|
||||
*/
|
||||
private XJavascriptCompiler(String sourceText) {
|
||||
this(sourceText, DEFAULT_FUNCTIONS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a compiler for expressions with specific set of functions
|
||||
* @param sourceText The expression to compile
|
||||
*/
|
||||
private XJavascriptCompiler(String sourceText, Map<String,Method> functions) {
|
||||
if (sourceText == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
this.sourceText = sourceText;
|
||||
this.functions = functions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles the given expression with the specified parent classloader
|
||||
*
|
||||
* @return A new compiled expression
|
||||
* @throws ParseException on failure to compile
|
||||
*/
|
||||
private Expression compileExpression(ClassLoader parent) throws ParseException {
|
||||
try {
|
||||
Tree antlrTree = getAntlrComputedExpressionTree();
|
||||
|
||||
beginCompile();
|
||||
recursiveCompile(antlrTree, Type.DOUBLE_TYPE);
|
||||
endCompile();
|
||||
|
||||
Class<? extends Expression> evaluatorClass = new Loader(parent)
|
||||
.define(COMPILED_EXPRESSION_CLASS, classWriter.toByteArray());
|
||||
Constructor<? extends Expression> constructor = evaluatorClass.getConstructor(String.class, String[].class);
|
||||
return constructor.newInstance(sourceText, externalsMap.keySet().toArray(new String[externalsMap.size()]));
|
||||
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException exception) {
|
||||
throw new IllegalStateException("An internal error occurred attempting to compile the expression (" + sourceText + ").", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void beginCompile() {
|
||||
classWriter.visit(CLASSFILE_VERSION,
|
||||
Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER | Opcodes.ACC_FINAL | Opcodes.ACC_SYNTHETIC,
|
||||
COMPILED_EXPRESSION_INTERNAL,
|
||||
null, EXPRESSION_TYPE.getInternalName(), null);
|
||||
String clippedSourceText = (sourceText.length() <= MAX_SOURCE_LENGTH) ?
|
||||
sourceText : (sourceText.substring(0, MAX_SOURCE_LENGTH - 3) + "...");
|
||||
classWriter.visitSource(clippedSourceText, null);
|
||||
|
||||
GeneratorAdapter constructor = new GeneratorAdapter(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC,
|
||||
EXPRESSION_CTOR, null, null, classWriter);
|
||||
constructor.loadThis();
|
||||
constructor.loadArgs();
|
||||
constructor.invokeConstructor(EXPRESSION_TYPE, EXPRESSION_CTOR);
|
||||
constructor.returnValue();
|
||||
constructor.endMethod();
|
||||
|
||||
gen = new GeneratorAdapter(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC,
|
||||
EVALUATE_METHOD, null, null, classWriter);
|
||||
}
|
||||
|
||||
private void recursiveCompile(Tree current, Type expected) {
|
||||
int type = current.getType();
|
||||
String text = current.getText();
|
||||
|
||||
switch (type) {
|
||||
case XJavascriptParser.AT_CALL:
|
||||
Tree identifier = current.getChild(0);
|
||||
String call = identifier.getText();
|
||||
int arguments = current.getChildCount() - 1;
|
||||
|
||||
Method method = functions.get(call);
|
||||
if (method == null) {
|
||||
throw new IllegalArgumentException("Unrecognized method call (" + call + ").");
|
||||
}
|
||||
|
||||
int arity = method.getParameterTypes().length;
|
||||
if (arguments != arity) {
|
||||
throw new IllegalArgumentException("Expected (" + arity + ") arguments for method call (" +
|
||||
call + "), but found (" + arguments + ").");
|
||||
}
|
||||
|
||||
for (int argument = 1; argument <= arguments; ++argument) {
|
||||
recursiveCompile(current.getChild(argument), Type.DOUBLE_TYPE);
|
||||
}
|
||||
|
||||
gen.invokeStatic(Type.getType(method.getDeclaringClass()),
|
||||
org.objectweb.asm.commons.Method.getMethod(method));
|
||||
|
||||
gen.cast(Type.DOUBLE_TYPE, expected);
|
||||
break;
|
||||
case XJavascriptParser.VARIABLE:
|
||||
int index;
|
||||
|
||||
// normalize quotes
|
||||
text = normalizeQuotes(text);
|
||||
|
||||
|
||||
if (externalsMap.containsKey(text)) {
|
||||
index = externalsMap.get(text);
|
||||
} else {
|
||||
index = externalsMap.size();
|
||||
externalsMap.put(text, index);
|
||||
}
|
||||
|
||||
gen.loadArg(1);
|
||||
gen.push(index);
|
||||
gen.arrayLoad(FUNCTION_VALUES_TYPE);
|
||||
gen.loadArg(0);
|
||||
gen.invokeVirtual(FUNCTION_VALUES_TYPE, DOUBLE_VAL_METHOD);
|
||||
gen.cast(Type.DOUBLE_TYPE, expected);
|
||||
break;
|
||||
case XJavascriptParser.HEX:
|
||||
pushLong(expected, Long.parseLong(text.substring(2), 16));
|
||||
break;
|
||||
case XJavascriptParser.OCTAL:
|
||||
pushLong(expected, Long.parseLong(text.substring(1), 8));
|
||||
break;
|
||||
case XJavascriptParser.DECIMAL:
|
||||
gen.push(Double.parseDouble(text));
|
||||
gen.cast(Type.DOUBLE_TYPE, expected);
|
||||
break;
|
||||
case XJavascriptParser.AT_NEGATE:
|
||||
recursiveCompile(current.getChild(0), Type.DOUBLE_TYPE);
|
||||
gen.visitInsn(Opcodes.DNEG);
|
||||
gen.cast(Type.DOUBLE_TYPE, expected);
|
||||
break;
|
||||
case XJavascriptParser.AT_ADD:
|
||||
pushArith(Opcodes.DADD, current, expected);
|
||||
break;
|
||||
case XJavascriptParser.AT_SUBTRACT:
|
||||
pushArith(Opcodes.DSUB, current, expected);
|
||||
break;
|
||||
case XJavascriptParser.AT_MULTIPLY:
|
||||
pushArith(Opcodes.DMUL, current, expected);
|
||||
break;
|
||||
case XJavascriptParser.AT_DIVIDE:
|
||||
pushArith(Opcodes.DDIV, current, expected);
|
||||
break;
|
||||
case XJavascriptParser.AT_MODULO:
|
||||
pushArith(Opcodes.DREM, current, expected);
|
||||
break;
|
||||
case XJavascriptParser.AT_BIT_SHL:
|
||||
pushShift(Opcodes.LSHL, current, expected);
|
||||
break;
|
||||
case XJavascriptParser.AT_BIT_SHR:
|
||||
pushShift(Opcodes.LSHR, current, expected);
|
||||
break;
|
||||
case XJavascriptParser.AT_BIT_SHU:
|
||||
pushShift(Opcodes.LUSHR, current, expected);
|
||||
break;
|
||||
case XJavascriptParser.AT_BIT_AND:
|
||||
pushBitwise(Opcodes.LAND, current, expected);
|
||||
break;
|
||||
case XJavascriptParser.AT_BIT_OR:
|
||||
pushBitwise(Opcodes.LOR, current, expected);
|
||||
break;
|
||||
case XJavascriptParser.AT_BIT_XOR:
|
||||
pushBitwise(Opcodes.LXOR, current, expected);
|
||||
break;
|
||||
case XJavascriptParser.AT_BIT_NOT:
|
||||
recursiveCompile(current.getChild(0), Type.LONG_TYPE);
|
||||
gen.push(-1L);
|
||||
gen.visitInsn(Opcodes.LXOR);
|
||||
gen.cast(Type.LONG_TYPE, expected);
|
||||
break;
|
||||
case XJavascriptParser.AT_COMP_EQ:
|
||||
pushCond(GeneratorAdapter.EQ, current, expected);
|
||||
break;
|
||||
case XJavascriptParser.AT_COMP_NEQ:
|
||||
pushCond(GeneratorAdapter.NE, current, expected);
|
||||
break;
|
||||
case XJavascriptParser.AT_COMP_LT:
|
||||
pushCond(GeneratorAdapter.LT, current, expected);
|
||||
break;
|
||||
case XJavascriptParser.AT_COMP_GT:
|
||||
pushCond(GeneratorAdapter.GT, current, expected);
|
||||
break;
|
||||
case XJavascriptParser.AT_COMP_LTE:
|
||||
pushCond(GeneratorAdapter.LE, current, expected);
|
||||
break;
|
||||
case XJavascriptParser.AT_COMP_GTE:
|
||||
pushCond(GeneratorAdapter.GE, current, expected);
|
||||
break;
|
||||
case XJavascriptParser.AT_BOOL_NOT:
|
||||
Label labelNotTrue = new Label();
|
||||
Label labelNotReturn = new Label();
|
||||
|
||||
recursiveCompile(current.getChild(0), Type.INT_TYPE);
|
||||
gen.visitJumpInsn(Opcodes.IFEQ, labelNotTrue);
|
||||
pushBoolean(expected, false);
|
||||
gen.goTo(labelNotReturn);
|
||||
gen.visitLabel(labelNotTrue);
|
||||
pushBoolean(expected, true);
|
||||
gen.visitLabel(labelNotReturn);
|
||||
break;
|
||||
case XJavascriptParser.AT_BOOL_AND:
|
||||
Label andFalse = new Label();
|
||||
Label andEnd = new Label();
|
||||
|
||||
recursiveCompile(current.getChild(0), Type.INT_TYPE);
|
||||
gen.visitJumpInsn(Opcodes.IFEQ, andFalse);
|
||||
recursiveCompile(current.getChild(1), Type.INT_TYPE);
|
||||
gen.visitJumpInsn(Opcodes.IFEQ, andFalse);
|
||||
pushBoolean(expected, true);
|
||||
gen.goTo(andEnd);
|
||||
gen.visitLabel(andFalse);
|
||||
pushBoolean(expected, false);
|
||||
gen.visitLabel(andEnd);
|
||||
break;
|
||||
case XJavascriptParser.AT_BOOL_OR:
|
||||
Label orTrue = new Label();
|
||||
Label orEnd = new Label();
|
||||
|
||||
recursiveCompile(current.getChild(0), Type.INT_TYPE);
|
||||
gen.visitJumpInsn(Opcodes.IFNE, orTrue);
|
||||
recursiveCompile(current.getChild(1), Type.INT_TYPE);
|
||||
gen.visitJumpInsn(Opcodes.IFNE, orTrue);
|
||||
pushBoolean(expected, false);
|
||||
gen.goTo(orEnd);
|
||||
gen.visitLabel(orTrue);
|
||||
pushBoolean(expected, true);
|
||||
gen.visitLabel(orEnd);
|
||||
break;
|
||||
case XJavascriptParser.AT_COND_QUE:
|
||||
Label condFalse = new Label();
|
||||
Label condEnd = new Label();
|
||||
|
||||
recursiveCompile(current.getChild(0), Type.INT_TYPE);
|
||||
gen.visitJumpInsn(Opcodes.IFEQ, condFalse);
|
||||
recursiveCompile(current.getChild(1), expected);
|
||||
gen.goTo(condEnd);
|
||||
gen.visitLabel(condFalse);
|
||||
recursiveCompile(current.getChild(2), expected);
|
||||
gen.visitLabel(condEnd);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unknown operation specified: (" + current.getText() + ").");
|
||||
}
|
||||
}
|
||||
|
||||
private void pushArith(int operator, Tree current, Type expected) {
|
||||
pushBinaryOp(operator, current, expected, Type.DOUBLE_TYPE, Type.DOUBLE_TYPE, Type.DOUBLE_TYPE);
|
||||
}
|
||||
|
||||
private void pushShift(int operator, Tree current, Type expected) {
|
||||
pushBinaryOp(operator, current, expected, Type.LONG_TYPE, Type.INT_TYPE, Type.LONG_TYPE);
|
||||
}
|
||||
|
||||
private void pushBitwise(int operator, Tree current, Type expected) {
|
||||
pushBinaryOp(operator, current, expected, Type.LONG_TYPE, Type.LONG_TYPE, Type.LONG_TYPE);
|
||||
}
|
||||
|
||||
private void pushBinaryOp(int operator, Tree current, Type expected, Type arg1, Type arg2, Type returnType) {
|
||||
recursiveCompile(current.getChild(0), arg1);
|
||||
recursiveCompile(current.getChild(1), arg2);
|
||||
gen.visitInsn(operator);
|
||||
gen.cast(returnType, expected);
|
||||
}
|
||||
|
||||
private void pushCond(int operator, Tree current, Type expected) {
|
||||
Label labelTrue = new Label();
|
||||
Label labelReturn = new Label();
|
||||
|
||||
recursiveCompile(current.getChild(0), Type.DOUBLE_TYPE);
|
||||
recursiveCompile(current.getChild(1), Type.DOUBLE_TYPE);
|
||||
|
||||
gen.ifCmp(Type.DOUBLE_TYPE, operator, labelTrue);
|
||||
pushBoolean(expected, false);
|
||||
gen.goTo(labelReturn);
|
||||
gen.visitLabel(labelTrue);
|
||||
pushBoolean(expected, true);
|
||||
gen.visitLabel(labelReturn);
|
||||
}
|
||||
|
||||
private void pushBoolean(Type expected, boolean truth) {
|
||||
switch (expected.getSort()) {
|
||||
case Type.INT:
|
||||
gen.push(truth);
|
||||
break;
|
||||
case Type.LONG:
|
||||
gen.push(truth ? 1L : 0L);
|
||||
break;
|
||||
case Type.DOUBLE:
|
||||
gen.push(truth ? 1. : 0.);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Invalid expected type: " + expected);
|
||||
}
|
||||
}
|
||||
|
||||
private void pushLong(Type expected, long i) {
|
||||
switch (expected.getSort()) {
|
||||
case Type.INT:
|
||||
gen.push((int) i);
|
||||
break;
|
||||
case Type.LONG:
|
||||
gen.push(i);
|
||||
break;
|
||||
case Type.DOUBLE:
|
||||
gen.push((double) i);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Invalid expected type: " + expected);
|
||||
}
|
||||
}
|
||||
|
||||
private void endCompile() {
|
||||
gen.returnValue();
|
||||
gen.endMethod();
|
||||
|
||||
classWriter.visitEnd();
|
||||
}
|
||||
|
||||
private Tree getAntlrComputedExpressionTree() throws ParseException {
|
||||
CharStream input = new ANTLRStringStream(sourceText);
|
||||
XJavascriptLexer lexer = new XJavascriptLexer(input);
|
||||
CommonTokenStream tokens = new CommonTokenStream(lexer);
|
||||
XJavascriptParser parser = new XJavascriptParser(tokens);
|
||||
|
||||
try {
|
||||
return parser.expression().tree;
|
||||
|
||||
} catch (RecognitionException exception) {
|
||||
throw new IllegalArgumentException(exception);
|
||||
} catch (RuntimeException exception) {
|
||||
if (exception.getCause() instanceof ParseException) {
|
||||
throw (ParseException)exception.getCause();
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalizeQuotes(String text) {
|
||||
StringBuilder out = new StringBuilder(text.length());
|
||||
boolean inDoubleQuotes = false;
|
||||
for (int i = 0; i < text.length(); ++i) {
|
||||
char c = text.charAt(i);
|
||||
if (c == '\\') {
|
||||
c = text.charAt(++i);
|
||||
if (c == '\\') {
|
||||
out.append('\\'); // re-escape the backslash
|
||||
}
|
||||
// no escape for double quote
|
||||
} else if (c == '\'') {
|
||||
if (inDoubleQuotes) {
|
||||
// escape in output
|
||||
out.append('\\');
|
||||
} else {
|
||||
int j = findSingleQuoteStringEnd(text, i);
|
||||
out.append(text, i, j); // copy up to end quote (leave end for append below)
|
||||
i = j;
|
||||
}
|
||||
} else if (c == '"') {
|
||||
c = '\''; // change beginning/ending doubles to singles
|
||||
inDoubleQuotes = !inDoubleQuotes;
|
||||
}
|
||||
out.append(c);
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private static int findSingleQuoteStringEnd(String text, int start) {
|
||||
++start; // skip beginning
|
||||
while (text.charAt(start) != '\'') {
|
||||
if (text.charAt(start) == '\\') {
|
||||
++start; // blindly consume escape value
|
||||
}
|
||||
++start;
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default set of functions available to expressions.
|
||||
* <p>
|
||||
* See the {@link org.apache.lucene.expressions.js package documentation}
|
||||
* for a list.
|
||||
*/
|
||||
public static final Map<String,Method> DEFAULT_FUNCTIONS;
|
||||
static {
|
||||
Map<String,Method> map = new HashMap<>();
|
||||
try {
|
||||
final Properties props = new Properties();
|
||||
try (Reader in = IOUtils.getDecodingReader(JavascriptCompiler.class,
|
||||
JavascriptCompiler.class.getSimpleName() + ".properties", StandardCharsets.UTF_8)) {
|
||||
props.load(in);
|
||||
}
|
||||
for (final String call : props.stringPropertyNames()) {
|
||||
final String[] vals = props.getProperty(call).split(",");
|
||||
if (vals.length != 3) {
|
||||
throw new Error("Syntax error while reading Javascript functions from resource");
|
||||
}
|
||||
final Class<?> clazz = Class.forName(vals[0].trim());
|
||||
final String methodName = vals[1].trim();
|
||||
final int arity = Integer.parseInt(vals[2].trim());
|
||||
@SuppressWarnings({"rawtypes", "unchecked"}) Class[] args = new Class[arity];
|
||||
Arrays.fill(args, double.class);
|
||||
Method method = clazz.getMethod(methodName, args);
|
||||
checkFunction(method, JavascriptCompiler.class.getClassLoader());
|
||||
map.put(call, method);
|
||||
}
|
||||
} catch (NoSuchMethodException | ClassNotFoundException | IOException e) {
|
||||
throw new Error("Cannot resolve function", e);
|
||||
}
|
||||
DEFAULT_FUNCTIONS = Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
private static void checkFunction(Method method, ClassLoader parent) {
|
||||
// We can only call the function if the given parent class loader of our compiled class has access to the method:
|
||||
final ClassLoader functionClassloader = method.getDeclaringClass().getClassLoader();
|
||||
if (functionClassloader != null) { // it is a system class iff null!
|
||||
boolean found = false;
|
||||
while (parent != null) {
|
||||
if (parent == functionClassloader) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
parent = parent.getParent();
|
||||
}
|
||||
if (!found) {
|
||||
throw new IllegalArgumentException(method + " is not declared by a class which is accessible by the given parent ClassLoader.");
|
||||
}
|
||||
}
|
||||
// do some checks if the signature is "compatible":
|
||||
if (!Modifier.isStatic(method.getModifiers())) {
|
||||
throw new IllegalArgumentException(method + " is not static.");
|
||||
}
|
||||
if (!Modifier.isPublic(method.getModifiers())) {
|
||||
throw new IllegalArgumentException(method + " is not public.");
|
||||
}
|
||||
if (!Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
|
||||
throw new IllegalArgumentException(method.getDeclaringClass().getName() + " is not public.");
|
||||
}
|
||||
for (Class<?> clazz : method.getParameterTypes()) {
|
||||
if (!clazz.equals(double.class)) {
|
||||
throw new IllegalArgumentException(method + " must take only double parameters");
|
||||
}
|
||||
}
|
||||
if (method.getReturnType() != double.class) {
|
||||
throw new IllegalArgumentException(method + " does not return a double.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,106 @@
|
|||
package org.apache.lucene.expressions.js;
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF 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.
|
||||
*/
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A helper to parse the context of a variable name, which is the base variable, followed by the
|
||||
* sequence of array (integer or string indexed) and member accesses.
|
||||
*/
|
||||
public class XVariableContext {
|
||||
|
||||
static {
|
||||
assert org.elasticsearch.Version.CURRENT.luceneVersion == org.apache.lucene.util.Version.LUCENE_4_9: "Remove this code once we upgrade to Lucene 4.10 (LUCENE-5806)";
|
||||
}
|
||||
|
||||
public static enum Type {
|
||||
MEMBER, // "dot" access
|
||||
STR_INDEX, // brackets with a string
|
||||
INT_INDEX // brackets with a positive integer
|
||||
}
|
||||
|
||||
public final Type type;
|
||||
public final String text;
|
||||
public final int integer;
|
||||
|
||||
private XVariableContext(Type c, String s, int i) {
|
||||
type = c;
|
||||
text = s;
|
||||
integer = i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a normalized javascript variable. All strings in the variable should be single quoted,
|
||||
* and no spaces (except possibly within strings).
|
||||
*/
|
||||
public static final XVariableContext[] parse(String variable) {
|
||||
char[] text = variable.toCharArray();
|
||||
List<XVariableContext> contexts = new ArrayList<>();
|
||||
int i = addMember(text, 0, contexts); // base variable is a "member" of the global namespace
|
||||
while (i < text.length) {
|
||||
if (text[i] == '[') {
|
||||
if (text[++i] == '\'') {
|
||||
i = addStringIndex(text, i, contexts);
|
||||
} else {
|
||||
i = addIntIndex(text, i, contexts);
|
||||
}
|
||||
++i; // move past end bracket
|
||||
} else { // text[i] == '.', ie object member
|
||||
i = addMember(text, i + 1, contexts);
|
||||
}
|
||||
}
|
||||
return contexts.toArray(new XVariableContext[contexts.size()]);
|
||||
}
|
||||
|
||||
// i points to start of member name
|
||||
private static int addMember(final char[] text, int i, List<XVariableContext> contexts) {
|
||||
int j = i + 1;
|
||||
while (j < text.length && text[j] != '[' && text[j] != '.') ++j; // find first array or member access
|
||||
contexts.add(new XVariableContext(Type.MEMBER, new String(text, i, j - i), -1));
|
||||
return j;
|
||||
}
|
||||
|
||||
// i points to start of single quoted index
|
||||
private static int addStringIndex(final char[] text, int i, List<XVariableContext> contexts) {
|
||||
++i; // move past quote
|
||||
int j = i;
|
||||
while (text[j] != '\'') { // find end of single quoted string
|
||||
if (text[j] == '\\') ++j; // skip over escapes
|
||||
++j;
|
||||
}
|
||||
StringBuffer buf = new StringBuffer(j - i); // space for string, without end quote
|
||||
while (i < j) { // copy string to buffer (without begin/end quotes)
|
||||
if (text[i] == '\\') ++i; // unescape escapes
|
||||
buf.append(text[i]);
|
||||
++i;
|
||||
}
|
||||
contexts.add(new XVariableContext(Type.STR_INDEX, buf.toString(), -1));
|
||||
return j + 1; // move past quote, return end bracket location
|
||||
}
|
||||
|
||||
// i points to start of integer index
|
||||
private static int addIntIndex(final char[] text, int i, List<XVariableContext> contexts) {
|
||||
int j = i + 1;
|
||||
while (text[j] != ']') ++j; // find end of array access
|
||||
int index = Integer.parseInt(new String(text, i, j - i));
|
||||
contexts.add(new XVariableContext(Type.INT_INDEX, null, index));
|
||||
return j ;
|
||||
}
|
||||
}
|
|
@ -27,6 +27,7 @@ import org.elasticsearch.common.inject.multibindings.MapBinder;
|
|||
import org.elasticsearch.common.inject.multibindings.Multibinder;
|
||||
import org.elasticsearch.common.logging.Loggers;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.script.expression.ExpressionScriptEngineService;
|
||||
import org.elasticsearch.script.groovy.GroovyScriptEngineService;
|
||||
import org.elasticsearch.script.mustache.MustacheScriptEngineService;
|
||||
|
||||
|
@ -34,7 +35,8 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
|
||||
/**
|
||||
*
|
||||
* An {@link org.elasticsearch.common.inject.Module} which manages {@link ScriptEngineService}s, as well
|
||||
* as named script
|
||||
*/
|
||||
public class ScriptModule extends AbstractModule {
|
||||
|
||||
|
@ -92,6 +94,13 @@ public class ScriptModule extends AbstractModule {
|
|||
Loggers.getLogger(ScriptService.class, settings).debug("failed to load mustache", t);
|
||||
}
|
||||
|
||||
try {
|
||||
settings.getClassLoader().loadClass("org.apache.lucene.expressions.Expression");
|
||||
multibinder.addBinding().to(ExpressionScriptEngineService.class);
|
||||
} catch (Throwable t) {
|
||||
Loggers.getLogger(ScriptService.class, settings).debug("failed to load lucene expressions", t);
|
||||
}
|
||||
|
||||
for (Class<? extends ScriptEngineService> scriptEngine : scriptEngines) {
|
||||
multibinder.addBinding().to(scriptEngine);
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ package org.elasticsearch.script;
|
|||
|
||||
import org.elasticsearch.common.lucene.ReaderContextAware;
|
||||
import org.elasticsearch.common.lucene.ScorerAware;
|
||||
import org.elasticsearch.search.SearchService;
|
||||
import org.elasticsearch.search.internal.SearchContext;
|
||||
import org.elasticsearch.search.lookup.SearchLookup;
|
||||
|
||||
|
@ -29,7 +28,7 @@ import java.util.Map;
|
|||
/**
|
||||
* A search script.
|
||||
*
|
||||
* @see ExplainableSearchScript for script which can explain a score
|
||||
* @see {@link ExplainableSearchScript} for script which can explain a score
|
||||
*/
|
||||
public interface SearchScript extends ExecutableScript, ReaderContextAware, ScorerAware {
|
||||
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* 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.expression;
|
||||
|
||||
import org.apache.lucene.expressions.Bindings;
|
||||
import org.apache.lucene.expressions.Expression;
|
||||
import org.apache.lucene.expressions.XSimpleBindings;
|
||||
import org.apache.lucene.index.AtomicReaderContext;
|
||||
import org.apache.lucene.queries.function.FunctionValues;
|
||||
import org.apache.lucene.queries.function.ValueSource;
|
||||
import org.apache.lucene.search.Scorer;
|
||||
import org.elasticsearch.script.SearchScript;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A bridge to evaluate an {@link Expression} against {@link Bindings} in the context
|
||||
* of a {@link SearchScript}.
|
||||
*/
|
||||
class ExpressionScript implements SearchScript {
|
||||
|
||||
/** Fake scorer for a single document */
|
||||
static class CannedScorer extends Scorer {
|
||||
protected int docid;
|
||||
protected float score;
|
||||
|
||||
public CannedScorer() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int docID() {
|
||||
return docid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float score() throws IOException {
|
||||
return score;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int freq() throws IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int nextDoc() throws IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int advance(int target) throws IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long cost() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
final Expression expression;
|
||||
final XSimpleBindings bindings;
|
||||
final CannedScorer scorer;
|
||||
final ValueSource source;
|
||||
final Map<String, CannedScorer> context;
|
||||
final ReplaceableConstValueSource specialValue; // _value
|
||||
FunctionValues values;
|
||||
|
||||
ExpressionScript(Expression e, XSimpleBindings b, ReplaceableConstValueSource v) {
|
||||
expression = e;
|
||||
bindings = b;
|
||||
scorer = new CannedScorer();
|
||||
context = Collections.singletonMap("scorer", scorer);
|
||||
source = expression.getValueSource(bindings);
|
||||
specialValue = v;
|
||||
}
|
||||
|
||||
double evaluate() {
|
||||
return values.doubleVal(scorer.docid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object run() { return new Double(evaluate()); }
|
||||
|
||||
@Override
|
||||
public float runAsFloat() { return (float)evaluate();}
|
||||
|
||||
@Override
|
||||
public long runAsLong() { return (long)evaluate(); }
|
||||
|
||||
@Override
|
||||
public double runAsDouble() { return evaluate(); }
|
||||
|
||||
@Override
|
||||
public Object unwrap(Object value) { return value; }
|
||||
|
||||
@Override
|
||||
public void setNextDocId(int d) {
|
||||
scorer.docid = d;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNextScore(float score) {
|
||||
// TODO: fix this API to remove setNextScore and just use a Scorer
|
||||
// Expressions know if they use the score or not, and should be able to pull from the scorer only
|
||||
// if they need it. Right now, score can only be used within a ScriptScoreFunction. But there shouldn't
|
||||
// be any reason a script values or aggregation can't use the score. It is also possible
|
||||
// these layers are preventing inlining of scoring into expressions.
|
||||
scorer.score = score;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNextReader(AtomicReaderContext leaf) {
|
||||
try {
|
||||
values = source.getValues(context, leaf);
|
||||
} catch (IOException e) {
|
||||
throw new ExpressionScriptExecutionException("Expression failed to bind for segment", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setScorer(Scorer s) {
|
||||
// noop: The scorer isn't actually ever set. Instead setNextScore is called.
|
||||
// NOTE: This seems broken. Why can't we just use the scorer and get rid of setNextScore?
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNextSource(Map<String, Object> source) {
|
||||
// noop: expressions don't use source data
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNextVar(String name, Object value) {
|
||||
assert(specialValue != null);
|
||||
// this should only be used for the special "_value" variable used in aggregations
|
||||
assert(name.equals("_value"));
|
||||
|
||||
if (value instanceof Number) {
|
||||
specialValue.setValue(((Number)value).doubleValue());
|
||||
} else {
|
||||
throw new ExpressionScriptExecutionException("Cannot use expression with text variable");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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.expression;
|
||||
|
||||
import org.elasticsearch.ElasticsearchException;
|
||||
|
||||
import java.text.ParseException;
|
||||
|
||||
/**
|
||||
* Exception representing a compilation error in an expression.
|
||||
*/
|
||||
public class ExpressionScriptCompilationException extends ElasticsearchException {
|
||||
public ExpressionScriptCompilationException(String msg, ParseException e) {
|
||||
super(msg, e);
|
||||
}
|
||||
public ExpressionScriptCompilationException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* 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.expression;
|
||||
|
||||
import org.apache.lucene.expressions.Expression;
|
||||
import org.apache.lucene.expressions.XSimpleBindings;
|
||||
import org.apache.lucene.expressions.js.XJavascriptCompiler;
|
||||
import org.apache.lucene.expressions.js.XVariableContext;
|
||||
import org.apache.lucene.queries.function.valuesource.DoubleConstValueSource;
|
||||
import org.apache.lucene.search.SortField;
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.common.component.AbstractComponent;
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.index.fielddata.IndexFieldData;
|
||||
import org.elasticsearch.index.mapper.FieldMapper;
|
||||
import org.elasticsearch.index.mapper.MapperService;
|
||||
import org.elasticsearch.index.mapper.core.NumberFieldMapper;
|
||||
import org.elasticsearch.script.ExecutableScript;
|
||||
import org.elasticsearch.script.ScriptEngineService;
|
||||
import org.elasticsearch.script.SearchScript;
|
||||
import org.elasticsearch.search.lookup.SearchLookup;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Provides the infrastructure for Lucene expressions as a scripting language for Elasticsearch. Only
|
||||
* {@link SearchScript}s are supported.
|
||||
*/
|
||||
public class ExpressionScriptEngineService extends AbstractComponent implements ScriptEngineService {
|
||||
|
||||
@Inject
|
||||
public ExpressionScriptEngineService(Settings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] types() {
|
||||
return new String[]{"expression"};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] extensions() {
|
||||
return new String[]{"expression"};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sandboxed() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object compile(String script) {
|
||||
try {
|
||||
// NOTE: validation is delayed to allow runtime vars, and we don't have access to per index stuff here
|
||||
return XJavascriptCompiler.compile(script);
|
||||
} catch (ParseException e) {
|
||||
throw new ExpressionScriptCompilationException("Failed to parse expression: " + script, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchScript search(Object compiledScript, SearchLookup lookup, @Nullable Map<String, Object> vars) {
|
||||
Expression expr = (Expression)compiledScript;
|
||||
MapperService mapper = lookup.doc().mapperService();
|
||||
// NOTE: if we need to do anything complicated with bindings in the future, we can just extend Bindings,
|
||||
// instead of complicating SimpleBindings (which should stay simple)
|
||||
XSimpleBindings bindings = new XSimpleBindings();
|
||||
ReplaceableConstValueSource specialValue = null;
|
||||
|
||||
for (String variable : expr.variables) {
|
||||
if (variable.equals("_score")) {
|
||||
bindings.add(new SortField("_score", SortField.Type.SCORE));
|
||||
|
||||
} else if (variable.equals("_value")) {
|
||||
specialValue = new ReplaceableConstValueSource();
|
||||
bindings.add("_value", specialValue);
|
||||
// noop: _value is special for aggregations, and is handled in ExpressionScriptBindings
|
||||
// TODO: if some uses it in a scoring expression, they will get a nasty failure when evaluating...need a
|
||||
// way to know this is for aggregations and so _value is ok to have...
|
||||
|
||||
} else if (vars != null && vars.containsKey(variable)) {
|
||||
// TODO: document and/or error if vars contains _score?
|
||||
// NOTE: by checking for the variable in vars first, it allows masking document fields with a global constant,
|
||||
// but if we were to reverse it, we could provide a way to supply dynamic defaults for documents missing the field?
|
||||
Object value = vars.get(variable);
|
||||
if (value instanceof Number) {
|
||||
bindings.add(variable, new DoubleConstValueSource(((Number)value).doubleValue()));
|
||||
} else {
|
||||
throw new ExpressionScriptCompilationException("Parameter [" + variable + "] must be a numeric type");
|
||||
}
|
||||
|
||||
} else {
|
||||
XVariableContext[] parts = XVariableContext.parse(variable);
|
||||
if (parts[0].text.equals("doc") == false) {
|
||||
throw new ExpressionScriptCompilationException("Unknown variable [" + parts[0].text + "] in expression");
|
||||
}
|
||||
if (parts.length < 2 || parts[1].type != XVariableContext.Type.STR_INDEX) {
|
||||
throw new ExpressionScriptCompilationException("Variable 'doc' in expression must be used with a specific field like: doc['myfield'].value");
|
||||
}
|
||||
if (parts.length < 3 || parts[2].type != XVariableContext.Type.MEMBER || parts[2].text.equals("value") == false) {
|
||||
throw new ExpressionScriptCompilationException("Invalid member for field data in expression. Only '.value' is currently supported.");
|
||||
}
|
||||
String fieldname = parts[1].text;
|
||||
|
||||
FieldMapper<?> field = mapper.smartNameFieldMapper(fieldname);
|
||||
if (field == null) {
|
||||
throw new ExpressionScriptCompilationException("Field [" + fieldname + "] used in expression does not exist in mappings");
|
||||
}
|
||||
if (field.isNumeric() == false) {
|
||||
// TODO: more context (which expression?)
|
||||
throw new ExpressionScriptCompilationException("Field [" + fieldname + "] used in expression must be numeric");
|
||||
}
|
||||
IndexFieldData<?> fieldData = lookup.doc().fieldDataService().getForField((NumberFieldMapper)field);
|
||||
bindings.add(variable, new FieldDataValueSource(fieldData));
|
||||
}
|
||||
}
|
||||
|
||||
return new ExpressionScript((Expression)compiledScript, bindings, specialValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExecutableScript executable(Object compiledScript, @Nullable Map<String, Object> vars) {
|
||||
throw new UnsupportedOperationException("Cannot use expressions for updates");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object execute(Object compiledScript, Map<String, Object> vars) {
|
||||
throw new UnsupportedOperationException("Cannot use expressions for updates");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object unwrap(Object value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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.expression;
|
||||
|
||||
import org.elasticsearch.ElasticsearchException;
|
||||
|
||||
/**
|
||||
* Exception used to wrap exceptions occuring while running expressions.
|
||||
*/
|
||||
public class ExpressionScriptExecutionException extends ElasticsearchException {
|
||||
public ExpressionScriptExecutionException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
public ExpressionScriptExecutionException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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.expression;
|
||||
|
||||
import org.apache.lucene.queries.function.ValueSource;
|
||||
import org.apache.lucene.queries.function.docvalues.DoubleDocValues;
|
||||
import org.elasticsearch.index.fielddata.AtomicNumericFieldData;
|
||||
import org.elasticsearch.index.fielddata.DoubleValues;
|
||||
|
||||
/**
|
||||
* A {@link org.apache.lucene.queries.function.FunctionValues} which wrap field data.
|
||||
*/
|
||||
class FieldDataFunctionValues extends DoubleDocValues {
|
||||
DoubleValues dataAccessor;
|
||||
|
||||
FieldDataFunctionValues(ValueSource parent, AtomicNumericFieldData d) {
|
||||
super(parent);
|
||||
dataAccessor = d.getDoubleValues();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double doubleVal(int i) {
|
||||
int numValues = dataAccessor.setDocument(i);
|
||||
if (numValues == 0) {
|
||||
// sparse fields get a value of 0 when the field doesn't exist
|
||||
return 0.0;
|
||||
}
|
||||
return dataAccessor.nextValue();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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.expression;
|
||||
|
||||
|
||||
import org.apache.lucene.index.AtomicReaderContext;
|
||||
import org.apache.lucene.queries.function.FunctionValues;
|
||||
import org.apache.lucene.queries.function.ValueSource;
|
||||
import org.elasticsearch.index.fielddata.AtomicFieldData;
|
||||
import org.elasticsearch.index.fielddata.AtomicNumericFieldData;
|
||||
import org.elasticsearch.index.fielddata.IndexFieldData;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A {@link ValueSource} wrapper for field data.
|
||||
*/
|
||||
class FieldDataValueSource extends ValueSource {
|
||||
|
||||
IndexFieldData<?> fieldData;
|
||||
|
||||
FieldDataValueSource(IndexFieldData<?> d) {
|
||||
fieldData = d;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FunctionValues getValues(Map context, AtomicReaderContext leaf) throws IOException {
|
||||
AtomicFieldData leafData = fieldData.load(leaf);
|
||||
assert(leafData instanceof AtomicNumericFieldData);
|
||||
return new FieldDataFunctionValues(this, (AtomicNumericFieldData)leafData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return fieldData.equals(other);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return fieldData.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "field(" + fieldData.getFieldNames().toString() + ")";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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.expression;
|
||||
|
||||
import org.apache.lucene.index.AtomicReaderContext;
|
||||
import org.apache.lucene.queries.function.FunctionValues;
|
||||
import org.apache.lucene.queries.function.ValueSource;
|
||||
import org.apache.lucene.queries.function.docvalues.DoubleDocValues;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A {@link ValueSource} which has a stub {@link FunctionValues} that holds a dynamically replaceable constant double.
|
||||
*/
|
||||
class ReplaceableConstValueSource extends ValueSource {
|
||||
double value;
|
||||
final FunctionValues fv;
|
||||
|
||||
public ReplaceableConstValueSource() {
|
||||
fv = new DoubleDocValues(this) {
|
||||
@Override
|
||||
public double doubleVal(int i) {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public FunctionValues getValues(Map map, AtomicReaderContext atomicReaderContext) throws IOException {
|
||||
return fv;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o == this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return System.identityHashCode(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "replaceableConstDouble";
|
||||
}
|
||||
|
||||
public void setValue(double v) {
|
||||
value = v;
|
||||
}
|
||||
}
|
|
@ -63,6 +63,10 @@ public class DocLookup implements Map {
|
|||
return this.mapperService;
|
||||
}
|
||||
|
||||
public IndexFieldDataService fieldDataService() {
|
||||
return this.fieldDataService;
|
||||
}
|
||||
|
||||
public void setNextReader(AtomicReaderContext context) {
|
||||
if (this.reader == context) { // if we are called with the same reader, don't invalidate source
|
||||
return;
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.benchmark.scripts.expression;
|
||||
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.script.AbstractSearchScript;
|
||||
import org.elasticsearch.script.ExecutableScript;
|
||||
import org.elasticsearch.script.NativeScriptFactory;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class NativeScript1 extends AbstractSearchScript {
|
||||
|
||||
public static class Factory implements NativeScriptFactory {
|
||||
|
||||
@Override
|
||||
public ExecutableScript newScript(@Nullable Map<String, Object> params) {
|
||||
return new NativeScript1();
|
||||
}
|
||||
}
|
||||
|
||||
public static final String NATIVE_SCRIPT_1 = "native_1";
|
||||
|
||||
@Override
|
||||
public Object run() {
|
||||
return docFieldLongs("x").getValue();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.benchmark.scripts.expression;
|
||||
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.script.AbstractSearchScript;
|
||||
import org.elasticsearch.script.ExecutableScript;
|
||||
import org.elasticsearch.script.NativeScriptFactory;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class NativeScript2 extends AbstractSearchScript {
|
||||
|
||||
public static class Factory implements NativeScriptFactory {
|
||||
|
||||
@Override
|
||||
public ExecutableScript newScript(@Nullable Map<String, Object> params) {
|
||||
return new NativeScript2();
|
||||
}
|
||||
}
|
||||
|
||||
public static final String NATIVE_SCRIPT_2 = "native_2";
|
||||
|
||||
@Override
|
||||
public Object run() {
|
||||
return docFieldLongs("x").getValue() + docFieldDoubles("y").getValue();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.benchmark.scripts.expression;
|
||||
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.script.AbstractSearchScript;
|
||||
import org.elasticsearch.script.ExecutableScript;
|
||||
import org.elasticsearch.script.NativeScriptFactory;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class NativeScript3 extends AbstractSearchScript {
|
||||
|
||||
public static class Factory implements NativeScriptFactory {
|
||||
|
||||
@Override
|
||||
public ExecutableScript newScript(@Nullable Map<String, Object> params) {
|
||||
return new NativeScript3();
|
||||
}
|
||||
}
|
||||
|
||||
public static final String NATIVE_SCRIPT_3 = "native_3";
|
||||
|
||||
@Override
|
||||
public Object run() {
|
||||
return 1.2 * docFieldLongs("x").getValue() / docFieldDoubles("y").getValue();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.benchmark.scripts.expression;
|
||||
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.script.AbstractSearchScript;
|
||||
import org.elasticsearch.script.ExecutableScript;
|
||||
import org.elasticsearch.script.NativeScriptFactory;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class NativeScript4 extends AbstractSearchScript {
|
||||
|
||||
public static class Factory implements NativeScriptFactory {
|
||||
|
||||
@Override
|
||||
public ExecutableScript newScript(@Nullable Map<String, Object> params) {
|
||||
return new NativeScript4();
|
||||
}
|
||||
}
|
||||
|
||||
public static final String NATIVE_SCRIPT_4 = "native_4";
|
||||
|
||||
@Override
|
||||
public Object run() {
|
||||
return Math.sqrt(Math.abs(docFieldDoubles("z").getValue())) + Math.log(Math.abs(docFieldLongs("x").getValue() * docFieldDoubles("y").getValue()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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.benchmark.scripts.expression;
|
||||
|
||||
import org.elasticsearch.plugins.AbstractPlugin;
|
||||
import org.elasticsearch.script.ScriptModule;
|
||||
|
||||
public class NativeScriptPlugin extends AbstractPlugin {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "native-benchmark-scripts";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Native benchmark script";
|
||||
}
|
||||
|
||||
public void onModule(ScriptModule module) {
|
||||
module.registerScript(NativeScript1.NATIVE_SCRIPT_1, NativeScript1.Factory.class);
|
||||
module.registerScript(NativeScript2.NATIVE_SCRIPT_2, NativeScript2.Factory.class);
|
||||
module.registerScript(NativeScript3.NATIVE_SCRIPT_3, NativeScript3.Factory.class);
|
||||
module.registerScript(NativeScript4.NATIVE_SCRIPT_4, NativeScript4.Factory.class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* 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.benchmark.scripts.expression;
|
||||
|
||||
import org.elasticsearch.ElasticsearchException;
|
||||
import org.elasticsearch.action.bulk.BulkRequestBuilder;
|
||||
import org.elasticsearch.action.search.SearchRequestBuilder;
|
||||
import org.elasticsearch.client.Client;
|
||||
import org.elasticsearch.client.IndicesAdminClient;
|
||||
import org.elasticsearch.common.StopWatch;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.unit.TimeValue;
|
||||
import org.elasticsearch.index.query.QueryBuilders;
|
||||
import org.elasticsearch.node.Node;
|
||||
import org.elasticsearch.search.sort.ScriptSortBuilder;
|
||||
import org.elasticsearch.search.sort.SortBuilders;
|
||||
import org.joda.time.PeriodType;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder;
|
||||
import static org.elasticsearch.node.NodeBuilder.nodeBuilder;
|
||||
|
||||
public class ScriptComparisonBenchmark {
|
||||
|
||||
static final String clusterName = ScriptComparisonBenchmark.class.getSimpleName();
|
||||
static final String indexName = "test";
|
||||
|
||||
static String[] langs = {
|
||||
"expression",
|
||||
"native",
|
||||
"groovy"
|
||||
};
|
||||
static String[][] scripts = {
|
||||
// the first value is the "reference" version (pure math)
|
||||
{
|
||||
"x",
|
||||
"doc['x'].value",
|
||||
NativeScript1.NATIVE_SCRIPT_1,
|
||||
"doc['x'].value"
|
||||
}, {
|
||||
"x + y",
|
||||
"doc['x'].value + doc['y'].value",
|
||||
NativeScript2.NATIVE_SCRIPT_2,
|
||||
"doc['x'].value + doc['y'].value",
|
||||
}, {
|
||||
"1.2 * x / y",
|
||||
"1.2 * doc['x'].value / doc['y'].value",
|
||||
NativeScript3.NATIVE_SCRIPT_3,
|
||||
"1.2 * doc['x'].value / doc['y'].value",
|
||||
}, {
|
||||
"sqrt(abs(z)) + ln(abs(x * y))",
|
||||
"sqrt(abs(doc['z'].value)) + ln(abs(doc['x'].value * doc['y'].value))",
|
||||
NativeScript4.NATIVE_SCRIPT_4,
|
||||
"sqrt(abs(doc['z'].value)) + log(abs(doc['x'].value * doc['y'].value))"
|
||||
}
|
||||
};
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
int numDocs = 1000000;
|
||||
int numQueries = 1000;
|
||||
Client client = setupIndex();
|
||||
indexDocs(client, numDocs);
|
||||
|
||||
for (int scriptNum = 0; scriptNum < scripts.length; ++scriptNum) {
|
||||
runBenchmark(client, scriptNum, numQueries);
|
||||
}
|
||||
}
|
||||
|
||||
static void runBenchmark(Client client, int scriptNum, int numQueries) {
|
||||
System.out.println("");
|
||||
System.out.println("Script: " + scripts[scriptNum][0]);
|
||||
System.out.println("--------------------------------");
|
||||
for (int langNum = 0; langNum < langs.length; ++langNum) {
|
||||
String lang = langs[langNum];
|
||||
String script = scripts[scriptNum][langNum + 1];
|
||||
|
||||
timeQueries(client, lang, script, numQueries / 10); // warmup
|
||||
TimeValue time = timeQueries(client, lang, script, numQueries);
|
||||
printResults(lang, time, numQueries);
|
||||
}
|
||||
}
|
||||
|
||||
static Client setupIndex() throws Exception {
|
||||
// create cluster
|
||||
Settings settings = settingsBuilder().put("plugin.types", NativeScriptPlugin.class.getName())
|
||||
.put("name", "node1")
|
||||
.build();
|
||||
Node node1 = nodeBuilder().clusterName(clusterName).settings(settings).node();
|
||||
Client client = node1.client();
|
||||
client.admin().cluster().prepareHealth(indexName).setWaitForGreenStatus().setTimeout("10s").execute().actionGet();
|
||||
|
||||
// delete the index, if it exists
|
||||
try {
|
||||
client.admin().indices().prepareDelete(indexName).execute().actionGet();
|
||||
} catch (ElasticsearchException e) {
|
||||
// ok if the index didn't exist
|
||||
}
|
||||
|
||||
// create mappings
|
||||
IndicesAdminClient admin = client.admin().indices();
|
||||
admin.prepareCreate(indexName).addMapping("doc", "x", "type=long", "y", "type=double");
|
||||
|
||||
client.admin().cluster().prepareHealth(indexName).setWaitForGreenStatus().setTimeout("10s").execute().actionGet();
|
||||
return client;
|
||||
}
|
||||
|
||||
static void indexDocs(Client client, int numDocs) {
|
||||
System.out.print("Indexing " + numDocs + " random docs...");
|
||||
BulkRequestBuilder bulkRequest = client.prepareBulk();
|
||||
Random r = new Random(1);
|
||||
for (int i = 0; i < numDocs; i++) {
|
||||
bulkRequest.add(client.prepareIndex("test", "doc", Integer.toString(i))
|
||||
.setSource("x", r.nextInt(), "y", r.nextDouble(), "z", r.nextDouble()));
|
||||
|
||||
if (i % 1000 == 0) {
|
||||
bulkRequest.execute().actionGet();
|
||||
bulkRequest = client.prepareBulk();
|
||||
}
|
||||
}
|
||||
bulkRequest.execute().actionGet();
|
||||
client.admin().indices().prepareRefresh("test").execute().actionGet();
|
||||
client.admin().indices().prepareFlush("test").setFull(true).execute().actionGet();
|
||||
System.out.println("done");
|
||||
}
|
||||
|
||||
static TimeValue timeQueries(Client client, String lang, String script, int numQueries) {
|
||||
ScriptSortBuilder sort = SortBuilders.scriptSort(script, "number").lang(lang);
|
||||
SearchRequestBuilder req = client.prepareSearch(indexName)
|
||||
.setQuery(QueryBuilders.matchAllQuery())
|
||||
.addSort(sort);
|
||||
|
||||
StopWatch timer = new StopWatch();
|
||||
timer.start();
|
||||
for (int i = 0; i < numQueries; ++i) {
|
||||
req.get();
|
||||
}
|
||||
timer.stop();
|
||||
return timer.totalTime();
|
||||
}
|
||||
|
||||
static void printResults(String lang, TimeValue time, int numQueries) {
|
||||
long avgReq = time.millis() / numQueries;
|
||||
System.out.println(lang + ": " + time.format(PeriodType.seconds()) + " (" + avgReq + " msec per req)");
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,272 @@
|
|||
/*
|
||||
* 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.expression;
|
||||
|
||||
import org.elasticsearch.ExceptionsHelper;
|
||||
import org.elasticsearch.action.get.GetRequestBuilder;
|
||||
import org.elasticsearch.action.search.SearchPhaseExecutionException;
|
||||
import org.elasticsearch.action.search.SearchRequestBuilder;
|
||||
import org.elasticsearch.action.search.SearchResponse;
|
||||
import org.elasticsearch.action.search.ShardSearchFailure;
|
||||
import org.elasticsearch.common.xcontent.ToXContent;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.index.query.QueryBuilders;
|
||||
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder;
|
||||
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
|
||||
import org.elasticsearch.search.SearchHits;
|
||||
import org.elasticsearch.search.aggregations.AggregationBuilders;
|
||||
import org.elasticsearch.search.aggregations.metrics.stats.Stats;
|
||||
import org.elasticsearch.search.sort.SortBuilder;
|
||||
import org.elasticsearch.search.sort.SortBuilders;
|
||||
import org.elasticsearch.search.sort.SortOrder;
|
||||
import org.elasticsearch.test.ElasticsearchIntegrationTest;
|
||||
import org.elasticsearch.test.hamcrest.ElasticsearchAssertions;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.greaterThan;
|
||||
|
||||
public class ExpressionScriptTests extends ElasticsearchIntegrationTest {
|
||||
|
||||
private SearchResponse runScript(String script, Object... params) {
|
||||
ensureGreen("test");
|
||||
|
||||
Map<String, Object> paramsMap = new HashMap<>();
|
||||
assert(params.length % 2 == 0);
|
||||
for (int i = 0; i < params.length; i += 2) {
|
||||
paramsMap.put(params[i].toString(), params[i + 1]);
|
||||
}
|
||||
|
||||
SearchRequestBuilder req = new SearchRequestBuilder(client()).setIndices("test");
|
||||
req.setQuery(QueryBuilders.matchAllQuery())
|
||||
.addSort(SortBuilders.fieldSort("_uid")
|
||||
.order(SortOrder.ASC)).addScriptField("foo", "expression", script, paramsMap);
|
||||
return req.get();
|
||||
}
|
||||
|
||||
public void testBasic() throws Exception {
|
||||
createIndex("test");
|
||||
ensureGreen("test");
|
||||
client().prepareIndex("test", "doc", "1").setSource("foo", 4).setRefresh(true).get();
|
||||
SearchResponse rsp = runScript("doc['foo'].value + 1");
|
||||
assertEquals(1, rsp.getHits().getTotalHits());
|
||||
assertEquals(5.0, rsp.getHits().getAt(0).field("foo").getValue());
|
||||
}
|
||||
|
||||
public void testScore() throws Exception {
|
||||
createIndex("test");
|
||||
ensureGreen("test");
|
||||
indexRandom(true,
|
||||
client().prepareIndex("test", "doc", "1").setSource("text", "hello goodbye"),
|
||||
client().prepareIndex("test", "doc", "2").setSource("text", "hello hello hello goodbye"),
|
||||
client().prepareIndex("test", "doc", "3").setSource("text", "hello hello goodebye"));
|
||||
ScoreFunctionBuilder score = ScoreFunctionBuilders.scriptFunction("1 / _score", "expression", Collections.EMPTY_MAP);
|
||||
SearchRequestBuilder req = new SearchRequestBuilder(client()).setIndices("test");
|
||||
req.setQuery(QueryBuilders.functionScoreQuery(QueryBuilders.termQuery("text", "hello"), score).boostMode("replace"));
|
||||
SearchResponse rsp = req.get();
|
||||
SearchHits hits = rsp.getHits();
|
||||
assertEquals(3, hits.getTotalHits());
|
||||
assertEquals("1", hits.getAt(0).getId());
|
||||
assertEquals("3", hits.getAt(1).getId());
|
||||
assertEquals("2", hits.getAt(2).getId());
|
||||
}
|
||||
|
||||
public void testSparseField() throws Exception {
|
||||
ElasticsearchAssertions.assertAcked(prepareCreate("test").addMapping("doc", "x", "type=long", "y", "type=long"));
|
||||
ensureGreen("test");
|
||||
indexRandom(true,
|
||||
client().prepareIndex("test", "doc", "1").setSource("x", 4),
|
||||
client().prepareIndex("test", "doc", "2").setSource("y", 2));
|
||||
SearchResponse rsp = runScript("doc['x'].value + 1");
|
||||
ElasticsearchAssertions.assertSearchResponse(rsp);
|
||||
SearchHits hits = rsp.getHits();
|
||||
assertEquals(2, rsp.getHits().getTotalHits());
|
||||
assertEquals(5.0, hits.getAt(0).field("foo").getValue());
|
||||
assertEquals(1.0, hits.getAt(1).field("foo").getValue());
|
||||
}
|
||||
|
||||
public void testMissingField() throws Exception {
|
||||
createIndex("test");
|
||||
ensureGreen("test");
|
||||
client().prepareIndex("test", "doc", "1").setSource("x", 4).setRefresh(true).get();
|
||||
try {
|
||||
runScript("doc['bogus'].value");
|
||||
fail("Expected missing field to cause failure");
|
||||
} catch (SearchPhaseExecutionException e) {
|
||||
assertThat(ExceptionsHelper.detailedMessage(e) + "should have contained ExpressionScriptCompilationException",
|
||||
ExceptionsHelper.detailedMessage(e).contains("ExpressionScriptCompilationException"), equalTo(true));
|
||||
assertThat(ExceptionsHelper.detailedMessage(e) + "should have contained missing field error",
|
||||
ExceptionsHelper.detailedMessage(e).contains("does not exist in mappings"), equalTo(true));
|
||||
}
|
||||
}
|
||||
|
||||
public void testParams() throws Exception {
|
||||
createIndex("test");
|
||||
ensureGreen("test");
|
||||
indexRandom(true,
|
||||
client().prepareIndex("test", "doc", "1").setSource("x", 10),
|
||||
client().prepareIndex("test", "doc", "2").setSource("x", 3),
|
||||
client().prepareIndex("test", "doc", "3").setSource("x", 5));
|
||||
// a = int, b = double, c = long
|
||||
SearchResponse rsp = runScript("doc['x'].value * a + b + ((c + doc['x'].value) > 5000000009 ? 1 : 0)", "a", 2, "b", 3.5, "c", 5000000000L);
|
||||
SearchHits hits = rsp.getHits();
|
||||
assertEquals(3, hits.getTotalHits());
|
||||
assertEquals(24.5, hits.getAt(0).field("foo").getValue());
|
||||
assertEquals(9.5, hits.getAt(1).field("foo").getValue());
|
||||
assertEquals(13.5, hits.getAt(2).field("foo").getValue());
|
||||
}
|
||||
|
||||
public void testCompileFailure() {
|
||||
client().prepareIndex("test", "doc", "1").setSource("x", 1).setRefresh(true).get();
|
||||
try {
|
||||
runScript("garbage%@#%@");
|
||||
fail("Expected expression compilation failure");
|
||||
} catch (SearchPhaseExecutionException e) {
|
||||
assertThat(ExceptionsHelper.detailedMessage(e) + "should have contained ExpressionScriptCompilationException",
|
||||
ExceptionsHelper.detailedMessage(e).contains("ExpressionScriptCompilationException"), equalTo(true));
|
||||
assertThat(ExceptionsHelper.detailedMessage(e) + "should have contained compilation failure",
|
||||
ExceptionsHelper.detailedMessage(e).contains("Failed to parse expression"), equalTo(true));
|
||||
}
|
||||
}
|
||||
|
||||
public void testNonNumericParam() {
|
||||
client().prepareIndex("test", "doc", "1").setSource("x", 1).setRefresh(true).get();
|
||||
try {
|
||||
runScript("a", "a", "astring");
|
||||
fail("Expected string parameter to cause failure");
|
||||
} catch (SearchPhaseExecutionException e) {
|
||||
assertThat(ExceptionsHelper.detailedMessage(e) + "should have contained ExpressionScriptCompilationException",
|
||||
ExceptionsHelper.detailedMessage(e).contains("ExpressionScriptCompilationException"), equalTo(true));
|
||||
assertThat(ExceptionsHelper.detailedMessage(e) + "should have contained non-numeric parameter error",
|
||||
ExceptionsHelper.detailedMessage(e).contains("must be a numeric type"), equalTo(true));
|
||||
}
|
||||
}
|
||||
|
||||
public void testNonNumericField() {
|
||||
client().prepareIndex("test", "doc", "1").setSource("text", "this is not a number").setRefresh(true).get();
|
||||
try {
|
||||
runScript("doc['text'].value");
|
||||
fail("Expected text field to cause execution failure");
|
||||
} catch (SearchPhaseExecutionException e) {
|
||||
assertThat(ExceptionsHelper.detailedMessage(e) + "should have contained ExpressionScriptCompilationException",
|
||||
ExceptionsHelper.detailedMessage(e).contains("ExpressionScriptCompilationException"), equalTo(true));
|
||||
assertThat(ExceptionsHelper.detailedMessage(e) + "should have contained non-numeric field error",
|
||||
ExceptionsHelper.detailedMessage(e).contains("must be numeric"), equalTo(true));
|
||||
}
|
||||
}
|
||||
|
||||
public void testInvalidGlobalVariable() {
|
||||
client().prepareIndex("test", "doc", "1").setSource("foo", 5).setRefresh(true).get();
|
||||
try {
|
||||
runScript("bogus");
|
||||
fail("Expected bogus variable to cause execution failure");
|
||||
} catch (SearchPhaseExecutionException e) {
|
||||
assertThat(ExceptionsHelper.detailedMessage(e) + "should have contained ExpressionScriptCompilationException",
|
||||
ExceptionsHelper.detailedMessage(e).contains("ExpressionScriptCompilationException"), equalTo(true));
|
||||
assertThat(ExceptionsHelper.detailedMessage(e) + "should have contained unknown variable error",
|
||||
ExceptionsHelper.detailedMessage(e).contains("Unknown variable"), equalTo(true));
|
||||
}
|
||||
}
|
||||
|
||||
public void testDocWithoutField() {
|
||||
client().prepareIndex("test", "doc", "1").setSource("foo", 5).setRefresh(true).get();
|
||||
try {
|
||||
runScript("doc");
|
||||
fail("Expected doc variable without field to cause execution failure");
|
||||
} catch (SearchPhaseExecutionException e) {
|
||||
assertThat(ExceptionsHelper.detailedMessage(e) + "should have contained ExpressionScriptCompilationException",
|
||||
ExceptionsHelper.detailedMessage(e).contains("ExpressionScriptCompilationException"), equalTo(true));
|
||||
assertThat(ExceptionsHelper.detailedMessage(e) + "should have contained a missing specific field error",
|
||||
ExceptionsHelper.detailedMessage(e).contains("must be used with a specific field"), equalTo(true));
|
||||
}
|
||||
}
|
||||
|
||||
public void testInvalidFieldMember() {
|
||||
client().prepareIndex("test", "doc", "1").setSource("foo", 5).setRefresh(true).get();
|
||||
try {
|
||||
runScript("doc['foo'].bogus");
|
||||
fail("Expected bogus field member to cause execution failure");
|
||||
} catch (SearchPhaseExecutionException e) {
|
||||
assertThat(ExceptionsHelper.detailedMessage(e) + "should have contained ExpressionScriptCompilationException",
|
||||
ExceptionsHelper.detailedMessage(e).contains("ExpressionScriptCompilationException"), equalTo(true));
|
||||
assertThat(ExceptionsHelper.detailedMessage(e) + "should have contained field member error",
|
||||
ExceptionsHelper.detailedMessage(e).contains("Invalid member for field"), equalTo(true));
|
||||
}
|
||||
}
|
||||
|
||||
public void testSpecialValueVariable() throws Exception {
|
||||
// i.e. _value for aggregations
|
||||
createIndex("test");
|
||||
ensureGreen("test");
|
||||
indexRandom(true,
|
||||
client().prepareIndex("test", "doc", "1").setSource("x", 5, "y", 1.2),
|
||||
client().prepareIndex("test", "doc", "2").setSource("x", 10, "y", 1.4),
|
||||
client().prepareIndex("test", "doc", "3").setSource("x", 13, "y", 1.8));
|
||||
|
||||
SearchRequestBuilder req = new SearchRequestBuilder(client()).setIndices("test");
|
||||
req.setQuery(QueryBuilders.matchAllQuery())
|
||||
.addAggregation(AggregationBuilders.stats("int_agg").field("x").script("_value * 3").lang("expression"))
|
||||
.addAggregation(AggregationBuilders.stats("double_agg").field("y").script("_value - 1.1").lang("expression"));
|
||||
|
||||
SearchResponse rsp = req.get();
|
||||
assertEquals(3, rsp.getHits().getTotalHits());
|
||||
|
||||
Stats stats = rsp.getAggregations().get("int_agg");
|
||||
assertEquals(39.0, stats.getMax(), 0.0001);
|
||||
assertEquals(15.0, stats.getMin(), 0.0001);
|
||||
|
||||
stats = rsp.getAggregations().get("double_agg");
|
||||
assertEquals(0.7, stats.getMax(), 0.0001);
|
||||
assertEquals(0.1, stats.getMin(), 0.0001);
|
||||
}
|
||||
|
||||
public void testStringSpecialValueVariable() throws Exception {
|
||||
// i.e. expression script for term aggregations, which is not allowed
|
||||
createIndex("test");
|
||||
ensureGreen("test");
|
||||
indexRandom(true,
|
||||
client().prepareIndex("test", "doc", "1").setSource("text", "hello"),
|
||||
client().prepareIndex("test", "doc", "2").setSource("text", "goodbye"),
|
||||
client().prepareIndex("test", "doc", "3").setSource("text", "hello"));
|
||||
|
||||
SearchRequestBuilder req = new SearchRequestBuilder(client()).setIndices("test");
|
||||
req.setQuery(QueryBuilders.matchAllQuery())
|
||||
.addAggregation(AggregationBuilders.terms("term_agg").field("text").script("_value").lang("expression"));
|
||||
|
||||
String message;
|
||||
try {
|
||||
// shards that don't have docs with the "text" field will not fail,
|
||||
// so we may or may not get a total failure
|
||||
SearchResponse rsp = req.get();
|
||||
assertThat(rsp.getShardFailures().length, greaterThan(0)); // at least the shards containing the docs should have failed
|
||||
message = rsp.getShardFailures()[0].reason();
|
||||
} catch (SearchPhaseExecutionException e) {
|
||||
message = ExceptionsHelper.detailedMessage(e);
|
||||
}
|
||||
assertThat(message + "should have contained ExpressionScriptExecutionException",
|
||||
message.contains("ExpressionScriptExecutionException"), equalTo(true));
|
||||
assertThat(message + "should have contained text variable error",
|
||||
message.contains("text variable"), equalTo(true));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue