Explorar o código

Speedup XContent Serialization of Settings and Mappers (#82766)

In many-shards benchmarks serializing mappers and settings
becomes fairly prominent during batched index creation or
setting updates.
Both mapping and setting serialization spent most of their
time on `org.elasticsearch.xcontent.XContentBuilder#unknownValue`
figuring out write type to serialize.

This commit makes it so the mapper parameters get serialized
by typed serializers (the generic XContentBuilder::field default we used will always
link to `org.elasticsearch.xcontent.XContentBuilder#field(java.lang.String, java.lang.Object)`
which is needlessly slow here when we know the type in the callsite creating the parameter
instance).
Also, for settings I added some educated guesses on the types expected that
cover most real world scenarios (for the non-flat case it's probably all scenarios
except for `null` setting values and that's the case that matters).
Armin Braun %!s(int64=3) %!d(string=hai) anos
pai
achega
2b3e41b1a3
Modificáronse 27 ficheiros con 276 adicións e 111 borrados
  1. 26 3
      libs/x-content/src/main/java/org/elasticsearch/xcontent/XContentBuilder.java
  2. 34 27
      modules/legacy-geo/src/main/java/org/elasticsearch/legacygeo/mapper/LegacyGeoShapeFieldMapper.java
  3. 8 2
      modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/ScaledFloatFieldMapper.java
  4. 5 1
      modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/TokenCountFieldMapper.java
  5. 4 1
      modules/parent-join/src/main/java/org/elasticsearch/join/mapper/ParentJoinFieldMapper.java
  6. 37 6
      server/src/main/java/org/elasticsearch/common/settings/Settings.java
  7. 4 2
      server/src/main/java/org/elasticsearch/index/mapper/AbstractPointGeometryFieldMapper.java
  8. 4 1
      server/src/main/java/org/elasticsearch/index/mapper/AbstractScriptFieldType.java
  9. 4 2
      server/src/main/java/org/elasticsearch/index/mapper/AbstractShapeGeometryFieldMapper.java
  10. 4 1
      server/src/main/java/org/elasticsearch/index/mapper/BooleanFieldMapper.java
  11. 11 9
      server/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java
  12. 7 2
      server/src/main/java/org/elasticsearch/index/mapper/CompositeRuntimeField.java
  13. 3 1
      server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java
  14. 14 11
      server/src/main/java/org/elasticsearch/index/mapper/DateScriptFieldType.java
  15. 61 23
      server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java
  16. 3 1
      server/src/main/java/org/elasticsearch/index/mapper/GeoPointFieldMapper.java
  17. 4 2
      server/src/main/java/org/elasticsearch/index/mapper/MetadataFieldMapper.java
  18. 4 1
      server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java
  19. 3 1
      server/src/main/java/org/elasticsearch/index/mapper/RangeFieldMapper.java
  20. 6 2
      server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java
  21. 6 1
      server/src/main/java/org/elasticsearch/index/mapper/TextParams.java
  22. 1 1
      server/src/test/java/org/elasticsearch/index/mapper/ParametrizedMapperTests.java
  23. 7 5
      x-pack/plugin/mapper-aggregate-metric/src/main/java/org/elasticsearch/xpack/aggregatemetric/mapper/AggregateDoubleMetricFieldMapper.java
  24. 2 1
      x-pack/plugin/mapper-constant-keyword/src/main/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldMapper.java
  25. 5 1
      x-pack/plugin/mapper-unsigned-long/src/main/java/org/elasticsearch/xpack/unsignedlong/UnsignedLongFieldMapper.java
  26. 3 1
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapper.java
  27. 6 2
      x-pack/plugin/vectors/src/main/java/org/elasticsearch/xpack/vectors/mapper/DenseVectorFieldMapper.java

+ 26 - 3
libs/x-content/src/main/java/org/elasticsearch/xcontent/XContentBuilder.java

@@ -157,9 +157,9 @@ public final class XContentBuilder implements Closeable, Flushable {
             dateTransformers.putAll(addlDateTransformers);
         }
 
-        WRITERS = Collections.unmodifiableMap(writers);
-        HUMAN_READABLE_TRANSFORMERS = Collections.unmodifiableMap(humanReadableTransformer);
-        DATE_TRANSFORMERS = Collections.unmodifiableMap(dateTransformers);
+        WRITERS = Map.copyOf(writers);
+        HUMAN_READABLE_TRANSFORMERS = Map.copyOf(humanReadableTransformer);
+        DATE_TRANSFORMERS = Map.copyOf(dateTransformers);
     }
 
     @FunctionalInterface
@@ -867,6 +867,29 @@ public final class XContentBuilder implements Closeable, Flushable {
         return field(name).value(value);
     }
 
+    public XContentBuilder field(String name, Number value) throws IOException {
+        field(name);
+        if (value instanceof Short) {
+            return value(value.shortValue());
+        } else if (value instanceof Integer) {
+            return value(value.intValue());
+        } else if (value instanceof Long) {
+            return value(value.longValue());
+        } else if (value instanceof Float) {
+            return value(value.floatValue());
+        } else if (value instanceof Double) {
+            return value(value.doubleValue());
+        } else if (value instanceof BigInteger) {
+            generator.writeNumber((BigInteger) value);
+            return this;
+        } else if (value instanceof BigDecimal) {
+            generator.writeNumber((BigDecimal) value);
+            return this;
+        } else {
+            return value(value);
+        }
+    }
+
     public XContentBuilder array(String name, Object... values) throws IOException {
         return field(name).values(values, true);
     }

+ 34 - 27
modules/legacy-geo/src/main/java/org/elasticsearch/legacygeo/mapper/LegacyGeoShapeFieldMapper.java

@@ -42,6 +42,7 @@ import org.elasticsearch.legacygeo.XShapeCollection;
 import org.elasticsearch.legacygeo.builders.ShapeBuilder;
 import org.elasticsearch.legacygeo.parsers.ShapeParser;
 import org.elasticsearch.legacygeo.query.LegacyGeoShapeQueryProcessor;
+import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xcontent.XContentParser;
 import org.locationtech.spatial4j.shape.Point;
 import org.locationtech.spatial4j.shape.Shape;
@@ -155,7 +156,9 @@ public class LegacyGeoShapeFieldMapper extends AbstractShapeGeometryFieldMapper<
             false,
             () -> SpatialStrategy.RECURSIVE,
             (n, c, o) -> SpatialStrategy.fromString(o.toString()),
-            m -> builder(m).strategy.get()
+            m -> builder(m).strategy.get(),
+            (b, f, v) -> b.field(f, v.getStrategyName()),
+            SpatialStrategy::getStrategyName
         ).deprecated();
         Parameter<String> tree = Parameter.stringParam("tree", false, m -> builder(m).tree.get(), Defaults.TREE).deprecated();
         Parameter<Integer> treeLevels = new Parameter<>(
@@ -163,28 +166,54 @@ public class LegacyGeoShapeFieldMapper extends AbstractShapeGeometryFieldMapper<
             false,
             () -> null,
             (n, c, o) -> o == null ? null : XContentMapValues.nodeIntegerValue(o),
-            m -> builder(m).treeLevels.get()
+            m -> builder(m).treeLevels.get(),
+            (b, f, v) -> {
+                if (v != null && v != 0) {
+                    b.field(f, v);
+                } else {
+                    b.field(f, Defaults.defaultTreeLevel(tree.get()));
+                }
+            },
+            Objects::toString
         ).deprecated();
         Parameter<DistanceUnit.Distance> precision = new Parameter<>(
             "precision",
             false,
             () -> null,
             (n, c, o) -> o == null ? null : DistanceUnit.Distance.parseDistance(o.toString()),
-            m -> builder(m).precision.get()
+            m -> builder(m).precision.get(),
+            (b, f, v) -> {
+                if (v == null) {
+                    b.field(f, "50.0m");
+                } else {
+                    b.field(f, v.toString());
+                }
+            },
+            Objects::toString
         ).deprecated();
         Parameter<Double> distanceErrorPct = new Parameter<>(
             "distance_error_pct",
             true,
             () -> null,
             (n, c, o) -> o == null ? null : XContentMapValues.nodeDoubleValue(o),
-            m -> builder(m).distanceErrorPct.get()
+            m -> builder(m).distanceErrorPct.get(),
+            XContentBuilder::field,
+            Objects::toString
         ).deprecated().acceptsNull();
         Parameter<Boolean> pointsOnly = new Parameter<>(
             "points_only",
             false,
             () -> null,
             (n, c, o) -> XContentMapValues.nodeBooleanValue(o),
-            m -> builder(m).pointsOnly.get()
+            m -> builder(m).pointsOnly.get(),
+            (b, f, v) -> {
+                if (v == null) {
+                    b.field(f, strategy.get() == SpatialStrategy.TERM);
+                } else {
+                    b.field(f, v);
+                }
+            },
+            Objects::toString
         ).deprecated().acceptsNull();
 
         Parameter<Map<String, String>> meta = Parameter.metaParam();
