Bläddra i källkod

Allow querying index_mode (#110676)

This change allows querying the `index.mode` setting via a new 
`_index_mode` metadata field, enabling APIs such as `field_caps` or
`resolve_indices` to target indices that are either time_series or logs
only. This approach avoids adding and handling a new parameter for
`index_mode` in these APIs. Both ES|QL and the `_search` API should also
work with this new field.
Nhat Nguyen 1 år sedan
förälder
incheckning
1964be565c
32 ändrade filer med 469 tillägg och 45 borttagningar
  1. 6 0
      benchmarks/src/main/java/org/elasticsearch/benchmark/compute/operator/ValuesSourceReaderBenchmark.java
  2. 5 0
      docs/changelog/110676.yaml
  3. 1 0
      modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java
  4. 59 0
      rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/field_caps/70_index_mode.yml
  5. 24 23
      rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/nodes.stats/11_indices_metrics.yml
  6. 17 0
      rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/tsdb/40_search.yml
  7. 5 6
      server/src/main/java/org/elasticsearch/index/fielddata/FieldDataContext.java
  8. 7 1
      server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java
  9. 119 0
      server/src/main/java/org/elasticsearch/index/mapper/IndexModeFieldMapper.java
  10. 6 0
      server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java
  11. 2 1
      server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java
  12. 8 1
      server/src/main/java/org/elasticsearch/index/query/SearchExecutionContext.java
  13. 2 0
      server/src/main/java/org/elasticsearch/indices/IndicesModule.java
  14. 1 1
      server/src/test/java/org/elasticsearch/index/IndexSortSettingsTests.java
  15. 4 1
      server/src/test/java/org/elasticsearch/index/fielddata/IndexFieldDataServiceTests.java
  16. 1 1
      server/src/test/java/org/elasticsearch/index/mapper/CompositeRuntimeFieldTests.java
  17. 2 0
      server/src/test/java/org/elasticsearch/index/mapper/DocumentMapperTests.java
  18. 98 0
      server/src/test/java/org/elasticsearch/index/mapper/IndexModeFieldTypeTests.java
  19. 2 2
      server/src/test/java/org/elasticsearch/index/mapper/TextFieldMapperTests.java
  20. 2 2
      server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldTypeTests.java
  21. 2 0
      server/src/test/java/org/elasticsearch/indices/IndicesModuleTests.java
  22. 9 2
      test/framework/src/main/java/org/elasticsearch/index/mapper/AbstractScriptFieldTypeTestCase.java
  23. 1 1
      test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java
  24. 12 1
      test/framework/src/main/java/org/elasticsearch/index/mapper/MapperTestCase.java
  25. 1 0
      test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java
  26. 4 1
      x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/MetadataAttribute.java
  27. 6 0
      x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/ValueSourceReaderTypeConversionTests.java
  28. 6 0
      x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/ValuesSourceReaderOperatorTests.java
  29. 44 0
      x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/TimeSeriesIT.java
  30. 6 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsPhysicalOperationProviders.java
  31. 1 1
      x-pack/plugin/mapper-aggregate-metric/src/test/java/org/elasticsearch/xpack/aggregatemetric/mapper/AggregateDoubleMetricFieldTypeTests.java
  32. 6 0
      x-pack/plugin/mapper-constant-keyword/src/test/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldMapperTests.java

+ 6 - 0
benchmarks/src/main/java/org/elasticsearch/benchmark/compute/operator/ValuesSourceReaderBenchmark.java

@@ -41,6 +41,7 @@ import org.elasticsearch.compute.lucene.LuceneSourceOperator;
 import org.elasticsearch.compute.lucene.ValuesSourceReaderOperator;
 import org.elasticsearch.compute.operator.topn.TopNOperator;
 import org.elasticsearch.core.IOUtils;
+import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.IndexVersion;
 import org.elasticsearch.index.mapper.BlockLoader;
 import org.elasticsearch.index.mapper.FieldNamesFieldMapper;
@@ -189,6 +190,11 @@ public class ValuesSourceReaderBenchmark {
                     return "benchmark";
                 }
 
+                @Override
+                public IndexSettings indexSettings() {
+                    throw new UnsupportedOperationException();
+                }
+
                 @Override
                 public MappedFieldType.FieldExtractPreference fieldExtractPreference() {
                     return MappedFieldType.FieldExtractPreference.NONE;

+ 5 - 0
docs/changelog/110676.yaml

@@ -0,0 +1,5 @@
+pr: 110676
+summary: Allow querying `index_mode`
+area: Mapping
+type: enhancement
+issues: []

+ 1 - 0
modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java

@@ -655,6 +655,7 @@ public class PercolateQueryBuilder extends AbstractQueryBuilder<PercolateQueryBu
                 IndexFieldData.Builder builder = fieldType.fielddataBuilder(
                     new FieldDataContext(
                         delegate.getFullyQualifiedIndex().getName(),
+                        delegate.getIndexSettings(),
                         delegate::lookup,
                         this::sourcePath,
                         fielddataOperation

+ 59 - 0
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/field_caps/70_index_mode.yml

@@ -0,0 +1,59 @@
+---
+setup:
+  - requires:
+      cluster_features: "mapper.query_index_mode"
+      reason: "require index_mode"
+
+  - do:
+      indices.create:
+        index: test_metrics
+        body:
+          settings:
+            index:
+              mode: time_series
+              routing_path: [container]
+              time_series:
+                start_time: 2021-04-28T00:00:00Z
+                end_time: 2021-04-29T00:00:00Z
+          mappings:
+            properties:
+              "@timestamp":
+                type: date
+              container:
+                type: keyword
+                time_series_dimension: true
+
+  - do:
+      indices.create:
+        index: test
+        body:
+          mappings:
+            properties:
+              "@timestamp":
+                type: date
+
+---
+Field-caps:
+  - do:
+      field_caps:
+        index: "test*"
+        fields: "*"
+        body: { index_filter: { term: { _index_mode: "time_series" } } }
+  - match: { indices: [ "test_metrics" ] }
+  - do:
+      field_caps:
+        index: "test*"
+        fields: "*"
+        body: { index_filter: { term: { _index_mode: "logs" } } }
+  - match: { indices: [ ] }
+  - do:
+      field_caps:
+        index: "test*"
+        fields: "*"
+        body: { index_filter: { term: { _index_mode: "standard" } } }
+  - match: { indices: [ "test" ] }
+  - do:
+      field_caps:
+        index: "test*"
+        fields: "*"
+  - match: { indices: [ "test" , "test_metrics" ] }

+ 24 - 23
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/nodes.stats/11_indices_metrics.yml

@@ -417,7 +417,7 @@
 
   - requires:
       test_runner_features: [arbitrary_key]
-      cluster_features: ["mapper.track_ignored_source"]
+      cluster_features: ["mapper.query_index_mode"]
       reason:  "_ignored_source added to mappings"
 
   - do:
@@ -478,34 +478,35 @@
   # 6. _ignored
   # 7. _ignored_source
   # 8. _index
-  # 9. _nested_path
-  # 10. _routing
-  # 11. _seq_no
-  # 12. _source
-  # 13. _tier
-  # 14. _version
-  # 15. @timestamp
-  # 16. authors.age
-  # 17. authors.company
-  # 18. authors.company.keyword
-  # 19. authors.name.last_name
-  # 20. authors.name.first_name
-  # 21. authors.name.full_name
-  # 22. link
-  # 23. title
-  # 24. url
+  # 9. _index_mode
+  # 10. _nested_path
+  # 11. _routing
+  # 12. _seq_no
+  # 13. _source
+  # 14. _tier
+  # 15. _version
+  # 16. @timestamp
+  # 17. authors.age
+  # 18. authors.company
+  # 19. authors.company.keyword
+  # 20. authors.name.last_name
+  # 21. authors.name.first_name
+  # 22. authors.name.full_name
+  # 23. link
+  # 24. title
+  # 25. url
   # Object mappers:
-  # 25. authors
-  # 26. authors.name
+  # 26. authors
+  # 27. authors.name
   # Runtime field mappers:
-  # 27. a_source_field
+  # 28. a_source_field
 
-  - gte: { nodes.$node_id.indices.mappings.total_count: 27 }
+  - gte: { nodes.$node_id.indices.mappings.total_count: 28 }
   - is_true: nodes.$node_id.indices.mappings.total_estimated_overhead
   - gte: { nodes.$node_id.indices.mappings.total_estimated_overhead_in_bytes: 26624 }
-  - match: { nodes.$node_id.indices.indices.index1.mappings.total_count: 27 }
+  - match: { nodes.$node_id.indices.indices.index1.mappings.total_count: 28 }
   - is_true: nodes.$node_id.indices.indices.index1.mappings.total_estimated_overhead
-  - match: { nodes.$node_id.indices.indices.index1.mappings.total_estimated_overhead_in_bytes: 27648 }
+  - match: { nodes.$node_id.indices.indices.index1.mappings.total_estimated_overhead_in_bytes: 28672 }
 
 ---
 "indices mappings does not exist in shards level":

+ 17 - 0
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/tsdb/40_search.yml

@@ -337,3 +337,20 @@ sort by tsid:
 
   - match: {hits.hits.7.sort: ["KCjEJ9R_BgO8TRX2QOd6dpR12oDh--qoyNZRQPy43y34Qdy2dpsyG0o", 1619635864467]}
   - match: {hits.hits.7.fields._tsid: [ "KCjEJ9R_BgO8TRX2QOd6dpR12oDh--qoyNZRQPy43y34Qdy2dpsyG0o"]}
+
+---
+aggs by index_mode:
+  - requires:
+      cluster_features: ["mapper.query_index_mode"]
+      reason: require _index_mode metadata field
+  - do:
+      search:
+        index: test
+        body:
+          aggs:
+            modes:
+              terms:
+                field: "_index_mode"
+  - match: {aggregations.modes.buckets.0.key: "time_series"}
+  - match: {aggregations.modes.buckets.0.doc_count: 8}
+

+ 5 - 6
server/src/main/java/org/elasticsearch/index/fielddata/FieldDataContext.java

@@ -8,6 +8,7 @@
 
 package org.elasticsearch.index.fielddata;
 
+import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.mapper.MappedFieldType;
 import org.elasticsearch.search.lookup.SearchLookup;
 
@@ -25,6 +26,7 @@ import java.util.function.Supplier;
  */
 public record FieldDataContext(
     String fullyQualifiedIndexName,
+    IndexSettings indexSettings,
     Supplier<SearchLookup> lookupSupplier,
     Function<String, Set<String>> sourcePathsLookup,
     MappedFieldType.FielddataOperation fielddataOperation
@@ -38,11 +40,8 @@ public record FieldDataContext(
      * @param reason the reason that runtime fields are not supported
      */
     public static FieldDataContext noRuntimeFields(String reason) {
-        return new FieldDataContext(
-            "",
-            () -> { throw new UnsupportedOperationException("Runtime fields not supported for [" + reason + "]"); },
-            Set::of,
-            MappedFieldType.FielddataOperation.SEARCH
-        );
+        return new FieldDataContext("", null, () -> {
+            throw new UnsupportedOperationException("Runtime fields not supported for [" + reason + "]");
+        }, Set::of, MappedFieldType.FielddataOperation.SEARCH);
     }
 }

+ 7 - 1
server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java

@@ -161,7 +161,13 @@ public final class DocumentParser {
         SearchLookup searchLookup = new SearchLookup(
             context.mappingLookup().indexTimeLookup()::get,
             (ft, lookup, fto) -> ft.fielddataBuilder(
-                new FieldDataContext(context.indexSettings().getIndex().getName(), lookup, context.mappingLookup()::sourcePaths, fto)
+                new FieldDataContext(
+                    context.indexSettings().getIndex().getName(),
+                    context.indexSettings(),
+                    lookup,
+                    context.mappingLookup()::sourcePaths,
+                    fto
+                )
             ).build(new IndexFieldDataCache.None(), new NoneCircuitBreakerService()),
             (ctx, doc) -> Source.fromBytes(context.sourceToParse().source())
         );

+ 119 - 0
server/src/main/java/org/elasticsearch/index/mapper/IndexModeFieldMapper.java

@@ -0,0 +1,119 @@
+/*
+ * 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.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.regex.Regex;
+import org.elasticsearch.features.NodeFeature;
+import org.elasticsearch.index.fielddata.FieldData;
+import org.elasticsearch.index.fielddata.FieldDataContext;
+import org.elasticsearch.index.fielddata.IndexFieldData;
+import org.elasticsearch.index.fielddata.ScriptDocValues;
+import org.elasticsearch.index.fielddata.plain.ConstantIndexFieldData;
+import org.elasticsearch.index.query.QueryRewriteContext;
+import org.elasticsearch.index.query.SearchExecutionContext;
+import org.elasticsearch.script.field.DelegateDocValuesField;
+import org.elasticsearch.search.aggregations.support.CoreValuesSourceType;
+import org.elasticsearch.search.fetch.StoredFieldsSpec;
+import org.elasticsearch.search.lookup.Source;
+
+import java.util.Collections;
+import java.util.List;
+
+public class IndexModeFieldMapper extends MetadataFieldMapper {
+
+    static final NodeFeature QUERYING_INDEX_MODE = new NodeFeature("mapper.query_index_mode");
+
+    public static final String NAME = "_index_mode";
+
+    public static final String CONTENT_TYPE = "_index_mode";
+
+    private static final IndexModeFieldMapper INSTANCE = new IndexModeFieldMapper();
+
+    public static final TypeParser PARSER = new FixedTypeParser(c -> INSTANCE);
+
+    static final class IndexModeFieldType extends ConstantFieldType {
+
+        static final IndexModeFieldType INSTANCE = new IndexModeFieldType();
+
+        private IndexModeFieldType() {
+            super(NAME, Collections.emptyMap());
+        }
+
+        @Override
+        public String typeName() {
+            return CONTENT_TYPE;
+        }
+
+        @Override
+        protected boolean matches(String pattern, boolean caseInsensitive, QueryRewriteContext context) {
+            final String indexMode = context.getIndexSettings().getMode().getName();
+            return Regex.simpleMatch(pattern, indexMode, caseInsensitive);
+        }
+
+        @Override
+        public Query existsQuery(SearchExecutionContext context) {
+            return new MatchAllDocsQuery();
+        }
+
+        @Override
+        public IndexFieldData.Builder fielddataBuilder(FieldDataContext fieldDataContext) {
+            final String indexMode = fieldDataContext.indexSettings().getMode().getName();
+            return new ConstantIndexFieldData.Builder(
+                indexMode,
+                name(),
+                CoreValuesSourceType.KEYWORD,
+                (dv, n) -> new DelegateDocValuesField(
+                    new ScriptDocValues.Strings(new ScriptDocValues.StringsSupplier(FieldData.toString(dv))),
+                    n
+                )
+            );
+        }
+
+        @Override
+        public BlockLoader blockLoader(BlockLoaderContext blContext) {
+            final String indexMode = blContext.indexSettings().getMode().getName();
+            return BlockLoader.constantBytes(new BytesRef(indexMode));
+        }
+
+        @Override
+        public ValueFetcher valueFetcher(SearchExecutionContext context, String format) {
+            return new ValueFetcher() {
+                private final List<Object> indexMode = List.of(context.getIndexSettings().getMode().getName());
+
+                @Override
+                public List<Object> fetchValues(Source source, int doc, List<Object> ignoredValues) {
+                    return indexMode;
+                }
+
+                @Override
+                public StoredFieldsSpec storedFieldsSpec() {
+                    return StoredFieldsSpec.NO_REQUIREMENTS;
+                }
+            };
+        }
+
+    }
+
+    public IndexModeFieldMapper() {
+        super(IndexModeFieldType.INSTANCE);
+    }
+
+    @Override
+    protected String contentType() {
+        return CONTENT_TYPE;
+    }
+
+    @Override
+    public SourceLoader.SyntheticFieldLoader syntheticFieldLoader() {
+        return SourceLoader.SyntheticFieldLoader.NOTHING;
+    }
+}

+ 6 - 0
server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java

@@ -35,6 +35,7 @@ import org.elasticsearch.common.geo.ShapeRelation;
 import org.elasticsearch.common.time.DateMathParser;
 import org.elasticsearch.common.unit.Fuzziness;
 import org.elasticsearch.core.Nullable;
+import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.fielddata.FieldDataContext;
 import org.elasticsearch.index.fielddata.IndexFieldData;
 import org.elasticsearch.index.query.DistanceFeatureQueryBuilder;
@@ -693,6 +694,11 @@ public abstract class MappedFieldType {
          */
         String indexName();
 
+        /**
+         * The index settings of the index
+         */
+        IndexSettings indexSettings();
+
         /**
          * How the field should be extracted into the BlockLoader. The default is {@link FieldExtractPreference#NONE}, which means
          * that the field type can choose where to load the field from. However, in some cases, the caller may have a preference.

+ 2 - 1
server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java

@@ -28,7 +28,8 @@ public class MapperFeatures implements FeatureSpecification {
             DenseVectorFieldMapper.INT4_QUANTIZATION,
             DenseVectorFieldMapper.BIT_VECTORS,
             DocumentMapper.INDEX_SORTING_ON_NESTED,
-            KeywordFieldMapper.KEYWORD_DIMENSION_IGNORE_ABOVE
+            KeywordFieldMapper.KEYWORD_DIMENSION_IGNORE_ABOVE,
+            IndexModeFieldMapper.QUERYING_INDEX_MODE
         );
     }
 }

+ 8 - 1
server/src/main/java/org/elasticsearch/index/query/SearchExecutionContext.java

@@ -338,6 +338,7 @@ public class SearchExecutionContext extends QueryRewriteContext {
             fieldType,
             new FieldDataContext(
                 getFullyQualifiedIndex().getName(),
+                getIndexSettings(),
                 () -> this.lookup().forkAndTrackFieldReferences(fieldType.name()),
                 this::sourcePath,
                 fielddataOperation
@@ -514,7 +515,13 @@ public class SearchExecutionContext extends QueryRewriteContext {
             this::getFieldType,
             (fieldType, searchLookup, fielddataOperation) -> indexFieldDataLookup.apply(
                 fieldType,
-                new FieldDataContext(getFullyQualifiedIndex().getName(), searchLookup, this::sourcePath, fielddataOperation)
+                new FieldDataContext(
+                    getFullyQualifiedIndex().getName(),
+                    getIndexSettings(),
+                    searchLookup,
+                    this::sourcePath,
+                    fielddataOperation
+                )
             ),
             sourceProvider,
             fieldLookupProvider

+ 2 - 0
server/src/main/java/org/elasticsearch/indices/IndicesModule.java

@@ -41,6 +41,7 @@ import org.elasticsearch.index.mapper.IdFieldMapper;
 import org.elasticsearch.index.mapper.IgnoredFieldMapper;
 import org.elasticsearch.index.mapper.IgnoredSourceFieldMapper;
 import org.elasticsearch.index.mapper.IndexFieldMapper;
+import org.elasticsearch.index.mapper.IndexModeFieldMapper;
 import org.elasticsearch.index.mapper.IpFieldMapper;
 import org.elasticsearch.index.mapper.IpScriptFieldType;
 import org.elasticsearch.index.mapper.KeywordFieldMapper;
@@ -258,6 +259,7 @@ public class IndicesModule extends AbstractModule {
         builtInMetadataMappers.put(TimeSeriesIdFieldMapper.NAME, TimeSeriesIdFieldMapper.PARSER);
         builtInMetadataMappers.put(TimeSeriesRoutingHashFieldMapper.NAME, TimeSeriesRoutingHashFieldMapper.PARSER);
         builtInMetadataMappers.put(IndexFieldMapper.NAME, IndexFieldMapper.PARSER);
+        builtInMetadataMappers.put(IndexModeFieldMapper.NAME, IndexModeFieldMapper.PARSER);
         builtInMetadataMappers.put(SourceFieldMapper.NAME, SourceFieldMapper.PARSER);
         builtInMetadataMappers.put(IgnoredSourceFieldMapper.NAME, IgnoredSourceFieldMapper.PARSER);
         builtInMetadataMappers.put(NestedPathFieldMapper.NAME, NestedPathFieldMapper.PARSER);

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

@@ -219,7 +219,7 @@ public class IndexSortSettingsTests extends ESTestCase {
             lookup::get,
             (ft, s) -> indexFieldDataService.getForField(
                 ft,
-                new FieldDataContext("test", s, Set::of, MappedFieldType.FielddataOperation.SEARCH)
+                new FieldDataContext("test", indexSettings, s, Set::of, MappedFieldType.FielddataOperation.SEARCH)
             )
         );
     }

+ 4 - 1
server/src/test/java/org/elasticsearch/index/fielddata/IndexFieldDataServiceTests.java

@@ -141,7 +141,10 @@ public class IndexFieldDataServiceTests extends ESSingleNodeTestCase {
             return (IndexFieldData.Builder) (cache, breakerService) -> null;
         });
         SearchLookup searchLookup = new SearchLookup(null, null, (ctx, doc) -> null);
-        ifdService.getForField(ft, new FieldDataContext("qualified", () -> searchLookup, null, MappedFieldType.FielddataOperation.SEARCH));
+        ifdService.getForField(
+            ft,
+            new FieldDataContext("qualified", null, () -> searchLookup, null, MappedFieldType.FielddataOperation.SEARCH)
+        );
         assertSame(searchLookup, searchLookupSetOnce.get().get());
     }
 

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

@@ -343,7 +343,7 @@ public class CompositeRuntimeFieldTests extends MapperServiceTestCase {
             SearchLookup searchLookup = new SearchLookup(
                 mapperService::fieldType,
                 (mft, lookupSupplier, fdo) -> mft.fielddataBuilder(
-                    new FieldDataContext("test", lookupSupplier, mapperService.mappingLookup()::sourcePaths, fdo)
+                    new FieldDataContext("test", null, lookupSupplier, mapperService.mappingLookup()::sourcePaths, fdo)
                 ).build(null, null),
                 SourceProvider.fromStoredFields()
             );

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

@@ -327,6 +327,7 @@ public class DocumentMapperTests extends MapperServiceTestCase {
                 .item(IgnoredFieldMapper.class)
                 .item(IgnoredSourceFieldMapper.class)
                 .item(IndexFieldMapper.class)
+                .item(IndexModeFieldMapper.class)
                 .item(NestedPathFieldMapper.class)
                 .item(ProvidedIdFieldMapper.class)
                 .item(RoutingFieldMapper.class)
@@ -345,6 +346,7 @@ public class DocumentMapperTests extends MapperServiceTestCase {
                 .item(IgnoredFieldMapper.CONTENT_TYPE)
                 .item(IgnoredSourceFieldMapper.NAME)
                 .item(IndexFieldMapper.CONTENT_TYPE)
+                .item(IndexModeFieldMapper.CONTENT_TYPE)
                 .item(NestedPathFieldMapper.NAME)
                 .item(RoutingFieldMapper.CONTENT_TYPE)
                 .item(SeqNoFieldMapper.CONTENT_TYPE)

+ 98 - 0
server/src/test/java/org/elasticsearch/index/mapper/IndexModeFieldTypeTests.java

@@ -0,0 +1,98 @@
+/*
+ * 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.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.search.MatchNoDocsQuery;
+import org.apache.lucene.search.Query;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.common.regex.Regex;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.index.IndexMode;
+import org.elasticsearch.index.IndexSettings;
+import org.elasticsearch.index.IndexVersion;
+import org.elasticsearch.index.query.SearchExecutionContext;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Predicate;
+
+public class IndexModeFieldTypeTests extends ConstantFieldTypeTestCase {
+
+    public void testTermQuery() {
+        MappedFieldType ft = getMappedFieldType();
+        for (IndexMode mode : IndexMode.values()) {
+            SearchExecutionContext context = createContext(mode);
+            for (IndexMode other : IndexMode.values()) {
+                Query query = ft.termQuery(other.getName(), context);
+                if (other.equals(mode)) {
+                    assertEquals(new MatchAllDocsQuery(), query);
+                } else {
+                    assertEquals(new MatchNoDocsQuery(), query);
+                }
+            }
+        }
+    }
+
+    public void testWildcardQuery() {
+        MappedFieldType ft = getMappedFieldType();
+
+        assertEquals(new MatchAllDocsQuery(), ft.wildcardQuery("stand*", null, createContext(IndexMode.STANDARD)));
+        assertEquals(new MatchNoDocsQuery(), ft.wildcardQuery("stand*", null, createContext(IndexMode.TIME_SERIES)));
+        assertEquals(new MatchNoDocsQuery(), ft.wildcardQuery("stand*", null, createContext(IndexMode.LOGS)));
+
+        assertEquals(new MatchNoDocsQuery(), ft.wildcardQuery("time*", null, createContext(IndexMode.STANDARD)));
+        assertEquals(new MatchAllDocsQuery(), ft.wildcardQuery("time*", null, createContext(IndexMode.TIME_SERIES)));
+        assertEquals(new MatchNoDocsQuery(), ft.wildcardQuery("time*", null, createContext(IndexMode.LOGS)));
+
+        assertEquals(new MatchNoDocsQuery(), ft.wildcardQuery("logs*", null, createContext(IndexMode.STANDARD)));
+        assertEquals(new MatchNoDocsQuery(), ft.wildcardQuery("logs*", null, createContext(IndexMode.TIME_SERIES)));
+        assertEquals(new MatchAllDocsQuery(), ft.wildcardQuery("logs*", null, createContext(IndexMode.LOGS)));
+    }
+
+    @Override
+    public MappedFieldType getMappedFieldType() {
+        return IndexModeFieldMapper.IndexModeFieldType.INSTANCE;
+    }
+
+    private SearchExecutionContext createContext(IndexMode mode) {
+        Settings.Builder settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current());
+        if (mode != null) {
+            settings.put(IndexSettings.MODE.getKey(), mode);
+        }
+        if (mode == IndexMode.TIME_SERIES) {
+            settings.putList(IndexMetadata.INDEX_ROUTING_PATH.getKey(), List.of("a,b,c"));
+        }
+        IndexMetadata indexMetadata = IndexMetadata.builder("index").settings(settings).numberOfShards(1).numberOfReplicas(0).build();
+        IndexSettings indexSettings = new IndexSettings(indexMetadata, settings.build());
+
+        Predicate<String> indexNameMatcher = pattern -> Regex.simpleMatch(pattern, "index");
+        return new SearchExecutionContext(
+            0,
+            0,
+            indexSettings,
+            null,
+            null,
+            null,
+            MappingLookup.EMPTY,
+            null,
+            null,
+            parserConfig(),
+            writableRegistry(),
+            null,
+            null,
+            System::currentTimeMillis,
+            null,
+            indexNameMatcher,
+            () -> true,
+            null,
+            Collections.emptyMap(),
+            MapperMetrics.NOOP
+        );
+    }
+}

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

@@ -599,7 +599,7 @@ public class TextFieldMapperTests extends MapperTestCase {
         Exception e = expectThrows(
             IllegalArgumentException.class,
             () -> disabledMapper.fieldType("field")
-                .fielddataBuilder(new FieldDataContext("index", null, null, MappedFieldType.FielddataOperation.SEARCH))
+                .fielddataBuilder(new FieldDataContext("index", null, null, null, MappedFieldType.FielddataOperation.SEARCH))
         );
         assertThat(
             e.getMessage(),
@@ -1309,7 +1309,7 @@ public class TextFieldMapperTests extends MapperTestCase {
             } : SourceProvider.fromStoredFields();
             SearchLookup searchLookup = new SearchLookup(null, null, sourceProvider);
             IndexFieldData<?> sfd = ft.fielddataBuilder(
-                new FieldDataContext("", () -> searchLookup, Set::of, MappedFieldType.FielddataOperation.SCRIPT)
+                new FieldDataContext("", null, () -> searchLookup, Set::of, MappedFieldType.FielddataOperation.SCRIPT)
             ).build(null, null);
             LeafFieldData lfd = sfd.load(getOnlyLeafReader(searcher.getIndexReader()).getContext());
             TextDocValuesField scriptDV = (TextDocValuesField) lfd.getScriptFieldFactory("field");

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

@@ -120,11 +120,11 @@ public class DenseVectorFieldTypeTests extends FieldTypeTestCase {
 
     public void testFielddataBuilder() {
         DenseVectorFieldType fft = createFloatFieldType();
-        FieldDataContext fdc = new FieldDataContext("test", () -> null, Set::of, MappedFieldType.FielddataOperation.SCRIPT);
+        FieldDataContext fdc = new FieldDataContext("test", null, () -> null, Set::of, MappedFieldType.FielddataOperation.SCRIPT);
         assertNotNull(fft.fielddataBuilder(fdc));
 
         DenseVectorFieldType bft = createByteFieldType();
-        FieldDataContext bdc = new FieldDataContext("test", () -> null, Set::of, MappedFieldType.FielddataOperation.SCRIPT);
+        FieldDataContext bdc = new FieldDataContext("test", null, () -> null, Set::of, MappedFieldType.FielddataOperation.SCRIPT);
         assertNotNull(bft.fielddataBuilder(bdc));
     }
 

+ 2 - 0
server/src/test/java/org/elasticsearch/indices/IndicesModuleTests.java

@@ -17,6 +17,7 @@ import org.elasticsearch.index.mapper.IdFieldMapper;
 import org.elasticsearch.index.mapper.IgnoredFieldMapper;
 import org.elasticsearch.index.mapper.IgnoredSourceFieldMapper;
 import org.elasticsearch.index.mapper.IndexFieldMapper;
+import org.elasticsearch.index.mapper.IndexModeFieldMapper;
 import org.elasticsearch.index.mapper.KeywordFieldMapper;
 import org.elasticsearch.index.mapper.Mapper;
 import org.elasticsearch.index.mapper.MapperParsingException;
@@ -85,6 +86,7 @@ public class IndicesModuleTests extends ESTestCase {
         TimeSeriesIdFieldMapper.NAME,
         TimeSeriesRoutingHashFieldMapper.NAME,
         IndexFieldMapper.NAME,
+        IndexModeFieldMapper.NAME,
         SourceFieldMapper.NAME,
         IgnoredSourceFieldMapper.NAME,
         NestedPathFieldMapper.NAME,

+ 9 - 2
test/framework/src/main/java/org/elasticsearch/index/mapper/AbstractScriptFieldTypeTestCase.java

@@ -25,6 +25,7 @@ import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.geo.ShapeRelation;
 import org.elasticsearch.common.xcontent.XContentHelper;
+import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.fielddata.FieldDataContext;
 import org.elasticsearch.index.fielddata.IndexFieldDataCache;
 import org.elasticsearch.index.query.ExistsQueryBuilder;
@@ -265,6 +266,7 @@ public abstract class AbstractScriptFieldTypeTestCase extends MapperServiceTestC
         SearchExecutionContext searchExecutionContext = mockContext();
         return new FieldDataContext(
             "test",
+            null,
             searchExecutionContext::lookup,
             mockContext()::sourcePath,
             MappedFieldType.FielddataOperation.SCRIPT
@@ -299,7 +301,7 @@ public abstract class AbstractScriptFieldTypeTestCase extends MapperServiceTestC
         when(context.allowExpensiveQueries()).thenReturn(allowExpensiveQueries);
         SearchLookup lookup = new SearchLookup(
             context::getFieldType,
-            (mft, lookupSupplier, fdo) -> mft.fielddataBuilder(new FieldDataContext("test", lookupSupplier, context::sourcePath, fdo))
+            (mft, lookupSupplier, fdo) -> mft.fielddataBuilder(new FieldDataContext("test", null, lookupSupplier, context::sourcePath, fdo))
                 .build(null, null),
             sourceProvider
         );
@@ -307,7 +309,7 @@ public abstract class AbstractScriptFieldTypeTestCase extends MapperServiceTestC
         when(context.getForField(any(), any())).then(args -> {
             MappedFieldType ft = args.getArgument(0);
             MappedFieldType.FielddataOperation fdo = args.getArgument(1);
-            return ft.fielddataBuilder(new FieldDataContext("test", context::lookup, context::sourcePath, fdo))
+            return ft.fielddataBuilder(new FieldDataContext("test", null, context::lookup, context::sourcePath, fdo))
                 .build(new IndexFieldDataCache.None(), new NoneCircuitBreakerService());
         });
         when(context.getMatchingFieldNames(any())).thenReturn(Set.of("dummy_field"));
@@ -452,6 +454,11 @@ public abstract class AbstractScriptFieldTypeTestCase extends MapperServiceTestC
                 throw new UnsupportedOperationException();
             }
 
+            @Override
+            public IndexSettings indexSettings() {
+                throw new UnsupportedOperationException();
+            }
+
             @Override
             public MappedFieldType.FieldExtractPreference fieldExtractPreference() {
                 return MappedFieldType.FieldExtractPreference.NONE;

+ 1 - 1
test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java

@@ -764,7 +764,7 @@ public abstract class MapperServiceTestCase extends FieldTypeTestCase {
     protected TriFunction<MappedFieldType, Supplier<SearchLookup>, MappedFieldType.FielddataOperation, IndexFieldData<?>> fieldDataLookup(
         Function<String, Set<String>> sourcePathsLookup
     ) {
-        return (mft, lookupSource, fdo) -> mft.fielddataBuilder(new FieldDataContext("test", lookupSource, sourcePathsLookup, fdo))
+        return (mft, lookupSource, fdo) -> mft.fielddataBuilder(new FieldDataContext("test", null, lookupSource, sourcePathsLookup, fdo))
             .build(new IndexFieldDataCache.None(), new NoneCircuitBreakerService());
     }
 

+ 12 - 1
test/framework/src/main/java/org/elasticsearch/index/mapper/MapperTestCase.java

@@ -578,7 +578,13 @@ public abstract class MapperTestCase extends MapperServiceTestCase {
             } : SourceProvider.fromStoredFields();
             SearchLookup searchLookup = new SearchLookup(null, null, sourceProvider);
             IndexFieldData<?> sfd = ft.fielddataBuilder(
-                new FieldDataContext("", () -> searchLookup, Set::of, MappedFieldType.FielddataOperation.SCRIPT)
+                new FieldDataContext(
+                    "",
+                    mapperService.getIndexSettings(),
+                    () -> searchLookup,
+                    Set::of,
+                    MappedFieldType.FielddataOperation.SCRIPT
+                )
             ).build(null, null);
             LeafFieldData lfd = sfd.load(getOnlyLeafReader(searcher.getIndexReader()).getContext());
             DocValuesScriptFieldFactory sff = lfd.getScriptFieldFactory("field");
@@ -1325,6 +1331,11 @@ public abstract class MapperTestCase extends MapperServiceTestCase {
                     throw new UnsupportedOperationException();
                 }
 
+                @Override
+                public IndexSettings indexSettings() {
+                    throw new UnsupportedOperationException();
+                }
+
                 @Override
                 public MappedFieldType.FieldExtractPreference fieldExtractPreference() {
                     return columnReader ? DOC_VALUES : NONE;

+ 1 - 0
test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java

@@ -354,6 +354,7 @@ public abstract class AggregatorTestCase extends ESTestCase {
             .fielddataBuilder(
                 new FieldDataContext(
                     indexSettings.getIndex().getName(),
+                    indexSettings,
                     context.lookupSupplier(),
                     context.sourcePathsLookup(),
                     context.fielddataOperation()

+ 4 - 1
x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/MetadataAttribute.java

@@ -13,6 +13,7 @@ import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.core.Tuple;
 import org.elasticsearch.index.mapper.IdFieldMapper;
 import org.elasticsearch.index.mapper.IgnoredFieldMapper;
+import org.elasticsearch.index.mapper.IndexModeFieldMapper;
 import org.elasticsearch.index.mapper.SourceFieldMapper;
 import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
 import org.elasticsearch.xpack.esql.core.tree.Source;
@@ -45,7 +46,9 @@ public class MetadataAttribute extends TypedAttribute {
         IgnoredFieldMapper.NAME,
         tuple(DataType.KEYWORD, true),
         SourceFieldMapper.NAME,
-        tuple(DataType.SOURCE, false)
+        tuple(DataType.SOURCE, false),
+        IndexModeFieldMapper.NAME,
+        tuple(DataType.KEYWORD, true)
     );
 
     private final boolean searchable;

+ 6 - 0
x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/ValueSourceReaderTypeConversionTests.java

@@ -62,6 +62,7 @@ import org.elasticsearch.compute.operator.TestResultPageSinkOperator;
 import org.elasticsearch.core.IOUtils;
 import org.elasticsearch.core.Releasables;
 import org.elasticsearch.core.TimeValue;
+import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.IndexVersion;
 import org.elasticsearch.index.mapper.BlockLoader;
 import org.elasticsearch.index.mapper.FieldNamesFieldMapper;
@@ -543,6 +544,11 @@ public class ValueSourceReaderTypeConversionTests extends AnyOperatorTestCase {
                 return "test_index";
             }
 
+            @Override
+            public IndexSettings indexSettings() {
+                throw new UnsupportedOperationException();
+            }
+
             @Override
             public MappedFieldType.FieldExtractPreference fieldExtractPreference() {
                 return MappedFieldType.FieldExtractPreference.NONE;

+ 6 - 0
x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/ValuesSourceReaderOperatorTests.java

@@ -51,6 +51,7 @@ import org.elasticsearch.compute.operator.OperatorTestCase;
 import org.elasticsearch.compute.operator.PageConsumerOperator;
 import org.elasticsearch.compute.operator.SourceOperator;
 import org.elasticsearch.core.IOUtils;
+import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.IndexVersion;
 import org.elasticsearch.index.mapper.BlockLoader;
 import org.elasticsearch.index.mapper.FieldNamesFieldMapper;
@@ -497,6 +498,11 @@ public class ValuesSourceReaderOperatorTests extends OperatorTestCase {
                 return "test_index";
             }
 
+            @Override
+            public IndexSettings indexSettings() {
+                throw new UnsupportedOperationException();
+            }
+
             @Override
             public MappedFieldType.FieldExtractPreference fieldExtractPreference() {
                 return MappedFieldType.FieldExtractPreference.NONE;

+ 44 - 0
x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/TimeSeriesIT.java

@@ -13,6 +13,7 @@ import org.elasticsearch.common.Rounding;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.xpack.esql.EsqlTestUtils;
+import org.elasticsearch.xpack.esql.core.type.DataType;
 import org.junit.Before;
 
 import java.time.ZoneOffset;
@@ -743,4 +744,47 @@ public class TimeSeriesIT extends AbstractEsqlIntegTestCase {
             assertThat((double) values.get(0).get(0), closeTo(rates.stream().mapToDouble(d -> 20. * d + 10.0 * Math.floor(d)).sum(), 0.1));
         }
     }
+
+    public void testIndexMode() {
+        createIndex("events");
+        int numDocs = between(1, 10);
+        for (int i = 0; i < numDocs; i++) {
+            index("events", Integer.toString(i), Map.of("v", i));
+        }
+        refresh("events");
+        List<ColumnInfoImpl> columns = List.of(
+            new ColumnInfoImpl("_index", DataType.KEYWORD),
+            new ColumnInfoImpl("_index_mode", DataType.KEYWORD)
+        );
+        try (EsqlQueryResponse resp = run("""
+            FROM events,hosts METADATA _index_mode, _index
+            | WHERE _index_mode == "time_series"
+            | STATS BY _index, _index_mode
+            """)) {
+            assertThat(resp.columns(), equalTo(columns));
+            List<List<Object>> values = EsqlTestUtils.getValuesList(resp);
+            assertThat(values, hasSize(1));
+            assertThat(values, equalTo(List.of(List.of("hosts", "time_series"))));
+        }
+        try (EsqlQueryResponse resp = run("""
+            FROM events,hosts METADATA _index_mode, _index
+            | WHERE _index_mode == "standard"
+            | STATS BY _index, _index_mode
+            """)) {
+            assertThat(resp.columns(), equalTo(columns));
+            List<List<Object>> values = EsqlTestUtils.getValuesList(resp);
+            assertThat(values, hasSize(1));
+            assertThat(values, equalTo(List.of(List.of("events", "standard"))));
+        }
+        try (EsqlQueryResponse resp = run("""
+            FROM events,hosts METADATA _index_mode, _index
+            | STATS BY _index, _index_mode
+            | SORT _index
+            """)) {
+            assertThat(resp.columns(), equalTo(columns));
+            List<List<Object>> values = EsqlTestUtils.getValuesList(resp);
+            assertThat(values, hasSize(2));
+            assertThat(values, equalTo(List.of(List.of("events", "standard"), List.of("hosts", "time_series"))));
+        }
+    }
 }

+ 6 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsPhysicalOperationProviders.java

@@ -33,6 +33,7 @@ import org.elasticsearch.compute.operator.Operator;
 import org.elasticsearch.compute.operator.OrdinalsGroupingOperator;
 import org.elasticsearch.compute.operator.SourceOperator;
 import org.elasticsearch.index.IndexMode;
+import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.mapper.BlockLoader;
 import org.elasticsearch.index.mapper.FieldNamesFieldMapper;
 import org.elasticsearch.index.mapper.MappedFieldType;
@@ -323,6 +324,11 @@ public class EsPhysicalOperationProviders extends AbstractPhysicalOperationProvi
                     return ctx.getFullyQualifiedIndex().getName();
                 }
 
+                @Override
+                public IndexSettings indexSettings() {
+                    return ctx.getIndexSettings();
+                }
+
                 @Override
                 public MappedFieldType.FieldExtractPreference fieldExtractPreference() {
                     return fieldExtractPreference;

+ 1 - 1
x-pack/plugin/mapper-aggregate-metric/src/test/java/org/elasticsearch/xpack/aggregatemetric/mapper/AggregateDoubleMetricFieldTypeTests.java

@@ -124,7 +124,7 @@ public class AggregateDoubleMetricFieldTypeTests extends FieldTypeTestCase {
                 SearchLookup lookup = new SearchLookup(
                     searchExecutionContext::getFieldType,
                     (mft, lookupSupplier, fdo) -> mft.fielddataBuilder(
-                        new FieldDataContext("test", lookupSupplier, searchExecutionContext::sourcePath, fdo)
+                        new FieldDataContext("test", null, lookupSupplier, searchExecutionContext::sourcePath, fdo)
                     ).build(null, null),
                     (ctx, doc) -> null
                 );

+ 6 - 0
x-pack/plugin/mapper-constant-keyword/src/test/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldMapperTests.java

@@ -16,6 +16,7 @@ import org.apache.lucene.util.BytesRef;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.compress.CompressedXContent;
 import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.mapper.BlockLoader;
 import org.elasticsearch.index.mapper.DocumentMapper;
 import org.elasticsearch.index.mapper.DocumentParsingException;
@@ -240,6 +241,11 @@ public class ConstantKeywordFieldMapperTests extends MapperTestCase {
                 throw new UnsupportedOperationException();
             }
 
+            @Override
+            public IndexSettings indexSettings() {
+                throw new UnsupportedOperationException();
+            }
+
             @Override
             public MappedFieldType.FieldExtractPreference fieldExtractPreference() {
                 return MappedFieldType.FieldExtractPreference.NONE;