Skip to content

Commit 36efb7c

Browse files
authored
Improve block loader for source only runtime fields of type long. (#134117)
By using the FallbackSyntheticSourceBlockLoader instead of generic LongScriptBlockLoader. Change RuntimeFieldSourceProviderOptimizationTests to no longer test block loading, but field data loading instead. In the latter case, the optimization that this test is testing will remain. Also deal with the situation that a field is both a normal field and runtime field. In that case fall back to script based block loading.
1 parent b03e1d4 commit 36efb7c

File tree

9 files changed

+288
-28
lines changed

9 files changed

+288
-28
lines changed

docs/changelog/134117.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 134117
2+
summary: Improve block loader for source only runtime fields of type long
3+
area: Mapping
4+
type: enhancement
5+
issues: []

server/src/main/java/org/elasticsearch/index/mapper/AbstractScriptFieldType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public abstract class AbstractScriptFieldType<LeafFactory> extends MappedFieldTy
4949
protected final Script script;
5050
private final Function<SearchLookup, LeafFactory> factory;
5151
private final boolean isResultDeterministic;
52-
private final boolean isParsedFromSource;
52+
protected final boolean isParsedFromSource;
5353

5454
protected AbstractScriptFieldType(
5555
String name,

server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.elasticsearch.index.query.SearchExecutionContext;
2727
import org.elasticsearch.indices.breaker.NoneCircuitBreakerService;
2828
import org.elasticsearch.plugins.internal.XContentMeteringParserDecorator;
29+
import org.elasticsearch.search.lookup.LeafFieldLookupProvider;
2930
import org.elasticsearch.search.lookup.SearchLookup;
3031
import org.elasticsearch.search.lookup.Source;
3132
import org.elasticsearch.xcontent.XContentBuilder;
@@ -171,6 +172,7 @@ private static void executeIndexTimeScripts(DocumentParserContext context) {
171172
}
172173
SearchLookup searchLookup = new SearchLookup(
173174
context.mappingLookup().indexTimeLookup()::get,
175+
fieldName -> context.mappingLookup().getMapper(fieldName) == null,
174176
(ft, lookup, fto) -> ft.fielddataBuilder(
175177
new FieldDataContext(
176178
context.indexSettings().getIndex().getName(),
@@ -180,7 +182,8 @@ private static void executeIndexTimeScripts(DocumentParserContext context) {
180182
fto
181183
)
182184
).build(new IndexFieldDataCache.None(), new NoneCircuitBreakerService()),
183-
(ctx, doc) -> Source.fromBytes(context.sourceToParse().source())
185+
(ctx, doc) -> Source.fromBytes(context.sourceToParse().source()),
186+
LeafFieldLookupProvider.fromStoredFields()
184187
);
185188
// field scripts can be called both by the loop at the end of this method and via
186189
// the document reader, so to ensure that we don't run them multiple times we

server/src/main/java/org/elasticsearch/index/mapper/LongScriptFieldType.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
import java.time.ZoneId;
3232
import java.util.Collection;
33+
import java.util.List;
3334
import java.util.Map;
3435
import java.util.Set;
3536
import java.util.function.Function;
@@ -109,7 +110,35 @@ public DocValueFormat docValueFormat(String format, ZoneId timeZone) {
109110

110111
@Override
111112
public BlockLoader blockLoader(BlockLoaderContext blContext) {
112-
return new LongScriptBlockDocValuesReader.LongScriptBlockLoader(leafFactory(blContext.lookup()));
113+
var indexSettings = blContext.indexSettings();
114+
if (isParsedFromSource && indexSettings.getIndexMappingSourceMode() == SourceFieldMapper.Mode.SYNTHETIC
115+
// A runtime and normal field can share the same name.
116+
// In that case there is no ignored source entry, and so we need to fail back to LongScriptBlockLoader.
117+
// We could optimize this, but at this stage feels like a rare scenario.
118+
&& blContext.lookup().onlyMappedAsRuntimeField(name())) {
119+
var reader = new NumberType.NumberFallbackSyntheticSourceReader(NumberType.LONG, null, true) {
120+
@Override
121+
public void writeToBlock(List<Number> values, BlockLoader.Builder blockBuilder) {
122+
var builder = (BlockLoader.LongBuilder) blockBuilder;
123+
for (var value : values) {
124+
builder.appendLong(value.longValue());
125+
}
126+
}
127+
};
128+
129+
return new FallbackSyntheticSourceBlockLoader(
130+
reader,
131+
name(),
132+
IgnoredSourceFieldMapper.ignoredSourceFormat(indexSettings.getIndexVersionCreated())
133+
) {
134+
@Override
135+
public Builder builder(BlockFactory factory, int expectedCount) {
136+
return factory.longs(expectedCount);
137+
}
138+
};
139+
} else {
140+
return new LongScriptBlockDocValuesReader.LongScriptBlockLoader(leafFactory(blContext.lookup()));
141+
}
113142
}
114143

115144
@Override

server/src/main/java/org/elasticsearch/index/query/SearchExecutionContext.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,7 @@ public void setLookupProviders(
534534
// as well as runtime fields loaded from _source that do need a source provider as part of executing the query
535535
this.lookup = new SearchLookup(
536536
this::getFieldType,
537+
fieldName -> mappingLookup.getMapper(fieldName) == null,
537538
(fieldType, searchLookup, fielddataOperation) -> indexFieldDataLookup.apply(
538539
fieldType,
539540
new FieldDataContext(

server/src/main/java/org/elasticsearch/search/lookup/SearchLookup.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public class SearchLookup implements SourceProvider {
4646
private final Set<String> fieldChain;
4747
private final SourceProvider sourceProvider;
4848
private final Function<String, MappedFieldType> fieldTypeLookup;
49+
private final Function<String, Boolean> onlyMappedAsRuntimeField;
4950
private final TriFunction<
5051
MappedFieldType,
5152
Supplier<SearchLookup>,
@@ -64,7 +65,7 @@ public SearchLookup(
6465
TriFunction<MappedFieldType, Supplier<SearchLookup>, MappedFieldType.FielddataOperation, IndexFieldData<?>> fieldDataLookup,
6566
SourceProvider sourceProvider
6667
) {
67-
this(fieldTypeLookup, fieldDataLookup, sourceProvider, LeafFieldLookupProvider.fromStoredFields());
68+
this(fieldTypeLookup, fieldName -> false, fieldDataLookup, sourceProvider, LeafFieldLookupProvider.fromStoredFields());
6869
}
6970

7071
/**
@@ -76,11 +77,13 @@ public SearchLookup(
7677
*/
7778
public SearchLookup(
7879
Function<String, MappedFieldType> fieldTypeLookup,
80+
Function<String, Boolean> onlyMappedAsRuntimeField,
7981
TriFunction<MappedFieldType, Supplier<SearchLookup>, MappedFieldType.FielddataOperation, IndexFieldData<?>> fieldDataLookup,
8082
SourceProvider sourceProvider,
8183
Function<LeafReaderContext, LeafFieldLookupProvider> fieldLookupProvider
8284
) {
8385
this.fieldTypeLookup = fieldTypeLookup;
86+
this.onlyMappedAsRuntimeField = onlyMappedAsRuntimeField;
8487
this.fieldChain = Collections.emptySet();
8588
this.sourceProvider = sourceProvider;
8689
this.fieldDataLookup = fieldDataLookup;
@@ -98,6 +101,7 @@ private SearchLookup(SearchLookup searchLookup, Set<String> fieldChain) {
98101
this.fieldChain = Collections.unmodifiableSet(fieldChain);
99102
this.sourceProvider = searchLookup.sourceProvider;
100103
this.fieldTypeLookup = searchLookup.fieldTypeLookup;
104+
this.onlyMappedAsRuntimeField = searchLookup.onlyMappedAsRuntimeField;
101105
this.fieldDataLookup = searchLookup.fieldDataLookup;
102106
this.fieldLookupProvider = searchLookup.fieldLookupProvider;
103107
}
@@ -106,6 +110,7 @@ private SearchLookup(SearchLookup searchLookup, SourceProvider sourceProvider) {
106110
this.fieldChain = searchLookup.fieldChain;
107111
this.sourceProvider = sourceProvider;
108112
this.fieldTypeLookup = searchLookup.fieldTypeLookup;
113+
this.onlyMappedAsRuntimeField = searchLookup.onlyMappedAsRuntimeField;
109114
this.fieldDataLookup = searchLookup.fieldDataLookup;
110115
this.fieldLookupProvider = searchLookup.fieldLookupProvider;
111116
}
@@ -144,6 +149,13 @@ public MappedFieldType fieldType(String fieldName) {
144149
return fieldTypeLookup.apply(fieldName);
145150
}
146151

152+
/**
153+
* @return whether a field is only mapped as runtime field. A runtime and normal field can share the same name.
154+
*/
155+
public boolean onlyMappedAsRuntimeField(String fieldName) {
156+
return onlyMappedAsRuntimeField.apply(fieldName);
157+
}
158+
147159
public IndexFieldData<?> getForField(MappedFieldType fieldType, MappedFieldType.FielddataOperation options) {
148160
return fieldDataLookup.apply(fieldType, () -> forkAndTrackFieldReferences(fieldType.name()), options);
149161
}

server/src/test/java/org/elasticsearch/index/mapper/LongScriptFieldTypeTests.java

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@
2929
import org.apache.lucene.store.Directory;
3030
import org.apache.lucene.tests.index.RandomIndexWriter;
3131
import org.apache.lucene.util.BytesRef;
32+
import org.elasticsearch.common.bytes.BytesArray;
3233
import org.elasticsearch.common.geo.ShapeRelation;
3334
import org.elasticsearch.common.lucene.search.function.ScriptScoreQuery;
35+
import org.elasticsearch.common.settings.Settings;
36+
import org.elasticsearch.common.xcontent.XContentHelper;
3437
import org.elasticsearch.index.IndexVersion;
3538
import org.elasticsearch.index.fielddata.LongScriptFieldData;
3639
import org.elasticsearch.index.fielddata.ScriptDocValues;
@@ -42,6 +45,10 @@
4245
import org.elasticsearch.script.ScriptFactory;
4346
import org.elasticsearch.script.ScriptType;
4447
import org.elasticsearch.search.MultiValueMode;
48+
import org.elasticsearch.search.lookup.SearchLookup;
49+
import org.elasticsearch.xcontent.XContentFactory;
50+
import org.elasticsearch.xcontent.XContentParserConfiguration;
51+
import org.elasticsearch.xcontent.XContentType;
4552

4653
import java.io.IOException;
4754
import java.util.ArrayList;
@@ -52,6 +59,8 @@
5259
import static org.hamcrest.Matchers.containsInAnyOrder;
5360
import static org.hamcrest.Matchers.equalTo;
5461
import static org.hamcrest.Matchers.greaterThan;
62+
import static org.hamcrest.Matchers.instanceOf;
63+
import static org.hamcrest.Matchers.nullValue;
5564

5665
public class LongScriptFieldTypeTests extends AbstractNonTextScriptFieldTypeTestCase {
5766

@@ -302,6 +311,83 @@ public void testBlockLoader() throws IOException {
302311
}
303312
}
304313

314+
public void testBlockLoaderSourceOnlyRuntimeField() throws IOException {
315+
try (
316+
Directory directory = newDirectory();
317+
RandomIndexWriter iw = new RandomIndexWriter(random(), directory, newIndexWriterConfig().setMergePolicy(NoMergePolicy.INSTANCE))
318+
) {
319+
iw.addDocuments(
320+
List.of(
321+
List.of(new StoredField("_source", new BytesRef("{\"test\": [1]}"))),
322+
List.of(new StoredField("_source", new BytesRef("{\"test\": [2]}")))
323+
)
324+
);
325+
try (DirectoryReader reader = iw.getReader()) {
326+
LongScriptFieldType fieldType = simpleSourceOnlyMappedFieldType();
327+
328+
// Assert implementations:
329+
BlockLoader loader = fieldType.blockLoader(blContext(Settings.EMPTY, true));
330+
assertThat(loader, instanceOf(LongScriptBlockDocValuesReader.LongScriptBlockLoader.class));
331+
// ignored source doesn't support column at a time loading:
332+
var columnAtATimeLoader = loader.columnAtATimeReader(reader.leaves().getFirst());
333+
assertThat(columnAtATimeLoader, instanceOf(LongScriptBlockDocValuesReader.class));
334+
var rowStrideReader = loader.rowStrideReader(reader.leaves().getFirst());
335+
assertThat(rowStrideReader, instanceOf(LongScriptBlockDocValuesReader.class));
336+
337+
// Assert values:
338+
assertThat(blockLoaderReadValuesFromColumnAtATimeReader(reader, fieldType, 0), equalTo(List.of(1L, 2L)));
339+
assertThat(blockLoaderReadValuesFromColumnAtATimeReader(reader, fieldType, 1), equalTo(List.of(2L)));
340+
assertThat(blockLoaderReadValuesFromRowStrideReader(reader, fieldType), equalTo(List.of(1L, 2L)));
341+
}
342+
}
343+
}
344+
345+
public void testBlockLoaderSourceOnlyRuntimeFieldWithSyntheticSource() throws IOException {
346+
var settings = Settings.builder().put("index.mapping.source.mode", "synthetic").build();
347+
try (
348+
Directory directory = newDirectory();
349+
RandomIndexWriter iw = new RandomIndexWriter(random(), directory, newIndexWriterConfig().setMergePolicy(NoMergePolicy.INSTANCE))
350+
) {
351+
352+
var document1 = createDocumentWithIgnoredSource("[1]");
353+
var document2 = createDocumentWithIgnoredSource("[2]");
354+
355+
iw.addDocuments(List.of(document1, document2));
356+
try (DirectoryReader reader = iw.getReader()) {
357+
LongScriptFieldType fieldType = simpleSourceOnlyMappedFieldType();
358+
359+
// Assert implementations:
360+
BlockLoader loader = fieldType.blockLoader(blContext(settings, true));
361+
assertThat(loader, instanceOf(FallbackSyntheticSourceBlockLoader.class));
362+
// ignored source doesn't support column at a time loading:
363+
var columnAtATimeLoader = loader.columnAtATimeReader(reader.leaves().getFirst());
364+
assertThat(columnAtATimeLoader, nullValue());
365+
var rowStrideReader = loader.rowStrideReader(reader.leaves().getFirst());
366+
assertThat(
367+
rowStrideReader.getClass().getName(),
368+
equalTo("org.elasticsearch.index.mapper.FallbackSyntheticSourceBlockLoader$IgnoredSourceRowStrideReader")
369+
);
370+
371+
// Assert values:
372+
assertThat(blockLoaderReadValuesFromRowStrideReader(settings, reader, fieldType, true), equalTo(List.of(1L, 2L)));
373+
}
374+
}
375+
}
376+
377+
private static LuceneDocument createDocumentWithIgnoredSource(String bytes) throws IOException {
378+
var doc = new LuceneDocument();
379+
var parser = XContentHelper.createParser(
380+
XContentParserConfiguration.EMPTY,
381+
new BytesArray(bytes),
382+
XContentFactory.xContent(XContentType.JSON).type()
383+
);
384+
parser.nextToken();
385+
var nameValue = new IgnoredSourceFieldMapper.NameValue("test", 0, XContentDataHelper.encodeToken(parser), doc);
386+
var ignoredSourceFormat = IgnoredSourceFieldMapper.ignoredSourceFormat(IndexVersion.current());
387+
ignoredSourceFormat.writeIgnoredFields(List.of(nameValue));
388+
return doc;
389+
}
390+
305391
@Override
306392
protected Query randomTermsQuery(MappedFieldType ft, SearchExecutionContext ctx) {
307393
return ft.termsQuery(List.of(randomLong()), ctx);
@@ -312,6 +398,10 @@ protected LongScriptFieldType simpleMappedFieldType() {
312398
return build("read_foo", Map.of(), OnScriptError.FAIL);
313399
}
314400

401+
private LongScriptFieldType simpleSourceOnlyMappedFieldType() {
402+
return build("read_test", Map.of(), OnScriptError.FAIL);
403+
}
404+
315405
@Override
316406
protected LongScriptFieldType loopFieldType() {
317407
return build("loop", Map.of(), OnScriptError.FAIL);
@@ -329,6 +419,32 @@ protected LongScriptFieldType build(String code, Map<String, Object> params, OnS
329419

330420
private static LongFieldScript.Factory factory(Script script) {
331421
switch (script.getIdOrCode()) {
422+
case "read_test":
423+
return new LongFieldScript.Factory() {
424+
@Override
425+
public LongFieldScript.LeafFactory newFactory(
426+
String fieldName,
427+
Map<String, Object> params,
428+
SearchLookup lookup,
429+
OnScriptError onScriptError
430+
) {
431+
return (ctx) -> new LongFieldScript(fieldName, params, lookup, onScriptError, ctx) {
432+
@Override
433+
@SuppressWarnings("unchecked")
434+
public void execute() {
435+
Map<String, Object> source = (Map<String, Object>) this.getParams().get("_source");
436+
for (Object foo : (List<?>) source.get("test")) {
437+
emit(((Number) foo).longValue());
438+
}
439+
};
440+
};
441+
}
442+
443+
@Override
444+
public boolean isParsedFromSource() {
445+
return true;
446+
}
447+
};
332448
case "read_foo":
333449
return (fieldName, params, lookup, onScriptError) -> (ctx) -> new LongFieldScript(
334450
fieldName,

0 commit comments

Comments
 (0)