Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix composable templates with subobjects: false #97317

Merged
merged 48 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
76ab263
Fix composable templates with subobjects: false
eyalkoren Jul 3, 2023
d3b2923
Update docs/changelog/97317.yaml
eyalkoren Jul 3, 2023
baf2a18
Merge remote-tracking branch 'upstream/main' into fix2-subobjects-com…
eyalkoren Jul 30, 2023
faf83a5
Add unit tests
eyalkoren Jul 31, 2023
c4fa163
Merge remote-tracking branch 'eyalkoren/fix2-subobjects-composable-te…
eyalkoren Jul 31, 2023
0ca0233
Change changelog summary
eyalkoren Jul 31, 2023
b45863e
Small refactor: switch from sequential to bulk component templates merge
eyalkoren Jul 31, 2023
a8cfed5
Merging mappings in raw map form
eyalkoren Aug 6, 2023
7e408c9
Merge remote-tracking branch 'upstream/main' into fix2-subobjects-com…
eyalkoren Aug 7, 2023
699943e
Spoteless apply
eyalkoren Aug 7, 2023
90e9634
Use type that supports ignore_malformed in test
eyalkoren Aug 7, 2023
b28ab16
Temporarily disable a test
eyalkoren Aug 7, 2023
1eaf3d7
Handle routing settings
eyalkoren Aug 7, 2023
2073731
Adjust tests to use explicit routing disabling
eyalkoren Aug 7, 2023
f5f457c
Applying review comments
eyalkoren Aug 8, 2023
0e347b7
Adding test case and fix for CreateIndex as well
eyalkoren Aug 8, 2023
e23b2f2
spotless
eyalkoren Aug 8, 2023
6db4fa0
Fix test- use type that supports ignore_malformed
eyalkoren Aug 8, 2023
ad1d283
Extract mapping under root before merging
eyalkoren Aug 9, 2023
83347bd
Better normalization of multiple sources
eyalkoren Aug 9, 2023
849e624
Merge remote-tracking branch 'eyalkoren/fix2-subobjects-composable-te…
eyalkoren Aug 9, 2023
e60c6b7
Merge remote-tracking branch 'upstream/main' into fix2-subobjects-com…
eyalkoren Aug 13, 2023
6d968a5
Merge additional usages of new merge from eyalkoren/mappings-bulk-mer…
eyalkoren Aug 13, 2023
a865e25
Never return null map from merge
eyalkoren Aug 13, 2023
67bb863
Spotless again :-(
eyalkoren Aug 13, 2023
4b8d20a
Restore null return, avoid empty map creation
eyalkoren Aug 14, 2023
11942fc
My god this spotless is annoying
eyalkoren Aug 14, 2023
d6c8fa6
Standardize mapping type conflicts error messages
eyalkoren Aug 14, 2023
cda0236
Adding comment
eyalkoren Aug 14, 2023
5f2451f
Merge remote-tracking branch 'upstream/main' into fix2-subobjects-com…
eyalkoren Aug 22, 2023
e3d7e76
Add tests
eyalkoren Aug 22, 2023
5dbcf02
Apply algorithm changes
eyalkoren Aug 23, 2023
59be7ed
Restore test
eyalkoren Aug 23, 2023
ef7c9ec
Restore test
eyalkoren Aug 23, 2023
ac60c6d
Restore test
eyalkoren Aug 23, 2023
5e2b52f
Add test case
eyalkoren Aug 23, 2023
cd1d116
spotless
eyalkoren Aug 23, 2023
32ef7f2
Update skip version
eyalkoren Aug 24, 2023
5ff854b
Update skip version
eyalkoren Aug 24, 2023
39c7d04
Add raw mapping merge unit tests
eyalkoren Aug 24, 2023
c3b160c
Merge remote-tracking branch 'upstream/main' into fix2-subobjects-com…
eyalkoren Aug 29, 2023
d54a8eb
Apply review proposals and add a test case
eyalkoren Aug 29, 2023
551685c
goddamned you spotless
eyalkoren Aug 29, 2023
a2e3d45
Merge remote-tracking branch 'upstream/main' into fix2-subobjects-com…
eyalkoren Sep 5, 2023
e243d5f
Remove subobects contradiction invalidation
eyalkoren Sep 5, 2023
160f6a8
Merge remote-tracking branch 'upstream/main' into fix2-subobjects-com…
eyalkoren Sep 13, 2023
817a346
Apply review suggestions
eyalkoren Sep 13, 2023
5f11c39
Apply spotless suggestions
felixbarny Sep 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/changelog/97317.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pr: 97317
summary: "Fix merges of mappings with `subobjects: false` for composable index templates"
area: Mapping
type: bug
issues:
- 96768
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,37 @@
index_patterns: ["purple-index"]
composed_of: ["red", "blue"]
ignore_missing_component_templates: ["foo"]

---
"Composable index templates that include subobjects: false":
- skip:
version: ' - 8.9.1'
reason: 'https://github.com/elastic/elasticsearch/issues/96768 fixed after 8.9.0'

- do:
cluster.put_component_template:
name: test-subobjects
body:
template:
mappings:
subobjects: false

- do:
cluster.put_component_template:
name: test-field
body:
template:
mappings:
properties:
parent.subfield:
type: keyword

- do:
indices.put_index_template:
name: logs-composable-template
body:
index_patterns:
- test-*
composed_of:
- test-subobjects
- test-field
Original file line number Diff line number Diff line change
Expand Up @@ -1595,9 +1595,7 @@ private static void validateCompositeTemplate(
List<CompressedXContent> mappings = collectMappings(stateWithIndex, templateName, indexName);
try {
MapperService mapperService = tempIndexService.mapperService();
for (CompressedXContent mapping : mappings) {
mapperService.merge(MapperService.SINGLE_MAPPING_NAME, mapping, MapperService.MergeReason.INDEX_TEMPLATE);
}
mapperService.merge(MapperService.SINGLE_MAPPING_NAME, mappings, MapperService.MergeReason.INDEX_TEMPLATE);

if (template.getDataStreamTemplate() != null) {
validateTimestampFieldMapping(mapperService.mappingLookup());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.cluster.metadata.MappingMetadata;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.Explicit;
import org.elasticsearch.common.compress.CompressedXContent;
import org.elasticsearch.common.compress.CompressorFactory;
import org.elasticsearch.common.settings.Setting;
Expand Down Expand Up @@ -39,6 +40,7 @@
import java.io.InputStream;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -353,24 +355,62 @@ public void merge(IndexMetadata indexMetadata, MergeReason reason) {
}
}

/**
* Merging the provided mappings. If the provided mapping list is ordered, merging will maintain the original ordering.
*/
public DocumentMapper merge(String type, List<CompressedXContent> mappingSources, MergeReason reason) {
final DocumentMapper currentMapper = this.mapper;
if (currentMapper != null && mappingSources.size() == 1 && currentMapper.mappingSource().equals(mappingSources.get(0))) {
return currentMapper;
}

List<Map<String, Object>> convertedMappings = new LinkedList<>();
Explicit<Boolean> explicitSubobjects = null;
for (CompressedXContent mappingSource : mappingSources) {
Map<String, Object> mapping = MappingParser.convertToMap(mappingSource);
eyalkoren marked this conversation as resolved.
Show resolved Hide resolved
convertedMappings.add(mapping);
Explicit<Boolean> tmpSubobjects = ObjectMapper.checkSubobjectsInMappingRoot(mapping);
if (tmpSubobjects != null) {
if (explicitSubobjects != null && tmpSubobjects.value() != explicitSubobjects.value()) {
throw new MapperParsingException("Failed to parse mappings: contradicting subobjects settings provided");
}
explicitSubobjects = tmpSubobjects;
}
}

DocumentMapper ret = null;
for (Map<String, Object> mapping : convertedMappings) {
ret = doMerge(type, reason, mapping, explicitSubobjects);
}
return ret;
}

public DocumentMapper merge(String type, CompressedXContent mappingSource, MergeReason reason) {
final DocumentMapper currentMapper = this.mapper;
if (currentMapper != null && currentMapper.mappingSource().equals(mappingSource)) {
return currentMapper;
}
synchronized (this) {
Mapping incomingMapping = parseMapping(type, mappingSource);
Mapping mapping = mergeMappings(this.mapper, incomingMapping, reason);
// TODO: In many cases the source here is equal to mappingSource so we need not serialize again.
// We should identify these cases reliably and save expensive serialization here
DocumentMapper newMapper = newDocumentMapper(mapping, reason, mapping.toCompressedXContent());
if (reason == MergeReason.MAPPING_UPDATE_PREFLIGHT) {
return newMapper;
}
this.mapper = newMapper;
assert assertSerialization(newMapper);
Map<String, Object> mappingSourceAsMap = MappingParser.convertToMap(mappingSource);
eyalkoren marked this conversation as resolved.
Show resolved Hide resolved
return doMerge(type, reason, mappingSourceAsMap, null);
}

private synchronized DocumentMapper doMerge(
String type,
MergeReason reason,
Map<String, Object> mappingSourceAsMap,
@Nullable Explicit<Boolean> explicitSubobjects
) {
Mapping incomingMapping = parseMapping(type, mappingSourceAsMap, explicitSubobjects);
Mapping mapping = mergeMappings(this.mapper, incomingMapping, reason);
// TODO: In many cases the source here is equal to mappingSource so we need not serialize again.
// We should identify these cases reliably and save expensive serialization here
DocumentMapper newMapper = newDocumentMapper(mapping, reason, mapping.toCompressedXContent());
if (reason == MergeReason.MAPPING_UPDATE_PREFLIGHT) {
return newMapper;
}
this.mapper = newMapper;
assert assertSerialization(newMapper);
return newMapper;
}

private DocumentMapper newDocumentMapper(Mapping mapping, MergeReason reason, CompressedXContent mappingSource) {
Expand All @@ -387,6 +427,22 @@ public Mapping parseMapping(String mappingType, CompressedXContent mappingSource
}
}

/**
* A method to parse mapping from a source in a map form, that allows to specify explicit/implicit {@code subobjects} configuration.
eyalkoren marked this conversation as resolved.
Show resolved Hide resolved
* Since parsing is affected by the {@code subobjects} setting, the resulted mapping may change according to this setting.
* @param mappingType the mapping type
* @param mappingSource mapping source already converted to a map form, but not yet processed otherwise
* @param explicitSubobjects subobjects configuration to use for this parsing operation
* @return a parsed mapping
*/
public Mapping parseMapping(String mappingType, Map<String, Object> mappingSource, @Nullable Explicit<Boolean> explicitSubobjects) {
try {
return mappingParser.parse(mappingType, mappingSource, explicitSubobjects);
} catch (Exception e) {
throw new MapperParsingException("Failed to parse mapping: {}", e, e.getMessage());
}
}

public static Mapping mergeMappings(DocumentMapper currentMapper, Mapping incomingMapping, MergeReason reason) {
Mapping newMapping;
if (currentMapper == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

package org.elasticsearch.index.mapper;

import org.elasticsearch.common.Explicit;
import org.elasticsearch.common.compress.CompressedXContent;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.core.Nullable;
Expand Down Expand Up @@ -73,19 +74,37 @@ private static String getRemainingFields(Map<?, ?> map) {
return remainingFields.toString();
}

@SuppressWarnings("unchecked")
Mapping parse(@Nullable String type, CompressedXContent source) throws MapperParsingException {
static Map<String, Object> convertToMap(CompressedXContent source) {
Objects.requireNonNull(source, "source cannot be null");
Map<String, Object> mapping = XContentHelper.convertToMap(source.compressedReference(), true, XContentType.JSON).v2();
if (mapping.isEmpty()) {
return XContentHelper.convertToMap(source.compressedReference(), true, XContentType.JSON).v2();
}

Mapping parse(@Nullable String type, CompressedXContent source) throws MapperParsingException {
Map<String, Object> mapping = convertToMap(source);
return parse(type, mapping, null);
}

/**
* A method to parse mapping from a source in a map form, that allows to specify explicit/implicit {@code subobjects} configuration.
eyalkoren marked this conversation as resolved.
Show resolved Hide resolved
* Since parsing is affected by the {@code subobjects} setting, the resulted mapping may change according to this setting.
* @param type the mapping type
* @param mappingSource mapping source already converted to a map form, but not yet processed otherwise
* @param explicitSubobjects subobjects configuration to use for this parsing operation
* @return a parsed mapping
* @throws MapperParsingException in case of parsing error
*/
@SuppressWarnings("unchecked")
Mapping parse(@Nullable String type, Map<String, Object> mappingSource, @Nullable Explicit<Boolean> explicitSubobjects)
throws MapperParsingException {
if (mappingSource.isEmpty()) {
if (type == null) {
throw new MapperParsingException("malformed mapping, no type name found");
throw new MapperParsingException("malformed mappingSource, no type name found");
eyalkoren marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
String rootName = mapping.keySet().iterator().next();
String rootName = mappingSource.keySet().iterator().next();
if (type == null || type.equals(rootName) || documentTypeResolver.apply(type).equals(rootName)) {
type = rootName;
mapping = (Map<String, Object>) mapping.get(rootName);
mappingSource = (Map<String, Object>) mappingSource.get(rootName);
}
}
if (type == null) {
Expand All @@ -94,19 +113,16 @@ Mapping parse(@Nullable String type, CompressedXContent source) throws MapperPar
if (type.isEmpty()) {
throw new MapperParsingException("type cannot be an empty string");
}
return parse(type, mapping);
}

private Mapping parse(String type, Map<String, Object> mapping) throws MapperParsingException {
final MappingParserContext mappingParserContext = mappingParserContextSupplier.get();

RootObjectMapper.Builder rootObjectMapper = RootObjectMapper.parse(type, mapping, mappingParserContext);
RootObjectMapper.Builder rootObjectMapper = RootObjectMapper.parse(type, mappingSource, mappingParserContext, explicitSubobjects);

Map<Class<? extends MetadataFieldMapper>, MetadataFieldMapper> metadataMappers = metadataMappersSupplier.get();
Map<String, Object> meta = null;
boolean isSourceSynthetic = mappingParserContext.getIndexSettings().getMode().isSyntheticSourceEnabled();

Iterator<Map.Entry<String, Object>> iterator = mapping.entrySet().iterator();
Iterator<Map.Entry<String, Object>> iterator = mappingSource.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Object> entry = iterator.next();
String fieldName = entry.getKey();
Expand Down Expand Up @@ -135,7 +151,7 @@ private Mapping parse(String type, Map<String, Object> mapping) throws MapperPar
}

@SuppressWarnings("unchecked")
Map<String, Object> removed = (Map<String, Object>) mapping.remove("_meta");
Map<String, Object> removed = (Map<String, Object>) mappingSource.remove("_meta");
if (removed != null) {
/*
* It may not be required to copy meta here to maintain immutability but the cost is pretty low here.
Expand All @@ -155,7 +171,7 @@ private Mapping parse(String type, Map<String, Object> mapping) throws MapperPar
}
if (mappingParserContext.indexVersionCreated().isLegacyIndexVersion() == false) {
// legacy indices are allowed to have extra definitions that we ignore (we will drop them on import)
checkNoRemainingFields(mapping, "Root mapping definition has unsupported parameters: ");
checkNoRemainingFields(mappingSource, "Root mapping definition has unsupported parameters: ");
}

return new Mapping(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.elasticsearch.common.logging.DeprecationCategory;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.xcontent.support.XContentMapValues;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.index.IndexVersion;
import org.elasticsearch.index.mapper.MapperService.MergeReason;
import org.elasticsearch.xcontent.ToXContent;
Expand Down Expand Up @@ -251,7 +252,7 @@ protected static Explicit<Boolean> parseSubobjects(Map<String, Object> node) {
if (subobjectsNode != null) {
return Explicit.explicitBoolean(XContentMapValues.nodeBooleanValue(subobjectsNode, "subobjects.subobjects"));
}
return Explicit.IMPLICIT_TRUE;
return Defaults.SUBOBJECTS;
}

protected static void parseProperties(
Expand Down Expand Up @@ -589,6 +590,27 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep

}

/**
eyalkoren marked this conversation as resolved.
Show resolved Hide resolved
* Checks the {@code subobjects} config in provided mapping's root.
* @param mapping a raw mapping in a map form
* @return an explicit {@code subobjects} value if such is configured directly under the root, or {@code null} otherwise
*/
@Nullable
static Explicit<Boolean> checkSubobjectsInMappingRoot(Map<String, Object> mapping) {
if (mapping.size() == 1) {
String rootName = mapping.keySet().iterator().next();
Object childNode = mapping.get(rootName);
if (childNode instanceof Map) {
@SuppressWarnings("unchecked")
Object subobjectsNode = ((Map<String, Object>) mapping.get(rootName)).get("subobjects");
if (subobjectsNode != null) {
return Explicit.explicitBoolean(XContentMapValues.nodeBooleanValue(subobjectsNode, "subobjects.subobjects"));
}
}
}
return null;
}

public SourceLoader.SyntheticFieldLoader syntheticFieldLoader(Stream<Mapper> extra) {
return new SyntheticSourceFieldLoader(
Stream.concat(extra, mappers.values().stream())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.elasticsearch.common.logging.DeprecationCategory;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.time.DateFormatter;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.index.IndexVersion;
import org.elasticsearch.index.mapper.DynamicTemplate.XContentFieldType;
import org.elasticsearch.index.mapper.MapperService.MergeReason;
Expand Down Expand Up @@ -399,9 +400,16 @@ protected void startSyntheticField(XContentBuilder b) throws IOException {
b.startObject();
}

public static RootObjectMapper.Builder parse(String name, Map<String, Object> node, MappingParserContext parserContext)
throws MapperParsingException {
public static RootObjectMapper.Builder parse(
String name,
Map<String, Object> node,
MappingParserContext parserContext,
@Nullable Explicit<Boolean> explicitSubobjects
) throws MapperParsingException {
Explicit<Boolean> subobjects = parseSubobjects(node);
if (subobjects.explicit() == false && explicitSubobjects != null) {
subobjects = explicitSubobjects;
}
RootObjectMapper.Builder builder = new Builder(name, subobjects);
Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator();
while (iterator.hasNext()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2504,6 +2504,61 @@ public void testAddInvalidTemplateIgnoreService() throws Exception {
assertThat(e.getMessage(), containsString("missing component templates [fail] that does not exist"));
}

public void testComposableTemplateWithSubobjectsFalse() throws Exception {
final MetadataIndexTemplateService service = getMetadataIndexTemplateService();
ClusterState state = ClusterState.EMPTY_STATE;

ComponentTemplate subobjects = new ComponentTemplate(new Template(null, new CompressedXContent("""
{
"subobjects": false
}
"""), null), null, null);

ComponentTemplate fieldMapping = new ComponentTemplate(new Template(null, new CompressedXContent("""
{
"properties": {
"parent.subfield": {
"type": "keyword"
}
}
}
"""), null), null, null);

state = service.addComponentTemplate(state, true, "subobjects", subobjects);
state = service.addComponentTemplate(state, true, "field_mapping", fieldMapping);
ComposableIndexTemplate it = new ComposableIndexTemplate(
List.of("test-*"),
new Template(null, null, null),
List.of("subobjects", "field_mapping"),
0L,
1L,
null,
null,
null
);
state = service.addIndexTemplateV2(state, true, "composable-template", it);

List<CompressedXContent> mappings = MetadataIndexTemplateService.collectMappings(state, "composable-template", "test-index");

assertNotNull(mappings);
assertThat(mappings.size(), equalTo(2));
List<Map<String, Object>> parsedMappings = mappings.stream().map(m -> {
try {
return MapperService.parseMapping(NamedXContentRegistry.EMPTY, m);
} catch (Exception e) {
logger.error(e);
fail("failed to parse mappings: " + m.string());
return null;
}
}).toList();

assertThat(parsedMappings.get(0), equalTo(Map.of("_doc", Map.of("subobjects", false))));
assertThat(
parsedMappings.get(1),
equalTo(Map.of("_doc", Map.of("properties", Map.of("parent.subfield", Map.of("type", "keyword")))))
);
}

private static List<Throwable> putTemplate(NamedXContentRegistry xContentRegistry, PutRequest request) {
ThreadPool testThreadPool = mock(ThreadPool.class);
ClusterService clusterService = ClusterServiceUtils.createClusterService(testThreadPool);
Expand Down
Loading