Skip to content

Commit d626197

Browse files
authored
Merge multiple ignored source entires for the same field (#111994) (#112116)
(cherry picked from commit a35da24) # Conflicts: # modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/logsdb/qa/StandardVersusLogsIndexModeRandomDataChallengeRestIT.java # test/framework/src/main/java/org/elasticsearch/logsdb/datageneration/datasource/DefaultMappingParametersHandler.java
1 parent 49dbedb commit d626197

File tree

7 files changed

+381
-29
lines changed

7 files changed

+381
-29
lines changed

docs/changelog/111994.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 111994
2+
summary: Merge multiple ignored source entires for the same field
3+
area: Logs
4+
type: bug
5+
issues:
6+
- 111694

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ void write(XContentBuilder builder) throws IOException {
8383
XContentDataHelper.decodeAndWrite(builder, value());
8484
}
8585

86-
private String getFieldName() {
86+
String getFieldName() {
8787
return parentOffset() == 0 ? name() : name().substring(parentOffset());
8888
}
8989
}

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

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99
package org.elasticsearch.index.mapper;
1010

1111
import org.apache.lucene.index.LeafReader;
12+
import org.apache.lucene.util.BytesRef;
1213
import org.elasticsearch.ElasticsearchParseException;
1314
import org.elasticsearch.common.Explicit;
1415
import org.elasticsearch.common.logging.DeprecationCategory;
1516
import org.elasticsearch.common.logging.DeprecationLogger;
1617
import org.elasticsearch.common.xcontent.support.XContentMapValues;
17-
import org.elasticsearch.core.CheckedConsumer;
1818
import org.elasticsearch.core.Nullable;
1919
import org.elasticsearch.index.IndexVersion;
2020
import org.elasticsearch.index.IndexVersions;
@@ -32,6 +32,7 @@
3232
import java.util.List;
3333
import java.util.Locale;
3434
import java.util.Map;
35+
import java.util.Objects;
3536
import java.util.TreeMap;
3637
import java.util.stream.Stream;
3738

@@ -74,7 +75,7 @@ DynamicFieldsBuilder getDynamicFieldsBuilder() {
7475
* If no dynamic settings are explicitly configured, we default to {@link #TRUE}
7576
*/
7677
static Dynamic getRootDynamic(MappingLookup mappingLookup) {
77-
ObjectMapper.Dynamic rootDynamic = mappingLookup.getMapping().getRoot().dynamic;
78+
Dynamic rootDynamic = mappingLookup.getMapping().getRoot().dynamic;
7879
return rootDynamic == null ? Defaults.DYNAMIC : rootDynamic;
7980
}
8081
}
@@ -142,13 +143,13 @@ public final void addDynamic(String name, String prefix, Mapper mapper, Document
142143
int firstDotIndex = name.indexOf('.');
143144
String immediateChild = name.substring(0, firstDotIndex);
144145
String immediateChildFullName = prefix == null ? immediateChild : prefix + "." + immediateChild;
145-
ObjectMapper.Builder parentBuilder = findObjectBuilder(immediateChildFullName, context);
146+
Builder parentBuilder = findObjectBuilder(immediateChildFullName, context);
146147
parentBuilder.addDynamic(name.substring(firstDotIndex + 1), immediateChildFullName, mapper, context);
147148
add(parentBuilder);
148149
}
149150
}
150151

151-
private static ObjectMapper.Builder findObjectBuilder(String fullName, DocumentParserContext context) {
152+
private static Builder findObjectBuilder(String fullName, DocumentParserContext context) {
152153
// does the object mapper already exist? if so, use that
153154
ObjectMapper objectMapper = context.mappingLookup().objectMappers().get(fullName);
154155
if (objectMapper != null) {
@@ -215,7 +216,7 @@ public Mapper.Builder parse(String name, Map<String, Object> node, MappingParser
215216
throws MapperParsingException {
216217
parserContext.incrementMappingObjectDepth(); // throws MapperParsingException if depth limit is exceeded
217218
Explicit<Boolean> subobjects = parseSubobjects(node);
218-
ObjectMapper.Builder builder = new Builder(name, subobjects);
219+
Builder builder = new Builder(name, subobjects);
219220
parseObjectFields(node, parserContext, builder);
220221
parserContext.decrementMappingObjectDepth();
221222
return builder;
@@ -237,7 +238,7 @@ protected static boolean parseObjectOrDocumentTypeProperties(
237238
String fieldName,
238239
Object fieldNode,
239240
MappingParserContext parserContext,
240-
ObjectMapper.Builder builder
241+
Builder builder
241242
) {
242243
if (fieldName.equals("dynamic")) {
243244
String value = fieldNode.toString();
@@ -284,11 +285,7 @@ protected static Explicit<Boolean> parseSubobjects(Map<String, Object> node) {
284285
return Defaults.SUBOBJECTS;
285286
}
286287

287-
protected static void parseProperties(
288-
ObjectMapper.Builder objBuilder,
289-
Map<String, Object> propsNode,
290-
MappingParserContext parserContext
291-
) {
288+
protected static void parseProperties(Builder objBuilder, Map<String, Object> propsNode, MappingParserContext parserContext) {
292289
Iterator<Map.Entry<String, Object>> iterator = propsNode.entrySet().iterator();
293290
while (iterator.hasNext()) {
294291
Map.Entry<String, Object> entry = iterator.next();
@@ -347,7 +344,7 @@ protected static void parseProperties(
347344
for (int i = fieldNameParts.length - 2; i >= 0; --i) {
348345
String intermediateObjectName = fieldNameParts[i];
349346
validateFieldName(intermediateObjectName, parserContext.indexVersionCreated());
350-
ObjectMapper.Builder intermediate = new ObjectMapper.Builder(intermediateObjectName, Defaults.SUBOBJECTS);
347+
Builder intermediate = new Builder(intermediateObjectName, Defaults.SUBOBJECTS);
351348
intermediate.add(fieldBuilder);
352349
fieldBuilder = intermediate;
353350
}
@@ -417,8 +414,8 @@ private static void validateFieldName(String fieldName, IndexVersion indexCreate
417414
/**
418415
* @return a Builder that will produce an empty ObjectMapper with the same configuration as this one
419416
*/
420-
public ObjectMapper.Builder newBuilder(IndexVersion indexVersionCreated) {
421-
ObjectMapper.Builder builder = new ObjectMapper.Builder(leafName(), subobjects);
417+
public Builder newBuilder(IndexVersion indexVersionCreated) {
418+
Builder builder = new Builder(leafName(), subobjects);
422419
builder.enabled = this.enabled;
423420
builder.dynamic = this.dynamic;
424421
return builder;
@@ -507,7 +504,7 @@ protected record MergeResult(
507504
Explicit<Boolean> enabled,
508505
Explicit<Boolean> subObjects,
509506
Explicit<Boolean> trackArraySource,
510-
ObjectMapper.Dynamic dynamic,
507+
Dynamic dynamic,
511508
Map<String, Mapper> mappers
512509
) {
513510
static MergeResult build(ObjectMapper existing, ObjectMapper mergeWithObject, MapperMergeContext parentMergeContext) {
@@ -789,9 +786,9 @@ public Stream<Map.Entry<String, StoredFieldLoader>> storedFieldLoaders() {
789786

790787
@Override
791788
public DocValuesLoader docValuesLoader(LeafReader leafReader, int[] docIdsInLeaf) throws IOException {
792-
List<SourceLoader.SyntheticFieldLoader.DocValuesLoader> loaders = new ArrayList<>();
789+
List<DocValuesLoader> loaders = new ArrayList<>();
793790
for (SourceLoader.SyntheticFieldLoader field : fields) {
794-
SourceLoader.SyntheticFieldLoader.DocValuesLoader loader = field.docValuesLoader(leafReader, docIdsInLeaf);
791+
DocValuesLoader loader = field.docValuesLoader(leafReader, docIdsInLeaf);
795792
if (loader != null) {
796793
loaders.add(loader);
797794
}
@@ -803,7 +800,7 @@ public DocValuesLoader docValuesLoader(LeafReader leafReader, int[] docIdsInLeaf
803800
}
804801

805802
private class ObjectDocValuesLoader implements DocValuesLoader {
806-
private final List<SourceLoader.SyntheticFieldLoader.DocValuesLoader> loaders;
803+
private final List<DocValuesLoader> loaders;
807804

808805
private ObjectDocValuesLoader(List<DocValuesLoader> loaders) {
809806
this.loaders = loaders;
@@ -812,7 +809,7 @@ private ObjectDocValuesLoader(List<DocValuesLoader> loaders) {
812809
@Override
813810
public boolean advanceToDoc(int docId) throws IOException {
814811
boolean anyLeafHasDocValues = false;
815-
for (SourceLoader.SyntheticFieldLoader.DocValuesLoader docValueLoader : loaders) {
812+
for (DocValuesLoader docValueLoader : loaders) {
816813
boolean leafHasValue = docValueLoader.advanceToDoc(docId);
817814
anyLeafHasDocValues |= leafHasValue;
818815
}
@@ -848,18 +845,24 @@ public void write(XContentBuilder b) throws IOException {
848845

849846
if (ignoredValues != null && ignoredValues.isEmpty() == false) {
850847
// Use an ordered map between field names and writer functions, to order writing by field name.
851-
Map<String, CheckedConsumer<XContentBuilder, IOException>> orderedFields = new TreeMap<>();
848+
Map<String, FieldWriter> orderedFields = new TreeMap<>();
852849
for (IgnoredSourceFieldMapper.NameValue value : ignoredValues) {
853-
orderedFields.put(value.name(), value::write);
850+
var existing = orderedFields.get(value.name());
851+
if (existing == null) {
852+
orderedFields.put(value.name(), new FieldWriter.IgnoredSource(value));
853+
} else if (existing instanceof FieldWriter.IgnoredSource isw) {
854+
isw.mergeWith(value);
855+
}
854856
}
855857
for (SourceLoader.SyntheticFieldLoader field : fields) {
856858
if (field.hasValue()) {
857859
// Skip if the field source is stored separately, to avoid double-printing.
858-
orderedFields.putIfAbsent(field.fieldName(), field::write);
860+
orderedFields.computeIfAbsent(field.fieldName(), k -> new FieldWriter.FieldLoader(field));
859861
}
860862
}
863+
861864
for (var writer : orderedFields.values()) {
862-
writer.accept(b);
865+
writer.writeTo(b);
863866
}
864867
ignoredValues = null;
865868
} else {
@@ -890,6 +893,42 @@ public boolean setIgnoredValues(Map<String, List<IgnoredSourceFieldMapper.NameVa
890893
public String fieldName() {
891894
return ObjectMapper.this.fullPath();
892895
}
896+
897+
interface FieldWriter {
898+
void writeTo(XContentBuilder builder) throws IOException;
899+
900+
record FieldLoader(SourceLoader.SyntheticFieldLoader loader) implements FieldWriter {
901+
@Override
902+
public void writeTo(XContentBuilder builder) throws IOException {
903+
loader.write(builder);
904+
}
905+
}
906+
907+
class IgnoredSource implements FieldWriter {
908+
private final String fieldName;
909+
private final String leafName;
910+
private final List<BytesRef> values;
911+
912+
IgnoredSource(IgnoredSourceFieldMapper.NameValue initialValue) {
913+
this.fieldName = initialValue.name();
914+
this.leafName = initialValue.getFieldName();
915+
this.values = new ArrayList<>();
916+
this.values.add(initialValue.value());
917+
}
918+
919+
@Override
920+
public void writeTo(XContentBuilder builder) throws IOException {
921+
XContentDataHelper.writeMerged(builder, leafName, values);
922+
}
923+
924+
public FieldWriter mergeWith(IgnoredSourceFieldMapper.NameValue nameValue) {
925+
assert Objects.equals(nameValue.name(), fieldName) : "IgnoredSource is merged with wrong field data";
926+
927+
values.add(nameValue.value());
928+
return this;
929+
}
930+
}
931+
}
893932
}
894933

895934
protected boolean isRoot() {

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import java.math.BigInteger;
2727
import java.nio.charset.StandardCharsets;
2828
import java.util.Arrays;
29+
import java.util.List;
30+
import java.util.Optional;
2931

3032
/**
3133
* Helper class for processing field data of any type, as provided by the {@link XContentParser}.
@@ -92,6 +94,70 @@ static void decodeAndWrite(XContentBuilder b, BytesRef r) throws IOException {
9294
}
9395
}
9496

97+
/**
98+
* Writes encoded values to provided builder. If there are multiple values they are merged into
99+
* a single resulting array.
100+
* @param b destination
101+
* @param fieldName name of the field that is written
102+
* @param encodedParts subset of field data encoded using methods of this class. Can contain arrays which will be flattened.
103+
* @throws IOException
104+
*/
105+
static void writeMerged(XContentBuilder b, String fieldName, List<BytesRef> encodedParts) throws IOException {
106+
if (encodedParts.isEmpty()) {
107+
return;
108+
}
109+
110+
if (encodedParts.size() == 1) {
111+
b.field(fieldName);
112+
XContentDataHelper.decodeAndWrite(b, encodedParts.get(0));
113+
return;
114+
}
115+
116+
b.startArray(fieldName);
117+
118+
for (var encodedValue : encodedParts) {
119+
Optional<XContentType> encodedXContentType = switch ((char) encodedValue.bytes[encodedValue.offset]) {
120+
case CBOR_OBJECT_ENCODING, JSON_OBJECT_ENCODING, YAML_OBJECT_ENCODING, SMILE_OBJECT_ENCODING -> Optional.of(
121+
getXContentType(encodedValue)
122+
);
123+
default -> Optional.empty();
124+
};
125+
if (encodedXContentType.isEmpty()) {
126+
// This is a plain value, we can just write it
127+
XContentDataHelper.decodeAndWrite(b, encodedValue);
128+
} else {
129+
// Encoded value could be an array which needs to be flattened
130+
// since we are already inside an array.
131+
try (
132+
XContentParser parser = encodedXContentType.get()
133+
.xContent()
134+
.createParser(
135+
XContentParserConfiguration.EMPTY,
136+
encodedValue.bytes,
137+
encodedValue.offset + 1,
138+
encodedValue.length - 1
139+
)
140+
) {
141+
if (parser.currentToken() == null) {
142+
parser.nextToken();
143+
}
144+
145+
// It's an array, we will flatten it.
146+
if (parser.currentToken() == XContentParser.Token.START_ARRAY) {
147+
while (parser.nextToken() != XContentParser.Token.END_ARRAY) {
148+
b.copyCurrentStructure(parser);
149+
}
150+
} else {
151+
// It is a single complex structure (an object), write it as is.
152+
b.copyCurrentStructure(parser);
153+
}
154+
}
155+
}
156+
}
157+
158+
b.endArray();
159+
}
160+
95161
/**
96162
* Returns the {@link XContentType} to use for creating an XContentBuilder to decode the passed value.
97163
*/

0 commit comments

Comments
 (0)