diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index d5f770ebb95fc..62844b52be57b 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -236,6 +236,7 @@ public void apply(Settings value, Settings current, Settings previous) { IndicesQueryCache.INDICES_CACHE_QUERY_COUNT_SETTING, IndicesQueryCache.INDICES_QUERIES_CACHE_ALL_SEGMENTS_SETTING, IndicesService.INDICES_ID_FIELD_DATA_ENABLED_SETTING, + IndicesService.INDICES_RECOVERY_SOURCE_ENABLED_SETTING, IndicesService.WRITE_DANGLING_INDICES_INFO_SETTING, MappingUpdatedAction.INDICES_MAPPING_DYNAMIC_TIMEOUT_SETTING, MappingUpdatedAction.INDICES_MAX_IN_FLIGHT_UPDATES_SETTING, diff --git a/server/src/main/java/org/elasticsearch/index/engine/Engine.java b/server/src/main/java/org/elasticsearch/index/engine/Engine.java index b07132eea75e8..32ee43ee88ef6 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/Engine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/Engine.java @@ -81,6 +81,7 @@ import org.elasticsearch.index.store.Store; import org.elasticsearch.index.translog.Translog; import org.elasticsearch.index.translog.TranslogStats; +import org.elasticsearch.indices.IndicesService; import org.elasticsearch.search.suggest.completion.CompletionStats; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.Transports; @@ -139,6 +140,7 @@ public abstract class Engine implements Closeable { protected final EventListener eventListener; protected final ReentrantLock failEngineLock = new ReentrantLock(); protected final SetOnce failedEngine = new SetOnce<>(); + protected final boolean enableRecoverySource; private final AtomicBoolean isClosing = new AtomicBoolean(); private final SubscribableListener drainOnCloseListener = new SubscribableListener<>(); @@ -167,6 +169,9 @@ protected Engine(EngineConfig engineConfig) { // we use the engine class directly here to make sure all subclasses have the same logger name this.logger = Loggers.getLogger(Engine.class, engineConfig.getShardId()); this.eventListener = engineConfig.getEventListener(); + this.enableRecoverySource = IndicesService.INDICES_RECOVERY_SOURCE_ENABLED_SETTING.get( + engineConfig.getIndexSettings().getSettings() + ); } /** diff --git a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java index 9e0fbd0bb691d..ca8281ef66844 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -98,6 +98,7 @@ import org.elasticsearch.index.translog.TranslogCorruptedException; import org.elasticsearch.index.translog.TranslogDeletionPolicy; import org.elasticsearch.index.translog.TranslogStats; +import org.elasticsearch.indices.IndicesService; import org.elasticsearch.search.suggest.completion.CompletionStats; import org.elasticsearch.threadpool.ThreadPool; @@ -3130,6 +3131,13 @@ public Translog.Snapshot newChangesSnapshot( boolean singleConsumer, boolean accessStats ) throws IOException { + if (enableRecoverySource == false) { + throw new IllegalStateException( + "Changes snapshot are unavailable when the " + + IndicesService.INDICES_RECOVERY_SOURCE_ENABLED_SETTING.getKey() + + " setting is disabled." + ); + } ensureOpen(); refreshIfNeeded(source, toSeqNo); Searcher searcher = acquireSearcher(source, SearcherScope.INTERNAL); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java index 908108bce31da..280eb1cb49b20 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java @@ -36,6 +36,8 @@ import java.util.List; import java.util.Locale; +import static org.elasticsearch.indices.IndicesService.INDICES_RECOVERY_SOURCE_ENABLED_SETTING; + public class SourceFieldMapper extends MetadataFieldMapper { public static final NodeFeature SYNTHETIC_SOURCE_FALLBACK = new NodeFeature("mapper.source.synthetic_source_fallback"); @@ -58,7 +60,17 @@ private enum Mode { Explicit.IMPLICIT_TRUE, Strings.EMPTY_ARRAY, Strings.EMPTY_ARRAY, - null + null, + true + ); + + private static final SourceFieldMapper DEFAULT_NO_RECOVERY_SOURCE = new SourceFieldMapper( + null, + Explicit.IMPLICIT_TRUE, + Strings.EMPTY_ARRAY, + Strings.EMPTY_ARRAY, + null, + false ); private static final SourceFieldMapper TSDB_DEFAULT = new SourceFieldMapper( @@ -66,7 +78,17 @@ private enum Mode { Explicit.IMPLICIT_TRUE, Strings.EMPTY_ARRAY, Strings.EMPTY_ARRAY, - IndexMode.TIME_SERIES + IndexMode.TIME_SERIES, + true + ); + + private static final SourceFieldMapper TSDB_DEFAULT_NO_RECOVERY_SOURCE = new SourceFieldMapper( + Mode.SYNTHETIC, + Explicit.IMPLICIT_TRUE, + Strings.EMPTY_ARRAY, + Strings.EMPTY_ARRAY, + IndexMode.TIME_SERIES, + false ); private static final SourceFieldMapper LOGSDB_DEFAULT = new SourceFieldMapper( @@ -74,7 +96,17 @@ private enum Mode { Explicit.IMPLICIT_TRUE, Strings.EMPTY_ARRAY, Strings.EMPTY_ARRAY, - IndexMode.LOGSDB + IndexMode.LOGSDB, + true + ); + + private static final SourceFieldMapper LOGSDB_DEFAULT_NO_RECOVERY_SOURCE = new SourceFieldMapper( + Mode.SYNTHETIC, + Explicit.IMPLICIT_TRUE, + Strings.EMPTY_ARRAY, + Strings.EMPTY_ARRAY, + IndexMode.LOGSDB, + false ); /* @@ -86,7 +118,17 @@ private enum Mode { Explicit.IMPLICIT_TRUE, Strings.EMPTY_ARRAY, Strings.EMPTY_ARRAY, - IndexMode.TIME_SERIES + IndexMode.TIME_SERIES, + true + ); + + private static final SourceFieldMapper TSDB_LEGACY_DEFAULT_NO_RECOVERY_SOURCE = new SourceFieldMapper( + null, + Explicit.IMPLICIT_TRUE, + Strings.EMPTY_ARRAY, + Strings.EMPTY_ARRAY, + IndexMode.TIME_SERIES, + false ); public static class Defaults { @@ -145,11 +187,19 @@ public static class Builder extends MetadataFieldMapper.Builder { private final boolean supportsNonDefaultParameterValues; - public Builder(IndexMode indexMode, final Settings settings, boolean supportsCheckForNonDefaultParams) { + private final boolean enableRecoverySource; + + public Builder( + IndexMode indexMode, + final Settings settings, + boolean supportsCheckForNonDefaultParams, + boolean enableRecoverySource + ) { super(Defaults.NAME); this.indexMode = indexMode; this.supportsNonDefaultParameterValues = supportsCheckForNonDefaultParams == false || settings.getAsBoolean(LOSSY_PARAMETERS_ALLOWED_SETTING_NAME, true); + this.enableRecoverySource = enableRecoverySource; } public Builder setSynthetic() { @@ -215,7 +265,8 @@ public SourceFieldMapper build() { enabled.get(), includes.getValue().toArray(Strings.EMPTY_ARRAY), excludes.getValue().toArray(Strings.EMPTY_ARRAY), - indexMode + indexMode, + enableRecoverySource ); if (indexMode != null) { indexMode.validateSourceFieldMapper(sourceFieldMapper); @@ -227,23 +278,25 @@ public SourceFieldMapper build() { public static final TypeParser PARSER = new ConfigurableTypeParser(c -> { var indexMode = c.getIndexSettings().getMode(); + boolean enableRecoverySource = INDICES_RECOVERY_SOURCE_ENABLED_SETTING.get(c.getSettings()); if (indexMode.isSyntheticSourceEnabled()) { if (indexMode == IndexMode.TIME_SERIES) { if (c.getIndexSettings().getIndexVersionCreated().onOrAfter(IndexVersions.V_8_7_0)) { - return TSDB_DEFAULT; + return enableRecoverySource ? TSDB_DEFAULT : TSDB_DEFAULT_NO_RECOVERY_SOURCE; } else { - return TSDB_LEGACY_DEFAULT; + return enableRecoverySource ? TSDB_LEGACY_DEFAULT : TSDB_LEGACY_DEFAULT_NO_RECOVERY_SOURCE; } } else if (indexMode == IndexMode.LOGSDB) { - return LOGSDB_DEFAULT; + return enableRecoverySource ? LOGSDB_DEFAULT : LOGSDB_DEFAULT_NO_RECOVERY_SOURCE; } } - return DEFAULT; + return enableRecoverySource ? DEFAULT : DEFAULT_NO_RECOVERY_SOURCE; }, c -> new Builder( c.getIndexSettings().getMode(), c.getSettings(), - c.indexVersionCreated().onOrAfter(IndexVersions.SOURCE_MAPPER_LOSSY_PARAMS_CHECK) + c.indexVersionCreated().onOrAfter(IndexVersions.SOURCE_MAPPER_LOSSY_PARAMS_CHECK), + INDICES_RECOVERY_SOURCE_ENABLED_SETTING.get(c.getSettings()) ) ); @@ -296,8 +349,16 @@ public BlockLoader blockLoader(BlockLoaderContext blContext) { private final SourceFilter sourceFilter; private final IndexMode indexMode; - - private SourceFieldMapper(Mode mode, Explicit enabled, String[] includes, String[] excludes, IndexMode indexMode) { + private final boolean enableRecoverySource; + + private SourceFieldMapper( + Mode mode, + Explicit enabled, + String[] includes, + String[] excludes, + IndexMode indexMode, + boolean enableRecoverySource + ) { super(new SourceFieldType((enabled.explicit() && enabled.value()) || (enabled.explicit() == false && mode != Mode.DISABLED))); assert enabled.explicit() == false || mode == null; this.mode = mode; @@ -310,6 +371,7 @@ private SourceFieldMapper(Mode mode, Explicit enabled, String[] include } this.complete = stored() && sourceFilter == null; this.indexMode = indexMode; + this.enableRecoverySource = enableRecoverySource; } private static SourceFilter buildSourceFilter(String[] includes, String[] excludes) { @@ -354,7 +416,7 @@ public void preParse(DocumentParserContext context) throws IOException { context.doc().add(new StoredField(fieldType().name(), ref.bytes, ref.offset, ref.length)); } - if (originalSource != null && adaptedSource != originalSource) { + if (enableRecoverySource && originalSource != null && adaptedSource != originalSource) { // if we omitted source or modified it we add the _recovery_source to ensure we have it for ops based recovery BytesRef ref = originalSource.toBytesRef(); context.doc().add(new StoredField(RECOVERY_SOURCE_NAME, ref.bytes, ref.offset, ref.length)); @@ -382,7 +444,7 @@ protected String contentType() { @Override public FieldMapper.Builder getMergeBuilder() { - return new Builder(indexMode, Settings.EMPTY, false).init(this); + return new Builder(indexMode, Settings.EMPTY, false, enableRecoverySource).init(this); } /** diff --git a/server/src/main/java/org/elasticsearch/indices/IndicesService.java b/server/src/main/java/org/elasticsearch/indices/IndicesService.java index 3016530292766..49d12639f12d8 100644 --- a/server/src/main/java/org/elasticsearch/indices/IndicesService.java +++ b/server/src/main/java/org/elasticsearch/indices/IndicesService.java @@ -209,6 +209,12 @@ public class IndicesService extends AbstractLifecycleComponent Setting.Property.NodeScope ); + public static final Setting INDICES_RECOVERY_SOURCE_ENABLED_SETTING = Setting.boolSetting( + "indices.recovery_source.enabled", + true, + Property.NodeScope + ); + static final NodeFeature SUPPORTS_AUTO_PUT = new NodeFeature("indices.auto_put_supported"); /** diff --git a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java index 77f5fa301c78d..a552865ab765e 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -129,6 +129,7 @@ import org.elasticsearch.index.translog.TranslogConfig; import org.elasticsearch.index.translog.TranslogDeletionPolicy; import org.elasticsearch.index.translog.TranslogOperationsUtils; +import org.elasticsearch.indices.IndicesService; import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; import org.elasticsearch.test.IndexSettingsModule; import org.elasticsearch.test.index.IndexVersionUtils; @@ -7773,6 +7774,24 @@ protected void commitIndexWriter(IndexWriter writer, Translog translog) throws I } } + public void testDisableRecoverySource() throws Exception { + Settings settings = Settings.builder() + .put(defaultSettings.getNodeSettings()) + .put(IndicesService.INDICES_RECOVERY_SOURCE_ENABLED_SETTING.getKey(), false) + .build(); + IndexSettings indexSettings = new IndexSettings(defaultSettings.getIndexMetadata(), settings, defaultSettings.getScopedSettings()); + try ( + Store store = createStore(); + InternalEngine engine = createEngine(indexSettings, store, createTempDir(), NoMergePolicy.INSTANCE) + ) { + IllegalStateException exc = expectThrows( + IllegalStateException.class, + () -> engine.newChangesSnapshot("test", 0, 1000, true, true, true) + ); + assertThat(exc.getMessage(), containsString("unavailable")); + } + } + private static void assertCommitGenerations(Map commits, List expectedGenerations) { assertCommitGenerations(commits.values().stream().map(Engine.IndexCommitRef::getIndexCommit).toList(), expectedGenerations); } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DynamicFieldsBuilderTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DynamicFieldsBuilderTests.java index a138f0910e6ec..c94e1aa7cecdc 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DynamicFieldsBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DynamicFieldsBuilderTests.java @@ -68,7 +68,7 @@ public void testCreateDynamicStringFieldAsKeywordForDimension() throws IOExcepti XContentParser parser = createParser(JsonXContent.jsonXContent, source); SourceToParse sourceToParse = new SourceToParse("test", new BytesArray(source), XContentType.JSON); - SourceFieldMapper sourceMapper = new SourceFieldMapper.Builder(null, Settings.EMPTY, false).setSynthetic().build(); + SourceFieldMapper sourceMapper = new SourceFieldMapper.Builder(null, Settings.EMPTY, false, true).setSynthetic().build(); RootObjectMapper root = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).add( new PassThroughObjectMapper.Builder("labels").setPriority(0).setContainsDimensions().dynamic(ObjectMapper.Dynamic.TRUE) ).build(MapperBuilderContext.root(false, false)); diff --git a/server/src/test/java/org/elasticsearch/index/mapper/SourceFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/SourceFieldMapperTests.java index d0350c1d92a83..c01c3a9b81ae2 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/SourceFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/SourceFieldMapperTests.java @@ -9,11 +9,16 @@ package org.elasticsearch.index.mapper; import org.apache.lucene.index.IndexableField; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.index.IndexMode; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.IndexVersions; +import org.elasticsearch.indices.IndicesService; import org.elasticsearch.test.index.IndexVersionUtils; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; @@ -22,6 +27,7 @@ import org.elasticsearch.xcontent.json.JsonXContent; import java.io.IOException; +import java.util.List; import java.util.Map; import static org.hamcrest.Matchers.containsString; @@ -347,4 +353,128 @@ public void testBypassCheckForNonDefaultParameterValuesInEarlierVersions() throw assertThat(sourceFieldMapper, notNullValue()); } } + + public void testRecoverySourceWithSourceExcludes() throws IOException { + { + MapperService mapperService = createMapperService( + topMapping(b -> b.startObject(SourceFieldMapper.NAME).field("excludes", List.of("field1")).endObject()) + ); + DocumentMapper docMapper = mapperService.documentMapper(); + ParsedDocument doc = docMapper.parse(source(b -> { b.field("field1", "value1"); })); + + assertNotNull(doc.rootDoc().getField("_recovery_source")); + assertThat(doc.rootDoc().getField("_recovery_source").binaryValue(), equalTo(new BytesRef("{\"field1\":\"value1\"}"))); + } + { + Settings settings = Settings.builder().put(IndicesService.INDICES_RECOVERY_SOURCE_ENABLED_SETTING.getKey(), false).build(); + MapperService mapperService = createMapperService( + settings, + topMapping(b -> b.startObject(SourceFieldMapper.NAME).field("enabled", false).endObject()) + ); + DocumentMapper docMapper = mapperService.documentMapper(); + ParsedDocument doc = docMapper.parse(source(b -> b.field("field1", "value1"))); + assertNull(doc.rootDoc().getField("_recovery_source")); + } + } + + public void testRecoverySourceWithSourceDisabled() throws IOException { + { + MapperService mapperService = createMapperService( + topMapping(b -> b.startObject(SourceFieldMapper.NAME).field("enabled", false).endObject()) + ); + DocumentMapper docMapper = mapperService.documentMapper(); + ParsedDocument doc = docMapper.parse(source(b -> { b.field("field1", "value1"); })); + assertNotNull(doc.rootDoc().getField("_recovery_source")); + assertThat(doc.rootDoc().getField("_recovery_source").binaryValue(), equalTo(new BytesRef("{\"field1\":\"value1\"}"))); + } + { + Settings settings = Settings.builder().put(IndicesService.INDICES_RECOVERY_SOURCE_ENABLED_SETTING.getKey(), false).build(); + MapperService mapperService = createMapperService( + settings, + topMapping(b -> b.startObject(SourceFieldMapper.NAME).field("enabled", false).endObject()) + ); + DocumentMapper docMapper = mapperService.documentMapper(); + ParsedDocument doc = docMapper.parse(source(b -> b.field("field1", "value1"))); + assertNull(doc.rootDoc().getField("_recovery_source")); + } + } + + public void testRecoverySourceWithSyntheticSource() throws IOException { + { + MapperService mapperService = createMapperService( + topMapping(b -> b.startObject(SourceFieldMapper.NAME).field("mode", "synthetic").endObject()) + ); + DocumentMapper docMapper = mapperService.documentMapper(); + ParsedDocument doc = docMapper.parse(source(b -> { b.field("field1", "value1"); })); + assertNotNull(doc.rootDoc().getField("_recovery_source")); + assertThat(doc.rootDoc().getField("_recovery_source").binaryValue(), equalTo(new BytesRef("{\"field1\":\"value1\"}"))); + } + { + Settings settings = Settings.builder().put(IndicesService.INDICES_RECOVERY_SOURCE_ENABLED_SETTING.getKey(), false).build(); + MapperService mapperService = createMapperService( + settings, + topMapping(b -> b.startObject(SourceFieldMapper.NAME).field("mode", "synthetic").endObject()) + ); + DocumentMapper docMapper = mapperService.documentMapper(); + ParsedDocument doc = docMapper.parse(source(b -> b.field("field1", "value1"))); + assertNull(doc.rootDoc().getField("_recovery_source")); + } + } + + public void testRecoverySourceWithLogs() throws IOException { + { + Settings settings = Settings.builder().put(IndexSettings.MODE.getKey(), IndexMode.LOGSDB.getName()).build(); + MapperService mapperService = createMapperService(settings, mapping(b -> {})); + DocumentMapper docMapper = mapperService.documentMapper(); + ParsedDocument doc = docMapper.parse(source(b -> { b.field("@timestamp", "2012-02-13"); })); + assertNotNull(doc.rootDoc().getField("_recovery_source")); + assertThat(doc.rootDoc().getField("_recovery_source").binaryValue(), equalTo(new BytesRef("{\"@timestamp\":\"2012-02-13\"}"))); + } + { + Settings settings = Settings.builder() + .put(IndexSettings.MODE.getKey(), IndexMode.LOGSDB.getName()) + .put(IndicesService.INDICES_RECOVERY_SOURCE_ENABLED_SETTING.getKey(), false) + .build(); + MapperService mapperService = createMapperService(settings, mapping(b -> {})); + DocumentMapper docMapper = mapperService.documentMapper(); + ParsedDocument doc = docMapper.parse(source(b -> b.field("@timestamp", "2012-02-13"))); + assertNull(doc.rootDoc().getField("_recovery_source")); + } + } + + public void testRecoverySourceWithTimeSeries() throws IOException { + { + Settings settings = Settings.builder() + .put(IndexSettings.MODE.getKey(), IndexMode.TIME_SERIES.getName()) + .put(IndexMetadata.INDEX_ROUTING_PATH.getKey(), "field") + .build(); + MapperService mapperService = createMapperService(settings, fieldMapping(b -> { + b.field("type", "keyword"); + b.field("time_series_dimension", true); + })); + DocumentMapper docMapper = mapperService.documentMapper(); + ParsedDocument doc = docMapper.parse(source("123", b -> b.field("@timestamp", "2012-02-13").field("field", "value1"), null)); + assertNotNull(doc.rootDoc().getField("_recovery_source")); + assertThat( + doc.rootDoc().getField("_recovery_source").binaryValue(), + equalTo(new BytesRef("{\"@timestamp\":\"2012-02-13\",\"field\":\"value1\"}")) + ); + } + { + Settings settings = Settings.builder() + .put(IndexSettings.MODE.getKey(), IndexMode.TIME_SERIES.getName()) + .put(IndexMetadata.INDEX_ROUTING_PATH.getKey(), "field") + .put(IndicesService.INDICES_RECOVERY_SOURCE_ENABLED_SETTING.getKey(), false) + .build(); + MapperService mapperService = createMapperService(settings, fieldMapping(b -> { + b.field("type", "keyword"); + b.field("time_series_dimension", true); + })); + DocumentMapper docMapper = mapperService.documentMapper(); + ParsedDocument doc = docMapper.parse( + source("123", b -> b.field("@timestamp", "2012-02-13").field("field", randomAlphaOfLength(5)), null) + ); + assertNull(doc.rootDoc().getField("_recovery_source")); + } + } } diff --git a/server/src/test/java/org/elasticsearch/index/query/SearchExecutionContextTests.java b/server/src/test/java/org/elasticsearch/index/query/SearchExecutionContextTests.java index 9cd1df700a618..c4b4f930e5e72 100644 --- a/server/src/test/java/org/elasticsearch/index/query/SearchExecutionContextTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/SearchExecutionContextTests.java @@ -383,7 +383,7 @@ public void testSearchRequestRuntimeFieldsAndMultifieldDetection() { public void testSyntheticSourceSearchLookup() throws IOException { // Build a mapping using synthetic source - SourceFieldMapper sourceMapper = new SourceFieldMapper.Builder(null, Settings.EMPTY, false).setSynthetic().build(); + SourceFieldMapper sourceMapper = new SourceFieldMapper.Builder(null, Settings.EMPTY, false, true).setSynthetic().build(); RootObjectMapper root = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).add( new KeywordFieldMapper.Builder("cat", IndexVersion.current()).ignoreAbove(100) ).build(MapperBuilderContext.root(true, false));