Ver Fonte

Text fields are stored by default in TSDB indices (#106338)

* Text fields are stored by default with synthetic source

Synthetic source requires text fields to be stored or have keyword
sub-field that supports synthetic source. If there are no keyword fields
 users currently have to explicitly set 'store' to 'true' or get a
validation exception. This is not the best experience. It is quite
likely that setting `store` to `true` is  the correct thing to do but
users still get an error and need to investigate it. With this change if
 `store` setting is not specified in such context it  will be set to
 `true` by default. Setting it explicitly to `false` results in the
 exception.

Closes #97039
Oleksandr Kolomiiets há 1 ano atrás
pai
commit
9e6b893896
19 ficheiros alterados com 332 adições e 56 exclusões
  1. 6 0
      docs/changelog/106338.yaml
  2. 5 2
      docs/reference/mapping/types/text.asciidoc
  3. 1 0
      rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/tsdb/90_unsupported_operations.yml
  4. 5 1
      server/src/main/java/org/elasticsearch/index/mapper/DynamicFieldsBuilder.java
  5. 23 0
      server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java
  6. 17 0
      server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java
  7. 48 20
      server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java
  8. 5 1
      server/src/main/java/org/elasticsearch/index/query/QueryRewriteContext.java
  9. 5 3
      server/src/test/java/org/elasticsearch/index/fielddata/AbstractFieldDataTestCase.java
  10. 20 7
      server/src/test/java/org/elasticsearch/index/fielddata/FilterFieldDataTests.java
  11. 15 9
      server/src/test/java/org/elasticsearch/index/fielddata/IndexFieldDataServiceTests.java
  12. 4 4
      server/src/test/java/org/elasticsearch/index/mapper/DocumentParserContextTests.java
  13. 72 0
      server/src/test/java/org/elasticsearch/index/mapper/MultiFieldsTests.java
  14. 3 3
      server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperMergeTests.java
  15. 2 2
      server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperTests.java
  16. 13 0
      server/src/test/java/org/elasticsearch/index/mapper/TextFieldAnalyzerModeTests.java
  17. 73 1
      server/src/test/java/org/elasticsearch/index/mapper/TextFieldMapperTests.java
  18. 5 1
      server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightBuilderTests.java
  19. 10 2
      server/src/test/java/org/elasticsearch/search/rescore/QueryRescorerBuilderTests.java

+ 6 - 0
docs/changelog/106338.yaml

@@ -0,0 +1,6 @@
+pr: 106338
+summary: Text fields are stored by default in TSDB indices
+area: TSDB
+type: enhancement
+issues:
+ - 97039

+ 5 - 2
docs/reference/mapping/types/text.asciidoc

@@ -133,8 +133,11 @@ The following parameters are accepted by `text` fields:
 <<mapping-store,`store`>>::
 
     Whether the field value should be stored and retrievable separately from
-    the <<mapping-source-field,`_source`>> field. Accepts `true` or `false`
-    (default).
+    the <<mapping-source-field,`_source`>> field. Accepts `true` or `false` (default).
+    This parameter will be automatically set to `true` for TSDB indices
+    (indices that have `index.mode` set to `time_series`)
+    if there is no <<keyword-synthetic-source, `keyword`>>
+    sub-field that supports synthetic `_source`.
 
 <<search-analyzer,`search_analyzer`>>::
 

+ 1 - 0
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/tsdb/90_unsupported_operations.yml

@@ -278,6 +278,7 @@ synthetic source text field:
                                 type: keyword
                           name:
                             type: text
+                            store: false
                       value:
                         type: long
                         time_series_metric: gauge

+ 5 - 1
server/src/main/java/org/elasticsearch/index/mapper/DynamicFieldsBuilder.java

@@ -333,7 +333,11 @@ final class DynamicFieldsBuilder {
                 );
             } else {
                 return createDynamicField(
-                    new TextFieldMapper.Builder(name, context.indexAnalyzers()).addMultiField(
+                    new TextFieldMapper.Builder(
+                        name,
+                        context.indexAnalyzers(),
+                        context.indexSettings().getMode().isSyntheticSourceEnabled()
+                    ).addMultiField(
                         new KeywordFieldMapper.Builder("keyword", context.indexSettings().getIndexVersionCreated()).ignoreAbove(256)
                     ),
                     context

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

@@ -450,13 +450,28 @@ public abstract class FieldMapper extends Mapper {
 
             private final Map<String, Function<MapperBuilderContext, FieldMapper>> mapperBuilders = new HashMap<>();
 
+            private boolean hasSyntheticSourceCompatibleKeywordField;
+
             public Builder add(FieldMapper.Builder builder) {
                 mapperBuilders.put(builder.name(), builder::build);
+
+                if (builder instanceof KeywordFieldMapper.Builder kwd) {
+                    if (kwd.hasNormalizer() == false && (kwd.hasDocValues() || kwd.isStored())) {
+                        hasSyntheticSourceCompatibleKeywordField = true;
+                    }
+                }
+
                 return this;
             }
 
             private void add(FieldMapper mapper) {
                 mapperBuilders.put(mapper.simpleName(), context -> mapper);
+
+                if (mapper instanceof KeywordFieldMapper kwd) {
+                    if (kwd.hasNormalizer() == false && (kwd.fieldType().hasDocValues() || kwd.fieldType().isStored())) {
+                        hasSyntheticSourceCompatibleKeywordField = true;
+                    }
+                }
             }
 
             private void update(FieldMapper toMerge, MapperMergeContext context) {
@@ -474,6 +489,10 @@ public abstract class FieldMapper extends Mapper {
                 return mapperBuilders.isEmpty() == false;
             }
 
+            public boolean hasSyntheticSourceCompatibleKeywordField() {
+                return hasSyntheticSourceCompatibleKeywordField;
+            }
+
             public MultiFields build(Mapper.Builder mainFieldBuilder, MapperBuilderContext context) {
                 if (mapperBuilders.isEmpty()) {
                     return empty();
@@ -1134,6 +1153,10 @@ public abstract class FieldMapper extends Mapper {
             return Parameter.boolParam("store", false, initializer, defaultValue);
         }
 
+        public static Parameter<Boolean> storeParam(Function<FieldMapper, Boolean> initializer, Supplier<Boolean> defaultValue) {
+            return Parameter.boolParam("store", false, initializer, defaultValue);
+        }
+
         public static Parameter<Boolean> docValuesParam(Function<FieldMapper, Boolean> initializer, boolean defaultValue) {
             return Parameter.boolParam("doc_values", false, initializer, defaultValue);
         }

+ 17 - 0
server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java

@@ -227,6 +227,10 @@ public final class KeywordFieldMapper extends FieldMapper {
             return this;
         }
 
+        public boolean hasNormalizer() {
+            return this.normalizer.get() != null;
+        }
+
         Builder nullValue(String nullValue) {
             this.nullValue.setValue(nullValue);
             return this;
@@ -237,6 +241,10 @@ public final class KeywordFieldMapper extends FieldMapper {
             return this;
         }
 
+        public boolean hasDocValues() {
+            return this.hasDocValues.get();
+        }
+
         public Builder dimension(boolean dimension) {
             this.dimension.setValue(dimension);
             return this;
@@ -247,6 +255,15 @@ public final class KeywordFieldMapper extends FieldMapper {
             return this;
         }
 
+        public Builder stored(boolean stored) {
+            this.stored.setValue(stored);
+            return this;
+        }
+
+        public boolean isStored() {
+            return this.stored.get();
+        }
+
         private FieldValues<String> scriptValues() {
             if (script.get() == null) {
                 return null;

+ 48 - 20
server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java

@@ -236,9 +236,11 @@ public final class TextFieldMapper extends FieldMapper {
     public static class Builder extends FieldMapper.Builder {
 
         private final IndexVersion indexCreatedVersion;
+        private final Parameter<Boolean> store;
+
+        private final boolean isSyntheticSourceEnabledViaIndexMode;
 
         private final Parameter<Boolean> index = Parameter.indexParam(m -> ((TextFieldMapper) m).index, true);
-        private final Parameter<Boolean> store = Parameter.storeParam(m -> ((TextFieldMapper) m).store, false);
 
         final Parameter<SimilarityProvider> similarity = TextParams.similarity(m -> ((TextFieldMapper) m).similarity);
 
@@ -283,12 +285,28 @@ public final class TextFieldMapper extends FieldMapper {
 
         final TextParams.Analyzers analyzers;
 
-        public Builder(String name, IndexAnalyzers indexAnalyzers) {
-            this(name, IndexVersion.current(), indexAnalyzers);
+        public Builder(String name, IndexAnalyzers indexAnalyzers, boolean isSyntheticSourceEnabledViaIndexMode) {
+            this(name, IndexVersion.current(), indexAnalyzers, isSyntheticSourceEnabledViaIndexMode);
         }
 
-        public Builder(String name, IndexVersion indexCreatedVersion, IndexAnalyzers indexAnalyzers) {
+        public Builder(
+            String name,
+            IndexVersion indexCreatedVersion,
+            IndexAnalyzers indexAnalyzers,
+            boolean isSyntheticSourceEnabledViaIndexMode
+        ) {
             super(name);
+
+            // If synthetic source is used we need to either store this field
+            // to recreate the source or use keyword multi-fields for that.
+            // So if there are no suitable multi-fields we will default to
+            // storing the field without requiring users to explicitly set 'store'.
+            //
+            // If 'store' parameter was explicitly provided we'll reject the request.
+            this.store = Parameter.storeParam(
+                m -> ((TextFieldMapper) m).store,
+                () -> isSyntheticSourceEnabledViaIndexMode && multiFieldsBuilder.hasSyntheticSourceCompatibleKeywordField() == false
+            );
             this.indexCreatedVersion = indexCreatedVersion;
             this.analyzers = new TextParams.Analyzers(
                 indexAnalyzers,
@@ -296,6 +314,7 @@ public final class TextFieldMapper extends FieldMapper {
                 m -> (((TextFieldMapper) m).positionIncrementGap),
                 indexCreatedVersion
             );
+            this.isSyntheticSourceEnabledViaIndexMode = isSyntheticSourceEnabledViaIndexMode;
         }
 
         public Builder index(boolean index) {
@@ -387,13 +406,9 @@ public final class TextFieldMapper extends FieldMapper {
             if (fieldType.stored()) {
                 return null;
             }
-            for (Mapper sub : multiFields) {
-                if (sub.typeName().equals(KeywordFieldMapper.CONTENT_TYPE)) {
-                    KeywordFieldMapper kwd = (KeywordFieldMapper) sub;
-                    if (kwd.hasNormalizer() == false && (kwd.fieldType().hasDocValues() || kwd.fieldType().isStored())) {
-                        return kwd.fieldType();
-                    }
-                }
+            var kwd = getKeywordFieldMapperForSyntheticSource(multiFields);
+            if (kwd != null) {
+                return kwd.fieldType();
             }
             return null;
         }
@@ -483,7 +498,7 @@ public final class TextFieldMapper extends FieldMapper {
     private static final IndexVersion MINIMUM_COMPATIBILITY_VERSION = IndexVersion.fromId(5000099);
 
     public static final TypeParser PARSER = new TypeParser(
-        (n, c) -> new Builder(n, c.indexVersionCreated(), c.getIndexAnalyzers()),
+        (n, c) -> new Builder(n, c.indexVersionCreated(), c.getIndexAnalyzers(), c.getIndexSettings().getMode().isSyntheticSourceEnabled()),
         MINIMUM_COMPATIBILITY_VERSION
     );
 
@@ -1203,6 +1218,8 @@ public final class TextFieldMapper extends FieldMapper {
     private final SubFieldInfo prefixFieldInfo;
     private final SubFieldInfo phraseFieldInfo;
 
+    private final boolean isSyntheticSourceEnabledViaIndexMode;
+
     private TextFieldMapper(
         String simpleName,
         FieldType fieldType,
@@ -1235,6 +1252,7 @@ public final class TextFieldMapper extends FieldMapper {
         this.indexPrefixes = builder.indexPrefixes.getValue();
         this.freqFilter = builder.freqFilter.getValue();
         this.fieldData = builder.fieldData.get();
+        this.isSyntheticSourceEnabledViaIndexMode = builder.isSyntheticSourceEnabledViaIndexMode;
     }
 
     @Override
@@ -1258,7 +1276,7 @@ public final class TextFieldMapper extends FieldMapper {
 
     @Override
     public FieldMapper.Builder getMergeBuilder() {
-        return new Builder(simpleName(), indexCreatedVersion, indexAnalyzers).init(this);
+        return new Builder(simpleName(), indexCreatedVersion, indexAnalyzers, isSyntheticSourceEnabledViaIndexMode).init(this);
     }
 
     @Override
@@ -1454,15 +1472,12 @@ public final class TextFieldMapper extends FieldMapper {
                 }
             };
         }
-        for (Mapper sub : this) {
-            if (sub.typeName().equals(KeywordFieldMapper.CONTENT_TYPE)) {
-                KeywordFieldMapper kwd = (KeywordFieldMapper) sub;
-                if (kwd.hasNormalizer() == false && (kwd.fieldType().hasDocValues() || kwd.fieldType().isStored())) {
 
-                    return kwd.syntheticFieldLoader(simpleName());
-                }
-            }
+        var kwd = getKeywordFieldMapperForSyntheticSource(this);
+        if (kwd != null) {
+            return kwd.syntheticFieldLoader(simpleName());
         }
+
         throw new IllegalArgumentException(
             String.format(
                 Locale.ROOT,
@@ -1473,4 +1488,17 @@ public final class TextFieldMapper extends FieldMapper {
             )
         );
     }
+
+    private static KeywordFieldMapper getKeywordFieldMapperForSyntheticSource(Iterable<? extends Mapper> multiFields) {
+        for (Mapper sub : multiFields) {
+            if (sub.typeName().equals(KeywordFieldMapper.CONTENT_TYPE)) {
+                KeywordFieldMapper kwd = (KeywordFieldMapper) sub;
+                if (kwd.hasNormalizer() == false && (kwd.fieldType().hasDocValues() || kwd.fieldType().isStored())) {
+                    return kwd;
+                }
+            }
+        }
+
+        return null;
+    }
 }

+ 5 - 1
server/src/main/java/org/elasticsearch/index/query/QueryRewriteContext.java

@@ -196,7 +196,11 @@ public class QueryRewriteContext {
         if (fieldMapping != null || allowUnmappedFields) {
             return fieldMapping;
         } else if (mapUnmappedFieldAsString) {
-            TextFieldMapper.Builder builder = new TextFieldMapper.Builder(name, getIndexAnalyzers());
+            TextFieldMapper.Builder builder = new TextFieldMapper.Builder(
+                name,
+                getIndexAnalyzers(),
+                getIndexSettings() != null && getIndexSettings().getMode().isSyntheticSourceEnabled()
+            );
             return builder.build(MapperBuilderContext.root(false, false)).fieldType();
         } else {
             throw new QueryShardException(this, "No field mapping can be found for the field with name [{}]", name);

+ 5 - 3
server/src/test/java/org/elasticsearch/index/fielddata/AbstractFieldDataTestCase.java

@@ -90,9 +90,11 @@ public abstract class AbstractFieldDataTestCase extends ESSingleNodeTestCase {
             if (docValues) {
                 fieldType = new KeywordFieldMapper.Builder(fieldName, IndexVersion.current()).build(context).fieldType();
             } else {
-                fieldType = new TextFieldMapper.Builder(fieldName, createDefaultIndexAnalyzers()).fielddata(true)
-                    .build(context)
-                    .fieldType();
+                fieldType = new TextFieldMapper.Builder(
+                    fieldName,
+                    createDefaultIndexAnalyzers(),
+                    indexService.getIndexSettings().getMode().isSyntheticSourceEnabled()
+                ).fielddata(true).build(context).fieldType();
             }
         } else if (type.equals("float")) {
             fieldType = new NumberFieldMapper.Builder(

+ 20 - 7
server/src/test/java/org/elasticsearch/index/fielddata/FilterFieldDataTests.java

@@ -52,10 +52,11 @@ public class FilterFieldDataTests extends AbstractFieldDataTestCase {
 
         {
             indexService.clearCaches(false, true);
-            MappedFieldType ft = new TextFieldMapper.Builder("high_freq", createDefaultIndexAnalyzers()).fielddata(true)
-                .fielddataFrequencyFilter(0, random.nextBoolean() ? 100 : 0.5d, 0)
-                .build(builderContext)
-                .fieldType();
+            MappedFieldType ft = new TextFieldMapper.Builder(
+                "high_freq",
+                createDefaultIndexAnalyzers(),
+                indexService.getIndexSettings().getMode().isSyntheticSourceEnabled()
+            ).fielddata(true).fielddataFrequencyFilter(0, random.nextBoolean() ? 100 : 0.5d, 0).build(builderContext).fieldType();
             IndexOrdinalsFieldData fieldData = searchExecutionContext.getForField(ft, MappedFieldType.FielddataOperation.SEARCH);
             for (LeafReaderContext context : contexts) {
                 LeafOrdinalsFieldData loadDirect = fieldData.loadDirect(context);
@@ -67,7 +68,11 @@ public class FilterFieldDataTests extends AbstractFieldDataTestCase {
         }
         {
             indexService.clearCaches(false, true);
-            MappedFieldType ft = new TextFieldMapper.Builder("high_freq", createDefaultIndexAnalyzers()).fielddata(true)
+            MappedFieldType ft = new TextFieldMapper.Builder(
+                "high_freq",
+                createDefaultIndexAnalyzers(),
+                indexService.getIndexSettings().getMode().isSyntheticSourceEnabled()
+            ).fielddata(true)
                 .fielddataFrequencyFilter(random.nextBoolean() ? 101 : 101d / 200.0d, 201, 100)
                 .build(builderContext)
                 .fieldType();
@@ -82,7 +87,11 @@ public class FilterFieldDataTests extends AbstractFieldDataTestCase {
 
         {
             indexService.clearCaches(false, true);// test # docs with value
-            MappedFieldType ft = new TextFieldMapper.Builder("med_freq", createDefaultIndexAnalyzers()).fielddata(true)
+            MappedFieldType ft = new TextFieldMapper.Builder(
+                "med_freq",
+                createDefaultIndexAnalyzers(),
+                indexService.getIndexSettings().getMode().isSyntheticSourceEnabled()
+            ).fielddata(true)
                 .fielddataFrequencyFilter(random.nextBoolean() ? 101 : 101d / 200.0d, Integer.MAX_VALUE, 101)
                 .build(builderContext)
                 .fieldType();
@@ -98,7 +107,11 @@ public class FilterFieldDataTests extends AbstractFieldDataTestCase {
 
         {
             indexService.clearCaches(false, true);
-            MappedFieldType ft = new TextFieldMapper.Builder("med_freq", createDefaultIndexAnalyzers()).fielddata(true)
+            MappedFieldType ft = new TextFieldMapper.Builder(
+                "med_freq",
+                createDefaultIndexAnalyzers(),
+                indexService.getIndexSettings().getMode().isSyntheticSourceEnabled()
+            ).fielddata(true)
                 .fielddataFrequencyFilter(random.nextBoolean() ? 101 : 101d / 200.0d, Integer.MAX_VALUE, 101)
                 .build(builderContext)
                 .fieldType();

+ 15 - 9
server/src/test/java/org/elasticsearch/index/fielddata/IndexFieldDataServiceTests.java

@@ -156,12 +156,16 @@ public class IndexFieldDataServiceTests extends ESSingleNodeTestCase {
         );
 
         final MapperBuilderContext context = MapperBuilderContext.root(false, false);
-        final MappedFieldType mapper1 = new TextFieldMapper.Builder("field_1", createDefaultIndexAnalyzers()).fielddata(true)
-            .build(context)
-            .fieldType();
-        final MappedFieldType mapper2 = new TextFieldMapper.Builder("field_2", createDefaultIndexAnalyzers()).fielddata(true)
-            .build(context)
-            .fieldType();
+        final MappedFieldType mapper1 = new TextFieldMapper.Builder(
+            "field_1",
+            createDefaultIndexAnalyzers(),
+            indexService.getIndexSettings().getMode().isSyntheticSourceEnabled()
+        ).fielddata(true).build(context).fieldType();
+        final MappedFieldType mapper2 = new TextFieldMapper.Builder(
+            "field_2",
+            createDefaultIndexAnalyzers(),
+            indexService.getIndexSettings().getMode().isSyntheticSourceEnabled()
+        ).fielddata(true).build(context).fieldType();
         final IndexWriter writer = new IndexWriter(new ByteBuffersDirectory(), new IndexWriterConfig(new KeywordAnalyzer()));
         Document doc = new Document();
         doc.add(new StringField("field_1", "thisisastring", Store.NO));
@@ -223,9 +227,11 @@ public class IndexFieldDataServiceTests extends ESSingleNodeTestCase {
         );
 
         final MapperBuilderContext context = MapperBuilderContext.root(false, false);
-        final MappedFieldType mapper1 = new TextFieldMapper.Builder("s", createDefaultIndexAnalyzers()).fielddata(true)
-            .build(context)
-            .fieldType();
+        final MappedFieldType mapper1 = new TextFieldMapper.Builder(
+            "s",
+            createDefaultIndexAnalyzers(),
+            indexService.getIndexSettings().getMode().isSyntheticSourceEnabled()
+        ).fielddata(true).build(context).fieldType();
         final IndexWriter writer = new IndexWriter(new ByteBuffersDirectory(), new IndexWriterConfig(new KeywordAnalyzer()));
         Document doc = new Document();
         doc.add(new StringField("s", "thisisastring", Store.NO));

+ 4 - 4
server/src/test/java/org/elasticsearch/index/mapper/DocumentParserContextTests.java

@@ -20,9 +20,9 @@ public class DocumentParserContextTests extends ESTestCase {
     private final MapperBuilderContext root = MapperBuilderContext.root(false, false);
 
     public void testDynamicMapperSizeMultipleMappers() {
-        context.addDynamicMapper(new TextFieldMapper.Builder("foo", createDefaultIndexAnalyzers()).build(root));
+        context.addDynamicMapper(new TextFieldMapper.Builder("foo", createDefaultIndexAnalyzers(), false).build(root));
         assertEquals(1, context.getNewFieldsSize());
-        context.addDynamicMapper(new TextFieldMapper.Builder("bar", createDefaultIndexAnalyzers()).build(root));
+        context.addDynamicMapper(new TextFieldMapper.Builder("bar", createDefaultIndexAnalyzers(), false).build(root));
         assertEquals(2, context.getNewFieldsSize());
         context.addDynamicRuntimeField(new TestRuntimeField("runtime1", "keyword"));
         assertEquals(3, context.getNewFieldsSize());
@@ -37,9 +37,9 @@ public class DocumentParserContextTests extends ESTestCase {
     }
 
     public void testDynamicMapperSizeSameFieldMultipleMappers() {
-        context.addDynamicMapper(new TextFieldMapper.Builder("foo", createDefaultIndexAnalyzers()).build(root));
+        context.addDynamicMapper(new TextFieldMapper.Builder("foo", createDefaultIndexAnalyzers(), false).build(root));
         assertEquals(1, context.getNewFieldsSize());
-        context.addDynamicMapper(new TextFieldMapper.Builder("foo", createDefaultIndexAnalyzers()).build(root));
+        context.addDynamicMapper(new TextFieldMapper.Builder("foo", createDefaultIndexAnalyzers(), false).build(root));
         assertEquals(1, context.getNewFieldsSize());
     }
 

+ 72 - 0
server/src/test/java/org/elasticsearch/index/mapper/MultiFieldsTests.java

@@ -0,0 +1,72 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.index.mapper;
+
+import org.elasticsearch.common.lucene.Lucene;
+import org.elasticsearch.index.IndexVersion;
+import org.elasticsearch.index.analysis.IndexAnalyzers;
+import org.elasticsearch.script.ScriptCompiler;
+import org.elasticsearch.test.ESTestCase;
+
+import java.util.Map;
+
+public class MultiFieldsTests extends ESTestCase {
+
+    public void testMultiFieldsBuilderHasSyntheticSourceCompatibleKeywordField() {
+        var isStored = randomBoolean();
+        var hasNormalizer = randomBoolean();
+
+        var builder = new FieldMapper.MultiFields.Builder();
+        assertFalse(builder.hasSyntheticSourceCompatibleKeywordField());
+
+        var keywordFieldMapperBuilder = getKeywordFieldMapperBuilder(isStored, hasNormalizer);
+        builder.add(keywordFieldMapperBuilder);
+
+        var expected = hasNormalizer == false;
+        assertEquals(expected, builder.hasSyntheticSourceCompatibleKeywordField());
+    }
+
+    public void testMultiFieldsBuilderHasSyntheticSourceCompatibleKeywordFieldDuringMerge() {
+        var isStored = randomBoolean();
+        var hasNormalizer = randomBoolean();
+
+        var builder = new TextFieldMapper.Builder("text_field", createDefaultIndexAnalyzers(), false);
+        assertFalse(builder.multiFieldsBuilder.hasSyntheticSourceCompatibleKeywordField());
+
+        var keywordFieldMapperBuilder = getKeywordFieldMapperBuilder(isStored, hasNormalizer);
+
+        var newField = new TextFieldMapper.Builder("text_field", createDefaultIndexAnalyzers(), false).addMultiField(
+            keywordFieldMapperBuilder
+        ).build(MapperBuilderContext.root(false, false));
+
+        builder.merge(newField, new FieldMapper.Conflicts("TextFieldMapper"), MapperMergeContext.root(false, false, Long.MAX_VALUE));
+
+        var expected = hasNormalizer == false;
+        assertEquals(expected, builder.multiFieldsBuilder.hasSyntheticSourceCompatibleKeywordField());
+    }
+
+    private KeywordFieldMapper.Builder getKeywordFieldMapperBuilder(boolean isStored, boolean hasNormalizer) {
+        var keywordFieldMapperBuilder = new KeywordFieldMapper.Builder(
+            "field",
+            IndexAnalyzers.of(Map.of(), Map.of("normalizer", Lucene.STANDARD_ANALYZER), Map.of()),
+            ScriptCompiler.NONE,
+            IndexVersion.current()
+        );
+        if (isStored) {
+            keywordFieldMapperBuilder.stored(true);
+            if (randomBoolean()) {
+                keywordFieldMapperBuilder.docValues(false);
+            }
+        }
+        if (hasNormalizer) {
+            keywordFieldMapperBuilder.normalizer("normalizer");
+        }
+        return keywordFieldMapperBuilder;
+    }
+}

+ 3 - 3
server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperMergeTests.java

@@ -27,10 +27,10 @@ public final class ObjectMapperMergeTests extends ESTestCase {
         rootBuilder.add(new ObjectMapper.Builder("disabled", Explicit.IMPLICIT_TRUE).enabled(disabledFieldEnabled));
         ObjectMapper.Builder fooBuilder = new ObjectMapper.Builder("foo", Explicit.IMPLICIT_TRUE).enabled(fooFieldEnabled);
         if (includeBarField) {
-            fooBuilder.add(new TextFieldMapper.Builder("bar", createDefaultIndexAnalyzers()));
+            fooBuilder.add(new TextFieldMapper.Builder("bar", createDefaultIndexAnalyzers(), false));
         }
         if (includeBazField) {
-            fooBuilder.add(new TextFieldMapper.Builder("baz", createDefaultIndexAnalyzers()));
+            fooBuilder.add(new TextFieldMapper.Builder("baz", createDefaultIndexAnalyzers(), false));
         }
         rootBuilder.add(fooBuilder);
         return rootBuilder.build(MapperBuilderContext.root(false, false));
@@ -366,7 +366,7 @@ public final class ObjectMapperMergeTests extends ESTestCase {
     }
 
     private TextFieldMapper.Builder createTextKeywordMultiField(String name, String multiFieldName) {
-        TextFieldMapper.Builder builder = new TextFieldMapper.Builder(name, createDefaultIndexAnalyzers());
+        TextFieldMapper.Builder builder = new TextFieldMapper.Builder(name, createDefaultIndexAnalyzers(), false);
         builder.multiFieldsBuilder.add(new KeywordFieldMapper.Builder(multiFieldName, IndexVersion.current()));
         return builder;
     }

+ 2 - 2
server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperTests.java

@@ -530,11 +530,11 @@ public class ObjectMapperTests extends MapperServiceTestCase {
     public void testNestedObjectWithMultiFieldsgetTotalFieldsCount() {
         ObjectMapper.Builder mapperBuilder = new ObjectMapper.Builder("parent_size_1", Explicit.IMPLICIT_TRUE).add(
             new ObjectMapper.Builder("child_size_2", Explicit.IMPLICIT_TRUE).add(
-                new TextFieldMapper.Builder("grand_child_size_3", createDefaultIndexAnalyzers()).addMultiField(
+                new TextFieldMapper.Builder("grand_child_size_3", createDefaultIndexAnalyzers(), false).addMultiField(
                     new KeywordFieldMapper.Builder("multi_field_size_4", IndexVersion.current())
                 )
                     .addMultiField(
-                        new TextFieldMapper.Builder("grand_child_size_5", createDefaultIndexAnalyzers()).addMultiField(
+                        new TextFieldMapper.Builder("grand_child_size_5", createDefaultIndexAnalyzers(), false).addMultiField(
                             new KeywordFieldMapper.Builder("multi_field_of_multi_field_size_6", IndexVersion.current())
                         )
                     )

+ 13 - 0
server/src/test/java/org/elasticsearch/index/mapper/TextFieldAnalyzerModeTests.java

@@ -13,6 +13,7 @@ import org.apache.lucene.analysis.TokenStream;
 import org.apache.lucene.analysis.standard.StandardAnalyzer;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.IndexVersion;
 import org.elasticsearch.index.analysis.AbstractTokenFilterFactory;
 import org.elasticsearch.index.analysis.AnalysisMode;
@@ -67,6 +68,9 @@ public class TextFieldAnalyzerModeTests extends ESTestCase {
         fieldNode.put("analyzer", "my_analyzer");
         MappingParserContext parserContext = mock(MappingParserContext.class);
         when(parserContext.indexVersionCreated()).thenReturn(IndexVersion.current());
+        when(parserContext.getIndexSettings()).thenReturn(
+            new IndexSettings(IndexMetadata.builder("index").settings(indexSettings(IndexVersion.current(), 1, 0)).build(), Settings.EMPTY)
+        );
 
         // check AnalysisMode.ALL works
         Map<String, NamedAnalyzer> analyzers = defaultAnalyzers();
@@ -102,6 +106,12 @@ public class TextFieldAnalyzerModeTests extends ESTestCase {
             }
             MappingParserContext parserContext = mock(MappingParserContext.class);
             when(parserContext.indexVersionCreated()).thenReturn(IndexVersion.current());
+            when(parserContext.getIndexSettings()).thenReturn(
+                new IndexSettings(
+                    IndexMetadata.builder("index").settings(indexSettings(IndexVersion.current(), 1, 0)).build(),
+                    Settings.EMPTY
+                )
+            );
 
             // check AnalysisMode.ALL and AnalysisMode.SEARCH_TIME works
             Map<String, NamedAnalyzer> analyzers = defaultAnalyzers();
@@ -143,6 +153,9 @@ public class TextFieldAnalyzerModeTests extends ESTestCase {
         fieldNode.put("analyzer", "my_analyzer");
         MappingParserContext parserContext = mock(MappingParserContext.class);
         when(parserContext.indexVersionCreated()).thenReturn(IndexVersion.current());
+        when(parserContext.getIndexSettings()).thenReturn(
+            new IndexSettings(IndexMetadata.builder("index").settings(indexSettings(IndexVersion.current(), 1, 0)).build(), Settings.EMPTY)
+        );
 
         // check that "analyzer" set to AnalysisMode.INDEX_TIME is blocked if there is no search analyzer
         AnalysisMode mode = AnalysisMode.INDEX_TIME;

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

@@ -44,9 +44,11 @@ import org.apache.lucene.tests.analysis.CannedTokenStream;
 import org.apache.lucene.tests.analysis.MockSynonymAnalyzer;
 import org.apache.lucene.tests.analysis.Token;
 import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.lucene.search.MultiPhrasePrefixQuery;
 import org.elasticsearch.core.CheckedConsumer;
+import org.elasticsearch.index.IndexMode;
 import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.IndexVersion;
 import org.elasticsearch.index.analysis.AnalyzerScope;
@@ -249,6 +251,64 @@ public class TextFieldMapperTests extends MapperTestCase {
         assertEquals(DocValuesType.NONE, fieldType.docValuesType());
     }
 
+    public void testStoreParameterDefaults() throws IOException {
+        var timeSeriesIndexMode = randomBoolean();
+        var isStored = randomBoolean();
+        var hasKeywordFieldForSyntheticSource = randomBoolean();
+
+        var indexSettingsBuilder = getIndexSettingsBuilder();
+        if (timeSeriesIndexMode) {
+            indexSettingsBuilder.put(IndexSettings.MODE.getKey(), IndexMode.TIME_SERIES)
+                .putList(IndexMetadata.INDEX_ROUTING_PATH.getKey(), "dimension")
+                .put(IndexSettings.TIME_SERIES_START_TIME.getKey(), "2000-01-08T23:40:53.384Z")
+                .put(IndexSettings.TIME_SERIES_END_TIME.getKey(), "2106-01-08T23:40:53.384Z");
+        }
+        var indexSettings = indexSettingsBuilder.build();
+
+        var mapping = mapping(b -> {
+            b.startObject("field");
+            b.field("type", "text");
+            if (isStored) {
+                b.field("store", isStored);
+            }
+            if (hasKeywordFieldForSyntheticSource) {
+                b.startObject("fields");
+                b.startObject("keyword");
+                b.field("type", "keyword");
+                b.endObject();
+                b.endObject();
+            }
+            b.endObject();
+
+            if (timeSeriesIndexMode) {
+                b.startObject("@timestamp");
+                b.field("type", "date");
+                b.endObject();
+                b.startObject("dimension");
+                b.field("type", "keyword");
+                b.field("time_series_dimension", "true");
+                b.endObject();
+            }
+        });
+        DocumentMapper mapper = createMapperService(getVersion(), indexSettings, () -> true, mapping).documentMapper();
+
+        var source = source(TimeSeriesRoutingHashFieldMapper.DUMMY_ENCODED_VALUE, b -> {
+            b.field("field", "1234");
+            if (timeSeriesIndexMode) {
+                b.field("@timestamp", randomMillisUpToYear9999());
+                b.field("dimension", "dimension1");
+            }
+        }, null);
+        ParsedDocument doc = mapper.parse(source);
+        List<IndexableField> fields = doc.rootDoc().getFields("field");
+        IndexableFieldType fieldType = fields.get(0).fieldType();
+        if (isStored || (timeSeriesIndexMode && hasKeywordFieldForSyntheticSource == false)) {
+            assertTrue(fieldType.stored());
+        } else {
+            assertFalse(fieldType.stored());
+        }
+    }
+
     public void testBWCSerialization() throws IOException {
         MapperService mapperService = createMapperService(fieldMapping(b -> {
             b.field("type", "text");
@@ -1138,7 +1198,8 @@ public class TextFieldMapperTests extends MapperTestCase {
                         delegate.expectedForSyntheticSource(),
                         delegate.expectedForBlockLoader(),
                         b -> {
-                            b.field("type", "text").field("store", true);
+                            b.field("type", "text");
+                            b.field("store", true);
                             if (indexText == false) {
                                 b.field("index", false);
                             }
@@ -1196,6 +1257,17 @@ public class TextFieldMapperTests extends MapperTestCase {
                             b.endObject();
                         }
                         b.endObject();
+                    }),
+                    new SyntheticSourceInvalidExample(err, b -> {
+                        b.field("type", "text");
+                        b.startObject("fields");
+                        {
+                            b.startObject("kwd");
+                            b.field("type", "keyword");
+                            b.field("doc_values", "false");
+                            b.endObject();
+                        }
+                        b.endObject();
                     })
                 );
             }

+ 5 - 1
server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightBuilderTests.java

@@ -317,7 +317,11 @@ public class HighlightBuilderTests extends ESTestCase {
         ) {
             @Override
             public MappedFieldType getFieldType(String name) {
-                TextFieldMapper.Builder builder = new TextFieldMapper.Builder(name, createDefaultIndexAnalyzers());
+                TextFieldMapper.Builder builder = new TextFieldMapper.Builder(
+                    name,
+                    createDefaultIndexAnalyzers(),
+                    idxSettings.getMode().isSyntheticSourceEnabled()
+                );
                 return builder.build(MapperBuilderContext.root(false, false)).fieldType();
             }
         };

+ 10 - 2
server/src/test/java/org/elasticsearch/search/rescore/QueryRescorerBuilderTests.java

@@ -160,7 +160,11 @@ public class QueryRescorerBuilderTests extends ESTestCase {
         ) {
             @Override
             public MappedFieldType getFieldType(String name) {
-                TextFieldMapper.Builder builder = new TextFieldMapper.Builder(name, createDefaultIndexAnalyzers());
+                TextFieldMapper.Builder builder = new TextFieldMapper.Builder(
+                    name,
+                    createDefaultIndexAnalyzers(),
+                    idxSettings.getMode().isSyntheticSourceEnabled()
+                );
                 return builder.build(MapperBuilderContext.root(false, false)).fieldType();
             }
         };
@@ -222,7 +226,11 @@ public class QueryRescorerBuilderTests extends ESTestCase {
         ) {
             @Override
             public MappedFieldType getFieldType(String name) {
-                TextFieldMapper.Builder builder = new TextFieldMapper.Builder(name, createDefaultIndexAnalyzers());
+                TextFieldMapper.Builder builder = new TextFieldMapper.Builder(
+                    name,
+                    createDefaultIndexAnalyzers(),
+                    idxSettings.getMode().isSyntheticSourceEnabled()
+                );
                 return builder.build(MapperBuilderContext.root(false, false)).fieldType();
             }
         };