@@ -215,32 +244,10 @@ public class LegacyGeoShapeFieldMapper extends AbstractShapeGeometryFieldMapper<
             if (version.onOrAfter(Version.V_7_0_0)) {
                 this.strategy.alwaysSerialize();
             }
-            this.strategy.setSerializer((b, f, v) -> b.field(f, v.getStrategyName()), SpatialStrategy::getStrategyName);
             // serialize treeLevels if treeLevels is configured, OR if defaults are requested and precision is not configured
             treeLevels.setSerializerCheck((id, ic, v) -> ic || (id && precision.get() == null));
-            treeLevels.setSerializer((b, f, v) -> {
-                if (v != null && v != 0) {
-                    b.field(f, v);
-                } else {
-                    b.field(f, Defaults.defaultTreeLevel(tree.get()));
-                }
-            }, Objects::toString);
             // serialize precision if precision is configured, OR if defaults are requested and treeLevels is not configured
             precision.setSerializerCheck((id, ic, v) -> ic || (id && treeLevels.get() == null));
-            precision.setSerializer((b, f, v) -> {
-                if (v == null) {
-                    b.field(f, "50.0m");
-                } else {
-                    b.field(f, v.toString());
-                }
-            }, Objects::toString);
-            pointsOnly.setSerializer((b, f, v) -> {
-                if (v == null) {
-                    b.field(f, strategy.get() == SpatialStrategy.TERM);
-                } else {
-                    b.field(f, v);
-                }
-            }, Objects::toString);
         }
 
         @Override

+ 8 - 2
modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/ScaledFloatFieldMapper.java

@@ -44,6 +44,7 @@ import org.elasticsearch.script.field.ToScriptField;
 import org.elasticsearch.search.DocValueFormat;
 import org.elasticsearch.search.aggregations.support.ValuesSourceType;
 import org.elasticsearch.search.lookup.SearchLookup;
+import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xcontent.XContentParser;
 import org.elasticsearch.xcontent.XContentParser.Token;
 
