diff --git a/server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java index 106609f97cdeb..f0e539b44ca44 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java @@ -701,23 +701,7 @@ public List createFields(String name, Number value, LONG("long", NumericType.LONG) { @Override public Long parse(Object value, boolean coerce) { - if (value instanceof Long) { - return (Long)value; - } - - double doubleValue = objectToDouble(value); - // this check does not guarantee that value is inside MIN_VALUE/MAX_VALUE because values up to 9223372036854776832 will - // be equal to Long.MAX_VALUE after conversion to double. More checks ahead. - if (doubleValue < Long.MIN_VALUE || doubleValue > Long.MAX_VALUE) { - throw new IllegalArgumentException("Value [" + value + "] is out of range for a long"); - } - if (!coerce && doubleValue % 1 != 0) { - throw new IllegalArgumentException("Value [" + value + "] has a decimal part"); - } - - // longs need special handling so we don't lose precision while parsing - String stringValue = (value instanceof BytesRef) ? ((BytesRef) value).utf8ToString() : value.toString(); - return Numbers.toLong(stringValue, coerce); + return objectToLong(value, coerce); } @Override @@ -854,7 +838,7 @@ Number valueForSearch(Number value) { /** * Returns true if the object is a number and has a decimal part */ - boolean hasDecimalPart(Object number) { + public static boolean hasDecimalPart(Object number) { if (number instanceof Number) { double doubleValue = ((Number) number).doubleValue(); return doubleValue % 1 != 0; @@ -898,6 +882,30 @@ private static double objectToDouble(Object value) { return doubleValue; } + + /** + * Converts and Object to a {@code long} by checking it against known + * types and checking its range. + */ + public static long objectToLong(Object value, boolean coerce) { + if (value instanceof Long) { + return (Long)value; + } + + double doubleValue = objectToDouble(value); + // this check does not guarantee that value is inside MIN_VALUE/MAX_VALUE because values up to 9223372036854776832 will + // be equal to Long.MAX_VALUE after conversion to double. More checks ahead. + if (doubleValue < Long.MIN_VALUE || doubleValue > Long.MAX_VALUE) { + throw new IllegalArgumentException("Value [" + value + "] is out of range for a long"); + } + if (!coerce && doubleValue % 1 != 0) { + throw new IllegalArgumentException("Value [" + value + "] has a decimal part"); + } + + // longs need special handling so we don't lose precision while parsing + String stringValue = (value instanceof BytesRef) ? ((BytesRef) value).utf8ToString() : value.toString(); + return Numbers.toLong(stringValue, coerce); + } } public static final class NumberFieldType extends SimpleMappedFieldType { diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/LongScriptFieldScript.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/LongScriptFieldScript.java index 9850cab424642..ba15b66f2396e 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/LongScriptFieldScript.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/LongScriptFieldScript.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.runtimefields; import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.util.ArrayUtil; import org.elasticsearch.painless.spi.Whitelist; import org.elasticsearch.painless.spi.WhitelistLoader; import org.elasticsearch.script.ScriptContext; @@ -16,10 +17,9 @@ import java.io.IOException; import java.util.List; import java.util.Map; -import java.util.function.LongConsumer; public abstract class LongScriptFieldScript extends AbstractScriptFieldScript { - static final ScriptContext CONTEXT = new ScriptContext<>("long_script_field", Factory.class); + public static final ScriptContext CONTEXT = new ScriptContext<>("long_script_field", Factory.class); static List whitelist() { return List.of(WhitelistLoader.loadFromResourceFiles(RuntimeFieldsPainlessExtension.class, "long_whitelist.txt")); @@ -32,14 +32,47 @@ public interface Factory extends ScriptFactory { } public interface LeafFactory { - LongScriptFieldScript newInstance(LeafReaderContext ctx, LongConsumer sync) throws IOException; + LongScriptFieldScript newInstance(LeafReaderContext ctx) throws IOException; } - private final LongConsumer sync; + private long[] values = new long[1]; + private int count; - public LongScriptFieldScript(Map params, SearchLookup searchLookup, LeafReaderContext ctx, LongConsumer sync) { + public LongScriptFieldScript(Map params, SearchLookup searchLookup, LeafReaderContext ctx) { super(params, searchLookup, ctx); - this.sync = sync; + } + + /** + * Execute the script for the provided {@code docId}. + */ + public final void runForDoc(int docId) { + count = 0; + setDocument(docId); + execute(); + } + + /** + * Values from the last time {@link #runForDoc(int)} was called. This array + * is mutable and will change with the next call of {@link #runForDoc(int)}. + * It is also oversized and will contain garbage at all indices at and + * above {@link #count()}. + */ + public final long[] values() { + return values; + } + + /** + * The number of results produced the last time {@link #runForDoc(int)} was called. + */ + public final int count() { + return count; + } + + private void collectValue(long v) { + if (values.length < count + 1) { + values = ArrayUtil.grow(values, count + 1); + } + values[count++] = v; } public static class Value { @@ -50,7 +83,7 @@ public Value(LongScriptFieldScript script) { } public void value(long v) { - script.sync.accept(v); + script.collectValue(v); } } } diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/StringScriptFieldScript.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/StringScriptFieldScript.java index 69a196446aaf0..09321de9a225d 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/StringScriptFieldScript.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/StringScriptFieldScript.java @@ -35,7 +35,7 @@ public interface LeafFactory { StringScriptFieldScript newInstance(LeafReaderContext ctx) throws IOException; } - protected final List results = new ArrayList<>(); + private final List results = new ArrayList<>(); public StringScriptFieldScript(Map params, SearchLookup searchLookup, LeafReaderContext ctx) { super(params, searchLookup, ctx); diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/fielddata/ScriptBinaryFieldData.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/fielddata/ScriptBinaryFieldData.java index 592006d6ca9d8..e014c12969aff 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/fielddata/ScriptBinaryFieldData.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/fielddata/ScriptBinaryFieldData.java @@ -9,7 +9,7 @@ import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.search.SortField; import org.apache.lucene.util.SetOnce; -import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.index.AbstractIndexComponent; import org.elasticsearch.index.IndexSettings; @@ -34,8 +34,6 @@ import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; public final class ScriptBinaryFieldData extends AbstractIndexComponent implements @@ -81,6 +79,7 @@ private ScriptBinaryFieldData( this.scriptFactory = scriptFactory; } + @Override public void setSearchLookup(SearchLookup searchLookup) { this.leafFactory.set(scriptFactory.newFactory(script.getParams(), searchLookup)); } @@ -100,11 +99,7 @@ public ScriptBinaryLeafFieldData load(LeafReaderContext context) { try { return loadDirect(context); } catch (Exception e) { - if (e instanceof ElasticsearchException) { - throw (ElasticsearchException) e; - } else { - throw new ElasticsearchException(e); - } + throw ExceptionsHelper.convertToElastic(e); } } @@ -165,16 +160,4 @@ public void close() { } } - - static class ScriptBinaryResult { - private final List result = new ArrayList<>(); - - void accept(String value) { - this.result.add(value); - } - - List getResult() { - return result; - } - } } diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/fielddata/ScriptLongDocValues.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/fielddata/ScriptLongDocValues.java new file mode 100644 index 0000000000000..2aa348cbcac9b --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/fielddata/ScriptLongDocValues.java @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.fielddata; + +import org.elasticsearch.index.fielddata.AbstractSortedNumericDocValues; +import org.elasticsearch.xpack.runtimefields.LongScriptFieldScript; + +import java.io.IOException; +import java.util.Arrays; + +public final class ScriptLongDocValues extends AbstractSortedNumericDocValues { + private final LongScriptFieldScript script; + private int cursor; + + ScriptLongDocValues(LongScriptFieldScript script) { + this.script = script; + } + + @Override + public boolean advanceExact(int docId) { + script.runForDoc(docId); + if (script.count() == 0) { + return false; + } + Arrays.sort(script.values(), 0, script.count()); + cursor = 0; + return true; + } + + @Override + public long nextValue() throws IOException { + return script.values()[cursor++]; + } + + @Override + public int docValueCount() { + return script.count(); + } +} diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/fielddata/ScriptLongFieldData.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/fielddata/ScriptLongFieldData.java new file mode 100644 index 0000000000000..78bd0422182cf --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/fielddata/ScriptLongFieldData.java @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.fielddata; + +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.SortedNumericDocValues; +import org.apache.lucene.util.SetOnce; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.fielddata.FieldData; +import org.elasticsearch.index.fielddata.IndexFieldData; +import org.elasticsearch.index.fielddata.IndexFieldDataCache; +import org.elasticsearch.index.fielddata.IndexNumericFieldData; +import org.elasticsearch.index.fielddata.LeafNumericFieldData; +import org.elasticsearch.index.fielddata.ScriptDocValues; +import org.elasticsearch.index.fielddata.SearchLookupAware; +import org.elasticsearch.index.fielddata.SortedBinaryDocValues; +import org.elasticsearch.index.fielddata.SortedNumericDoubleValues; +import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.indices.breaker.CircuitBreakerService; +import org.elasticsearch.script.Script; +import org.elasticsearch.search.aggregations.support.CoreValuesSourceType; +import org.elasticsearch.search.aggregations.support.ValuesSourceType; +import org.elasticsearch.search.lookup.SearchLookup; +import org.elasticsearch.xpack.runtimefields.LongScriptFieldScript; + +import java.io.IOException; + +public final class ScriptLongFieldData extends IndexNumericFieldData implements SearchLookupAware { + + public static class Builder implements IndexFieldData.Builder { + + private final Script script; + private final LongScriptFieldScript.Factory scriptFactory; + + public Builder(Script script, LongScriptFieldScript.Factory scriptFactory) { + this.script = script; + this.scriptFactory = scriptFactory; + } + + @Override + public ScriptLongFieldData build( + IndexSettings indexSettings, + MappedFieldType fieldType, + IndexFieldDataCache cache, + CircuitBreakerService breakerService, + MapperService mapperService + ) { + return new ScriptLongFieldData(indexSettings.getIndex(), fieldType.name(), script, scriptFactory); + } + } + + private final Index index; + private final String fieldName; + private final Script script; + private final LongScriptFieldScript.Factory scriptFactory; + private final SetOnce leafFactory = new SetOnce<>(); + + private ScriptLongFieldData(Index index, String fieldName, Script script, LongScriptFieldScript.Factory scriptFactory) { + this.index = index; + this.fieldName = fieldName; + this.script = script; + this.scriptFactory = scriptFactory; + } + + @Override + public void setSearchLookup(SearchLookup searchLookup) { + this.leafFactory.set(scriptFactory.newFactory(script.getParams(), searchLookup)); + } + + @Override + public String getFieldName() { + return fieldName; + } + + @Override + public ValuesSourceType getValuesSourceType() { + return CoreValuesSourceType.NUMERIC; + } + + @Override + public ScriptLongLeafFieldData load(LeafReaderContext context) { + try { + return loadDirect(context); + } catch (Exception e) { + throw ExceptionsHelper.convertToElastic(e); + } + } + + @Override + public ScriptLongLeafFieldData loadDirect(LeafReaderContext context) throws IOException { + return new ScriptLongLeafFieldData(new ScriptLongDocValues(leafFactory.get().newInstance(context))); + } + + @Override + public NumericType getNumericType() { + return NumericType.LONG; + } + + @Override + protected boolean sortRequiresCustomComparator() { + return true; + } + + @Override + public void clear() {} + + @Override + public Index index() { + return index; + } + + public static class ScriptLongLeafFieldData implements LeafNumericFieldData { + private final ScriptLongDocValues scriptBinaryDocValues; + + ScriptLongLeafFieldData(ScriptLongDocValues scriptBinaryDocValues) { + this.scriptBinaryDocValues = scriptBinaryDocValues; + } + + @Override + public ScriptDocValues getScriptValues() { + return new ScriptDocValues.Longs(getLongValues()); + } + + @Override + public SortedBinaryDocValues getBytesValues() { + return FieldData.toString(scriptBinaryDocValues); + } + + @Override + public SortedNumericDoubleValues getDoubleValues() { + return FieldData.castToDouble(getLongValues()); + } + + @Override + public SortedNumericDocValues getLongValues() { + return scriptBinaryDocValues; + } + + @Override + public long ramBytesUsed() { + return 0; + } + + @Override + public void close() { + + } + } +} diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptMappedFieldType.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptMappedFieldType.java new file mode 100644 index 0000000000000..71340ebc0e3d0 --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptMappedFieldType.java @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.mapper; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.xcontent.ToXContent.Params; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.mapper.TextSearchInfo; +import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.script.Script; + +import java.io.IOException; +import java.util.Map; + +import static org.elasticsearch.search.SearchService.ALLOW_EXPENSIVE_QUERIES; + +/** + * Abstract base {@linkplain MappedFieldType} for scripted fields. + */ +abstract class AbstractScriptMappedFieldType extends MappedFieldType { + protected final Script script; + + AbstractScriptMappedFieldType(String name, Script script, Map meta) { + super(name, false, false, TextSearchInfo.NONE, meta); + this.script = script; + } + + protected abstract String runtimeType(); + + void mapperXContentBody(XContentBuilder builder, Params params) throws IOException { + builder.field("runtime_type", runtimeType()); + builder.field("script", script.getIdOrCode()); // TODO For some reason this doesn't allow us to do the full xcontent of the script. + } + + @Override + public final String typeName() { + return ScriptFieldMapper.CONTENT_TYPE; + } + + @Override + public final String familyTypeName() { + return runtimeType(); + } + + @Override + public final boolean isSearchable() { + return true; + } + + @Override + public final boolean isAggregatable() { + return true; + } + + protected final void checkAllowExpensiveQueries(QueryShardContext context) { + if (context.allowExpensiveQueries() == false) { + throw new ElasticsearchException( + "queries cannot be executed against [" + + ScriptFieldMapper.CONTENT_TYPE + + "] fields while [" + + ALLOW_EXPENSIVE_QUERIES.getKey() + + "] is set to [false]." + ); + } + } +} diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptFieldMapper.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptFieldMapper.java index 3746c4cb8b6ea..cc81089d6e14d 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptFieldMapper.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptFieldMapper.java @@ -12,11 +12,13 @@ import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.Mapper; import org.elasticsearch.index.mapper.MapperParsingException; +import org.elasticsearch.index.mapper.NumberFieldMapper.NumberType; import org.elasticsearch.index.mapper.ParametrizedFieldMapper; import org.elasticsearch.index.mapper.ParseContext; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptType; +import org.elasticsearch.xpack.runtimefields.LongScriptFieldScript; import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; import java.io.IOException; @@ -60,8 +62,8 @@ protected void parseCreateField(ParseContext context) { @Override protected void doXContentBody(XContentBuilder builder, boolean includeDefaults, Params params) throws IOException { super.doXContentBody(builder, includeDefaults, params); - RuntimeKeywordMappedFieldType fieldType = (RuntimeKeywordMappedFieldType) fieldType(); - fieldType.doXContentBody(builder, includeDefaults, params); + AbstractScriptMappedFieldType fieldType = (AbstractScriptMappedFieldType) fieldType(); + fieldType.mapperXContentBody(builder, params); } @Override @@ -78,7 +80,20 @@ public static class Builder extends ParametrizedFieldMapper.Builder { builder.script.getValue(), StringScriptFieldScript.CONTEXT ); - return new RuntimeKeywordMappedFieldType( + return new ScriptKeywordMappedFieldType( + builder.buildFullName(context), + builder.script.getValue(), + factory, + builder.meta.getValue() + ); + }, + NumberType.LONG.typeName(), + (builder, context) -> { + LongScriptFieldScript.Factory factory = builder.scriptCompiler.compile( + builder.script.getValue(), + LongScriptFieldScript.CONTEXT + ); + return new ScriptLongMappedFieldType( builder.buildFullName(context), builder.script.getValue(), factory, @@ -136,11 +151,10 @@ public ScriptFieldMapper build(BuilderContext context) { if (fieldTypeResolver == null) { throw new IllegalArgumentException("runtime_type [" + runtimeType.getValue() + "] not supported"); } - MappedFieldType mappedFieldType = fieldTypeResolver.apply(this, context); // TODO copy to and multi_fields should not be supported, parametrized field mapper needs to be adapted return new ScriptFieldMapper( name, - mappedFieldType, + fieldTypeResolver.apply(this, context), multiFieldsBuilder.build(this, context), copyTo.build(), runtimeType.getValue(), diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeKeywordMappedFieldType.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptKeywordMappedFieldType.java similarity index 76% rename from x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeKeywordMappedFieldType.java rename to x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptKeywordMappedFieldType.java index c940502ecb736..b45b665d8c550 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeKeywordMappedFieldType.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptKeywordMappedFieldType.java @@ -9,16 +9,11 @@ import org.apache.lucene.search.MultiTermQuery.RewriteMethod; import org.apache.lucene.search.Query; import org.apache.lucene.util.BytesRef; -import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.lucene.BytesRefs; import org.elasticsearch.common.time.DateMathParser; import org.elasticsearch.common.unit.Fuzziness; -import org.elasticsearch.common.xcontent.ToXContent.Params; -import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.mapper.KeywordFieldMapper; -import org.elasticsearch.index.mapper.MappedFieldType; -import org.elasticsearch.index.mapper.TextSearchInfo; import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.script.Script; import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; @@ -32,7 +27,6 @@ import org.elasticsearch.xpack.runtimefields.query.StringScriptFieldTermsQuery; import org.elasticsearch.xpack.runtimefields.query.StringScriptFieldWildcardQuery; -import java.io.IOException; import java.time.ZoneId; import java.util.List; import java.util.Map; @@ -40,19 +34,23 @@ import java.util.Set; import static java.util.stream.Collectors.toSet; -import static org.elasticsearch.search.SearchService.ALLOW_EXPENSIVE_QUERIES; -public final class RuntimeKeywordMappedFieldType extends MappedFieldType { +public final class ScriptKeywordMappedFieldType extends AbstractScriptMappedFieldType { private final Script script; private final StringScriptFieldScript.Factory scriptFactory; - RuntimeKeywordMappedFieldType(String name, Script script, StringScriptFieldScript.Factory scriptFactory, Map meta) { - super(name, false, false, TextSearchInfo.NONE, meta); + ScriptKeywordMappedFieldType(String name, Script script, StringScriptFieldScript.Factory scriptFactory, Map meta) { + super(name, script, meta); this.script = script; this.scriptFactory = scriptFactory; } + @Override + protected String runtimeType() { + return KeywordFieldMapper.CONTENT_TYPE; + } + @Override public Object valueForDisplay(Object value) { if (value == null) { @@ -63,26 +61,6 @@ public Object valueForDisplay(Object value) { return binaryValue.utf8ToString(); } - @Override - public String typeName() { - return ScriptFieldMapper.CONTENT_TYPE; - } - - @Override - public String familyTypeName() { - return KeywordFieldMapper.CONTENT_TYPE; - } - - @Override - public boolean isSearchable() { - return true; - } - - @Override - public boolean isAggregatable() { - return true; - } - @Override public ScriptBinaryFieldData.Builder fielddataBuilder(String fullyQualifiedIndexName) { // TODO once we get SearchLookup as an argument, we can already call scriptFactory.newFactory here and pass through the result @@ -93,18 +71,6 @@ private StringScriptFieldScript.LeafFactory leafFactory(QueryShardContext contex return scriptFactory.newFactory(script.getParams(), context.lookup()); } - private void checkAllowExpensiveQueries(QueryShardContext context) { - if (context.allowExpensiveQueries() == false) { - throw new ElasticsearchException( - "queries cannot be executed against [" - + ScriptFieldMapper.CONTENT_TYPE - + "] fields while [" - + ALLOW_EXPENSIVE_QUERIES.getKey() - + "] is set to [false]." - ); - } - } - @Override public Query existsQuery(QueryShardContext context) { checkAllowExpensiveQueries(context); @@ -185,9 +151,4 @@ public Query wildcardQuery(String value, RewriteMethod method, QueryShardContext checkAllowExpensiveQueries(context); return new StringScriptFieldWildcardQuery(script, leafFactory(context), name(), value); } - - void doXContentBody(XContentBuilder builder, boolean includeDefaults, Params params) throws IOException { - builder.field("runtime_type", "keyword"); - builder.field("script", script.getIdOrCode()); // TODO For some reason this doesn't allow us to do the full xcontent of the script. - } } diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptLongMappedFieldType.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptLongMappedFieldType.java new file mode 100644 index 0000000000000..ad2690e9db65f --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptLongMappedFieldType.java @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.mapper; + +import org.apache.lucene.search.Query; +import org.elasticsearch.common.lucene.search.Queries; +import org.elasticsearch.index.mapper.NumberFieldMapper.NumberType; +import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.script.Script; +import org.elasticsearch.xpack.runtimefields.LongScriptFieldScript; +import org.elasticsearch.xpack.runtimefields.fielddata.ScriptLongFieldData; +import org.elasticsearch.xpack.runtimefields.query.LongScriptFieldExistsQuery; +import org.elasticsearch.xpack.runtimefields.query.LongScriptFieldTermQuery; + +import java.util.Map; + +public class ScriptLongMappedFieldType extends AbstractScriptMappedFieldType { + private final LongScriptFieldScript.Factory scriptFactory; + + ScriptLongMappedFieldType(String name, Script script, LongScriptFieldScript.Factory scriptFactory, Map meta) { + super(name, script, meta); + this.scriptFactory = scriptFactory; + } + + @Override + protected String runtimeType() { + return NumberType.LONG.typeName(); + } + + @Override + public Object valueForDisplay(Object value) { + return value; // These should come back as a Long + } + + @Override + public ScriptLongFieldData.Builder fielddataBuilder(String fullyQualifiedIndexName) { + // TODO once we get SearchLookup as an argument, we can already call scriptFactory.newFactory here and pass through the result + return new ScriptLongFieldData.Builder(script, scriptFactory); + } + + private LongScriptFieldScript.LeafFactory leafFactory(QueryShardContext context) { + return scriptFactory.newFactory(script.getParams(), context.lookup()); + } + + @Override + public Query existsQuery(QueryShardContext context) { + checkAllowExpensiveQueries(context); + return new LongScriptFieldExistsQuery(script, leafFactory(context), name()); + } + + @Override + public Query termQuery(Object value, QueryShardContext context) { + if (NumberType.hasDecimalPart(value)) { + return Queries.newMatchNoDocsQuery("Value [" + value + "] has a decimal part"); + } + checkAllowExpensiveQueries(context); + return new LongScriptFieldTermQuery(script, leafFactory(context), name(), NumberType.objectToLong(value, true)); + } +} diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractLongScriptFieldQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractLongScriptFieldQuery.java new file mode 100644 index 0000000000000..7ebdb1047374d --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractLongScriptFieldQuery.java @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.search.ConstantScoreScorer; +import org.apache.lucene.search.ConstantScoreWeight; +import org.apache.lucene.search.DocIdSetIterator; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.QueryVisitor; +import org.apache.lucene.search.ScoreMode; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.TwoPhaseIterator; +import org.apache.lucene.search.Weight; +import org.elasticsearch.script.Script; +import org.elasticsearch.xpack.runtimefields.LongScriptFieldScript; +import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; + +import java.io.IOException; +import java.util.Objects; + +/** + * Abstract base class for building queries based on {@link StringScriptFieldScript}. + */ +abstract class AbstractLongScriptFieldQuery extends AbstractScriptFieldQuery { + private final LongScriptFieldScript.LeafFactory leafFactory; + + AbstractLongScriptFieldQuery(Script script, LongScriptFieldScript.LeafFactory leafFactory, String fieldName) { + super(script, fieldName); + this.leafFactory = Objects.requireNonNull(leafFactory); + } + + /** + * Does the value match this query? + */ + protected abstract boolean matches(long[] values, int count); + + @Override + public final Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException { + return new ConstantScoreWeight(this, boost) { + @Override + public boolean isCacheable(LeafReaderContext ctx) { + return false; // scripts aren't really cacheable at this point + } + + @Override + public Scorer scorer(LeafReaderContext ctx) throws IOException { + LongScriptFieldScript script = leafFactory.newInstance(ctx); + DocIdSetIterator approximation = DocIdSetIterator.all(ctx.reader().maxDoc()); + TwoPhaseIterator twoPhase = new TwoPhaseIterator(approximation) { + @Override + public boolean matches() throws IOException { + script.runForDoc(approximation().docID()); + return AbstractLongScriptFieldQuery.this.matches(script.values(), script.count()); + } + + @Override + public float matchCost() { + return MATCH_COST; + } + }; + return new ConstantScoreScorer(this, score(), scoreMode, twoPhase); + } + }; + } + + @Override + public final void visit(QueryVisitor visitor) { + // No subclasses contain any Terms because those have to be strings. + if (visitor.acceptField(fieldName())) { + visitor.visitLeaf(this); + } + } +} diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractScriptFieldQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractScriptFieldQuery.java new file mode 100644 index 0000000000000..a69dab5512c9c --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractScriptFieldQuery.java @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.apache.lucene.search.Query; +import org.elasticsearch.script.Script; + +import java.util.Objects; + +/** + * Abstract base class for building queries based on script fields. + */ +abstract class AbstractScriptFieldQuery extends Query { + /** + * We don't have the infrastructure to estimate the match cost of a script + * so we just use a big number. + */ + protected static final float MATCH_COST = 9000f; + + private final Script script; + private final String fieldName; + + AbstractScriptFieldQuery(Script script, String fieldName) { + this.script = Objects.requireNonNull(script); + this.fieldName = Objects.requireNonNull(fieldName); + } + + final Script script() { + return script; + } + + final String fieldName() { + return fieldName; + } + + @Override + public int hashCode() { + return Objects.hash(getClass(), script, fieldName); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || getClass() != obj.getClass()) { + return false; + } + AbstractScriptFieldQuery other = (AbstractScriptFieldQuery) obj; + return script.equals(other.script) && fieldName.equals(other.fieldName); + } +} diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractStringScriptFieldQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractStringScriptFieldQuery.java index d98d8397841b5..b17bfc5ac137c 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractStringScriptFieldQuery.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractStringScriptFieldQuery.java @@ -11,14 +11,12 @@ import org.apache.lucene.search.ConstantScoreWeight; import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.search.IndexSearcher; -import org.apache.lucene.search.Query; import org.apache.lucene.search.ScoreMode; import org.apache.lucene.search.Scorer; import org.apache.lucene.search.TwoPhaseIterator; import org.apache.lucene.search.Weight; import org.elasticsearch.script.Script; import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; -import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript.LeafFactory; import java.io.IOException; import java.util.List; @@ -27,15 +25,12 @@ /** * Abstract base class for building queries based on {@link StringScriptFieldScript}. */ -abstract class AbstractStringScriptFieldQuery extends Query { - private final Script script; +abstract class AbstractStringScriptFieldQuery extends AbstractScriptFieldQuery { private final StringScriptFieldScript.LeafFactory leafFactory; - private final String fieldName; - AbstractStringScriptFieldQuery(Script script, LeafFactory leafFactory, String fieldName) { - this.script = script; + AbstractStringScriptFieldQuery(Script script, StringScriptFieldScript.LeafFactory leafFactory, String fieldName) { + super(script, fieldName); this.leafFactory = Objects.requireNonNull(leafFactory); - this.fieldName = Objects.requireNonNull(fieldName); } /** @@ -63,34 +58,11 @@ public boolean matches() throws IOException { @Override public float matchCost() { - // TODO we don't have a good way of estimating the complexity of the script so we just go with 9000 - return 9000f; + return MATCH_COST; } }; return new ConstantScoreScorer(this, score(), scoreMode, twoPhase); } }; } - - final Script script() { - return script; - } - - protected final String fieldName() { - return fieldName; - } - - @Override - public int hashCode() { - return Objects.hash(getClass(), script, fieldName); - } - - @Override - public boolean equals(Object obj) { - if (obj == null || getClass() != obj.getClass()) { - return false; - } - AbstractStringScriptFieldQuery other = (AbstractStringScriptFieldQuery) obj; - return script.equals(other.script) && fieldName.equals(other.fieldName); - } } diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/LongScriptFieldExistsQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/LongScriptFieldExistsQuery.java new file mode 100644 index 0000000000000..4ee8e58aa6ed7 --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/LongScriptFieldExistsQuery.java @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.elasticsearch.script.Script; +import org.elasticsearch.xpack.runtimefields.LongScriptFieldScript; + +public class LongScriptFieldExistsQuery extends AbstractLongScriptFieldQuery { + public LongScriptFieldExistsQuery(Script script, LongScriptFieldScript.LeafFactory leafFactory, String fieldName) { + super(script, leafFactory, fieldName); + } + + @Override + protected boolean matches(long[] values, int count) { + return count > 0; + } + + @Override + public final String toString(String field) { + if (fieldName().contentEquals(field)) { + return getClass().getSimpleName(); + } + return fieldName() + ":" + getClass().getSimpleName(); + } + + // Superclass's equals and hashCode are great for this class +} diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/LongScriptFieldTermQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/LongScriptFieldTermQuery.java new file mode 100644 index 0000000000000..9f50edfb5bec7 --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/LongScriptFieldTermQuery.java @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.elasticsearch.script.Script; +import org.elasticsearch.xpack.runtimefields.LongScriptFieldScript; + +import java.util.Objects; + +public class LongScriptFieldTermQuery extends AbstractLongScriptFieldQuery { + private final long term; + + public LongScriptFieldTermQuery(Script script, LongScriptFieldScript.LeafFactory leafFactory, String fieldName, long term) { + super(script, leafFactory, fieldName); + this.term = term; + } + + @Override + protected boolean matches(long[] values, int count) { + for (int i = 0; i < count; i++) { + if (term == values[i]) { + return true; + } + } + return false; + } + + @Override + public final String toString(String field) { + if (fieldName().contentEquals(field)) { + return Long.toString(term); + } + return fieldName() + ":" + term; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), term); + } + + @Override + public boolean equals(Object obj) { + if (false == super.equals(obj)) { + return false; + } + LongScriptFieldTermQuery other = (LongScriptFieldTermQuery) obj; + return term == other.term; + } + + long term() { + return term; + } +} diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldExistsQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldExistsQuery.java index dedc7ce5d41ae..9f1ae5f110c3e 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldExistsQuery.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldExistsQuery.java @@ -24,9 +24,9 @@ protected boolean matches(List values) { @Override public final String toString(String field) { if (fieldName().contentEquals(field)) { - return "ScriptFieldExists"; + return getClass().getSimpleName(); } - return fieldName() + ":ScriptFieldExists"; + return fieldName() + ":" + getClass().getSimpleName(); } // Superclass's equals and hashCode are great for this class diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/LongScriptFieldScriptTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/LongScriptFieldScriptTests.java index d8d8da6d715f4..7494183fc26a2 100644 --- a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/LongScriptFieldScriptTests.java +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/LongScriptFieldScriptTests.java @@ -101,13 +101,14 @@ protected LongScriptFieldScript.LeafFactory newLeafFactory( @Override protected IntFunction> newInstance(LongScriptFieldScript.LeafFactory leafFactory, LeafReaderContext context) throws IOException { - List results = new ArrayList<>(); - LongScriptFieldScript script = leafFactory.newInstance(context, results::add); + LongScriptFieldScript script = leafFactory.newInstance(context); return docId -> { - results.clear(); - script.setDocument(docId); - script.execute(); - return results; + script.runForDoc(docId); + List list = new ArrayList<>(script.count()); + for (int i = 0; i < script.count(); i++) { + list.add(script.values()[i]); + } + return list; }; } } diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptMappedFieldTypeTestCase.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptMappedFieldTypeTestCase.java new file mode 100644 index 0000000000000..1b7845baa6c1f --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptMappedFieldTypeTestCase.java @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.mapper; + +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.search.lookup.SearchLookup; +import org.elasticsearch.test.ESTestCase; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +abstract class AbstractScriptMappedFieldTypeTestCase extends ESTestCase { + protected QueryShardContext mockContext() { + return mockContext(true); + } + + protected QueryShardContext mockContext(boolean allowExpensiveQueries) { + MapperService mapperService = mock(MapperService.class); + QueryShardContext context = mock(QueryShardContext.class); + when(context.allowExpensiveQueries()).thenReturn(allowExpensiveQueries); + when(context.lookup()).thenReturn(new SearchLookup(mapperService, mft -> null)); + return context; + } + +} diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptFieldMapperTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptFieldMapperTests.java index bbbf12db68c09..e9c0faaca80b8 100644 --- a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptFieldMapperTests.java +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptFieldMapperTests.java @@ -13,7 +13,6 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.index.mapper.FieldMapper; -import org.elasticsearch.index.mapper.KeywordFieldMapper; import org.elasticsearch.index.mapper.MapperParsingException; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.plugins.Plugin; @@ -22,9 +21,11 @@ import org.elasticsearch.script.ScriptEngine; import org.elasticsearch.test.ESSingleNodeTestCase; import org.elasticsearch.test.InternalSettingsPlugin; +import org.elasticsearch.xpack.runtimefields.LongScriptFieldScript; import org.elasticsearch.xpack.runtimefields.RuntimeFields; import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; +import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.Map; @@ -106,46 +107,32 @@ public void testStoredScriptsAreNotSupported() throws Exception { } public void testUnsupportedRuntimeType() throws Exception { - XContentBuilder mapping = XContentFactory.jsonBuilder() - .startObject() - .startObject("_doc") - .startObject("properties") - .startObject("field") - .field("type", "script") - .field("runtime_type", "unsupported") - .startObject("script") - .field("source", "keyword('test')") - .field("lang", "test") - .endObject() - .endObject() - .endObject() - .endObject() - .endObject(); - - MapperParsingException exc = expectThrows(MapperParsingException.class, () -> createIndex("test", Settings.EMPTY, mapping)); + MapperParsingException exc = expectThrows( + MapperParsingException.class, + () -> createIndex("test", Settings.EMPTY, mapping("unsupported")) + ); assertEquals("Failed to parse mapping: runtime_type [unsupported] not supported", exc.getMessage()); } + public void testKeyword() throws IOException { + MapperService mapperService = createIndex("test", Settings.EMPTY, mapping("keyword")).mapperService(); + FieldMapper mapper = (FieldMapper) mapperService.documentMapper().mappers().getMapper("field"); + assertThat(mapper, instanceOf(ScriptFieldMapper.class)); + assertEquals(Strings.toString(mapping("keyword")), Strings.toString(mapperService.documentMapper())); + } + + public void testLong() throws IOException { + MapperService mapperService = createIndex("test", Settings.EMPTY, mapping("long")).mapperService(); + FieldMapper mapper = (FieldMapper) mapperService.documentMapper().mappers().getMapper("field"); + assertThat(mapper, instanceOf(ScriptFieldMapper.class)); + assertEquals(Strings.toString(mapping("long")), Strings.toString(mapperService.documentMapper())); + } + public void testFieldCaps() throws Exception { for (String runtimeType : runtimeTypes) { - { - XContentBuilder mapping = XContentFactory.jsonBuilder() - .startObject() - .startObject("_doc") - .startObject("properties") - .startObject("field") - .field("type", "script") - .field("runtime_type", runtimeType) - .startObject("script") - .field("source", runtimeType + "('test')") - .field("lang", "test") - .endObject() - .endObject() - .endObject() - .endObject() - .endObject(); - createIndex("test_script", Settings.EMPTY, mapping); - } + String scriptIndex = "test_" + runtimeType + "_script"; + String concreteIndex = "test_" + runtimeType + "_concrete"; + createIndex(scriptIndex, Settings.EMPTY, mapping(runtimeType)); { XContentBuilder mapping = XContentFactory.jsonBuilder() .startObject() @@ -157,13 +144,13 @@ public void testFieldCaps() throws Exception { .endObject() .endObject() .endObject(); - createIndex("test_concrete", Settings.EMPTY, mapping); + createIndex(concreteIndex, Settings.EMPTY, mapping); } - FieldCapabilitiesResponse response = client().prepareFieldCaps("test_*").setFields("field").get(); - assertThat(response.getIndices(), arrayContainingInAnyOrder("test_script", "test_concrete")); + FieldCapabilitiesResponse response = client().prepareFieldCaps("test_" + runtimeType + "_*").setFields("field").get(); + assertThat(response.getIndices(), arrayContainingInAnyOrder(scriptIndex, concreteIndex)); Map field = response.getField("field"); assertEquals(1, field.size()); - FieldCapabilities fieldCapabilities = field.get(KeywordFieldMapper.CONTENT_TYPE); + FieldCapabilities fieldCapabilities = field.get(runtimeType); assertTrue(fieldCapabilities.isSearchable()); assertTrue(fieldCapabilities.isAggregatable()); assertEquals(runtimeType, fieldCapabilities.getType()); @@ -173,27 +160,29 @@ public void testFieldCaps() throws Exception { } } - public void testDefaultMapping() throws Exception { - XContentBuilder mapping = XContentFactory.jsonBuilder() - .startObject() - .startObject("_doc") - .startObject("properties") - .startObject("field") - .field("type", "script") - .field("runtime_type", randomFrom(runtimeTypes)) - .startObject("script") - .field("source", "keyword('test')") - .field("lang", "test") - .endObject() - .endObject() - .endObject() - .endObject() - .endObject(); - - MapperService mapperService = createIndex("test", Settings.EMPTY, mapping).mapperService(); - FieldMapper mapper = (FieldMapper) mapperService.documentMapper().mappers().getMapper("field"); - assertThat(mapper, instanceOf(ScriptFieldMapper.class)); - assertEquals(Strings.toString(mapping), Strings.toString(mapperService.documentMapper())); + private XContentBuilder mapping(String type) throws IOException { + XContentBuilder mapping = XContentFactory.jsonBuilder().startObject(); + { + mapping.startObject("_doc"); + { + mapping.startObject("properties"); + { + mapping.startObject("field"); + { + mapping.field("type", "script").field("runtime_type", type); + mapping.startObject("script"); + { + mapping.field("source", "dummy_source").field("lang", "test"); + } + mapping.endObject(); + } + mapping.endObject(); + } + mapping.endObject(); + } + mapping.endObject(); + } + return mapping.endObject(); } public static class TestScriptPlugin extends Plugin implements ScriptPlugin { @@ -212,27 +201,41 @@ public FactoryType compile( ScriptContext context, Map paramsMap ) { - if ("keyword('test')".equals(code)) { - StringScriptFieldScript.Factory factory = (params, lookup) -> ctx -> new StringScriptFieldScript( + if ("dummy_source".equals(code)) { + @SuppressWarnings("unchecked") + FactoryType castFactory = (FactoryType) dummyScriptFactory(context); + return castFactory; + } + throw new IllegalArgumentException("No test script for [" + code + "]"); + } + + private Object dummyScriptFactory(ScriptContext context) { + if (context == StringScriptFieldScript.CONTEXT) { + return (StringScriptFieldScript.Factory) (params, lookup) -> ctx -> new StringScriptFieldScript( params, lookup, ctx ) { @Override public void execute() { - this.results.add("test"); + new StringScriptFieldScript.Value(this).value("test"); } }; - @SuppressWarnings("unchecked") - FactoryType castFactory = (FactoryType) factory; - return castFactory; } - throw new IllegalArgumentException("No test script for [" + code + "]"); + if (context == LongScriptFieldScript.CONTEXT) { + return (LongScriptFieldScript.Factory) (params, lookup) -> ctx -> new LongScriptFieldScript(params, lookup, ctx) { + @Override + public void execute() { + new LongScriptFieldScript.Value(this).value(1); + } + }; + } + throw new IllegalArgumentException("No test script for [" + context + "]"); } @Override public Set> getSupportedContexts() { - return Set.of(StringScriptFieldScript.CONTEXT); + return Set.of(StringScriptFieldScript.CONTEXT, LongScriptFieldScript.CONTEXT); } }; } diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeKeywordMappedFieldTypeTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptKeywordMappedFieldTypeTests.java similarity index 89% rename from x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeKeywordMappedFieldTypeTests.java rename to x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptKeywordMappedFieldTypeTests.java index ee316c85ce34c..4d1c990d94403 100644 --- a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeKeywordMappedFieldTypeTests.java +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptKeywordMappedFieldTypeTests.java @@ -26,7 +26,6 @@ import org.elasticsearch.common.unit.Fuzziness; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.fielddata.SortedBinaryDocValues; -import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.painless.PainlessPlugin; import org.elasticsearch.painless.PainlessScriptEngine; @@ -35,8 +34,6 @@ import org.elasticsearch.script.ScriptModule; import org.elasticsearch.script.ScriptService; import org.elasticsearch.script.ScriptType; -import org.elasticsearch.search.lookup.SearchLookup; -import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.runtimefields.RuntimeFields; import org.elasticsearch.xpack.runtimefields.RuntimeFieldsPainlessExtension; import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; @@ -50,10 +47,8 @@ import static java.util.Collections.emptyMap; import static org.hamcrest.Matchers.equalTo; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -public class RuntimeKeywordMappedFieldTypeTests extends ESTestCase { +public class ScriptKeywordMappedFieldTypeTests extends AbstractScriptMappedFieldTypeTestCase { public void testDocValues() throws IOException { try (Directory directory = newDirectory(); RandomIndexWriter iw = new RandomIndexWriter(random(), directory)) { iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": [1]}")))); @@ -61,7 +56,7 @@ public void testDocValues() throws IOException { List results = new ArrayList<>(); try (DirectoryReader reader = iw.getReader()) { IndexSearcher searcher = newSearcher(reader); - RuntimeKeywordMappedFieldType ft = build( + ScriptKeywordMappedFieldType ft = build( "for (def v : source.foo) {value(v.toString() + params.param)}", Map.of("param", "-suffix") ); @@ -113,7 +108,7 @@ public void testExistsQuery() throws IOException { } public void testExistsQueryIsExpensive() throws IOException { - checkExpensiveQuery(RuntimeKeywordMappedFieldType::existsQuery); + checkExpensiveQuery(ScriptKeywordMappedFieldType::existsQuery); } public void testFuzzyQuery() throws IOException { @@ -219,7 +214,7 @@ public void testTermQuery() throws IOException { iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": 2}")))); try (DirectoryReader reader = iw.getReader()) { IndexSearcher searcher = newSearcher(reader); - RuntimeKeywordMappedFieldType fieldType = build("value(source.foo.toString() + params.param)", Map.of("param", "-suffix")); + ScriptKeywordMappedFieldType fieldType = build("value(source.foo.toString() + params.param)", Map.of("param", "-suffix")); assertThat(searcher.count(fieldType.termQuery("1-suffix", mockContext())), equalTo(1)); } } @@ -261,15 +256,15 @@ public void testWildcardQueryIsExpensive() throws IOException { checkExpensiveQuery((ft, ctx) -> ft.wildcardQuery(randomAlphaOfLengthBetween(1, 1000), null, ctx)); } - private RuntimeKeywordMappedFieldType build(String code) throws IOException { + private ScriptKeywordMappedFieldType build(String code) throws IOException { return build(new Script(code)); } - private RuntimeKeywordMappedFieldType build(String code, Map params) throws IOException { + private ScriptKeywordMappedFieldType build(String code, Map params) throws IOException { return build(new Script(ScriptType.INLINE, PainlessScriptEngine.NAME, code, params)); } - private RuntimeKeywordMappedFieldType build(Script script) throws IOException { + private ScriptKeywordMappedFieldType build(Script script) throws IOException { PainlessPlugin painlessPlugin = new PainlessPlugin(); painlessPlugin.loadExtensions(new ExtensionLoader() { @Override @@ -281,24 +276,12 @@ public List loadExtensions(Class extensionPointType) { ScriptModule scriptModule = new ScriptModule(Settings.EMPTY, List.of(painlessPlugin, new RuntimeFields())); try (ScriptService scriptService = new ScriptService(Settings.EMPTY, scriptModule.engines, scriptModule.contexts)) { StringScriptFieldScript.Factory factory = scriptService.compile(script, StringScriptFieldScript.CONTEXT); - return new RuntimeKeywordMappedFieldType("test", script, factory, emptyMap()); + return new ScriptKeywordMappedFieldType("test", script, factory, emptyMap()); } } - private QueryShardContext mockContext() { - return mockContext(true); - } - - private QueryShardContext mockContext(boolean allowExpensiveQueries) { - MapperService mapperService = mock(MapperService.class); - QueryShardContext context = mock(QueryShardContext.class); - when(context.allowExpensiveQueries()).thenReturn(allowExpensiveQueries); - when(context.lookup()).thenReturn(new SearchLookup(mapperService, mft -> null)); - return context; - } - - private void checkExpensiveQuery(BiConsumer queryBuilder) throws IOException { - RuntimeKeywordMappedFieldType ft = build("value('cat')"); + private void checkExpensiveQuery(BiConsumer queryBuilder) throws IOException { + ScriptKeywordMappedFieldType ft = build("value('cat')"); Exception e = expectThrows(ElasticsearchException.class, () -> queryBuilder.accept(ft, mockContext(false))); assertThat( e.getMessage(), diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptLongMappedFieldTypeTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptLongMappedFieldTypeTests.java new file mode 100644 index 0000000000000..e4279b54bd3cb --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptLongMappedFieldTypeTests.java @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.mapper; + +import org.apache.lucene.document.StoredField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.RandomIndexWriter; +import org.apache.lucene.index.SortedNumericDocValues; +import org.apache.lucene.search.Collector; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.LeafCollector; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.Scorable; +import org.apache.lucene.search.ScoreMode; +import org.apache.lucene.store.Directory; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.Version; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.painless.PainlessPlugin; +import org.elasticsearch.plugins.ExtensiblePlugin.ExtensionLoader; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptModule; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.script.ScriptType; +import org.elasticsearch.xpack.runtimefields.LongScriptFieldScript; +import org.elasticsearch.xpack.runtimefields.RuntimeFields; +import org.elasticsearch.xpack.runtimefields.RuntimeFieldsPainlessExtension; +import org.elasticsearch.xpack.runtimefields.fielddata.ScriptLongFieldData; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; + +import static java.util.Collections.emptyMap; +import static org.hamcrest.Matchers.equalTo; + +public class ScriptLongMappedFieldTypeTests extends AbstractScriptMappedFieldTypeTestCase { + public void testDocValues() throws IOException { + try (Directory directory = newDirectory(); RandomIndexWriter iw = new RandomIndexWriter(random(), directory)) { + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": [1]}")))); + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": [2, 1]}")))); + List results = new ArrayList<>(); + try (DirectoryReader reader = iw.getReader()) { + IndexSearcher searcher = newSearcher(reader); + ScriptLongMappedFieldType ft = build("for (def v : source.foo) {value(v + params.param)}", Map.of("param", 1)); + IndexMetadata imd = IndexMetadata.builder("test") + .settings(Settings.builder().put("index.version.created", Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(1) + .build(); + ScriptLongFieldData ifd = ft.fielddataBuilder("test").build(new IndexSettings(imd, Settings.EMPTY), ft, null, null, null); + ifd.setSearchLookup(mockContext().lookup()); + searcher.search(new MatchAllDocsQuery(), new Collector() { + @Override + public ScoreMode scoreMode() { + return ScoreMode.COMPLETE_NO_SCORES; + } + + @Override + public LeafCollector getLeafCollector(LeafReaderContext context) throws IOException { + SortedNumericDocValues dv = ifd.load(context).getLongValues(); + return new LeafCollector() { + @Override + public void setScorer(Scorable scorer) throws IOException {} + + @Override + public void collect(int doc) throws IOException { + if (dv.advanceExact(doc)) { + for (int i = 0; i < dv.docValueCount(); i++) { + results.add(dv.nextValue()); + } + } + } + }; + } + }); + assertThat(results, equalTo(List.of(2L, 2L, 3L))); + } + } + } + + public void testExistsQuery() throws IOException { + try (Directory directory = newDirectory(); RandomIndexWriter iw = new RandomIndexWriter(random(), directory)) { + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": [1]}")))); + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": []}")))); + try (DirectoryReader reader = iw.getReader()) { + IndexSearcher searcher = newSearcher(reader); + assertThat(searcher.count(build("for (def v : source.foo) { value(v)}").existsQuery(mockContext())), equalTo(1)); + } + } + } + + public void testExistsQueryIsExpensive() throws IOException { + checkExpensiveQuery(ScriptLongMappedFieldType::existsQuery); + } + + public void testTermQuery() throws IOException { + try (Directory directory = newDirectory(); RandomIndexWriter iw = new RandomIndexWriter(random(), directory)) { + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": 1}")))); + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": 2}")))); + try (DirectoryReader reader = iw.getReader()) { + IndexSearcher searcher = newSearcher(reader); + assertThat(searcher.count(build("value(source.foo)").termQuery("1", mockContext())), equalTo(1)); + assertThat(searcher.count(build("value(source.foo)").termQuery(1, mockContext())), equalTo(1)); + assertThat(searcher.count(build("value(source.foo)").termQuery(1.1, mockContext())), equalTo(0)); + assertThat( + searcher.count(build("value(source.foo + params.param)", Map.of("param", 1)).termQuery(2, mockContext())), + equalTo(1) + ); + } + } + } + + public void testTermQueryIsExpensive() throws IOException { + checkExpensiveQuery((ft, ctx) -> ft.termQuery(randomLong(), ctx)); + } + + private ScriptLongMappedFieldType build(String code) throws IOException { + return build(new Script(code)); + } + + private ScriptLongMappedFieldType build(String code, Map params) throws IOException { + return build(new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, code, params)); + } + + private ScriptLongMappedFieldType build(Script script) throws IOException { + PainlessPlugin painlessPlugin = new PainlessPlugin(); + painlessPlugin.loadExtensions(new ExtensionLoader() { + @Override + @SuppressWarnings("unchecked") // We only ever load painless extensions here so it is fairly safe. + public List loadExtensions(Class extensionPointType) { + return (List) List.of(new RuntimeFieldsPainlessExtension()); + } + }); + ScriptModule scriptModule = new ScriptModule(Settings.EMPTY, List.of(painlessPlugin, new RuntimeFields())); + try (ScriptService scriptService = new ScriptService(Settings.EMPTY, scriptModule.engines, scriptModule.contexts)) { + LongScriptFieldScript.Factory factory = scriptService.compile(script, LongScriptFieldScript.CONTEXT); + return new ScriptLongMappedFieldType("test", script, factory, emptyMap()); + } + } + + private void checkExpensiveQuery(BiConsumer queryBuilder) throws IOException { + ScriptLongMappedFieldType ft = build("value(1)"); + Exception e = expectThrows(ElasticsearchException.class, () -> queryBuilder.accept(ft, mockContext(false))); + assertThat( + e.getMessage(), + equalTo("queries cannot be executed against [script] fields while [search.allow_expensive_queries] is set to [false].") + ); + } +} diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/AbstractLongScriptFieldQueryTestCase.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/AbstractLongScriptFieldQueryTestCase.java new file mode 100644 index 0000000000000..8c42ddcff7ea5 --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/AbstractLongScriptFieldQueryTestCase.java @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.apache.lucene.index.Term; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.QueryVisitor; +import org.apache.lucene.util.automaton.ByteRunAutomaton; +import org.elasticsearch.xpack.runtimefields.LongScriptFieldScript; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Mockito.mock; + +public abstract class AbstractLongScriptFieldQueryTestCase extends AbstractScriptFieldQueryTestCase< + T> { + protected final LongScriptFieldScript.LeafFactory leafFactory = mock(LongScriptFieldScript.LeafFactory.class); + + @Override + public final void testVisit() { + T query = createTestInstance(); + List leavesVisited = new ArrayList<>(); + query.visit(new QueryVisitor() { + @Override + public void consumeTerms(Query query, Term... terms) { + fail(); + } + + @Override + public void consumeTermsMatching(Query query, String field, Supplier automaton) { + fail(); + } + + @Override + public void visitLeaf(Query query) { + leavesVisited.add(query); + } + }); + assertThat(leavesVisited, equalTo(List.of(query))); + } +} diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/AbstractScriptFieldQueryTestCase.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/AbstractScriptFieldQueryTestCase.java new file mode 100644 index 0000000000000..25cb5e595b94a --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/AbstractScriptFieldQueryTestCase.java @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.elasticsearch.script.Script; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.EqualsHashCodeTestUtils; + +import static org.hamcrest.Matchers.equalTo; + +public abstract class AbstractScriptFieldQueryTestCase extends ESTestCase { + protected abstract T createTestInstance(); + + protected abstract T copy(T orig); + + protected abstract T mutate(T orig); + + protected final Script randomScript() { + return new Script(randomAlphaOfLength(10)); + } + + public final void testEqualsAndHashCode() { + EqualsHashCodeTestUtils.checkEqualsAndHashCode(createTestInstance(), this::copy, this::mutate); + } + + public abstract void testMatches(); + + public final void testToString() { + T query = createTestInstance(); + assertThat(query.toString(), equalTo(query.fieldName() + ":" + query.toString(query.fieldName()))); + } + + protected abstract void assertToString(T query); + + public abstract void testVisit(); +} diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/AbstractStringScriptFieldQueryTestCase.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/AbstractStringScriptFieldQueryTestCase.java index 711b4015e64de..d1a88a166a686 100644 --- a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/AbstractStringScriptFieldQueryTestCase.java +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/AbstractStringScriptFieldQueryTestCase.java @@ -10,9 +10,6 @@ import org.apache.lucene.search.Query; import org.apache.lucene.search.QueryVisitor; import org.apache.lucene.util.automaton.ByteRunAutomaton; -import org.elasticsearch.script.Script; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.test.EqualsHashCodeTestUtils; import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; import java.util.ArrayList; @@ -24,34 +21,10 @@ import static org.hamcrest.Matchers.sameInstance; import static org.mockito.Mockito.mock; -public abstract class AbstractStringScriptFieldQueryTestCase extends ESTestCase { +public abstract class AbstractStringScriptFieldQueryTestCase extends + AbstractScriptFieldQueryTestCase { protected final StringScriptFieldScript.LeafFactory leafFactory = mock(StringScriptFieldScript.LeafFactory.class); - protected abstract T createTestInstance(); - - protected abstract T copy(T orig); - - protected abstract T mutate(T orig); - - protected final Script randomScript() { - return new Script(randomAlphaOfLength(10)); - } - - public final void testEqualsAndHashCode() { - EqualsHashCodeTestUtils.checkEqualsAndHashCode(createTestInstance(), this::copy, this::mutate); - } - - public abstract void testMatches(); - - public final void testToString() { - T query = createTestInstance(); - assertThat(query.toString(), equalTo(query.fieldName() + ":" + query.toString(query.fieldName()))); - } - - protected abstract void assertToString(T query); - - public abstract void testVisit(); - /** * {@link Query#visit Visit} a query, collecting {@link ByteRunAutomaton automata}, * failing if there are any terms or if there are more than one automaton. diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/LongScriptFieldExistsQueryTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/LongScriptFieldExistsQueryTests.java new file mode 100644 index 0000000000000..706dc3cd023eb --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/LongScriptFieldExistsQueryTests.java @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import static org.hamcrest.Matchers.equalTo; + +public class LongScriptFieldExistsQueryTests extends AbstractLongScriptFieldQueryTestCase { + @Override + protected LongScriptFieldExistsQuery createTestInstance() { + return new LongScriptFieldExistsQuery(randomScript(), leafFactory, randomAlphaOfLength(5)); + } + + @Override + protected LongScriptFieldExistsQuery copy(LongScriptFieldExistsQuery orig) { + return new LongScriptFieldExistsQuery(orig.script(), leafFactory, orig.fieldName()); + } + + @Override + protected LongScriptFieldExistsQuery mutate(LongScriptFieldExistsQuery orig) { + if (randomBoolean()) { + new LongScriptFieldExistsQuery(randomValueOtherThan(orig.script(), this::randomScript), leafFactory, orig.fieldName()); + } + return new LongScriptFieldExistsQuery(orig.script(), leafFactory, orig.fieldName() + "modified"); + } + + @Override + public void testMatches() { + assertTrue(createTestInstance().matches(new long[0], randomIntBetween(1, Integer.MAX_VALUE))); + assertFalse(createTestInstance().matches(new long[0], 0)); + assertFalse(createTestInstance().matches(new long[] { 1, 2, 3 }, 0)); + } + + @Override + protected void assertToString(LongScriptFieldExistsQuery query) { + assertThat(query.toString(query.fieldName()), equalTo("ScriptFieldExists")); + } +} diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/LongScriptFieldTermQueryTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/LongScriptFieldTermQueryTests.java new file mode 100644 index 0000000000000..c423f09a2899e --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/LongScriptFieldTermQueryTests.java @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.elasticsearch.script.Script; +import org.elasticsearch.test.ESTestCase; + +import static org.hamcrest.Matchers.equalTo; + +public class LongScriptFieldTermQueryTests extends AbstractLongScriptFieldQueryTestCase { + @Override + protected LongScriptFieldTermQuery createTestInstance() { + return new LongScriptFieldTermQuery(randomScript(), leafFactory, randomAlphaOfLength(5), randomLong()); + } + + @Override + protected LongScriptFieldTermQuery copy(LongScriptFieldTermQuery orig) { + return new LongScriptFieldTermQuery(orig.script(), leafFactory, orig.fieldName(), orig.term()); + } + + @Override + protected LongScriptFieldTermQuery mutate(LongScriptFieldTermQuery orig) { + Script script = orig.script(); + String fieldName = orig.fieldName(); + long term = orig.term(); + switch (randomInt(2)) { + case 0: + script = randomValueOtherThan(script, this::randomScript); + break; + case 1: + fieldName += "modified"; + break; + case 2: + term = randomValueOtherThan(term, ESTestCase::randomLong); + break; + default: + fail(); + } + return new LongScriptFieldTermQuery(script, leafFactory, fieldName, term); + } + + @Override + public void testMatches() { + LongScriptFieldTermQuery query = new LongScriptFieldTermQuery(randomScript(), leafFactory, "test", 1); + assertTrue(query.matches(new long[] { 1 }, 1)); // Match because value matches + assertFalse(query.matches(new long[] { 2 }, 1)); // No match because wrong value + assertFalse(query.matches(new long[] { 2, 1 }, 1)); // No match because value after count of values + assertTrue(query.matches(new long[] { 2, 1 }, 2)); // Match because one value matches + } + + @Override + protected void assertToString(LongScriptFieldTermQuery query) { + assertThat(query.toString(query.fieldName()), equalTo(Long.toString(query.term()))); + } +} diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/10_keyword.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/10_keyword.yml index 604f11e8f7dbd..87d51f70923ba 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/10_keyword.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/10_keyword.yml @@ -14,7 +14,7 @@ setup: temperature: type: long voltage: - type: float + type: double node: type: keyword day_of_week: diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/20_long.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/20_long.yml new file mode 100644 index 0000000000000..d5f716b362630 --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/20_long.yml @@ -0,0 +1,103 @@ +--- +setup: + - do: + indices.create: + index: sensor + body: + settings: + number_of_shards: 1 + number_of_replicas: 0 + mappings: + properties: + timestamp: + type: date + temperature: + type: long + voltage: + type: double + node: + type: keyword + voltage_times_ten: + type: script + runtime_type: long + script: | + for (double v : doc['voltage']) { + value((long)(v * 10)); + } + even_voltage_times_ten: + type: script + runtime_type: long + script: | + for (long tenV: doc['voltage_times_ten']) { + if (tenV % 2 == 0) { + value(tenV); + } + } + + - do: + bulk: + index: sensor + refresh: true + body: | + {"index":{}} + {"timestamp": 1516729294000, "temperature": 200, "voltage": 5.2, "node": "a"} + {"index":{}} + {"timestamp": 1516642894000, "temperature": 201, "voltage": 5.8, "node": "b"} + {"index":{}} + {"timestamp": 1516556494000, "temperature": 202, "voltage": 5.1, "node": "a"} + {"index":{}} + {"timestamp": 1516470094000, "temperature": 198, "voltage": 5.6, "node": "b"} + {"index":{}} + {"timestamp": 1516383694000, "temperature": 200, "voltage": 4.2, "node": "c"} + {"index":{}} + {"timestamp": 1516297294000, "temperature": 202, "voltage": 4.0, "node": "c"} + +--- +"docvalue_fields": + - do: + search: + index: sensor + body: + sort: timestamp + docvalue_fields: [voltage_times_ten] + - match: {hits.total.value: 6} + - match: {hits.hits.0.fields.voltage_times_ten: [40] } + - match: {hits.hits.1.fields.voltage_times_ten: [42] } + - match: {hits.hits.2.fields.voltage_times_ten: [56] } + - match: {hits.hits.3.fields.voltage_times_ten: [51] } + - match: {hits.hits.4.fields.voltage_times_ten: [58] } + - match: {hits.hits.5.fields.voltage_times_ten: [52] } + +--- +"terms agg": + - do: + search: + index: sensor + body: + aggs: + v10: + terms: + field: voltage_times_ten + - match: {hits.total.value: 6} + - match: {aggregations.v10.buckets.0.key: 40.0} + - match: {aggregations.v10.buckets.0.doc_count: 1} + - match: {aggregations.v10.buckets.1.key: 42.0} + - match: {aggregations.v10.buckets.1.doc_count: 1} + +--- +"exists query": + - do: + search: + index: sensor + body: + query: + exists: + field: even_voltage_times_ten + sort: timestamp + docvalue_fields: [voltage_times_ten] + - match: {hits.total.value: 5} + - match: {hits.hits.0._source.voltage: 4.0} + - match: {hits.hits.1._source.voltage: 4.2} + - match: {hits.hits.2._source.voltage: 5.6} + - match: {hits.hits.3._source.voltage: 5.8} + - match: {hits.hits.4._source.voltage: 5.2}