Skip to content

Commit 30d72ed

Browse files
authored
Merge field mappers when updating mappings with [subobjects:false] (#120370) (#120520)
* Merge field mappers when updating mappings with [subobjects:false] * Update docs/changelog/120370.yaml * fix and add tests * node feature guarding (cherry picked from commit 0a5d2e7) # Conflicts: # server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java
1 parent 1270bde commit 30d72ed

File tree

5 files changed

+147
-6
lines changed

5 files changed

+147
-6
lines changed

docs/changelog/120370.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 120370
2+
summary: "Merge field mappers when updating mappings with [subobjects:false]"
3+
area: Mapping
4+
type: bug
5+
issues:
6+
- 120216

rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.put_mapping/10_basic.yml

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,92 @@
211211
body:
212212
_source:
213213
mode: synthetic
214+
215+
---
216+
"modify field type with subobjects:false":
217+
- requires:
218+
cluster_features: [ "mapper.subobjects_false_mapping_update_fix" ]
219+
reason: requires fix for mapping updates when [subobjects:false] is set
220+
- do:
221+
indices.create:
222+
index: test_index
223+
body:
224+
mappings:
225+
subobjects: false
226+
properties:
227+
user.id:
228+
type: long
229+
user.name:
230+
type: text
231+
232+
- do:
233+
catch: bad_request
234+
indices.put_mapping:
235+
index: test_index
236+
body:
237+
properties:
238+
user.id:
239+
type: keyword
240+
241+
- match: { error.type: "illegal_argument_exception" }
242+
- match: { error.reason: "mapper [user.id] cannot be changed from type [long] to [keyword]" }
243+
244+
- do:
245+
indices.put_mapping:
246+
index: test_index
247+
body:
248+
properties:
249+
user.name:
250+
type: text
251+
fields:
252+
raw:
253+
type: keyword
254+
255+
- is_true: acknowledged
256+
257+
258+
---
259+
"modify nested field type with subobjects:false":
260+
- requires:
261+
cluster_features: [ "mapper.subobjects_false_mapping_update_fix" ]
262+
reason: requires fix for mapping updates when [subobjects:false] is set
263+
- do:
264+
indices.create:
265+
index: test_index
266+
body:
267+
mappings:
268+
properties:
269+
path:
270+
properties:
271+
to:
272+
subobjects: false
273+
properties:
274+
user.id:
275+
type: long
276+
user.name:
277+
type: text
278+
279+
- do:
280+
catch: bad_request
281+
indices.put_mapping:
282+
index: test_index
283+
body:
284+
properties:
285+
path.to.user.id:
286+
type: keyword
287+
288+
- match: { error.type: "illegal_argument_exception" }
289+
- match: { error.reason: "mapper [path.to.user.id] cannot be changed from type [long] to [keyword]" }
290+
291+
- do:
292+
indices.put_mapping:
293+
index: test_index
294+
body:
295+
properties:
296+
path.to.user.name:
297+
type: text
298+
fields:
299+
raw:
300+
type: keyword
301+
302+
- is_true: acknowledged

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ public Set<NodeFeature> getTestFeatures() {
7171
CONSTANT_KEYWORD_SYNTHETIC_SOURCE_WRITE_FIX,
7272
META_FETCH_FIELDS_ERROR_CODE_CHANGED,
7373
SPARSE_VECTOR_STORE_SUPPORT,
74-
SourceFieldMapper.SYNTHETIC_RECOVERY_SOURCE
74+
SourceFieldMapper.SYNTHETIC_RECOVERY_SOURCE,
75+
ObjectMapper.SUBOBJECTS_FALSE_MAPPING_UPDATE_FIX
7576
);
7677
}
7778
}

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public class ObjectMapper extends Mapper {
4949
private static final Logger logger = LogManager.getLogger(ObjectMapper.class);
5050
private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(ObjectMapper.class);
5151
public static final FeatureFlag SUB_OBJECTS_AUTO_FEATURE_FLAG = new FeatureFlag("sub_objects_auto");
52+
static final NodeFeature SUBOBJECTS_FALSE_MAPPING_UPDATE_FIX = new NodeFeature("mapper.subobjects_false_mapping_update_fix");
5253

5354
public static final String CONTENT_TYPE = "object";
5455
static final String STORE_ARRAY_SOURCE_PARAM = "store_array_source";
@@ -663,11 +664,21 @@ private static Map<String, Mapper> buildMergedMappers(
663664
if (subobjects.isPresent()
664665
&& subobjects.get() == Subobjects.DISABLED
665666
&& mergeWithMapper instanceof ObjectMapper objectMapper) {
666-
// An existing mapping that has set `subobjects: false` is merged with a mapping with sub-objects
667-
objectMapper.asFlattenedFieldMappers(objectMergeContext.getMapperBuilderContext())
668-
.stream()
669-
.filter(m -> objectMergeContext.decrementFieldBudgetIfPossible(m.getTotalFieldsCount()))
670-
.forEach(m -> putMergedMapper(mergedMappers, m));
667+
// An existing mapping that has set `subobjects: false` is merged with a mapping with sub-objects.
668+
List<FieldMapper> flattenedMappers = objectMapper.asFlattenedFieldMappers(
669+
objectMergeContext.getMapperBuilderContext()
670+
);
671+
for (FieldMapper flattenedMapper : flattenedMappers) {
672+
if (objectMergeContext.decrementFieldBudgetIfPossible(flattenedMapper.getTotalFieldsCount())) {
673+
var conflict = mergedMappers.get(flattenedMapper.leafName());
674+
if (objectMergeContext.getMapperBuilderContext().getMergeReason() == MergeReason.INDEX_TEMPLATE
675+
|| conflict == null) {
676+
putMergedMapper(mergedMappers, flattenedMapper);
677+
} else {
678+
putMergedMapper(mergedMappers, conflict.merge(flattenedMapper, objectMergeContext));
679+
}
680+
}
681+
}
671682
} else if (objectMergeContext.decrementFieldBudgetIfPossible(mergeWithMapper.getTotalFieldsCount())) {
672683
putMergedMapper(mergedMappers, mergeWithMapper);
673684
} else if (mergeWithMapper instanceof ObjectMapper om) {

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,40 @@ public void testDisallowFieldReplacementForIndexTemplates() throws IOException {
329329
assertThat(e.getMessage(), containsString("can't merge a non object mapping [object.field1] with an object mapping"));
330330
}
331331

332+
public void testFieldReplacementSubobjectsFalse() throws IOException {
333+
MapperService mapperService = createMapperService(mapping(b -> {
334+
b.startObject("obj").field("type", "object").field("subobjects", false).startObject("properties");
335+
{
336+
b.startObject("my.field").field("type", "keyword").endObject();
337+
}
338+
b.endObject().endObject();
339+
}));
340+
DocumentMapper mapper = mapperService.documentMapper();
341+
assertNull(mapper.mapping().getRoot().dynamic());
342+
Mapping mergeWith = mapperService.parseMapping(
343+
"_doc",
344+
MergeReason.INDEX_TEMPLATE,
345+
new CompressedXContent(BytesReference.bytes(topMapping(b -> {
346+
b.startObject("properties").startObject("obj").field("type", "object").field("subobjects", false).startObject("properties");
347+
{
348+
b.startObject("my.field").field("type", "long").endObject();
349+
}
350+
b.endObject().endObject().endObject();
351+
})))
352+
);
353+
354+
// Fails on mapping update.
355+
IllegalArgumentException exception = expectThrows(
356+
IllegalArgumentException.class,
357+
() -> mapper.mapping().merge(mergeWith, MergeReason.MAPPING_UPDATE, Long.MAX_VALUE)
358+
);
359+
assertEquals("mapper [obj.my.field] cannot be changed from type [keyword] to [long]", exception.getMessage());
360+
361+
// Passes on template merging.
362+
Mapping merged = mapper.mapping().merge(mergeWith, MergeReason.INDEX_TEMPLATE, Long.MAX_VALUE);
363+
assertThat(((ObjectMapper) merged.getRoot().getMapper("obj")).getMapper("my.field"), instanceOf(NumberFieldMapper.class));
364+
}
365+
332366
public void testUnknownLegacyFields() throws Exception {
333367
MapperService service = createMapperService(IndexVersion.fromId(5000099), Settings.EMPTY, () -> false, mapping(b -> {
334368
b.startObject("name");

0 commit comments

Comments
 (0)