@@ -55,6 +56,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.function.Supplier;
 
 /** A {@link FieldMapper} for scaled floats. Values are internally multiplied
@@ -84,7 +86,9 @@ public class ScaledFloatFieldMapper extends FieldMapper {
             false,
             () -> null,
             (n, c, o) -> XContentMapValues.nodeDoubleValue(o),
-            m -> toType(m).scalingFactor
+            m -> toType(m).scalingFactor,
+            XContentBuilder::field,
+            Objects::toString
         ).addValidator(v -> {
             if (v == null) {
                 throw new IllegalArgumentException("Field [scaling_factor] is required");
@@ -98,7 +102,9 @@ public class ScaledFloatFieldMapper extends FieldMapper {
             false,
             () -> null,
             (n, c, o) -> o == null ? null : XContentMapValues.nodeDoubleValue(o),
-            m -> toType(m).nullValue
+            m -> toType(m).nullValue,
+            XContentBuilder::field,
+            Objects::toString
         ).acceptsNull();
 
         private final Parameter<Map<String, String>> meta = Parameter.metaParam();

+ 5 - 1
modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/TokenCountFieldMapper.java

@@ -21,11 +21,13 @@ import org.elasticsearch.index.mapper.MapperParsingException;
 import org.elasticsearch.index.mapper.NumberFieldMapper;
 import org.elasticsearch.index.mapper.ValueFetcher;
 import org.elasticsearch.index.query.SearchExecutionContext;
+import org.elasticsearch.xcontent.XContentBuilder;
 
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 import static org.elasticsearch.common.xcontent.support.XContentMapValues.nodeIntegerValue;
 
@@ -52,7 +54,9 @@ public class TokenCountFieldMapper extends FieldMapper {
             false,
             () -> null,
             (n, c, o) -> o == null ? null : nodeIntegerValue(o),
-            m -> toType(m).nullValue
+            m -> toType(m).nullValue,
+            XContentBuilder::field,
+            Objects::toString
         ).acceptsNull();
         private final Parameter<Boolean> enablePositionIncrements = Parameter.boolParam(
             "enable_position_increments",

+ 4 - 1
modules/parent-join/src/main/java/org/elasticsearch/join/mapper/ParentJoinFieldMapper.java

@@ -44,6 +44,7 @@ import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
@@ -100,7 +101,9 @@ public final class ParentJoinFieldMapper extends FieldMapper {
             true,
             Collections::emptyList,
             (n, c, o) -> Relations.parse(o),
-            m -> toType(m).relations
+            m -> toType(m).relations,
+            XContentBuilder::field,
+            Objects::toString
         ).setMergeValidator(ParentJoinFieldMapper::checkRelationsConflicts);
 
         final Parameter<Map<String, String>> meta = Parameter.metaParam();

+ 37 - 6
server/src/main/java/org/elasticsearch/common/settings/Settings.java

@@ -615,17 +615,48 @@ public final class Settings implements ToXContentFragment {
     public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
         Settings settings = SettingsFilter.filterSettings(params, this);
         if (params.paramAsBoolean("flat_settings", false) == false) {
-            for (Map.Entry<String, Object> entry : settings.getAsStructuredMap().entrySet()) {
-                builder.field(entry.getKey(), entry.getValue());
-            }
+            toXContentFlat(builder, settings);
         } else {
-            for (Map.Entry<String, Object> entry : settings.settings.entrySet()) {
-                builder.field(entry.getKey(), entry.getValue());
-            }
+            toXContent(builder, settings);
         }
         return builder;
     }
 
+    @SuppressWarnings("unchecked")
+    private static void toXContent(XContentBuilder builder, Settings settings) throws IOException {
+        for (Map.Entry<String, Object> entry : settings.settings.entrySet()) {
+            final Object value = entry.getValue();
+            final String key = entry.getKey();
+            if (value instanceof String) {
+                // most setting values are string
+                builder.field(key, (String) value);
+            } else if (value instanceof List) {
+                // all setting lists are lists of String so we can save the expensive type detection in the builder
+                builder.stringListField(key, (List<String>) value);
+            } else {
+                // this should be rare, let the builder figure out the type
+                builder.field(key, value);
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private void toXContentFlat(XContentBuilder builder, Settings settings) throws IOException {
+        for (Map.Entry<String, Object> entry : settings.getAsStructuredMap().entrySet()) {
+            final Object value = entry.getValue();
+            final String key = entry.getKey();
+            if (value instanceof String) {
+                builder.field(key, (String) value);
+            } else if (value instanceof Map) {
+                // lots of maps in flattened settings so far cheaper to check here then to have the XContent builder figure out the type
+                builder.field(key).map((Map<String, ?>) value);
+            } else {
+                // this should be rare, let the builder figure out the type
+                builder.field(key, value);
+            }
+        }
+    }
+
     /**
      * Parsers the generated xcontent from {@link Settings#toXContent(XContentBuilder, Params)} into a new Settings object.
      * Note this method requires the parser to either be positioned on a null token or on

+ 4 - 2
server/src/main/java/org/elasticsearch/index/mapper/AbstractPointGeometryFieldMapper.java

@@ -15,6 +15,7 @@ import org.elasticsearch.core.CheckedConsumer;
 import org.elasticsearch.xcontent.XContentParser;
 
 import java.io.IOException;
+import java.util.Objects;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Supplier;
@@ -25,9 +26,10 @@ public abstract class AbstractPointGeometryFieldMapper<T> extends AbstractGeomet
     public static <T> Parameter<T> nullValueParam(
         Function<FieldMapper, T> initializer,
         TriFunction<String, MappingParserContext, Object, T> parser,
-        Supplier<T> def
+        Supplier<T> def,
+        Serializer<T> serializer
     ) {
-        return new Parameter<T>("null_value", false, def, parser, initializer);
+        return new Parameter<T>("null_value", false, def, parser, initializer, serializer, Objects::toString);
     }
 
     protected final T nullValue;

+ 4 - 1
server/src/main/java/org/elasticsearch/index/mapper/AbstractScriptFieldType.java

@@ -22,6 +22,7 @@ import org.elasticsearch.script.CompositeFieldScript;
 import org.elasticsearch.script.Script;
 import org.elasticsearch.script.ScriptContext;
 import org.elasticsearch.search.lookup.SearchLookup;
+import org.elasticsearch.xcontent.XContentBuilder;
 
 import java.time.ZoneId;
 import java.util.ArrayList;
@@ -218,7 +219,9 @@ abstract class AbstractScriptFieldType<LeafFactory> extends MappedFieldType {
             true,
             () -> null,
             RuntimeField::parseScript,
-            RuntimeField.initializerNotSupported()
+            RuntimeField.initializerNotSupported(),
+            XContentBuilder::field,
+            Objects::toString
         ).setSerializerCheck((id, ic, v) -> ic);
 
         Builder(String name, ScriptContext<Factory> scriptContext) {

+ 4 - 2
server/src/main/java/org/elasticsearch/index/mapper/AbstractShapeGeometryFieldMapper.java

@@ -32,8 +32,10 @@ public abstract class AbstractShapeGeometryFieldMapper<T> extends AbstractGeomet
             true,
             () -> IMPLICIT_RIGHT,
             (n, c, o) -> new Explicit<>(Orientation.fromString(o.toString()), true),
-            initializer
-        ).setSerializer((b, f, v) -> b.field(f, v.value()), v -> v.value().toString());
+            initializer,
+            (b, f, v) -> b.field(f, v.value()),
+            v -> v.value().toString()
+        );
     }
 
     public abstract static class AbstractShapeGeometryFieldType<T> extends AbstractGeometryFieldType<T> {

+ 4 - 1
server/src/main/java/org/elasticsearch/index/mapper/BooleanFieldMapper.java

@@ -32,6 +32,7 @@ import org.elasticsearch.script.field.BooleanDocValuesField;
 import org.elasticsearch.search.DocValueFormat;
 import org.elasticsearch.search.lookup.FieldValues;
 import org.elasticsearch.search.lookup.SearchLookup;
+import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xcontent.XContentParser;
 
 import java.io.IOException;
@@ -80,7 +81,9 @@ public class BooleanFieldMapper extends FieldMapper {
             false,
             () -> null,
             (n, c, o) -> o == null ? null : XContentMapValues.nodeBooleanValue(o),
-            m -> toType(m).nullValue
+            m -> toType(m).nullValue,
+            XContentBuilder::field,
+            Objects::toString
         ).acceptsNull();
 
         private final Parameter<Script> script = Parameter.scriptParam(m -> toType(m).script);

+ 11 - 9
server/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java

@@ -131,15 +131,17 @@ public class CompletionFieldMapper extends FieldMapper {
             false,
             () -> null,
             (n, c, o) -> ContextMappings.load(o),
-            m -> builder(m).contexts.get()
-        ).setSerializer((b, n, c) -> {
-            if (c == null) {
-                return;
-            }
-            b.startArray(n);
-            c.toXContent(b, ToXContent.EMPTY_PARAMS);
-            b.endArray();
-        }, Objects::toString);
+            m -> builder(m).contexts.get(),
+            (b, n, c) -> {
+                if (c == null) {
+                    return;
+                }
+                b.startArray(n);
+                c.toXContent(b, ToXContent.EMPTY_PARAMS);
+                b.endArray();
+            },
+            Objects::toString
+        );
         private final Parameter<Integer> maxInputLength = Parameter.intParam(
             "max_input_length",
             true,

+ 7 - 2
server/src/main/java/org/elasticsearch/index/mapper/CompositeRuntimeField.java

@@ -19,6 +19,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.function.Function;
 import java.util.stream.Stream;
 
@@ -36,7 +37,9 @@ public class CompositeRuntimeField implements RuntimeField {
             false,
             () -> null,
             RuntimeField::parseScript,
-            RuntimeField.initializerNotSupported()
+            RuntimeField.initializerNotSupported(),
+            XContentBuilder::field,
+            Objects::toString
         ).addValidator(s -> {
             if (s == null) {
                 throw new IllegalArgumentException("composite runtime field [" + name + "] must declare a [script]");
@@ -48,7 +51,9 @@ public class CompositeRuntimeField implements RuntimeField {
             false,
             Collections::emptyMap,
             (f, p, o) -> parseFields(f, o),
-            RuntimeField.initializerNotSupported()
+            RuntimeField.initializerNotSupported(),
+            XContentBuilder::field,
+            Objects::toString
         ).addValidator(objectMap -> {
             if (objectMap == null || objectMap.isEmpty()) {
                 throw new IllegalArgumentException("composite runtime field [" + name + "] must declare its [fields]");

+ 3 - 1
server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java

@@ -225,7 +225,9 @@ public final class DateFieldMapper extends FieldMapper {
             false,
             () -> Locale.ROOT,
             (n, c, o) -> LocaleUtils.parse(o.toString()),
-            m -> toType(m).locale
+            m -> toType(m).locale,
+            (xContentBuilder, n, v) -> xContentBuilder.field(n, v.toString()),
+            Objects::toString
         );
 
         private final Parameter<String> nullValue = Parameter.stringParam("null_value", false, m -> toType(m).nullValueAsString, null)

+ 14 - 11
server/src/main/java/org/elasticsearch/index/mapper/DateScriptFieldType.java

@@ -53,24 +53,27 @@ public class DateScriptFieldType extends AbstractScriptFieldType<DateFieldScript
             "format",
             true,
             RuntimeField.initializerNotSupported(),
-            null
-        ).setSerializer((b, n, v) -> {
-            if (v != null && false == v.equals(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.pattern())) {
-                b.field(n, v);
+            null,
+            (b, n, v) -> {
+                if (v != null && false == v.equals(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.pattern())) {
+                    b.field(n, v);
+                }
             }
-        }, Object::toString).acceptsNull();
+        ).acceptsNull();
 
         private final FieldMapper.Parameter<Locale> locale = new FieldMapper.Parameter<>(
             "locale",
             true,
             () -> null,
             (n, c, o) -> o == null ? null : LocaleUtils.parse(o.toString()),
-            RuntimeField.initializerNotSupported()
-        ).setSerializer((b, n, v) -> {
-            if (v != null && false == v.equals(Locale.ROOT)) {
-                b.field(n, v.toString());
-            }
-        }, Object::toString).acceptsNull();
+            RuntimeField.initializerNotSupported(),
+            (b, n, v) -> {
+                if (v != null && false == v.equals(Locale.ROOT)) {
+                    b.field(n, v.toString());
+                }
+            },
+            Object::toString
+        ).acceptsNull();
 
         Builder(String name) {
             super(name, DateFieldScript.CONTEXT);

+ 61 - 23
server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java

@@ -637,10 +637,10 @@ public abstract class FieldMapper extends Mapper implements Cloneable {
         private final TriFunction<String, MappingParserContext, Object, T> parser;
         private final Function<FieldMapper, T> initializer;
         private boolean acceptsNull = false;
-        private List<Consumer<T>> validators = new ArrayList<>();
-        private Serializer<T> serializer = XContentBuilder::field;
+        private final List<Consumer<T>> validators = new ArrayList<>();
+        private final Serializer<T> serializer;
         private SerializerCheck<T> serializerCheck = (includeDefaults, isConfigured, value) -> includeDefaults || isConfigured;
-        private Function<T, String> conflictSerializer = Objects::toString;
+        private final Function<T, String> conflictSerializer;
         private boolean deprecated;
         private MergeValidator<T> mergeValidator;
         private T value;
@@ -655,13 +655,20 @@ public abstract class FieldMapper extends Mapper implements Cloneable {
          * @param defaultValue  the default value for the parameter, used if unspecified in mappings
          * @param parser        a function that converts an object to a parameter value
          * @param initializer   a function that reads a parameter value from an existing mapper
+         * @param serializer    a function that serializes a parameter value, prefer type specific x-content generation methods here for
+         *                      good performance as this is used on the hot-path during cluster state updates.
+         *                      This should explicitly not be linked with {@link XContentBuilder#field(String, Object)} by callers through
+         *                      the use of default values or other indirection to this constructor.
+         * @param conflictSerializer a function that serializes a parameter value on conflict
          */
         public Parameter(
             String name,
             boolean updateable,
             Supplier<T> defaultValue,
             TriFunction<String, MappingParserContext, Object, T> parser,
-            Function<FieldMapper, T> initializer
+            Function<FieldMapper, T> initializer,
+            Serializer<T> serializer,
+            Function<T, String> conflictSerializer
         ) {
             this.name = name;
             this.defaultValue = Objects.requireNonNull(defaultValue);
@@ -669,6 +676,8 @@ public abstract class FieldMapper extends Mapper implements Cloneable {
             this.parser = parser;
             this.initializer = initializer;
             this.mergeValidator = (previous, toMerge, conflicts) -> updateable || Objects.equals(previous, toMerge);
+            this.serializer = serializer;
+            this.conflictSerializer = conflictSerializer;
         }
 
         /**
@@ -745,15 +754,6 @@ public abstract class FieldMapper extends Mapper implements Cloneable {
             return this;
         }
 
-        /**
-         * Configure a custom serializer for this parameter
-         */
-        public Parameter<T> setSerializer(Serializer<T> serializer, Function<T, String> conflictSerializer) {
-            this.serializer = serializer;
-            this.conflictSerializer = conflictSerializer;
-            return this;
-        }
-
         /**
          * Configure a custom serialization check for this parameter
          */
@@ -859,7 +859,15 @@ public abstract class FieldMapper extends Mapper implements Cloneable {
             Function<FieldMapper, Boolean> initializer,
             boolean defaultValue
         ) {
-            return new Parameter<>(name, updateable, () -> defaultValue, (n, c, o) -> XContentMapValues.nodeBooleanValue(o), initializer);
+            return new Parameter<>(
+                name,
+                updateable,
+                () -> defaultValue,
+                (n, c, o) -> XContentMapValues.nodeBooleanValue(o),
+                initializer,
+                XContentBuilder::field,
+                Objects::toString
+            );
         }
 
         /**
@@ -881,8 +889,10 @@ public abstract class FieldMapper extends Mapper implements Cloneable {
                 updateable,
                 defaultValue ? () -> Explicit.IMPLICIT_TRUE : () -> Explicit.IMPLICIT_FALSE,
                 (n, c, o) -> Explicit.explicitBoolean(XContentMapValues.nodeBooleanValue(o)),
-                initializer
-            ).setSerializer((b, n, v) -> b.field(n, v.value()), v -> Boolean.toString(v.value()));
+                initializer,
+                (b, n, v) -> b.field(n, v.value()),
+                v -> Boolean.toString(v.value())
+            );
         }
 
         /**
@@ -898,7 +908,15 @@ public abstract class FieldMapper extends Mapper implements Cloneable {
             Function<FieldMapper, Integer> initializer,
             int defaultValue
         ) {
-            return new Parameter<>(name, updateable, () -> defaultValue, (n, c, o) -> XContentMapValues.nodeIntegerValue(o), initializer);
+            return new Parameter<>(
+                name,
+                updateable,
+                () -> defaultValue,
+                (n, c, o) -> XContentMapValues.nodeIntegerValue(o),
+                initializer,
+                XContentBuilder::field,
+                Objects::toString
+            );
         }
 
         /**
@@ -914,7 +932,25 @@ public abstract class FieldMapper extends Mapper implements Cloneable {
             Function<FieldMapper, String> initializer,
             String defaultValue
         ) {
-            return new Parameter<>(name, updateable, () -> defaultValue, (n, c, o) -> XContentMapValues.nodeStringValue(o), initializer);
+            return stringParam(name, updateable, initializer, defaultValue, XContentBuilder::field);
+        }
+
+        public static Parameter<String> stringParam(
+            String name,
+            boolean updateable,
+            Function<FieldMapper, String> initializer,
+            String defaultValue,
+            Serializer<String> serializer
+        ) {
+            return new Parameter<>(
+                name,
+                updateable,
+                () -> defaultValue,
+                (n, c, o) -> XContentMapValues.nodeStringValue(o),
+                initializer,
+                serializer,
+                Function.identity()
+            );
         }
 
         @SuppressWarnings("unchecked")
@@ -931,7 +967,7 @@ public abstract class FieldMapper extends Mapper implements Cloneable {
                     strValues.add(item.toString());
                 }
                 return strValues;
-            }, initializer);
+            }, initializer, XContentBuilder::stringListField, Objects::toString);
         }
 
         /**
@@ -1010,7 +1046,7 @@ public abstract class FieldMapper extends Mapper implements Cloneable {
                 } catch (IllegalArgumentException e) {
                     throw new MapperParsingException("Unknown value [" + o + "] for field [" + name + "] - accepted values are " + values);
                 }
-            }, initializer).addValidator(v -> {
+            }, initializer, XContentBuilder::field, Objects::toString).addValidator(v -> {
                 if (v != null && values.contains(v) == false) {
                     throw new MapperParsingException("Unknown value [" + v + "] for field [" + name + "] - accepted values are " + values);
                 }
@@ -1037,7 +1073,7 @@ public abstract class FieldMapper extends Mapper implements Cloneable {
                     throw new IllegalArgumentException("analyzer [" + analyzerName + "] has not been configured in mappings");
                 }
                 return a;
-            }, initializer).setSerializer((b, n, v) -> b.field(n, v.name()), NamedAnalyzer::name);
+            }, initializer, (b, n, v) -> b.field(n, v.name()), NamedAnalyzer::name);
         }
 
         /**
@@ -1049,7 +1085,9 @@ public abstract class FieldMapper extends Mapper implements Cloneable {
                 true,
                 Collections::emptyMap,
                 (n, c, o) -> TypeParsers.parseMeta(n, o),
-                m -> m.fieldType().meta()
+                m -> m.fieldType().meta(),
+                XContentBuilder::stringStringMap,
+                Objects::toString
             );
         }
 
@@ -1080,7 +1118,7 @@ public abstract class FieldMapper extends Mapper implements Cloneable {
                     throw new IllegalArgumentException("stored scripts are not supported on field [" + n + "]");
                 }
                 return script;
-            }, initializer).acceptsNull();
+            }, initializer, XContentBuilder::field, Objects::toString).acceptsNull();
         }
 
         /**

+ 3 - 1
server/src/main/java/org/elasticsearch/index/mapper/GeoPointFieldMapper.java

@@ -40,6 +40,7 @@ import org.elasticsearch.script.field.GeoPointDocValuesField;
 import org.elasticsearch.search.aggregations.support.CoreValuesSourceType;
 import org.elasticsearch.search.lookup.FieldValues;
 import org.elasticsearch.search.lookup.SearchLookup;
+import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xcontent.XContentParser;
 import org.elasticsearch.xcontent.support.MapXContentParser;
 
@@ -86,7 +87,8 @@ public class GeoPointFieldMapper extends AbstractPointGeometryFieldMapper<GeoPoi
             this.nullValue = nullValueParam(
                 m -> builder(m).nullValue.get(),
                 (n, c, o) -> parseNullValue(o, ignoreZValue.get().value(), ignoreMalformed.get().value()),
-                () -> null
+                () -> null,
+                XContentBuilder::field
             ).acceptsNull();
             this.scriptCompiler = Objects.requireNonNull(scriptCompiler);
             this.script.precludesParameters(nullValue, ignoreMalformed, ignoreZValue);

+ 4 - 2
server/src/main/java/org/elasticsearch/index/mapper/MetadataFieldMapper.java

@@ -57,8 +57,10 @@ public abstract class MetadataFieldMapper extends FieldMapper {
             true,
             defaultValue ? () -> Explicit.IMPLICIT_TRUE : () -> Explicit.IMPLICIT_FALSE,
             (n, c, o) -> Explicit.explicitBoolean(XContentMapValues.nodeBooleanValue(o)),
-            initializer
-        ).setSerializer((b, n, v) -> b.field(n, v.value()), v -> Boolean.toString(v.value()));
+            initializer,
+            (b, n, v) -> b.field(n, v.value()),
+            v -> Boolean.toString(v.value())
+        );
     }
 
     /**

+ 4 - 1
server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java

@@ -53,6 +53,7 @@ import org.elasticsearch.script.field.ShortDocValuesField;
 import org.elasticsearch.search.DocValueFormat;
 import org.elasticsearch.search.lookup.FieldValues;
 import org.elasticsearch.search.lookup.SearchLookup;
+import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xcontent.XContentParser;
 import org.elasticsearch.xcontent.XContentParser.Token;
 
@@ -138,7 +139,9 @@ public class NumberFieldMapper extends FieldMapper {
                 false,
                 () -> null,
                 (n, c, o) -> o == null ? null : type.parse(o, false),
-                m -> toType(m).nullValue
+                m -> toType(m).nullValue,
+                XContentBuilder::field,
+                Objects::toString
             ).acceptsNull();
 
             this.dimension = TimeSeriesParams.dimensionParam(m -> toType(m).dimension).addValidator(v -> {

+ 3 - 1
server/src/main/java/org/elasticsearch/index/mapper/RangeFieldMapper.java

@@ -81,7 +81,9 @@ public class RangeFieldMapper extends FieldMapper {
             false,
             () -> Locale.ROOT,
             (n, c, o) -> LocaleUtils.parse(o.toString()),
-            m -> toType(m).locale
+            m -> toType(m).locale,
+            (xContentBuilder, n, v) -> xContentBuilder.field(n, v.toString()),
+            Objects::toString
         );
         private final Parameter<Map<String, String>> meta = Parameter.metaParam();
 

+ 6 - 2
server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java

@@ -242,7 +242,9 @@ public class TextFieldMapper extends FieldMapper {
             true,
             () -> DEFAULT_FILTER,
             TextFieldMapper::parseFrequencyFilter,
-            m -> ((TextFieldMapper) m).freqFilter
+            m -> ((TextFieldMapper) m).freqFilter,
+            XContentBuilder::field,
+            Objects::toString
         );
         final Parameter<Boolean> eagerGlobalOrdinals = Parameter.boolParam(
             "eager_global_ordinals",
@@ -257,7 +259,9 @@ public class TextFieldMapper extends FieldMapper {
             false,
             () -> null,
             TextFieldMapper::parsePrefixConfig,
-            m -> ((TextFieldMapper) m).indexPrefixes
+            m -> ((TextFieldMapper) m).indexPrefixes,
+            XContentBuilder::field,
+            Objects::toString
         ).acceptsNull();
 
         private final Parameter<Map<String, String>> meta = Parameter.metaParam();

+ 6 - 1
server/src/main/java/org/elasticsearch/index/mapper/TextParams.java

@@ -117,7 +117,12 @@ public final class TextParams {
     }
 
     public static Parameter<SimilarityProvider> similarity(Function<FieldMapper, SimilarityProvider> init) {
-        return new Parameter<>("similarity", false, () -> null, (n, c, o) -> TypeParsers.resolveSimilarity(c, n, o), init).setSerializer(
+        return new Parameter<>(
+            "similarity",
+            false,
+            () -> null,
+            (n, c, o) -> TypeParsers.resolveSimilarity(c, n, o),
+            init,
             (b, f, v) -> b.field(f, v == null ? null : v.name()),
             v -> v == null ? null : v.name()
         ).acceptsNull();

+ 1 - 1
server/src/test/java/org/elasticsearch/index/mapper/ParametrizedMapperTests.java

@@ -93,7 +93,7 @@ public class ParametrizedMapperTests extends MapperServiceTestCase {
         final Parameter<StringWrapper> wrapper = new Parameter<>("wrapper", false, () -> new StringWrapper("default"), (n, c, o) -> {
             if (o == null) return null;
             return new StringWrapper(o.toString());
-        }, m -> toType(m).wrapper).setSerializer((b, n, v) -> b.field(n, v.name), v -> "wrapper_" + v.name);
+        }, m -> toType(m).wrapper, (b, n, v) -> b.field(n, v.name), v -> "wrapper_" + v.name);
         final Parameter<Integer> intValue = Parameter.intParam("int_value", true, m -> toType(m).intValue, 5).addValidator(n -> {
             if (n > 50) {
                 throw new IllegalArgumentException("Value of [n] cannot be greater than 50");

+ 7 - 5
x-pack/plugin/mapper-aggregate-metric/src/main/java/org/elasticsearch/xpack/aggregatemetric/mapper/AggregateDoubleMetricFieldMapper.java

@@ -44,6 +44,7 @@ import org.elasticsearch.search.MultiValueMode;
 import org.elasticsearch.search.lookup.SearchLookup;
 import org.elasticsearch.search.sort.BucketedSort;
 import org.elasticsearch.search.sort.SortOrder;
+import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xcontent.XContentParser;
 import org.elasticsearch.xcontent.XContentSubParser;
 import org.elasticsearch.xpack.aggregatemetric.aggregations.support.AggregateMetricsValuesSourceType;
@@ -60,6 +61,7 @@ import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
@@ -107,7 +109,7 @@ public class AggregateDoubleMetricFieldMapper extends FieldMapper {
     }
 
     public static class Defaults {
-        public static final Set<Metric> METRICS = Collections.emptySet();
+        public static final EnumSet<Metric> METRICS = EnumSet.noneOf(Metric.class);
     }
 
     public static class Builder extends FieldMapper.Builder {
@@ -116,7 +118,7 @@ public class AggregateDoubleMetricFieldMapper extends FieldMapper {
 
         private final Parameter<Boolean> ignoreMalformed;
 
-        private final Parameter<Set<Metric>> metrics = new Parameter<>(Names.METRICS, false, () -> Defaults.METRICS, (n, c, o) -> {
+        private final Parameter<EnumSet<Metric>> metrics = new Parameter<>(Names.METRICS, false, () -> Defaults.METRICS, (n, c, o) -> {
             @SuppressWarnings("unchecked")
             List<String> metricsList = (List<String>) o;
             EnumSet<Metric> parsedMetrics = EnumSet.noneOf(Metric.class);
@@ -129,7 +131,7 @@ public class AggregateDoubleMetricFieldMapper extends FieldMapper {
                 }
             }
             return parsedMetrics;
-        }, m -> toType(m).metrics).addValidator(v -> {
+        }, m -> toType(m).metrics, XContentBuilder::enumSet, Objects::toString).addValidator(v -> {
             if (v == null || v.isEmpty()) {
                 throw new IllegalArgumentException("Property [" + Names.METRICS + "] is required for field [" + name() + "].");
             }
@@ -151,7 +153,7 @@ public class AggregateDoubleMetricFieldMapper extends FieldMapper {
             } catch (IllegalArgumentException e) {
                 throw new IllegalArgumentException("Metric [" + o.toString() + "] is not supported.", e);
             }
-        }, m -> toType(m).defaultMetric);
+        }, m -> toType(m).defaultMetric, XContentBuilder::field, Objects::toString);
 
         public Builder(String name, Boolean ignoreMalformedByDefault) {
             super(name);
@@ -515,7 +517,7 @@ public class AggregateDoubleMetricFieldMapper extends FieldMapper {
     private final boolean ignoreMalformedByDefault;
 
     /** A set of metrics supported */
-    private final Set<Metric> metrics;
+    private final EnumSet<Metric> metrics;
 
     /** The default metric to be when querying this field type */
     protected Metric defaultMetric;

+ 2 - 1
x-pack/plugin/mapper-constant-keyword/src/main/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldMapper.java

@@ -38,6 +38,7 @@ import org.elasticsearch.index.mapper.ValueFetcher;
 import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.search.aggregations.support.CoreValuesSourceType;
 import org.elasticsearch.search.lookup.SearchLookup;
+import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xcontent.XContentParser;
 import org.elasticsearch.xpack.constantkeyword.ConstantKeywordDocValuesField;
 import org.elasticsearch.xpack.core.termsenum.action.SimpleTermCountEnum;
@@ -77,7 +78,7 @@ public class ConstantKeywordFieldMapper extends FieldMapper {
                 throw new MapperParsingException("Property [value] on field [" + n + "] must be a number or a string, but got [" + o + "]");
             }
             return o.toString();
-        }, m -> toType(m).fieldType().value);
+        }, m -> toType(m).fieldType().value, XContentBuilder::field, Objects::toString);
         private final Parameter<Map<String, String>> meta = Parameter.metaParam();
 
         public Builder(String name) {

+ 5 - 1
x-pack/plugin/mapper-unsigned-long/src/main/java/org/elasticsearch/xpack/unsignedlong/UnsignedLongFieldMapper.java

@@ -40,6 +40,7 @@ import org.elasticsearch.index.mapper.ValueFetcher;
 import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.search.DocValueFormat;
 import org.elasticsearch.search.lookup.SearchLookup;
+import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xcontent.XContentParser;
 
 import java.io.IOException;
@@ -52,6 +53,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.function.Function;
 import java.util.function.Supplier;
 
@@ -105,7 +107,9 @@ public class UnsignedLongFieldMapper extends FieldMapper {
                 false,
                 () -> null,
                 (n, c, o) -> parseNullValueAsString(o),
-                m -> toType(m).nullValue
+                m -> toType(m).nullValue,
+                XContentBuilder::field,
+                Objects::toString
             ).acceptsNull();
 
             this.dimension = TimeSeriesParams.dimensionParam(m -> toType(m).dimension).addValidator(v -> {

+ 3 - 1
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapper.java

@@ -25,6 +25,7 @@ import org.elasticsearch.index.mapper.GeoShapeFieldMapper;
 import org.elasticsearch.index.mapper.MappedFieldType;
 import org.elasticsearch.index.mapper.MapperBuilderContext;
 import org.elasticsearch.index.query.SearchExecutionContext;
+import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xcontent.XContentParser;
 import org.elasticsearch.xpack.spatial.common.CartesianPoint;
 import org.elasticsearch.xpack.spatial.index.query.ShapeQueryPointProcessor;
@@ -67,7 +68,8 @@ public class PointFieldMapper extends AbstractPointGeometryFieldMapper<Cartesian
             this.nullValue = nullValueParam(
                 m -> builder(m).nullValue.get(),
                 (n, c, o) -> o == null ? null : parseNullValue(o, ignoreZValue.get().value(), ignoreMalformed.get().value()),
-                () -> null
+                () -> null,
+                XContentBuilder::field
             ).acceptsNull();
         }
 

+ 6 - 2
x-pack/plugin/vectors/src/main/java/org/elasticsearch/xpack/vectors/mapper/DenseVectorFieldMapper.java

@@ -71,7 +71,9 @@ public class DenseVectorFieldMapper extends FieldMapper implements PerFieldKnnVe
             false,
             () -> null,
             (n, c, o) -> XContentMapValues.nodeIntegerValue(o),
-            m -> toType(m).dims
+            m -> toType(m).dims,
+            XContentBuilder::field,
+            Objects::toString
         ).addValidator(dims -> {
             if (dims == null) {
                 throw new MapperParsingException("Missing required parameter [dims] for field [" + name + "]");
@@ -102,7 +104,9 @@ public class DenseVectorFieldMapper extends FieldMapper implements PerFieldKnnVe
             false,
             () -> null,
             (n, c, o) -> o == null ? null : parseIndexOptions(n, o),
-            m -> toType(m).indexOptions
+            m -> toType(m).indexOptions,
+            XContentBuilder::field,
+            Objects::toString
         );
         private final Parameter<Map<String, String>> meta = Parameter.metaParam();