diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.fields.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.fields.txt index 6b6c2e32f1f75..9e5b000438dd7 100644 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.fields.txt +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.fields.txt @@ -29,4 +29,9 @@ class org.elasticsearch.script.field.DelegateDocValuesField @dynamic_type { class org.elasticsearch.script.field.BinaryDocValuesField @dynamic_type { ByteBuffer get(ByteBuffer) ByteBuffer get(int, ByteBuffer) -} \ No newline at end of file +} + +class org.elasticsearch.script.field.BooleanDocValuesField @dynamic_type { + boolean get(boolean) + boolean get(int, boolean) +} diff --git a/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/50_script_doc_values.yml b/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/50_script_doc_values.yml index 8f00cdb90c106..42d3f6f10a5d9 100644 --- a/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/50_script_doc_values.yml +++ b/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/50_script_doc_values.yml @@ -58,6 +58,19 @@ setup: scaled_float: 3.14 token_count: count all these words please + - do: + index: + index: test + id: 2 + body: {} + + - do: + index: + index: test + id: 3 + body: + boolean: [true, false, true] + - do: indices.refresh: {} @@ -67,6 +80,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -77,12 +91,92 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: source: "doc['boolean'].value" - match: { hits.hits.0.fields.field.0: true } + - do: + search: + rest_total_hits_as_int: true + body: + query: { term: { _id: 1 } } + script_fields: + field: + script: + source: "field('boolean').get(false)" + - match: { hits.hits.0.fields.field.0: true } + + - do: + search: + rest_total_hits_as_int: true + body: + query: { term: { _id: 1 } } + script_fields: + + field: + script: + source: "field('boolean').get(false)" + - match: { hits.hits.0.fields.field.0: true } + + - do: + search: + rest_total_hits_as_int: true + body: + query: { term: { _id: 2 } } + script_fields: + field: + script: + source: "field('boolean').get(false)" + - match: { hits.hits.0.fields.field.0: false } + + - do: + search: + rest_total_hits_as_int: true + body: + query: { term: { _id: 1 } } + script_fields: + field: + script: + source: "field('boolean').get(1, false)" + - match: { hits.hits.0.fields.field.0: false } + + - do: + search: + rest_total_hits_as_int: true + body: + query: { term: { _id: 1 } } + script_fields: + field: + script: + source: "int total = 0; for (boolean b : field('boolean')) { total += b ? 1 : 0; } total;" + - match: { hits.hits.0.fields.field.0: 1 } + + - do: + search: + rest_total_hits_as_int: true + body: + query: { term: { _id: 3 } } + script_fields: + field: + script: + source: "int total = 0; for (boolean b : field('boolean')) { total += b ? 1 : 0; } total + field('boolean').size();" + - match: { hits.hits.0.fields.field.0: 5 } + + - do: + search: + rest_total_hits_as_int: true + body: + query: { term: { _id: 2 } } + script_fields: + field: + script: + source: "field('boolean').size()" + - match: { hits.hits.0.fields.field.0: 0 } + + --- "date": - skip: @@ -92,6 +186,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -102,6 +197,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -114,6 +210,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -125,6 +222,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -136,6 +234,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: centroid: script: @@ -147,6 +246,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: bbox: script: @@ -160,6 +260,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: topLeft: script: @@ -176,6 +277,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: type: script: @@ -186,6 +288,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: width: script: @@ -202,6 +305,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -212,6 +316,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -224,6 +329,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -234,6 +340,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -249,6 +356,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -259,6 +367,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -271,6 +380,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -281,6 +391,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -293,6 +404,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -303,6 +415,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -315,6 +428,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -325,6 +439,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -337,6 +452,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -347,6 +463,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -359,6 +476,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -369,6 +487,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -381,6 +500,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -391,6 +511,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -403,6 +524,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -413,6 +535,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -425,6 +548,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: @@ -435,6 +559,7 @@ setup: search: rest_total_hits_as_int: true body: + query: { term: { _id: 1 } } script_fields: field: script: diff --git a/server/src/main/java/org/elasticsearch/index/fielddata/ScriptDocValues.java b/server/src/main/java/org/elasticsearch/index/fielddata/ScriptDocValues.java index d6afb24ab7067..9003a32db09f0 100644 --- a/server/src/main/java/org/elasticsearch/index/fielddata/ScriptDocValues.java +++ b/server/src/main/java/org/elasticsearch/index/fielddata/ScriptDocValues.java @@ -18,13 +18,13 @@ import org.elasticsearch.common.time.DateUtils; import org.elasticsearch.geometry.utils.Geohash; import org.elasticsearch.script.field.BinaryDocValuesField; +import org.elasticsearch.script.field.BooleanDocValuesField; import java.io.IOException; import java.time.Instant; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.AbstractList; -import java.util.Arrays; import java.util.Comparator; import java.util.function.UnaryOperator; @@ -455,60 +455,31 @@ public GeoBoundingBox getBoundingBox() { public static final class Booleans extends ScriptDocValues { - private final SortedNumericDocValues in; - private boolean[] values = new boolean[0]; - private int count; + private final BooleanDocValuesField booleanDocValuesField; - public Booleans(SortedNumericDocValues in) { - this.in = in; + public Booleans(BooleanDocValuesField booleanDocValuesField) { + this.booleanDocValuesField = booleanDocValuesField; } @Override public void setNextDocId(int docId) throws IOException { - if (in.advanceExact(docId)) { - resize(in.docValueCount()); - for (int i = 0; i < count; i++) { - values[i] = in.nextValue() == 1; - } - } else { - resize(0); - } - } - - /** - * Set the {@link #size()} and ensure that the {@link #values} array can - * store at least that many entries. - */ - protected void resize(int newSize) { - count = newSize; - values = grow(values, count); + throw new UnsupportedOperationException(); } public boolean getValue() { + throwIfEmpty(); return get(0); } @Override public Boolean get(int index) { - if (count == 0) { - throw new IllegalStateException( - "A document doesn't have a value for a field! " - + "Use doc[].size()==0 to check if a document is missing a field!" - ); - } - return values[index]; + throwIfEmpty(); + return booleanDocValuesField.getInternal(index); } @Override public int size() { - return count; - } - - private static boolean[] grow(boolean[] array, int minSize) { - assert minSize >= 0 : "size must be positive (got " + minSize + "): likely integer overflow?"; - if (array.length < minSize) { - return Arrays.copyOf(array, ArrayUtil.oversize(minSize, 1)); - } else return array; + return booleanDocValuesField.size(); } } diff --git a/server/src/main/java/org/elasticsearch/index/fielddata/plain/LeafLongFieldData.java b/server/src/main/java/org/elasticsearch/index/fielddata/plain/LeafLongFieldData.java index 877c1d0df84b9..987ffb7059fd5 100644 --- a/server/src/main/java/org/elasticsearch/index/fielddata/plain/LeafLongFieldData.java +++ b/server/src/main/java/org/elasticsearch/index/fielddata/plain/LeafLongFieldData.java @@ -16,6 +16,7 @@ import org.elasticsearch.index.fielddata.ScriptDocValues; import org.elasticsearch.index.fielddata.SortedBinaryDocValues; import org.elasticsearch.index.fielddata.SortedNumericDoubleValues; +import org.elasticsearch.script.field.BooleanDocValuesField; import org.elasticsearch.script.field.DelegateDocValuesField; import org.elasticsearch.script.field.DocValuesField; import org.elasticsearch.search.DocValueFormat; @@ -56,7 +57,7 @@ public final DocValuesField getScriptField(String name) { name ); case BOOLEAN: - return new DelegateDocValuesField(new ScriptDocValues.Booleans(getLongValues()), name); + return new BooleanDocValuesField(getLongValues(), name); default: return new DelegateDocValuesField(new ScriptDocValues.Longs(getLongValues()), name); } diff --git a/server/src/main/java/org/elasticsearch/script/field/BooleanDocValuesField.java b/server/src/main/java/org/elasticsearch/script/field/BooleanDocValuesField.java new file mode 100644 index 0000000000000..6eed4eef37e3a --- /dev/null +++ b/server/src/main/java/org/elasticsearch/script/field/BooleanDocValuesField.java @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.script.field; + +import org.apache.lucene.index.SortedNumericDocValues; +import org.apache.lucene.util.ArrayUtil; +import org.elasticsearch.index.fielddata.ScriptDocValues; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; + +public class BooleanDocValuesField implements DocValuesField { + + private final SortedNumericDocValues input; + private final String name; + + private boolean[] values = new boolean[0]; + private int count; + + private ScriptDocValues.Booleans booleans = null; + + public BooleanDocValuesField(SortedNumericDocValues input, String name) { + this.input = input; + this.name = name; + } + + /** + * Set the current document ID. + * + * @param docId + */ + @Override + public void setNextDocId(int docId) throws IOException { + if (input.advanceExact(docId)) { + resize(input.docValueCount()); + for (int i = 0; i < count; i++) { + values[i] = input.nextValue() == 1; + } + } else { + resize(0); + } + } + + private void resize(int newSize) { + count = newSize; + + assert count >= 0 : "size must be positive (got " + count + "): likely integer overflow?"; + if (values.length < count) { + values = Arrays.copyOf(values, ArrayUtil.oversize(count, 1)); + } + } + + /** + * Returns a {@code ScriptDocValues} of the appropriate type for this field. + * This is used to support backwards compatibility for accessing field values + * through the {@code doc} variable. + */ + @Override + public ScriptDocValues getScriptDocValues() { + if (booleans == null) { + booleans = new ScriptDocValues.Booleans(this); + } + + return booleans; + } + + /** + * Returns the name of this field. + */ + @Override + public String getName() { + return name; + } + + /** + * Returns {@code true} if this field has no values, otherwise {@code false}. + */ + @Override + public boolean isEmpty() { + return count == 0; + } + + /** + * Returns the number of values this field has. + */ + @Override + public int size() { + return count; + } + + /** + * Returns an iterator over elements of type {@code T}. + * + * @return an Iterator. + */ + @Override + public Iterator iterator() { + return new Iterator() { + private int index = 0; + + @Override + public boolean hasNext() { + return index < count; + } + + @Override + public Boolean next() { + if (hasNext() == false) { + throw new NoSuchElementException(); + } + return values[index++]; + } + }; + } + + public boolean get(boolean defaultValue) { + return get(0, defaultValue); + } + + public boolean get(int index, boolean defaultValue) { + if (isEmpty() || index < 0 || index >= count) { + return defaultValue; + } + + return values[index]; + } + + // this method is required to support the old-style "doc" access in ScriptDocValues + public boolean getInternal(int index) { + return values[index]; + } +} diff --git a/server/src/test/java/org/elasticsearch/index/mapper/BooleanScriptFieldTypeTests.java b/server/src/test/java/org/elasticsearch/index/mapper/BooleanScriptFieldTypeTests.java index f99b6ee3e1390..746635718a927 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/BooleanScriptFieldTypeTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/BooleanScriptFieldTypeTests.java @@ -38,6 +38,7 @@ import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptCompiler; import org.elasticsearch.script.ScriptType; +import org.elasticsearch.script.field.BooleanDocValuesField; import org.elasticsearch.search.MultiValueMode; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xcontent.XContentParser; @@ -137,6 +138,23 @@ public double execute(ExplanationHolder explanation) { }; } }, searchContext.lookup(), 2.5f, "test", 0, Version.CURRENT)), equalTo(1)); + assertThat(searcher.count(new ScriptScoreQuery(new MatchAllDocsQuery(), new Script("test"), new ScoreScript.LeafFactory() { + @Override + public boolean needs_score() { + return false; + } + + @Override + public ScoreScript newInstance(DocReader docReader) { + return new ScoreScript(Map.of(), searchContext.lookup(), docReader) { + @Override + public double execute(ExplanationHolder explanation) { + BooleanDocValuesField booleans = (BooleanDocValuesField) field("test"); + return booleans.getInternal(0) ? 3 : 0; + } + }; + } + }, searchContext.lookup(), 2.5f, "test", 0, Version.CURRENT)), equalTo(1)); } } } diff --git a/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperTestCase.java index 1ee397de644c5..7da507d04430a 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperTestCase.java @@ -28,9 +28,9 @@ import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.fielddata.IndexFieldDataCache; -import org.elasticsearch.index.fielddata.ScriptDocValues; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; +import org.elasticsearch.script.field.DocValuesField; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.lookup.LeafStoredFieldsLookup; import org.elasticsearch.search.lookup.SearchLookup; @@ -666,24 +666,23 @@ public final void testIndexTimeFieldData() throws IOException { LeafReaderContext ctx = ir.leaves().get(0); - ScriptDocValues fieldData = fieldType.fielddataBuilder("test", () -> { throw new UnsupportedOperationException(); }) + DocValuesField docValuesField = fieldType.fielddataBuilder("test", () -> { throw new UnsupportedOperationException(); }) .build(new IndexFieldDataCache.None(), new NoneCircuitBreakerService()) .load(ctx) - .getScriptField("test") - .getScriptDocValues(); + .getScriptField("test"); - fieldData.setNextDocId(0); + docValuesField.setNextDocId(0); DocumentLeafReader reader = new DocumentLeafReader(doc.rootDoc(), Collections.emptyMap()); - ScriptDocValues indexData = fieldType.fielddataBuilder("test", () -> { throw new UnsupportedOperationException(); }) + DocValuesField indexData = fieldType.fielddataBuilder("test", () -> { throw new UnsupportedOperationException(); }) .build(new IndexFieldDataCache.None(), new NoneCircuitBreakerService()) .load(reader.getContext()) - .getScriptField("test") - .getScriptDocValues(); + .getScriptField("test"); + indexData.setNextDocId(0); // compare index and search time fielddata - assertThat(fieldData, equalTo(indexData)); + assertThat(docValuesField.getScriptDocValues(), equalTo(indexData.getScriptDocValues())); }); }