diff --git a/server/src/main/java/org/elasticsearch/index/mapper/BooleanScriptFieldType.java b/server/src/main/java/org/elasticsearch/index/mapper/BooleanScriptFieldType.java index b22ef6333ede1..16565224c9e1e 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/BooleanScriptFieldType.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/BooleanScriptFieldType.java @@ -27,9 +27,12 @@ import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.search.runtime.BooleanScriptFieldExistsQuery; import org.elasticsearch.search.runtime.BooleanScriptFieldTermQuery; +import org.elasticsearch.xcontent.XContentParser; +import java.io.IOException; import java.time.ZoneId; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.function.Function; @@ -114,9 +117,52 @@ public DocValueFormat docValueFormat(String format, ZoneId timeZone) { @Override public BlockLoader blockLoader(BlockLoaderContext blContext) { + FallbackSyntheticSourceBlockLoader fallbackSyntheticSourceBlockLoader = fallbackSyntheticSourceBlockLoader( + blContext, + BlockLoader.BlockFactory::booleans, + this::fallbackSyntheticSourceBlockLoaderReader + ); + + if (fallbackSyntheticSourceBlockLoader != null) { + return fallbackSyntheticSourceBlockLoader; + } return new BooleanScriptBlockDocValuesReader.BooleanScriptBlockLoader(leafFactory(blContext.lookup())); } + private FallbackSyntheticSourceBlockLoader.Reader fallbackSyntheticSourceBlockLoaderReader() { + return new FallbackSyntheticSourceBlockLoader.SingleValueReader(null) { + @Override + public void convertValue(Object value, List accumulator) { + try { + if (value instanceof Boolean b) { + accumulator.add(b); + } else { + accumulator.add(Booleans.parseBoolean(value.toString(), false)); + } + } catch (Exception e) { + // value is malformed, skip it + } + } + + @Override + public void writeToBlock(List values, BlockLoader.Builder blockBuilder) { + var booleanBuilder = (BlockLoader.BooleanBuilder) blockBuilder; + for (boolean value : values) { + booleanBuilder.appendBoolean(value); + } + } + + @Override + protected void parseNonNullValue(XContentParser parser, List accumulator) throws IOException { + try { + accumulator.add(parser.booleanValue()); + } catch (Exception e) { + // value is malformed, skip it + } + } + }; + } + @Override public BooleanScriptFieldData.Builder fielddataBuilder(FieldDataContext fieldDataContext) { return new BooleanScriptFieldData.Builder(name(), leafFactory(fieldDataContext.lookupSupplier().get()), BooleanDocValuesField::new); 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 54656ab1af3ee..97cc3a5aa4ae1 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/BooleanScriptFieldTypeTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/BooleanScriptFieldTypeTests.java @@ -31,6 +31,8 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.lucene.search.function.ScriptScoreQuery; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.Booleans; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.fielddata.BooleanScriptFieldData; import org.elasticsearch.index.fielddata.ScriptDocValues; @@ -44,6 +46,7 @@ import org.elasticsearch.script.ScriptType; import org.elasticsearch.script.field.BooleanDocValuesField; import org.elasticsearch.search.MultiValueMode; +import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentParser.Token; @@ -52,6 +55,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -60,9 +64,13 @@ import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.nullValue; public class BooleanScriptFieldTypeTests extends AbstractNonTextScriptFieldTypeTestCase { + private static final Boolean MALFORMED_BOOLEAN = null; + private static final Boolean EMPTY_STR_BOOLEAN = false; + @Override protected ScriptFactory parseFromSource() { return BooleanFieldScript.PARSE_FROM_SOURCE; @@ -453,6 +461,137 @@ public void testBlockLoader() throws IOException { } } + public void testBlockLoaderSourceOnlyRuntimeField() throws IOException { + try ( + Directory directory = newDirectory(); + RandomIndexWriter iw = new RandomIndexWriter(random(), directory, newIndexWriterConfig().setMergePolicy(NoMergePolicy.INSTANCE)) + ) { + // given + // try multiple variations of boolean as they're all encoded slightly differently + iw.addDocuments( + List.of( + List.of(new StoredField("_source", new BytesRef("{\"test\": [false]}"))), + List.of(new StoredField("_source", new BytesRef("{\"test\": [true]}"))), + List.of(new StoredField("_source", new BytesRef("{\"test\": [\"false\"]}"))), + List.of(new StoredField("_source", new BytesRef("{\"test\": [\"true\"]}"))), + List.of(new StoredField("_source", new BytesRef("{\"test\": [\"\"]}"))), + // ensure a malformed value doesn't crash + List.of(new StoredField("_source", new BytesRef("{\"test\": [\"potato\"]}"))) + ) + ); + BooleanScriptFieldType fieldType = simpleSourceOnlyMappedFieldType(); + List expected = Arrays.asList(false, true, false, true, EMPTY_STR_BOOLEAN, MALFORMED_BOOLEAN); + + try (DirectoryReader reader = iw.getReader()) { + // when + BlockLoader loader = fieldType.blockLoader(blContext(Settings.EMPTY, true)); + + // then + + // assert loader is of expected instance type + assertThat(loader, instanceOf(BooleanScriptBlockDocValuesReader.BooleanScriptBlockLoader.class)); + + // ignored source doesn't support column at a time loading: + var columnAtATimeLoader = loader.columnAtATimeReader(reader.leaves().getFirst()); + assertThat(columnAtATimeLoader, instanceOf(BooleanScriptBlockDocValuesReader.class)); + + var rowStrideReader = loader.rowStrideReader(reader.leaves().getFirst()); + assertThat(rowStrideReader, instanceOf(BooleanScriptBlockDocValuesReader.class)); + + // assert values + assertThat(blockLoaderReadValuesFromColumnAtATimeReader(reader, fieldType, 0), equalTo(expected)); + assertThat(blockLoaderReadValuesFromRowStrideReader(reader, fieldType), equalTo(expected)); + } + } + } + + public void testBlockLoaderSourceOnlyRuntimeFieldWithSyntheticSource() throws IOException { + try ( + Directory directory = newDirectory(); + RandomIndexWriter iw = new RandomIndexWriter(random(), directory, newIndexWriterConfig().setMergePolicy(NoMergePolicy.INSTANCE)) + ) { + // given + // try multiple variations of boolean as they're all encoded slightly differently + iw.addDocuments( + List.of( + createDocumentWithIgnoredSource("false"), + createDocumentWithIgnoredSource("true"), + createDocumentWithIgnoredSource("[false]"), + createDocumentWithIgnoredSource("[true]"), + createDocumentWithIgnoredSource("[\"false\"]"), + createDocumentWithIgnoredSource("[\"true\"]"), + createDocumentWithIgnoredSource("[\"\"]"), + // ensure a malformed value doesn't crash + createDocumentWithIgnoredSource("[\"potato\"]") + ) + ); + + Settings settings = Settings.builder().put("index.mapping.source.mode", "synthetic").build(); + BooleanScriptFieldType fieldType = simpleSourceOnlyMappedFieldType(); + List expected = Arrays.asList(false, true, false, true, false, true, EMPTY_STR_BOOLEAN, MALFORMED_BOOLEAN); + + try (DirectoryReader reader = iw.getReader()) { + // when + BlockLoader loader = fieldType.blockLoader(blContext(settings, true)); + + // then + + // assert loader is of expected instance type + assertThat(loader, instanceOf(FallbackSyntheticSourceBlockLoader.class)); + + // ignored source doesn't support column at a time loading: + var columnAtATimeLoader = loader.columnAtATimeReader(reader.leaves().getFirst()); + assertThat(columnAtATimeLoader, nullValue()); + + var rowStrideReader = loader.rowStrideReader(reader.leaves().getFirst()); + assertThat( + rowStrideReader.getClass().getName(), + equalTo("org.elasticsearch.index.mapper.FallbackSyntheticSourceBlockLoader$IgnoredSourceRowStrideReader") + ); + + // assert values + assertThat(blockLoaderReadValuesFromRowStrideReader(settings, reader, fieldType, true), equalTo(expected)); + } + } + } + + /** + * Returns a source only mapped field type. This is useful, since the available build() function doesn't override isParsedFromSource() + */ + private BooleanScriptFieldType simpleSourceOnlyMappedFieldType() { + Script script = new Script(ScriptType.INLINE, "test", "", emptyMap()); + BooleanFieldScript.Factory factory = new BooleanFieldScript.Factory() { + @Override + public BooleanFieldScript.LeafFactory newFactory( + String fieldName, + Map params, + SearchLookup searchLookup, + OnScriptError onScriptError + ) { + return ctx -> new BooleanFieldScript(fieldName, params, searchLookup, onScriptError, ctx) { + @Override + @SuppressWarnings("unchecked") + public void execute() { + Map source = (Map) this.getParams().get("_source"); + for (Object foo : (List) source.get("test")) { + try { + emit(Booleans.parseBoolean(foo.toString(), false)); + } catch (Exception e) { + // skip + } + } + } + }; + } + + @Override + public boolean isParsedFromSource() { + return true; + } + }; + return new BooleanScriptFieldType("test", factory, script, emptyMap(), OnScriptError.FAIL); + } + private void assertSameCount(IndexSearcher searcher, String source, Object queryDescription, Query scriptedQuery, Query ootbQuery) throws IOException { assertThat(