Skip to content

Commit ff0f83f

Browse files
authored
Make field limit more predictable (#102885)
Today, we're counting all mappers, including mappers for subfields that aren't explicitly added to the mapping towards the field limit. This means that some field types, such as `search_as_you_type` or `percolator` count as more than one field even though that's not apparent to users as they're just defining them as a single field in the mapping. This change makes it so that each field mapper only counts as one. We're still counting multi-fields. This makes it easier to understand for users why the field limit is hit. ~In addition to that, it also simplifies #96235 as it makes the implementation of `Mapper.Builder#getTotalFieldsCount` much easier and easier to align with `Mapper#getTotalFieldsCount`. This reduces the risk of over- or under-estimating the field count of a `Mapper.Builder` in `DocumentParserContext#addDynamicMapper`, which in turn reduces the risk of data loss due to the issue described here: #96235 (comment) *Edit: due to #103865, we don't need an implementation of `getTotalFieldsCount` or `mapperSize` in `Mapper.Builder`. Still, this PR more closely aligns `Mapper#getTotalFieldsCount` with `MappingLookup#getTotalFieldsCount`, which `DocumentParserContext#addDynamicMapper` uses to determine whether the field limit is hit* A potential risk of this is that we're now effectively allowing more fields in the mapping. It may be surprising to users that more fields can be added to a mapping. Although, I'd not expect negative consequences from that. Generally, I'd expect users to be happy about any change that reduces the risk of data loss. We could also think about whether to apply the new counting logic only to new indices (depending on the `IndexVersion`). However, that would add more complexity and I'm not convinced about the value. We'd then need to maintain two different ways of counting fields and also require passing in the `IndexVersion` to `MappingLookup` which previously didn't require the `IndexVersion`. This PR is meant as a conversation starter. It would also simplify #96235 but I don't think this blocks that PR in any way. I'm curious about the opinion of @javanna and @jpountz on this.
1 parent bfa21b5 commit ff0f83f

File tree

15 files changed

+83
-48
lines changed

15 files changed

+83
-48
lines changed

docs/changelog/102885.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 102885
2+
summary: Make field limit more predictable
3+
area: Mapping
4+
type: enhancement
5+
issues: []

modules/legacy-geo/src/test/java/org/elasticsearch/legacygeo/mapper/LegacyGeoShapeFieldMapperTests.java

+6
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ protected boolean supportsStoredFields() {
7676
return false;
7777
}
7878

79+
@Override
80+
public void testTotalFieldsCount() throws IOException {
81+
super.testTotalFieldsCount();
82+
assertWarnings("Parameter [strategy] is deprecated and will be removed in a future version");
83+
}
84+
7985
@Override
8086
protected void registerParameters(ParameterChecker checker) throws IOException {
8187

server/src/main/java/org/elasticsearch/index/IndexService.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ public NodeMappingStats getNodeMappingStats() {
330330
if (mapperService == null) {
331331
return null;
332332
}
333-
long totalCount = mapperService().mappingLookup().getTotalFieldsCount();
333+
long totalCount = mapperService().mappingLookup().getTotalMapperCount();
334334
long totalEstimatedOverhead = totalCount * 1024L; // 1KiB estimated per mapping
335335
NodeMappingStats indexNodeMappingStats = new NodeMappingStats(totalCount, totalEstimatedOverhead);
336336
return indexNodeMappingStats;

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ public final boolean addDynamicMapper(Mapper mapper) {
335335
if (mappingLookup.getMapper(mapper.name()) == null
336336
&& mappingLookup.objectMappers().containsKey(mapper.name()) == false
337337
&& dynamicMappers.containsKey(mapper.name()) == false) {
338-
int mapperSize = mapper.mapperSize();
338+
int mapperSize = mapper.getTotalFieldsCount();
339339
int additionalFieldsToAdd = getNewFieldsSize() + mapperSize;
340340
if (indexSettings().isIgnoreDynamicFieldsBeyondLimit()) {
341341
if (mappingLookup.exceedsLimit(indexSettings().getMappingTotalFieldsLimit(), additionalFieldsToAdd)) {

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

+5
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ public void validate(MappingLookup mappers) {
113113
}
114114
}
115115

116+
@Override
117+
public int getTotalFieldsCount() {
118+
return 1;
119+
}
120+
116121
public static class TypeParser implements Mapper.TypeParser {
117122
@Override
118123
public Mapper.Builder parse(String name, Map<String, Object> node, MappingParserContext parserContext)

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import java.util.function.Consumer;
5252
import java.util.function.Function;
5353
import java.util.function.Supplier;
54+
import java.util.stream.Stream;
5455

5556
import static org.elasticsearch.core.Strings.format;
5657

@@ -428,6 +429,11 @@ protected void doXContentBody(XContentBuilder builder, Params params) throws IOE
428429

429430
protected abstract String contentType();
430431

432+
@Override
433+
public int getTotalFieldsCount() {
434+
return 1 + Stream.of(multiFields.mappers).mapToInt(FieldMapper::getTotalFieldsCount).sum();
435+
}
436+
431437
public Map<String, NamedAnalyzer> indexAnalyzers() {
432438
return Map.of();
433439
}
@@ -455,7 +461,7 @@ private void add(FieldMapper mapper) {
455461

456462
private void update(FieldMapper toMerge, MapperMergeContext context) {
457463
if (mapperBuilders.containsKey(toMerge.simpleName()) == false) {
458-
if (context.decrementFieldBudgetIfPossible(toMerge.mapperSize())) {
464+
if (context.decrementFieldBudgetIfPossible(toMerge.getTotalFieldsCount())) {
459465
add(toMerge);
460466
}
461467
} else {

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

+3-11
Original file line numberDiff line numberDiff line change
@@ -137,16 +137,8 @@ public static FieldType freezeAndDeduplicateFieldType(FieldType fieldType) {
137137
}
138138

139139
/**
140-
* Returns the size this mapper counts against the {@linkplain MapperService#INDEX_MAPPING_TOTAL_FIELDS_LIMIT_SETTING field limit}.
141-
* <p>
142-
* Needs to be in sync with {@link MappingLookup#getTotalFieldsCount()}.
140+
* The total number of fields as defined in the mapping.
141+
* Defines how this mapper counts towards {@link MapperService#INDEX_MAPPING_TOTAL_FIELDS_LIMIT_SETTING}.
143142
*/
144-
public int mapperSize() {
145-
int size = 1;
146-
for (Mapper mapper : this) {
147-
size += mapper.mapperSize();
148-
}
149-
return size;
150-
}
151-
143+
public abstract int getTotalFieldsCount();
152144
}

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

+11-1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ private CacheKey() {}
5555
private final List<FieldMapper> indexTimeScriptMappers;
5656
private final Mapping mapping;
5757
private final Set<String> completionFields;
58+
private final int totalFieldsCount;
5859

5960
/**
6061
* Creates a new {@link MappingLookup} instance by parsing the provided mapping and extracting its field definitions.
@@ -127,6 +128,7 @@ private MappingLookup(
127128
Collection<ObjectMapper> objectMappers,
128129
Collection<FieldAliasMapper> aliasMappers
129130
) {
131+
this.totalFieldsCount = mapping.getRoot().getTotalFieldsCount();
130132
this.mapping = mapping;
131133
Map<String, Mapper> fieldMappers = new HashMap<>();
132134
Map<String, ObjectMapper> objects = new HashMap<>();
@@ -223,6 +225,14 @@ FieldTypeLookup fieldTypesLookup() {
223225
* Returns the total number of fields defined in the mappings, including field mappers, object mappers as well as runtime fields.
224226
*/
225227
public long getTotalFieldsCount() {
228+
return totalFieldsCount;
229+
}
230+
231+
/**
232+
* Returns the total number of mappers defined in the mappings, including field mappers and their sub-fields
233+
* (which are not explicitly defined in the mappings), multi-fields, object mappers, runtime fields and metadata field mappers.
234+
*/
235+
public long getTotalMapperCount() {
226236
return fieldMappers.size() + objectMappers.size() + runtimeFieldMappersCount;
227237
}
228238

@@ -286,7 +296,7 @@ boolean exceedsLimit(long limit, int additionalFieldsToAdd) {
286296
}
287297

288298
long remainingFieldsUntilLimit(long mappingTotalFieldsLimit) {
289-
return mappingTotalFieldsLimit - getTotalFieldsCount() + mapping.getSortedMetadataMappers().length;
299+
return mappingTotalFieldsLimit - totalFieldsCount;
290300
}
291301

292302
private void checkDimensionFieldLimit(long limit) {

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

+7-2
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@ public ObjectMapper build(MapperBuilderContext context) {
182182
}
183183
}
184184

185+
@Override
186+
public int getTotalFieldsCount() {
187+
return 1 + mappers.values().stream().mapToInt(Mapper::getTotalFieldsCount).sum();
188+
}
189+
185190
public static class TypeParser implements Mapper.TypeParser {
186191
@Override
187192
public boolean supportsVersion(IndexVersion indexCreatedVersion) {
@@ -547,7 +552,7 @@ private static Map<String, Mapper> buildMergedMappers(
547552
Mapper mergeIntoMapper = mergedMappers.get(mergeWithMapper.simpleName());
548553
Mapper merged = null;
549554
if (mergeIntoMapper == null) {
550-
if (objectMergeContext.decrementFieldBudgetIfPossible(mergeWithMapper.mapperSize())) {
555+
if (objectMergeContext.decrementFieldBudgetIfPossible(mergeWithMapper.getTotalFieldsCount())) {
551556
merged = mergeWithMapper;
552557
} else if (mergeWithMapper instanceof ObjectMapper om) {
553558
merged = truncateObjectMapper(reason, objectMergeContext, om);
@@ -581,7 +586,7 @@ private static ObjectMapper truncateObjectMapper(MergeReason reason, MapperMerge
581586
// there's not enough capacity for the whole object mapper,
582587
// so we're just trying to add the shallow object, without it's sub-fields
583588
ObjectMapper shallowObjectMapper = objectMapper.withoutMappers();
584-
if (context.decrementFieldBudgetIfPossible(shallowObjectMapper.mapperSize())) {
589+
if (context.decrementFieldBudgetIfPossible(shallowObjectMapper.getTotalFieldsCount())) {
585590
// now trying to add the sub-fields one by one via a merge, until we hit the limit
586591
return shallowObjectMapper.merge(objectMapper, reason, context);
587592
}

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

+2-6
Original file line numberDiff line numberDiff line change
@@ -576,11 +576,7 @@ private static boolean processField(
576576
}
577577

578578
@Override
579-
public int mapperSize() {
580-
int size = runtimeFields().size();
581-
for (Mapper mapper : this) {
582-
size += mapper.mapperSize();
583-
}
584-
return size;
579+
public int getTotalFieldsCount() {
580+
return mappers.values().stream().mapToInt(Mapper::getTotalFieldsCount).sum() + runtimeFields.size();
585581
}
586582
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public void testParsing() throws IOException {
3434
);
3535
DocumentMapper mapper = createDocumentMapper(mapping);
3636
assertEquals(mapping, mapper.mappingSource().toString());
37-
assertEquals(2, mapper.mapping().getRoot().mapperSize());
37+
assertEquals(2, mapper.mapping().getRoot().getTotalFieldsCount());
3838
}
3939

4040
public void testParsingWithMissingPath() {

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

+20-20
Original file line numberDiff line numberDiff line change
@@ -213,10 +213,10 @@ public void testMergeWithLimit() {
213213
final ObjectMapper mergedAdd1 = rootObjectMapper.merge(mergeWith, MapperMergeContext.root(false, false, 1));
214214

215215
// THEN "baz" new field is added to merged mapping
216-
assertEquals(3, rootObjectMapper.mapperSize());
217-
assertEquals(4, mergeWith.mapperSize());
218-
assertEquals(3, mergedAdd0.mapperSize());
219-
assertEquals(4, mergedAdd1.mapperSize());
216+
assertEquals(3, rootObjectMapper.getTotalFieldsCount());
217+
assertEquals(4, mergeWith.getTotalFieldsCount());
218+
assertEquals(3, mergedAdd0.getTotalFieldsCount());
219+
assertEquals(4, mergedAdd1.getTotalFieldsCount());
220220
}
221221

222222
public void testMergeWithLimitTruncatedObjectField() {
@@ -231,11 +231,11 @@ public void testMergeWithLimitTruncatedObjectField() {
231231
ObjectMapper mergedAdd1 = root.merge(mergeWith, MapperMergeContext.root(false, false, 1));
232232
ObjectMapper mergedAdd2 = root.merge(mergeWith, MapperMergeContext.root(false, false, 2));
233233
ObjectMapper mergedAdd3 = root.merge(mergeWith, MapperMergeContext.root(false, false, 3));
234-
assertEquals(0, root.mapperSize());
235-
assertEquals(0, mergedAdd0.mapperSize());
236-
assertEquals(1, mergedAdd1.mapperSize());
237-
assertEquals(2, mergedAdd2.mapperSize());
238-
assertEquals(3, mergedAdd3.mapperSize());
234+
assertEquals(0, root.getTotalFieldsCount());
235+
assertEquals(0, mergedAdd0.getTotalFieldsCount());
236+
assertEquals(1, mergedAdd1.getTotalFieldsCount());
237+
assertEquals(2, mergedAdd2.getTotalFieldsCount());
238+
assertEquals(3, mergedAdd3.getTotalFieldsCount());
239239

240240
ObjectMapper parent1 = (ObjectMapper) mergedAdd1.getMapper("parent");
241241
assertNull(parent1.getMapper("child1"));
@@ -262,9 +262,9 @@ public void testMergeSameObjectDifferentFields() {
262262

263263
ObjectMapper mergedAdd0 = root.merge(mergeWith, MapperMergeContext.root(false, false, 0));
264264
ObjectMapper mergedAdd1 = root.merge(mergeWith, MapperMergeContext.root(false, false, 1));
265-
assertEquals(2, root.mapperSize());
266-
assertEquals(2, mergedAdd0.mapperSize());
267-
assertEquals(3, mergedAdd1.mapperSize());
265+
assertEquals(2, root.getTotalFieldsCount());
266+
assertEquals(2, mergedAdd0.getTotalFieldsCount());
267+
assertEquals(3, mergedAdd1.getTotalFieldsCount());
268268

269269
ObjectMapper parent0 = (ObjectMapper) mergedAdd0.getMapper("parent");
270270
assertNotNull(parent0.getMapper("child1"));
@@ -285,13 +285,13 @@ public void testMergeWithLimitMultiField() {
285285
createTextKeywordMultiField("text", "keyword2")
286286
).build(MapperBuilderContext.root(false, false));
287287

288-
assertEquals(2, mergeInto.mapperSize());
289-
assertEquals(2, mergeWith.mapperSize());
288+
assertEquals(2, mergeInto.getTotalFieldsCount());
289+
assertEquals(2, mergeWith.getTotalFieldsCount());
290290

291291
ObjectMapper mergedAdd0 = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, 0));
292292
ObjectMapper mergedAdd1 = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, 1));
293-
assertEquals(2, mergedAdd0.mapperSize());
294-
assertEquals(3, mergedAdd1.mapperSize());
293+
assertEquals(2, mergedAdd0.getTotalFieldsCount());
294+
assertEquals(3, mergedAdd1.getTotalFieldsCount());
295295
}
296296

297297
public void testMergeWithLimitRuntimeField() {
@@ -302,13 +302,13 @@ public void testMergeWithLimitRuntimeField() {
302302
new TestRuntimeField("existing_runtime_field", "keyword")
303303
).addRuntimeField(new TestRuntimeField("new_runtime_field", "keyword")).build(MapperBuilderContext.root(false, false));
304304

305-
assertEquals(3, mergeInto.mapperSize());
306-
assertEquals(2, mergeWith.mapperSize());
305+
assertEquals(3, mergeInto.getTotalFieldsCount());
306+
assertEquals(2, mergeWith.getTotalFieldsCount());
307307

308308
ObjectMapper mergedAdd0 = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, 0));
309309
ObjectMapper mergedAdd1 = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, 1));
310-
assertEquals(3, mergedAdd0.mapperSize());
311-
assertEquals(4, mergedAdd1.mapperSize());
310+
assertEquals(3, mergedAdd0.getTotalFieldsCount());
311+
assertEquals(4, mergedAdd1.getTotalFieldsCount());
312312
}
313313

314314
private static RootObjectMapper createRootSubobjectFalseLeafWithDots() {

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

+8-3
Original file line numberDiff line numberDiff line change
@@ -530,15 +530,20 @@ public void testSyntheticSourceDocValuesFieldWithout() throws IOException {
530530
assertThat(mapper.mapping().getRoot().syntheticFieldLoader().docValuesLoader(null, null), nullValue());
531531
}
532532

533-
public void testNestedObjectWithMultiFieldsMapperSize() throws IOException {
533+
public void testNestedObjectWithMultiFieldsgetTotalFieldsCount() {
534534
ObjectMapper.Builder mapperBuilder = new ObjectMapper.Builder("parent_size_1", Explicit.IMPLICIT_TRUE).add(
535535
new ObjectMapper.Builder("child_size_2", Explicit.IMPLICIT_TRUE).add(
536536
new TextFieldMapper.Builder("grand_child_size_3", createDefaultIndexAnalyzers()).addMultiField(
537537
new KeywordFieldMapper.Builder("multi_field_size_4", IndexVersion.current())
538-
).addMultiField(new KeywordFieldMapper.Builder("multi_field_size_5", IndexVersion.current()))
538+
)
539+
.addMultiField(
540+
new TextFieldMapper.Builder("grand_child_size_5", createDefaultIndexAnalyzers()).addMultiField(
541+
new KeywordFieldMapper.Builder("multi_field_of_multi_field_size_6", IndexVersion.current())
542+
)
543+
)
539544
)
540545
);
541-
assertThat(mapperBuilder.build(MapperBuilderContext.root(false, false)).mapperSize(), equalTo(5));
546+
assertThat(mapperBuilder.build(MapperBuilderContext.root(false, false)).getTotalFieldsCount(), equalTo(6));
542547
}
543548

544549
public void testWithoutMappers() throws IOException {

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ public void testRuntimeSection() throws IOException {
166166
}));
167167
MapperService mapperService = createMapperService(mapping);
168168
assertEquals(mapping, mapperService.documentMapper().mappingSource().toString());
169-
assertEquals(3, mapperService.documentMapper().mapping().getRoot().mapperSize());
169+
assertEquals(3, mapperService.documentMapper().mapping().getRoot().getTotalFieldsCount());
170170
}
171171

172172
public void testRuntimeSectionRejectedUpdate() throws IOException {

test/framework/src/main/java/org/elasticsearch/index/mapper/MapperTestCase.java

+5
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,11 @@ public final void testMinimalToMaximal() throws IOException {
427427
assertParseMaximalWarnings();
428428
}
429429

430+
public void testTotalFieldsCount() throws IOException {
431+
MapperService mapperService = createMapperService(fieldMapping(this::minimalMapping));
432+
assertEquals(1, mapperService.documentMapper().mapping().getRoot().getTotalFieldsCount());
433+
}
434+
430435
protected final void assertParseMinimalWarnings() {
431436
String[] warnings = getParseMinimalWarnings();
432437
if (warnings.length > 0) {

0 commit comments

Comments
 (0)