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:
Ryan Ernst 2014-07-15 07:46:43 -07:00
parent 1464bea00f
commit 64ab22816c
24 changed files with 6344 additions and 8 deletions

View File

@ -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.

View File

@ -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>

View File

@ -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 + ")");
}
}
}
}
}

View File

@ -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&lt;String,Method&gt; functions = new HashMap&lt;&gt;();
* // 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

View File

@ -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 ;
}
}

View File

@ -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);
}

View File

@ -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 {

View File

@ -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");
}
}
}

View File

@ -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);
}
}

View File

@ -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() {}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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() + ")";
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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()));
}
}

View File

@ -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);
}
}

View File

@ -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)");
}
}

View File

@ -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));
}
}