Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog/135373.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 135373
summary: Improve block loader for source only runtime date fields
area: Mapping
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
import org.elasticsearch.search.runtime.LongScriptFieldRangeQuery;
import org.elasticsearch.search.runtime.LongScriptFieldTermQuery;
import org.elasticsearch.search.runtime.LongScriptFieldTermsQuery;
import org.elasticsearch.xcontent.XContentParser;

import java.io.IOException;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.ArrayList;
Expand Down Expand Up @@ -183,9 +185,59 @@ public DocValueFormat docValueFormat(@Nullable String format, ZoneId timeZone) {

@Override
public BlockLoader blockLoader(BlockLoaderContext blContext) {
FallbackSyntheticSourceBlockLoader fallbackSyntheticSourceBlockLoader = fallbackSyntheticSourceBlockLoader(
blContext,
BlockLoader.BlockFactory::longs,
this::fallbackSyntheticSourceBlockLoaderReader
);

if (fallbackSyntheticSourceBlockLoader != null) {
return fallbackSyntheticSourceBlockLoader;
}
return new DateScriptBlockDocValuesReader.DateScriptBlockLoader(leafFactory(blContext.lookup()));
}

private FallbackSyntheticSourceBlockLoader.Reader<?> fallbackSyntheticSourceBlockLoaderReader() {
return new FallbackSyntheticSourceBlockLoader.SingleValueReader<Long>(null) {
@Override
public void convertValue(Object value, List<Long> accumulator) {
try {
if (value instanceof Number) {
accumulator.add(((Number) value).longValue());
} else {
// when the value is given a string formatted date; ex. 2020-07-22T16:09:41.355Z
accumulator.add(dateTimeFormatter.parseMillis(value.toString()));
}
} catch (Exception e) {
// ensure a malformed value doesn't crash
}
}

@Override
public void writeToBlock(List<Long> values, BlockLoader.Builder blockBuilder) {
var longBuilder = (BlockLoader.LongBuilder) blockBuilder;
for (Long value : values) {
longBuilder.appendLong(value);
}
}

@Override
protected void parseNonNullValue(XContentParser parser, List<Long> accumulator) throws IOException {
try {
String dateAsStr = parser.textOrNull();

if (dateAsStr == null) {
accumulator.add(dateTimeFormatter.parseMillis(null));
} else {
accumulator.add(dateTimeFormatter.parseMillis(dateAsStr));
}
} catch (Exception e) {
// ensure a malformed value doesn't crash
}
}
};
}

@Override
public DateScriptFieldData.Builder fielddataBuilder(FieldDataContext fieldDataContext) {
return new DateScriptFieldData.Builder(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.geo.ShapeRelation;
import org.elasticsearch.common.lucene.search.function.ScriptScoreQuery;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.time.DateFormatter;
import org.elasticsearch.index.IndexVersion;
import org.elasticsearch.index.fielddata.DateScriptFieldData;
Expand All @@ -47,6 +48,7 @@
import org.elasticsearch.script.ScriptFactory;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.search.MultiValueMode;
import org.elasticsearch.search.lookup.SearchLookup;
import org.elasticsearch.xcontent.XContentBuilder;

import java.io.IOException;
Expand All @@ -55,6 +57,7 @@
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

Expand All @@ -65,9 +68,12 @@
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.nullValue;

public class DateScriptFieldTypeTests extends AbstractNonTextScriptFieldTypeTestCase {

private static final Long MALFORMED_DATE = null;

@Override
protected ScriptFactory parseFromSource() {
return DateFieldScript.PARSE_FROM_SOURCE;
Expand Down Expand Up @@ -502,6 +508,155 @@ 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
iw.addDocuments(
List.of(
List.of(new StoredField("_source", new BytesRef("{\"test\": [1595432181354]}"))),
List.of(new StoredField("_source", new BytesRef("{\"test\": [\"2020-07-22T16:09:41.355Z\"]}"))),
List.of(new StoredField("_source", new BytesRef("{\"test\": [null]}"))),
List.of(new StoredField("_source", new BytesRef("{\"test\": []}"))),
List.of(new StoredField("_source", new BytesRef("{\"test\": [\"malformed\"]}")))
)
);
DateScriptFieldType fieldType = simpleSourceOnlyMappedFieldType();
List<Long> expected = Arrays.asList(
1595432181354L,
Instant.parse("2020-07-22T16:09:41.355Z").toEpochMilli(),
null,
null,
MALFORMED_DATE
);

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(DateScriptBlockDocValuesReader.DateScriptBlockLoader.class));

// ignored source doesn't support column at a time loading:
var columnAtATimeLoader = loader.columnAtATimeReader(reader.leaves().getFirst());
assertThat(columnAtATimeLoader, instanceOf(DateScriptBlockDocValuesReader.class));

var rowStrideReader = loader.rowStrideReader(reader.leaves().getFirst());
assertThat(rowStrideReader, instanceOf(DateScriptBlockDocValuesReader.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
iw.addDocuments(
List.of(
createDocumentWithIgnoredSource("[1595432181354]"),
createDocumentWithIgnoredSource("[\"2020-07-22T16:09:41.355Z\"]"),
createDocumentWithIgnoredSource("[\"\"]"),
createDocumentWithIgnoredSource("[\"malformed\"]")
)
);

Settings settings = Settings.builder().put("index.mapping.source.mode", "synthetic").build();
DateScriptFieldType fieldType = simpleSourceOnlyMappedFieldType();
List<Long> expected = Arrays.asList(
1595432181354L,
Instant.parse("2020-07-22T16:09:41.355Z").toEpochMilli(),
null,
MALFORMED_DATE
);

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 DateScriptFieldType simpleSourceOnlyMappedFieldType() {
Script script = new Script(ScriptType.INLINE, "test", "", emptyMap());
DateFieldScript.Factory factory = new DateFieldScript.Factory() {
@Override
public DateFieldScript.LeafFactory newFactory(
String fieldName,
Map<String, Object> params,
SearchLookup searchLookup,
DateFormatter formatter,
OnScriptError onScriptError
) {
return ctx -> new DateFieldScript(
fieldName,
params,
searchLookup,
DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER,
onScriptError,
ctx
) {
@Override
@SuppressWarnings("unchecked")
public void execute() {
Map<String, Object> source = (Map<String, Object>) this.getParams().get("_source");
for (Object timestamp : (List<?>) source.get("test")) {
Parse parse = new Parse(this);
try {
emit(parse.parse(timestamp));
} catch (Exception e) {
// skip
}
}
}
};
}

@Override
public boolean isParsedFromSource() {
return true;
}
};
return new DateScriptFieldType(
"test",
factory,
DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER,
script,
emptyMap(),
OnScriptError.FAIL
);
}

@Override
protected Query randomTermsQuery(MappedFieldType ft, SearchExecutionContext ctx) {
return ft.termsQuery(randomList(1, 100, DateScriptFieldTypeTests::randomDate), ctx);
Expand Down