Browse Source

Create aliases for PassThroughObjectMapper subfields in FieldTypeLookup (#106829)

* Alias PassThroughObjectMapper subfields in FieldTypeLookup

* small refactor

* conflict resolution

* add check for circular deps

* use lexicographical ordering for conflicting aliases

* use lexicographical ordering for conflicting aliases

* simplify loop producing aliases

* use priorities for passthrough deduplication

* make priority param required

* add passthrough disclaimer

* small refactor

* small refactor

* revert refactor

* node feature for pass-through priotity
Kostas Krikellas 1 year ago
parent
commit
25e35510ab
24 changed files with 443 additions and 514 deletions
  1. 17 25
      modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/TSDBPassthroughIndexingIT.java
  2. 1 1
      modules/data-streams/src/test/java/org/elasticsearch/datastreams/DataStreamGetWriteIndexTests.java
  3. 4 2
      modules/data-streams/src/test/java/org/elasticsearch/datastreams/DataStreamIndexSettingsProviderTests.java
  4. 48 14
      modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/150_tsdb.yml
  5. 35 0
      server/src/main/java/org/elasticsearch/index/mapper/FieldTypeLookup.java
  6. 5 1
      server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java
  7. 24 11
      server/src/main/java/org/elasticsearch/index/mapper/MappingLookup.java
  8. 75 4
      server/src/main/java/org/elasticsearch/index/mapper/PassThroughObjectMapper.java
  9. 1 131
      server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java
  10. 1 1
      server/src/test/java/org/elasticsearch/index/mapper/DynamicFieldsBuilderTests.java
  11. 1 1
      server/src/test/java/org/elasticsearch/index/mapper/FieldAliasMapperValidationTests.java
  12. 1 1
      server/src/test/java/org/elasticsearch/index/mapper/FieldNamesFieldTypeTests.java
  13. 121 36
      server/src/test/java/org/elasticsearch/index/mapper/FieldTypeLookupTests.java
  14. 1 1
      server/src/test/java/org/elasticsearch/index/mapper/MappingLookupTests.java
  15. 92 11
      server/src/test/java/org/elasticsearch/index/mapper/PassThroughObjectMapperTests.java
  16. 0 262
      server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java
  17. 1 1
      server/src/test/java/org/elasticsearch/index/query/SearchExecutionContextTests.java
  18. 4 5
      server/src/test/java/org/elasticsearch/indices/IndicesRequestCacheTests.java
  19. 1 1
      server/src/test/java/org/elasticsearch/search/suggest/AbstractSuggestionBuilderTestCase.java
  20. 1 1
      test/framework/src/main/java/org/elasticsearch/cluster/metadata/DataStreamTestHelper.java
  21. 4 0
      test/framework/src/main/java/org/elasticsearch/index/mapper/MockFieldMapper.java
  22. 3 2
      test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java
  23. 1 1
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/DocumentSubsetBitsetCacheTests.java
  24. 1 1
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexReaderWrapperIntegrationTests.java

+ 17 - 25
modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/TSDBPassthroughIndexingIT.java

@@ -17,6 +17,8 @@ import org.elasticsearch.action.admin.indices.shrink.ResizeType;
 import org.elasticsearch.action.admin.indices.template.put.TransportPutComposableIndexTemplateAction;
 import org.elasticsearch.action.bulk.BulkRequest;
 import org.elasticsearch.action.delete.DeleteRequest;
+import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest;
+import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse;
 import org.elasticsearch.action.get.GetRequest;
 import org.elasticsearch.action.index.IndexRequest;
 import org.elasticsearch.action.search.SearchRequest;
@@ -70,6 +72,7 @@ public class TSDBPassthroughIndexingIT extends ESSingleNodeTestCase {
               },
               "attributes": {
                 "type": "passthrough",
+                "priority": 0,
                 "dynamic": true,
                 "time_series_dimension": true
               },
@@ -197,31 +200,20 @@ public class TSDBPassthroughIndexingIT extends ESSingleNodeTestCase {
         assertMap(attributes.get("pod.ip"), matchesMap().entry("type", "ip").entry("time_series_dimension", true));
         assertMap(attributes.get("pod.uid"), matchesMap().entry("type", "keyword").entry("time_series_dimension", true));
         assertMap(attributes.get("pod.name"), matchesMap().entry("type", "keyword").entry("time_series_dimension", true));
-        // alias field mappers:
-        assertMap(
-            ObjectPath.eval("properties.metricset", mapping),
-            matchesMap().entry("type", "alias").entry("path", "attributes.metricset")
-        );
-        assertMap(
-            ObjectPath.eval("properties.number.properties.long", mapping),
-            matchesMap().entry("type", "alias").entry("path", "attributes.number.long")
-        );
-        assertMap(
-            ObjectPath.eval("properties.number.properties.double", mapping),
-            matchesMap().entry("type", "alias").entry("path", "attributes.number.double")
-        );
-        assertMap(
-            ObjectPath.eval("properties.pod.properties", mapping),
-            matchesMap().extraOk().entry("name", matchesMap().entry("type", "alias").entry("path", "attributes.pod.name"))
-        );
-        assertMap(
-            ObjectPath.eval("properties.pod.properties", mapping),
-            matchesMap().extraOk().entry("uid", matchesMap().entry("type", "alias").entry("path", "attributes.pod.uid"))
-        );
-        assertMap(
-            ObjectPath.eval("properties.pod.properties", mapping),
-            matchesMap().extraOk().entry("ip", matchesMap().entry("type", "alias").entry("path", "attributes.pod.ip"))
-        );
+
+        FieldCapabilitiesResponse fieldCaps = client().fieldCaps(new FieldCapabilitiesRequest().fields("*").indices("k8s")).actionGet();
+        assertTrue(fieldCaps.getField("attributes.metricset").get("keyword").isDimension());
+        assertTrue(fieldCaps.getField("metricset").get("keyword").isDimension());
+        assertTrue(fieldCaps.getField("attributes.number.long").get("long").isDimension());
+        assertTrue(fieldCaps.getField("number.long").get("long").isDimension());
+        assertTrue(fieldCaps.getField("attributes.number.double").get("float").isDimension());
+        assertTrue(fieldCaps.getField("number.double").get("float").isDimension());
+        assertTrue(fieldCaps.getField("attributes.pod.ip").get("ip").isDimension());
+        assertTrue(fieldCaps.getField("pod.ip").get("ip").isDimension());
+        assertTrue(fieldCaps.getField("attributes.pod.uid").get("keyword").isDimension());
+        assertTrue(fieldCaps.getField("pod.uid").get("keyword").isDimension());
+        assertTrue(fieldCaps.getField("attributes.pod.name").get("keyword").isDimension());
+        assertTrue(fieldCaps.getField("pod.name").get("keyword").isDimension());
     }
 
     public void testIndexingGettingAndSearchingShrunkIndex() throws Exception {

+ 1 - 1
modules/data-streams/src/test/java/org/elasticsearch/datastreams/DataStreamGetWriteIndexTests.java

@@ -240,7 +240,7 @@ public class DataStreamGetWriteIndexTests extends ESTestCase {
                 new MetadataFieldMapper[] { dtfm },
                 Collections.emptyMap()
             );
-            MappingLookup mappingLookup = MappingLookup.fromMappers(mapping, List.of(dtfm, dateFieldMapper), List.of(), List.of());
+            MappingLookup mappingLookup = MappingLookup.fromMappers(mapping, List.of(dtfm, dateFieldMapper), List.of());
             indicesService = DataStreamTestHelper.mockIndicesServices(mappingLookup);
         }
 

+ 4 - 2
modules/data-streams/src/test/java/org/elasticsearch/datastreams/DataStreamIndexSettingsProviderTests.java

@@ -631,10 +631,12 @@ public class DataStreamIndexSettingsProviderTests extends ESTestCase {
                     "properties": {
                         "labels": {
                             "type": "passthrough",
-                            "time_series_dimension": true
+                            "time_series_dimension": true,
+                            "priority": 2
                         },
                         "metrics": {
-                            "type": "passthrough"
+                            "type": "passthrough",
+                            "priority": 1
                         },
                         "another_field": {
                             "type": "keyword"

+ 48 - 14
modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/150_tsdb.yml

@@ -195,8 +195,8 @@ index without timestamp with pipeline:
 ---
 dynamic templates:
   - requires:
-      cluster_features: ["gte_v8.13.0"]
-      reason: "Support for dynamic fields was added in 8.13"
+      cluster_features: ["mapper.pass_through_priority"]
+      reason: support for priority in passthrough objects
   - do:
       allowed_warnings:
         - "index template [my-dynamic-template] has index patterns [k9s*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [my-dynamic-template] will take precedence during new index creation"
@@ -219,6 +219,7 @@ dynamic templates:
                   type: passthrough
                   dynamic: true
                   time_series_dimension: true
+                  priority: 0
               dynamic_templates:
                 - counter_metric:
                     mapping:
@@ -326,8 +327,8 @@ dynamic templates:
 ---
 dynamic templates - conflicting aliases:
   - requires:
-      cluster_features: ["gte_v8.13.0"]
-      reason: "Support for dynamic fields was added in 8.13"
+      cluster_features: ["mapper.pass_through_priority"]
+      reason: support for priority in passthrough objects
   - do:
       allowed_warnings:
         - "index template [my-dynamic-template] has index patterns [k9s*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [my-dynamic-template] will take precedence during new index creation"
@@ -350,10 +351,12 @@ dynamic templates - conflicting aliases:
                   type: passthrough
                   dynamic: true
                   time_series_dimension: true
+                  priority: 2
                 resource_attributes:
                   type: passthrough
                   dynamic: true
                   time_series_dimension: true
+                  priority: 1
               dynamic_templates:
                 - counter_metric:
                     mapping:
@@ -391,7 +394,7 @@ dynamic templates - conflicting aliases:
             filterA:
               filter:
                 term:
-                  dim: "C"
+                  dim: A
               aggs:
                 tsids:
                   terms:
@@ -410,7 +413,7 @@ dynamic templates - conflicting aliases:
             filterA:
               filter:
                 term:
-                  attributes.dim: A
+                  resource_attributes.dim: C
               aggs:
                 tsids:
                   terms:
@@ -423,8 +426,8 @@ dynamic templates - conflicting aliases:
 ---
 dynamic templates with nesting:
   - requires:
-      cluster_features: ["gte_v8.13.0"]
-      reason: "Support for dynamic fields was added in 8.13"
+      cluster_features: ["mapper.pass_through_priority"]
+      reason: support for priority in passthrough objects
   - do:
       allowed_warnings:
         - "index template [my-dynamic-template] has index patterns [k9s*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [my-dynamic-template] will take precedence during new index creation"
@@ -447,6 +450,7 @@ dynamic templates with nesting:
                   type: passthrough
                   dynamic: true
                   time_series_dimension: true
+                  priority: 2
                 resource:
                   type: object
                   properties:
@@ -454,6 +458,7 @@ dynamic templates with nesting:
                       type: passthrough
                       dynamic: true
                       time_series_dimension: true
+                      priority: 1
               dynamic_templates:
                 - counter_metric:
                     mapping:
@@ -580,8 +585,9 @@ dynamic templates with nesting:
 ---
 dynamic templates with incremental indexing:
   - requires:
-      cluster_features: ["gte_v8.13.0"]
-      reason: "Support for dynamic fields was added in 8.13"
+      cluster_features: ["mapper.pass_through_priority"]
+      reason: support for priority in passthrough objects
+
   - do:
       allowed_warnings:
         - "index template [my-dynamic-template] has index patterns [k9s*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [my-dynamic-template] will take precedence during new index creation"
@@ -604,6 +610,7 @@ dynamic templates with incremental indexing:
                   type: passthrough
                   dynamic: true
                   time_series_dimension: true
+                  priority: 2
                 resource:
                   type: object
                   properties:
@@ -611,6 +618,7 @@ dynamic templates with incremental indexing:
                       type: passthrough
                       dynamic: true
                       time_series_dimension: true
+                      priority: 1
               dynamic_templates:
                 - counter_metric:
                     mapping:
@@ -774,8 +782,8 @@ dynamic templates with incremental indexing:
 ---
 subobject in passthrough object auto flatten:
   - requires:
-      cluster_features: ["gte_v8.13.0"]
-      reason: "Support for passthrough fields was added in 8.13"
+      cluster_features: ["mapper.pass_through_priority"]
+      reason: support for priority in passthrough objects
   - do:
       allowed_warnings:
         - "index template [my-passthrough-template] has index patterns [k9s*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [my-passthrough-template] will take precedence during new index creation"
@@ -794,6 +802,7 @@ subobject in passthrough object auto flatten:
                 attributes:
                   type: passthrough
                   time_series_dimension: true
+                  priority: 0
                   properties:
                     subcategory:
                       type: object
@@ -833,11 +842,36 @@ enable subobjects in passthrough object:
               index:
                 number_of_shards: 1
                 mode: time_series
-                time_series:
-                  start_time: 2023-08-31T13:03:08.138Z
 
             mappings:
               properties:
                 attributes:
                   type: passthrough
                   subobjects: true
+
+---
+passthrough objects with duplicate priority:
+  - requires:
+      cluster_features: ["mapper.pass_through_priority"]
+      reason: support for priority in passthrough objects
+  - do:
+      catch: /has a conflicting param/
+      indices.put_index_template:
+        name: my-dynamic-template
+        body:
+          index_patterns: [k9s*]
+          data_stream: {}
+          template:
+            settings:
+              index:
+                number_of_shards: 1
+                mode: time_series
+
+            mappings:
+              properties:
+                attributes:
+                  type: passthrough
+                  priority: 1
+                resource.attributes:
+                  type: passthrough
+                  priority: 1

+ 35 - 0
server/src/main/java/org/elasticsearch/index/mapper/FieldTypeLookup.java

@@ -14,6 +14,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -38,9 +39,14 @@ final class FieldTypeLookup {
 
     private final int maxParentPathDots;
 
+    FieldTypeLookup(Collection<FieldMapper> fieldMappers, Collection<FieldAliasMapper> fieldAliasMappers) {
+        this(fieldMappers, fieldAliasMappers, List.of(), List.of());
+    }
+
     FieldTypeLookup(
         Collection<FieldMapper> fieldMappers,
         Collection<FieldAliasMapper> fieldAliasMappers,
+        Collection<PassThroughObjectMapper> passThroughMappers,
         Collection<RuntimeField> runtimeFields
     ) {
 
@@ -86,6 +92,35 @@ final class FieldTypeLookup {
             }
         }
 
+        // Pass-though subfields can be referenced without the prefix corresponding to the
+        // PassThroughObjectMapper name. This is achieved by adding a second reference to their
+        // MappedFieldType using the remaining suffix.
+        Map<String, PassThroughObjectMapper> passThroughFieldAliases = new HashMap<>();
+        for (PassThroughObjectMapper passThroughMapper : passThroughMappers) {
+            for (Mapper subfield : passThroughMapper.mappers.values()) {
+                if (subfield instanceof FieldMapper fieldMapper) {
+                    String name = fieldMapper.simpleName();
+                    // Check for conflict between PassThroughObjectMapper subfields.
+                    PassThroughObjectMapper conflict = passThroughFieldAliases.put(name, passThroughMapper);
+                    if (conflict != null) {
+                        if (conflict.priority() > passThroughMapper.priority()) {
+                            // Keep the conflicting field if it has higher priority.
+                            passThroughFieldAliases.put(name, conflict);
+                            continue;
+                        }
+                    } else if (fullNameToFieldType.containsKey(name)) {
+                        // There's an existing field or alias for the same field.
+                        continue;
+                    }
+                    MappedFieldType fieldType = fieldMapper.fieldType();
+                    fullNameToFieldType.put(name, fieldType);
+                    if (fieldType instanceof DynamicFieldType) {
+                        dynamicFieldTypes.put(name, (DynamicFieldType) fieldType);
+                    }
+                }
+            }
+        }
+
         for (MappedFieldType fieldType : RuntimeField.collectFieldTypes(runtimeFields).values()) {
             // this will override concrete fields with runtime fields that have the same name
             fullNameToFieldType.put(fieldType.name(), fieldType);

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

@@ -19,6 +19,10 @@ import java.util.Set;
 public class MapperFeatures implements FeatureSpecification {
     @Override
     public Set<NodeFeature> getFeatures() {
-        return Set.of(IgnoredSourceFieldMapper.TRACK_IGNORED_SOURCE, RangeFieldMapper.NULL_VALUES_OFF_BY_ONE_FIX);
+        return Set.of(
+            IgnoredSourceFieldMapper.TRACK_IGNORED_SOURCE,
+            PassThroughObjectMapper.PASS_THROUGH_PRIORITY,
+            RangeFieldMapper.NULL_VALUES_OFF_BY_ONE_FIX
+        );
     }
 }

+ 24 - 11
server/src/main/java/org/elasticsearch/index/mapper/MappingLookup.java

@@ -42,7 +42,7 @@ public final class MappingLookup {
      * A lookup representing an empty mapping. It can be used to look up fields, although it won't hold any, but it does not
      * hold a valid {@link DocumentParser}, {@link IndexSettings} or {@link IndexAnalyzers}.
      */
-    public static final MappingLookup EMPTY = fromMappers(Mapping.EMPTY, List.of(), List.of(), List.of());
+    public static final MappingLookup EMPTY = fromMappers(Mapping.EMPTY, List.of(), List.of());
 
     private final CacheKey cacheKey = new CacheKey();
 
@@ -70,24 +70,29 @@ public final class MappingLookup {
         List<ObjectMapper> newObjectMappers = new ArrayList<>();
         List<FieldMapper> newFieldMappers = new ArrayList<>();
         List<FieldAliasMapper> newFieldAliasMappers = new ArrayList<>();
+        List<PassThroughObjectMapper> newPassThroughMappers = new ArrayList<>();
         for (MetadataFieldMapper metadataMapper : mapping.getSortedMetadataMappers()) {
             if (metadataMapper != null) {
                 newFieldMappers.add(metadataMapper);
             }
         }
         for (Mapper child : mapping.getRoot()) {
-            collect(child, newObjectMappers, newFieldMappers, newFieldAliasMappers);
+            collect(child, newObjectMappers, newFieldMappers, newFieldAliasMappers, newPassThroughMappers);
         }
-        return new MappingLookup(mapping, newFieldMappers, newObjectMappers, newFieldAliasMappers);
+        return new MappingLookup(mapping, newFieldMappers, newObjectMappers, newFieldAliasMappers, newPassThroughMappers);
     }
 
     private static void collect(
         Mapper mapper,
         Collection<ObjectMapper> objectMappers,
         Collection<FieldMapper> fieldMappers,
-        Collection<FieldAliasMapper> fieldAliasMappers
+        Collection<FieldAliasMapper> fieldAliasMappers,
+        Collection<PassThroughObjectMapper> passThroughMappers
     ) {
-        if (mapper instanceof ObjectMapper objectMapper) {
+        if (mapper instanceof PassThroughObjectMapper passThroughObjectMapper) {
+            passThroughMappers.add(passThroughObjectMapper);
+            objectMappers.add(passThroughObjectMapper);
+        } else if (mapper instanceof ObjectMapper objectMapper) {
             objectMappers.add(objectMapper);
         } else if (mapper instanceof FieldMapper fieldMapper) {
             fieldMappers.add(fieldMapper);
@@ -98,7 +103,7 @@ public final class MappingLookup {
         }
 
         for (Mapper child : mapper) {
-            collect(child, objectMappers, fieldMappers, fieldAliasMappers);
+            collect(child, objectMappers, fieldMappers, fieldAliasMappers, passThroughMappers);
         }
     }
 
@@ -114,22 +119,29 @@ public final class MappingLookup {
      * @param mappers the field mappers
      * @param objectMappers the object mappers
      * @param aliasMappers the field alias mappers
+     * @param passThroughMappers the pass-through mappers
      * @return the newly created lookup instance
      */
     public static MappingLookup fromMappers(
         Mapping mapping,
         Collection<FieldMapper> mappers,
         Collection<ObjectMapper> objectMappers,
-        Collection<FieldAliasMapper> aliasMappers
+        Collection<FieldAliasMapper> aliasMappers,
+        Collection<PassThroughObjectMapper> passThroughMappers
     ) {
-        return new MappingLookup(mapping, mappers, objectMappers, aliasMappers);
+        return new MappingLookup(mapping, mappers, objectMappers, aliasMappers, passThroughMappers);
+    }
+
+    public static MappingLookup fromMappers(Mapping mapping, Collection<FieldMapper> mappers, Collection<ObjectMapper> objectMappers) {
+        return new MappingLookup(mapping, mappers, objectMappers, List.of(), List.of());
     }
 
     private MappingLookup(
         Mapping mapping,
         Collection<FieldMapper> mappers,
         Collection<ObjectMapper> objectMappers,
-        Collection<FieldAliasMapper> aliasMappers
+        Collection<FieldAliasMapper> aliasMappers,
+        Collection<PassThroughObjectMapper> passThroughMappers
     ) {
         this.totalFieldsCount = mapping.getRoot().getTotalFieldsCount();
         this.mapping = mapping;
@@ -175,8 +187,9 @@ public final class MappingLookup {
             }
         }
 
+        PassThroughObjectMapper.checkForDuplicatePriorities(passThroughMappers);
         final Collection<RuntimeField> runtimeFields = mapping.getRoot().runtimeFields();
-        this.fieldTypeLookup = new FieldTypeLookup(mappers, aliasMappers, runtimeFields);
+        this.fieldTypeLookup = new FieldTypeLookup(mappers, aliasMappers, passThroughMappers, runtimeFields);
 
         Map<String, InferenceFieldMetadata> inferenceFields = new HashMap<>();
         for (FieldMapper mapper : mappers) {
@@ -190,7 +203,7 @@ public final class MappingLookup {
             // without runtime fields this is the same as the field type lookup
             this.indexTimeLookup = fieldTypeLookup;
         } else {
-            this.indexTimeLookup = new FieldTypeLookup(mappers, aliasMappers, Collections.emptyList());
+            this.indexTimeLookup = new FieldTypeLookup(mappers, aliasMappers, passThroughMappers, Collections.emptyList());
         }
         // make all fields into compact+fast immutable maps
         this.fieldMappers = Map.copyOf(fieldMappers);

+ 75 - 4
server/src/main/java/org/elasticsearch/index/mapper/PassThroughObjectMapper.java

@@ -9,14 +9,18 @@
 package org.elasticsearch.index.mapper;
 
 import org.elasticsearch.common.Explicit;
+import org.elasticsearch.features.NodeFeature;
 import org.elasticsearch.index.IndexVersion;
 import org.elasticsearch.xcontent.XContentBuilder;
 
 import java.io.IOException;
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
 
 import static org.elasticsearch.common.xcontent.support.XContentMapValues.nodeBooleanValue;
+import static org.elasticsearch.common.xcontent.support.XContentMapValues.nodeIntegerValue;
 
 /**
  * Mapper for pass-through objects.
@@ -24,15 +28,28 @@ import static org.elasticsearch.common.xcontent.support.XContentMapValues.nodeBo
  * Pass-through objects allow creating fields inside them that can also be referenced directly in search queries.
  * They also include parameters that affect how nested fields get configured. For instance, if parameter "time_series_dimension"
  * is set, eligible subfields are marked as dimensions and keyword fields are additionally included in routing and tsid calculations.
+ *
+ * In case different pass-through objects contain subfields with the same name (excluding the pass-through prefix), their aliases conflict.
+ * To resolve this, the pass-through spec specifies which object takes precedence through required parameter "priority"; non-negative
+ * integer values are accepted, with the highest priority value winning in case of conflicting aliases.
+ *
+ * Note that this is an experimental, undocumented mapper type, currently intended for prototyping purposes only.
+ * It has not been vetted for use in production systems.
  */
 public class PassThroughObjectMapper extends ObjectMapper {
     public static final String CONTENT_TYPE = "passthrough";
+    public static final String PRIORITY_PARAM_NAME = "priority";
+
+    static final NodeFeature PASS_THROUGH_PRIORITY = new NodeFeature("mapper.pass_through_priority");
 
     public static class Builder extends ObjectMapper.Builder {
 
         // Controls whether subfields are configured as time-series dimensions.
         protected Explicit<Boolean> timeSeriesDimensionSubFields = Explicit.IMPLICIT_FALSE;
 
+        // Controls which pass-through fields take precedence in case of conflicting aliases.
+        protected int priority = -1;
+
         public Builder(String name) {
             // Subobjects are not currently supported.
             super(name, Explicit.IMPLICIT_FALSE);
@@ -52,6 +69,11 @@ public class PassThroughObjectMapper extends ObjectMapper {
             return this;
         }
 
+        public PassThroughObjectMapper.Builder setPriority(int priority) {
+            this.priority = priority;
+            return this;
+        }
+
         @Override
         public PassThroughObjectMapper build(MapperBuilderContext context) {
             return new PassThroughObjectMapper(
@@ -60,7 +82,8 @@ public class PassThroughObjectMapper extends ObjectMapper {
                 enabled,
                 dynamic,
                 buildMappers(context.createChildContext(name(), timeSeriesDimensionSubFields.value(), dynamic)),
-                timeSeriesDimensionSubFields
+                timeSeriesDimensionSubFields,
+                priority
             );
         }
     }
@@ -68,34 +91,51 @@ public class PassThroughObjectMapper extends ObjectMapper {
     // If set, its subfields are marked as time-series dimensions (for the types supporting this).
     private final Explicit<Boolean> timeSeriesDimensionSubFields;
 
+    private final int priority;
+
     PassThroughObjectMapper(
         String name,
         String fullPath,
         Explicit<Boolean> enabled,
         Dynamic dynamic,
         Map<String, Mapper> mappers,
-        Explicit<Boolean> timeSeriesDimensionSubFields
+        Explicit<Boolean> timeSeriesDimensionSubFields,
+        int priority
     ) {
         // Subobjects are not currently supported.
         super(name, fullPath, enabled, Explicit.IMPLICIT_FALSE, Explicit.IMPLICIT_FALSE, dynamic, mappers);
         this.timeSeriesDimensionSubFields = timeSeriesDimensionSubFields;
+        this.priority = priority;
+        if (priority < 0) {
+            throw new MapperException("Pass-through object [" + fullPath + "] is missing a non-negative value for parameter [priority]");
+        }
     }
 
     @Override
     PassThroughObjectMapper withoutMappers() {
-        return new PassThroughObjectMapper(simpleName(), fullPath(), enabled, dynamic, Map.of(), timeSeriesDimensionSubFields);
+        return new PassThroughObjectMapper(simpleName(), fullPath(), enabled, dynamic, Map.of(), timeSeriesDimensionSubFields, priority);
+    }
+
+    @Override
+    public String typeName() {
+        return CONTENT_TYPE;
     }
 
     public boolean containsDimensions() {
         return timeSeriesDimensionSubFields.value();
     }
 
+    public int priority() {
+        return priority;
+    }
+
     @Override
     public PassThroughObjectMapper.Builder newBuilder(IndexVersion indexVersionCreated) {
         PassThroughObjectMapper.Builder builder = new PassThroughObjectMapper.Builder(simpleName());
         builder.enabled = enabled;
         builder.dynamic = dynamic;
         builder.timeSeriesDimensionSubFields = timeSeriesDimensionSubFields;
+        builder.priority = priority;
         return builder;
     }
 
@@ -118,7 +158,8 @@ public class PassThroughObjectMapper extends ObjectMapper {
             mergeResult.enabled(),
             mergeResult.dynamic(),
             mergeResult.mappers(),
-            containsDimensions
+            containsDimensions,
+            Math.max(priority, mergeWithObject.priority)
         );
     }
 
@@ -129,6 +170,9 @@ public class PassThroughObjectMapper extends ObjectMapper {
         if (timeSeriesDimensionSubFields.explicit()) {
             builder.field(TimeSeriesParams.TIME_SERIES_DIMENSION_PARAM, timeSeriesDimensionSubFields.value());
         }
+        if (priority >= 0) {
+            builder.field(PRIORITY_PARAM_NAME, priority);
+        }
         if (dynamic != null) {
             builder.field("dynamic", dynamic.name().toLowerCase(Locale.ROOT));
         }
@@ -157,6 +201,33 @@ public class PassThroughObjectMapper extends ObjectMapper {
                 );
                 node.remove(TimeSeriesParams.TIME_SERIES_DIMENSION_PARAM);
             }
+            fieldNode = node.get(PRIORITY_PARAM_NAME);
+            if (fieldNode != null) {
+                builder.priority = nodeIntegerValue(fieldNode);
+                node.remove(PRIORITY_PARAM_NAME);
+            }
+        }
+    }
+
+    /**
+     * Checks the passed objects for duplicate or negative priorities.
+     * @param passThroughMappers objects to check
+     */
+    public static void checkForDuplicatePriorities(Collection<PassThroughObjectMapper> passThroughMappers) {
+        Map<Integer, String> seen = new HashMap<>();
+        for (PassThroughObjectMapper mapper : passThroughMappers) {
+            String conflict = seen.put(mapper.priority, mapper.name());
+            if (conflict != null) {
+                throw new MapperException(
+                    "Pass-through object ["
+                        + mapper.name()
+                        + "] has a conflicting param [priority="
+                        + mapper.priority
+                        + "] with object ["
+                        + conflict
+                        + "]"
+                );
+            }
         }
     }
 }

+ 1 - 131
server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java

@@ -18,8 +18,6 @@ import org.elasticsearch.index.IndexVersion;
 import org.elasticsearch.index.IndexVersions;
 import org.elasticsearch.index.mapper.DynamicTemplate.XContentFieldType;
 import org.elasticsearch.index.mapper.MapperService.MergeReason;
-import org.elasticsearch.logging.LogManager;
-import org.elasticsearch.logging.Logger;
 import org.elasticsearch.xcontent.ToXContent;
 import org.elasticsearch.xcontent.XContentBuilder;
 
@@ -77,8 +75,6 @@ public class RootObjectMapper extends ObjectMapper {
         protected Explicit<Boolean> dateDetection = Defaults.DATE_DETECTION;
         protected Explicit<Boolean> numericDetection = Defaults.NUMERIC_DETECTION;
 
-        private static final Logger logger = LogManager.getLogger(RootObjectMapper.Builder.class);
-
         public Builder(String name, Explicit<Boolean> subobjects) {
             super(name, subobjects);
         }
@@ -111,15 +107,13 @@ public class RootObjectMapper extends ObjectMapper {
 
         @Override
         public RootObjectMapper build(MapperBuilderContext context) {
-            Map<String, Mapper> mappers = buildMappers(context.createChildContext(null, dynamic));
-            mappers.putAll(getAliasMappers(mappers, context));
             return new RootObjectMapper(
                 name(),
                 enabled,
                 subobjects,
                 trackArraySource,
                 dynamic,
-                mappers,
+                buildMappers(context.createChildContext(null, dynamic)),
                 new HashMap<>(runtimeFields),
                 dynamicDateTimeFormatters,
                 dynamicTemplates,
@@ -127,130 +121,6 @@ public class RootObjectMapper extends ObjectMapper {
                 numericDetection
             );
         }
-
-        Map<String, Mapper> getAliasMappers(Map<String, Mapper> mappers, MapperBuilderContext context) {
-            Map<String, Mapper> newMappers = new HashMap<>();
-            Map<String, ObjectMapper.Builder> objectIntermediates = new HashMap<>(1);
-            Map<String, ObjectMapper.Builder> objectIntermediatesFullName = new HashMap<>(1);
-            getAliasMappers(mappers, mappers, newMappers, objectIntermediates, objectIntermediatesFullName, context, 0);
-            for (var entry : objectIntermediates.entrySet()) {
-                newMappers.put(entry.getKey(), entry.getValue().build(context));
-            }
-            return newMappers;
-        }
-
-        void getAliasMappers(
-            Map<String, Mapper> mappers,
-            Map<String, Mapper> topLevelMappers,
-            Map<String, Mapper> aliasMappers,
-            Map<String, ObjectMapper.Builder> objectIntermediates,
-            Map<String, ObjectMapper.Builder> objectIntermediatesFullName,
-            MapperBuilderContext context,
-            int level
-        ) {
-            if (level >= MAX_NESTING_LEVEL_FOR_PASS_THROUGH_OBJECTS) {
-                logger.warn("Exceeded maximum nesting level for searching for pass-through object fields within object fields.");
-                return;
-            }
-            for (Mapper mapper : mappers.values()) {
-                // Create aliases for all fields in child passthrough mappers and place them under the root object.
-                if (mapper instanceof PassThroughObjectMapper passthroughMapper) {
-                    for (Mapper internalMapper : passthroughMapper.mappers.values()) {
-                        if (internalMapper instanceof FieldMapper fieldMapper) {
-                            // If there's a conflicting alias with the same name at the root level, we don't want to throw an error
-                            // to avoid indexing disruption.
-                            // TODO: record an error without affecting document indexing, so that it can be investigated later.
-                            Mapper conflict = mappers.get(fieldMapper.simpleName());
-                            if (conflict != null) {
-                                if (conflict.typeName().equals(FieldAliasMapper.CONTENT_TYPE) == false
-                                    || ((FieldAliasMapper) conflict).path().equals(fieldMapper.mappedFieldType.name()) == false) {
-                                    logger.warn(
-                                        "Root alias for field "
-                                            + fieldMapper.name()
-                                            + " conflicts with existing field or alias, skipping alias creation."
-                                    );
-                                }
-                            } else {
-                                // Check if the field name contains dots, as aliases require nesting within objects in this case.
-                                String[] fieldNameParts = fieldMapper.simpleName().split("\\.");
-                                if (fieldNameParts.length == 0) {
-                                    throw new IllegalArgumentException("field name cannot contain only dots");
-                                }
-                                if (fieldNameParts.length == 1) {
-                                    // No nesting required, add the alias directly to the root.
-                                    FieldAliasMapper aliasMapper = new FieldAliasMapper.Builder(fieldMapper.simpleName()).path(
-                                        fieldMapper.mappedFieldType.name()
-                                    ).build(context);
-                                    aliasMappers.put(aliasMapper.simpleName(), aliasMapper);
-                                } else {
-                                    conflict = topLevelMappers.get(fieldNameParts[0]);
-                                    if (conflict != null) {
-                                        if (isConflictingObject(conflict, fieldNameParts)) {
-                                            throw new IllegalArgumentException(
-                                                "Conflicting objects created during alias generation for pass-through field: ["
-                                                    + conflict.name()
-                                                    + "]"
-                                            );
-                                        }
-                                    }
-
-                                    // Nest the alias within object(s).
-                                    String realFieldName = fieldNameParts[fieldNameParts.length - 1];
-                                    Mapper.Builder fieldBuilder = new FieldAliasMapper.Builder(realFieldName).path(
-                                        fieldMapper.mappedFieldType.name()
-                                    );
-                                    ObjectMapper.Builder intermediate = null;
-                                    for (int i = fieldNameParts.length - 2; i >= 0; --i) {
-                                        String intermediateObjectName = fieldNameParts[i];
-                                        intermediate = objectIntermediatesFullName.computeIfAbsent(
-                                            concatStrings(fieldNameParts, i),
-                                            s -> new ObjectMapper.Builder(intermediateObjectName, ObjectMapper.Defaults.SUBOBJECTS)
-                                        );
-                                        intermediate.add(fieldBuilder);
-                                        fieldBuilder = intermediate;
-                                    }
-                                    objectIntermediates.putIfAbsent(fieldNameParts[0], intermediate);
-                                }
-                            }
-                        }
-                    }
-                } else if (mapper instanceof ObjectMapper objectMapper) {
-                    // Call recursively to check child fields. The level guards against long recursive call sequences.
-                    getAliasMappers(
-                        objectMapper.mappers,
-                        topLevelMappers,
-                        aliasMappers,
-                        objectIntermediates,
-                        objectIntermediatesFullName,
-                        context,
-                        level + 1
-                    );
-                }
-            }
-        }
-    }
-
-    private static String concatStrings(String[] parts, int last) {
-        StringBuilder builder = new StringBuilder();
-        for (int i = 0; i <= last; i++) {
-            builder.append('.');
-            builder.append(parts[i]);
-        }
-        return builder.toString();
-    }
-
-    private static boolean isConflictingObject(Mapper mapper, String[] parts) {
-        for (int i = 0; i < parts.length - 1; i++) {
-            if (mapper == null) {
-                return true;
-            }
-            if (mapper instanceof ObjectMapper objectMapper) {
-                mapper = objectMapper.getMapper(parts[i + 1]);
-            } else {
-                return true;
-            }
-        }
-        return mapper == null;
     }
 
     private final Explicit<DateFormatter[]> dynamicDateTimeFormatters;

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

@@ -70,7 +70,7 @@ public class DynamicFieldsBuilderTests extends ESTestCase {
 
         SourceFieldMapper sourceMapper = new SourceFieldMapper.Builder(null, Settings.EMPTY, false).setSynthetic().build();
         RootObjectMapper root = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).add(
-            new PassThroughObjectMapper.Builder("labels").setContainsDimensions().dynamic(ObjectMapper.Dynamic.TRUE)
+            new PassThroughObjectMapper.Builder("labels").setPriority(0).setContainsDimensions().dynamic(ObjectMapper.Dynamic.TRUE)
         ).build(MapperBuilderContext.root(false, false));
         Mapping mapping = new Mapping(root, new MetadataFieldMapper[] { sourceMapper }, Map.of());
 

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

@@ -193,6 +193,6 @@ public class FieldAliasMapperValidationTests extends ESTestCase {
             new MetadataFieldMapper[0],
             Collections.emptyMap()
         );
-        return MappingLookup.fromMappers(mapping, fieldMappers, objectMappers, fieldAliasMappers);
+        return MappingLookup.fromMappers(mapping, fieldMappers, objectMappers, fieldAliasMappers, emptyList());
     }
 }

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

@@ -35,7 +35,7 @@ public class FieldNamesFieldTypeTests extends ESTestCase {
             settings
         );
         List<FieldMapper> mappers = Stream.of(fieldNamesFieldType, fieldType).<FieldMapper>map(MockFieldMapper::new).toList();
-        MappingLookup mappingLookup = MappingLookup.fromMappers(Mapping.EMPTY, mappers, emptyList(), emptyList());
+        MappingLookup mappingLookup = MappingLookup.fromMappers(Mapping.EMPTY, mappers, emptyList());
         SearchExecutionContext searchExecutionContext = SearchExecutionContextHelper.createSimple(indexSettings, null, null);
         Query termQuery = fieldNamesFieldType.termQuery("field_name", searchExecutionContext);
         assertEquals(new TermQuery(new Term(FieldNamesFieldMapper.CONTENT_TYPE, "field_name")), termQuery);

+ 121 - 36
server/src/test/java/org/elasticsearch/index/mapper/FieldTypeLookupTests.java

@@ -8,6 +8,7 @@
 
 package org.elasticsearch.index.mapper;
 
+import org.elasticsearch.common.Explicit;
 import org.elasticsearch.index.mapper.flattened.FlattenedFieldMapper;
 import org.elasticsearch.test.ESTestCase;
 import org.hamcrest.Matchers;
@@ -16,6 +17,7 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import static java.util.Collections.emptyList;
@@ -30,7 +32,7 @@ import static org.hamcrest.Matchers.hasSize;
 public class FieldTypeLookupTests extends ESTestCase {
 
     public void testEmpty() {
-        FieldTypeLookup lookup = new FieldTypeLookup(Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
+        FieldTypeLookup lookup = new FieldTypeLookup(emptyList(), emptyList());
         assertNull(lookup.get("foo"));
         Collection<String> names = lookup.getMatchingFieldNames("foo");
         assertNotNull(names);
@@ -39,7 +41,7 @@ public class FieldTypeLookupTests extends ESTestCase {
 
     public void testAddNewField() {
         MockFieldMapper f = new MockFieldMapper("foo");
-        FieldTypeLookup lookup = new FieldTypeLookup(Collections.singletonList(f), emptyList(), Collections.emptyList());
+        FieldTypeLookup lookup = new FieldTypeLookup(Collections.singletonList(f), emptyList());
         assertNull(lookup.get("bar"));
         assertEquals(f.fieldType(), lookup.get("foo"));
     }
@@ -48,11 +50,7 @@ public class FieldTypeLookupTests extends ESTestCase {
         MockFieldMapper field = new MockFieldMapper("foo");
         FieldAliasMapper alias = new FieldAliasMapper("alias", "alias", "foo");
 
-        FieldTypeLookup lookup = new FieldTypeLookup(
-            Collections.singletonList(field),
-            Collections.singletonList(alias),
-            Collections.emptyList()
-        );
+        FieldTypeLookup lookup = new FieldTypeLookup(Collections.singletonList(field), Collections.singletonList(alias));
 
         MappedFieldType aliasType = lookup.get("alias");
         assertEquals(field.fieldType(), aliasType);
@@ -79,6 +77,7 @@ public class FieldTypeLookupTests extends ESTestCase {
         FieldTypeLookup lookup = new FieldTypeLookup(
             List.of(field1, field2, field3, flattened),
             List.of(alias1, alias2),
+            List.of(),
             List.of(runtimeField, multi)
         );
 
@@ -124,7 +123,7 @@ public class FieldTypeLookupTests extends ESTestCase {
         // Adding a subfield that is not multi-field
         MockFieldMapper subfield = new MockFieldMapper.Builder("field.subfield4").build(MapperBuilderContext.root(false, false));
 
-        FieldTypeLookup lookup = new FieldTypeLookup(List.of(field, subfield), emptyList(), emptyList());
+        FieldTypeLookup lookup = new FieldTypeLookup(List.of(field, subfield), emptyList());
 
         assertEquals(Set.of("field"), lookup.sourcePaths("field"));
         assertEquals(Set.of("field"), lookup.sourcePaths("field.subfield1"));
@@ -148,11 +147,7 @@ public class FieldTypeLookupTests extends ESTestCase {
             .copyTo("field.nested")
             .build(MapperBuilderContext.root(false, false));
 
-        FieldTypeLookup lookup = new FieldTypeLookup(
-            Arrays.asList(field, nestedField, otherField, otherNestedField),
-            emptyList(),
-            emptyList()
-        );
+        FieldTypeLookup lookup = new FieldTypeLookup(Arrays.asList(field, nestedField, otherField, otherNestedField), emptyList());
 
         assertEquals(Set.of("other_field", "other_field.nested", "field"), lookup.sourcePaths("field"));
         assertEquals(Set.of("other_field", "other_field.nested", "field"), lookup.sourcePaths("field.subfield1"));
@@ -172,7 +167,12 @@ public class FieldTypeLookupTests extends ESTestCase {
             )
         );
 
-        FieldTypeLookup fieldTypeLookup = new FieldTypeLookup(List.of(concrete), emptyList(), List.of(runtime, runtimeLong, multi));
+        FieldTypeLookup fieldTypeLookup = new FieldTypeLookup(
+            List.of(concrete),
+            emptyList(),
+            emptyList(),
+            List.of(runtime, runtimeLong, multi)
+        );
         assertThat(fieldTypeLookup.get("concrete"), instanceOf(MockFieldMapper.FakeFieldType.class));
         assertThat(fieldTypeLookup.get("string"), instanceOf(TestRuntimeField.TestRuntimeFieldType.class));
         assertThat(fieldTypeLookup.get("string").typeName(), equalTo("type"));
@@ -202,6 +202,7 @@ public class FieldTypeLookupTests extends ESTestCase {
         FieldTypeLookup fieldTypeLookup = new FieldTypeLookup(
             List.of(field, concrete, subfield, flattened),
             emptyList(),
+            emptyList(),
             List.of(fieldOverride, runtime, subfieldOverride, flattenedRuntime)
         );
         assertThat(fieldTypeLookup.get("field"), instanceOf(TestRuntimeField.TestRuntimeFieldType.class));
@@ -223,7 +224,12 @@ public class FieldTypeLookupTests extends ESTestCase {
         TestRuntimeField field2 = new TestRuntimeField("field2", "type");
         TestRuntimeField subfield = new TestRuntimeField("object.subfield", "type");
 
-        FieldTypeLookup fieldTypeLookup = new FieldTypeLookup(List.of(field1, concrete), emptyList(), List.of(field2, subfield));
+        FieldTypeLookup fieldTypeLookup = new FieldTypeLookup(
+            List.of(field1, concrete),
+            emptyList(),
+            emptyList(),
+            List.of(field2, subfield)
+        );
         {
             Set<String> sourcePaths = fieldTypeLookup.sourcePaths("field1");
             assertEquals(1, sourcePaths.size());
@@ -245,7 +251,7 @@ public class FieldTypeLookupTests extends ESTestCase {
         String fieldName = "object1.object2.field";
         FlattenedFieldMapper mapper = createFlattenedMapper(fieldName);
 
-        FieldTypeLookup lookup = new FieldTypeLookup(singletonList(mapper), emptyList(), emptyList());
+        FieldTypeLookup lookup = new FieldTypeLookup(singletonList(mapper), emptyList());
         assertEquals(mapper.fieldType(), lookup.get(fieldName));
 
         String objectKey = "key1.key2";
@@ -271,7 +277,7 @@ public class FieldTypeLookupTests extends ESTestCase {
         String aliasName = "alias";
         FieldAliasMapper alias = new FieldAliasMapper(aliasName, aliasName, fieldName);
 
-        FieldTypeLookup lookup = new FieldTypeLookup(singletonList(mapper), singletonList(alias), emptyList());
+        FieldTypeLookup lookup = new FieldTypeLookup(singletonList(mapper), singletonList(alias), emptyList(), emptyList());
         assertEquals(mapper.fieldType(), lookup.get(aliasName));
 
         String objectKey = "key1.key2";
@@ -293,35 +299,31 @@ public class FieldTypeLookupTests extends ESTestCase {
         FlattenedFieldMapper mapper2 = createFlattenedMapper(field2);
         FlattenedFieldMapper mapper3 = createFlattenedMapper(field3);
 
-        FieldTypeLookup lookup = new FieldTypeLookup(Arrays.asList(mapper1, mapper2), emptyList(), emptyList());
+        FieldTypeLookup lookup = new FieldTypeLookup(Arrays.asList(mapper1, mapper2), emptyList());
         assertNotNull(lookup.get(field1 + ".some.key"));
         assertNotNull(lookup.get(field2 + ".some.key"));
 
-        lookup = new FieldTypeLookup(Arrays.asList(mapper1, mapper2, mapper3), emptyList(), emptyList());
+        lookup = new FieldTypeLookup(Arrays.asList(mapper1, mapper2, mapper3), emptyList());
         assertNotNull(lookup.get(field1 + ".some.key"));
         assertNotNull(lookup.get(field2 + ".some.key"));
         assertNotNull(lookup.get(field3 + ".some.key"));
     }
 
     public void testUnmappedLookupWithDots() {
-        FieldTypeLookup lookup = new FieldTypeLookup(emptyList(), emptyList(), emptyList());
+        FieldTypeLookup lookup = new FieldTypeLookup(emptyList(), emptyList());
         assertNull(lookup.get("object.child"));
     }
 
     public void testMaxDynamicKeyDepth() {
         {
-            FieldTypeLookup lookup = new FieldTypeLookup(Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
+            FieldTypeLookup lookup = new FieldTypeLookup(emptyList(), emptyList());
             assertEquals(0, lookup.getMaxParentPathDots());
         }
 
         // Add a flattened object field.
         {
             String name = "object1.object2.field";
-            FieldTypeLookup lookup = new FieldTypeLookup(
-                Collections.singletonList(createFlattenedMapper(name)),
-                Collections.emptyList(),
-                Collections.emptyList()
-            );
+            FieldTypeLookup lookup = new FieldTypeLookup(Collections.singletonList(createFlattenedMapper(name)), emptyList());
             assertEquals(2, lookup.getMaxParentPathDots());
         }
 
@@ -330,8 +332,7 @@ public class FieldTypeLookupTests extends ESTestCase {
             String name = "object1.object2.field";
             FieldTypeLookup lookup = new FieldTypeLookup(
                 Collections.singletonList(createFlattenedMapper(name)),
-                Collections.singletonList(new FieldAliasMapper("alias", "alias", "object1.object2.field")),
-                Collections.emptyList()
+                Collections.singletonList(new FieldAliasMapper("alias", "alias", "object1.object2.field"))
             );
             assertEquals(2, lookup.getMaxParentPathDots());
         }
@@ -341,8 +342,7 @@ public class FieldTypeLookupTests extends ESTestCase {
             String name = "object1.object2.field";
             FieldTypeLookup lookup = new FieldTypeLookup(
                 Collections.singletonList(createFlattenedMapper(name)),
-                Collections.singletonList(new FieldAliasMapper("alias", "object1.object2.object3.alias", "object1.object2.field")),
-                Collections.emptyList()
+                Collections.singletonList(new FieldAliasMapper("alias", "object1.object2.object3.alias", "object1.object2.field"))
             );
             assertEquals(2, lookup.getMaxParentPathDots());
         }
@@ -353,8 +353,9 @@ public class FieldTypeLookupTests extends ESTestCase {
             IllegalArgumentException iae = expectThrows(
                 IllegalArgumentException.class,
                 () -> new FieldTypeLookup(
-                    Collections.emptySet(),
-                    Collections.emptySet(),
+                    emptyList(),
+                    emptyList(),
+                    emptyList(),
                     List.of(new TestRuntimeField("field", "type"), new TestRuntimeField("field", "long"))
                 )
             );
@@ -368,7 +369,7 @@ public class FieldTypeLookupTests extends ESTestCase {
             TestRuntimeField runtime = new TestRuntimeField("multi.first", "runtime");
             IllegalArgumentException iae = expectThrows(
                 IllegalArgumentException.class,
-                () -> new FieldTypeLookup(Collections.emptySet(), Collections.emptySet(), List.of(multi, runtime))
+                () -> new FieldTypeLookup(emptyList(), emptyList(), emptyList(), List.of(multi, runtime))
             );
             assertEquals(iae.getMessage(), "Found two runtime fields with same name [multi.first]");
         }
@@ -383,7 +384,7 @@ public class FieldTypeLookupTests extends ESTestCase {
 
             IllegalArgumentException iae = expectThrows(
                 IllegalArgumentException.class,
-                () -> new FieldTypeLookup(Collections.emptySet(), Collections.emptySet(), List.of(multi))
+                () -> new FieldTypeLookup(emptyList(), emptyList(), emptyList(), List.of(multi))
             );
             assertEquals(iae.getMessage(), "Found two runtime fields with same name [multi]");
         }
@@ -401,7 +402,7 @@ public class FieldTypeLookupTests extends ESTestCase {
             );
             IllegalStateException ise = expectThrows(
                 IllegalStateException.class,
-                () -> new FieldTypeLookup(Collections.emptySet(), Collections.emptySet(), Collections.singletonList(multi))
+                () -> new FieldTypeLookup(emptyList(), emptyList(), emptyList(), Collections.singletonList(multi))
             );
             assertEquals("Found sub-fields with name not belonging to the parent field they are part of [first, second]", ise.getMessage());
         }
@@ -415,7 +416,7 @@ public class FieldTypeLookupTests extends ESTestCase {
             );
             IllegalStateException ise = expectThrows(
                 IllegalStateException.class,
-                () -> new FieldTypeLookup(Collections.emptySet(), Collections.emptySet(), Collections.singletonList(multi))
+                () -> new FieldTypeLookup(emptyList(), emptyList(), emptyList(), Collections.singletonList(multi))
             );
             assertEquals("Found sub-fields with name not belonging to the parent field they are part of [multi.]", ise.getMessage());
         }
@@ -424,4 +425,88 @@ public class FieldTypeLookupTests extends ESTestCase {
     private static FlattenedFieldMapper createFlattenedMapper(String fieldName) {
         return new FlattenedFieldMapper.Builder(fieldName).build(MapperBuilderContext.root(false, false));
     }
+
+    private PassThroughObjectMapper createPassThroughMapper(String name, Map<String, Mapper> mappers, int priority) {
+        return new PassThroughObjectMapper(
+            name,
+            name,
+            Explicit.EXPLICIT_TRUE,
+            ObjectMapper.Dynamic.FALSE,
+            mappers,
+            Explicit.EXPLICIT_FALSE,
+            priority
+        );
+    }
+
+    public void testAddRootAliasesForPassThroughFields() {
+        MockFieldMapper foo = new MockFieldMapper("attributes.foo");
+        MockFieldMapper bar = new MockFieldMapper(
+            new MockFieldMapper.FakeFieldType("attributes.much.more.deeply.nested.bar"),
+            "much.more.deeply.nested.bar"
+        );
+        PassThroughObjectMapper attributes = createPassThroughMapper(
+            "attributes",
+            Map.of("foo", foo, "much.more.deeply.nested.bar", bar),
+            0
+        );
+
+        MockFieldMapper baz = new MockFieldMapper("resource.attributes.baz");
+        MockFieldMapper bag = new MockFieldMapper(
+            new MockFieldMapper.FakeFieldType("resource.attributes.much.more.deeply.nested.bag"),
+            "much.more.deeply.nested.bag"
+        );
+        PassThroughObjectMapper resourceAttributes = createPassThroughMapper(
+            "resource.attributes",
+            Map.of("baz", baz, "much.more.deeply.nested.bag", bag),
+            1
+        );
+
+        FieldTypeLookup lookup = new FieldTypeLookup(
+            List.of(foo, bar, baz, bag),
+            List.of(),
+            List.of(attributes, resourceAttributes),
+            List.of()
+        );
+        assertEquals(foo.fieldType(), lookup.get("foo"));
+        assertEquals(bar.fieldType(), lookup.get("much.more.deeply.nested.bar"));
+        assertEquals(baz.fieldType(), lookup.get("baz"));
+        assertEquals(bag.fieldType(), lookup.get("much.more.deeply.nested.bag"));
+    }
+
+    public void testNoPassThroughField() {
+        MockFieldMapper field = new MockFieldMapper("labels.foo");
+        PassThroughObjectMapper attributes = createPassThroughMapper("attributes", Map.of(), 0);
+
+        FieldTypeLookup lookup = new FieldTypeLookup(List.of(field), List.of(), List.of(attributes), List.of());
+        assertNull(lookup.get("foo"));
+    }
+
+    public void testAddRootAliasForConflictingPassThroughFields() {
+        MockFieldMapper attributeField = new MockFieldMapper("attributes.foo");
+        PassThroughObjectMapper attributes = createPassThroughMapper("attributes", Map.of("foo", attributeField), 1);
+
+        MockFieldMapper resourceAttributeField = new MockFieldMapper("resource.attributes.foo");
+        PassThroughObjectMapper resourceAttributes = createPassThroughMapper(
+            "resource.attributes",
+            Map.of("foo", resourceAttributeField),
+            0
+        );
+
+        FieldTypeLookup lookup = new FieldTypeLookup(
+            List.of(attributeField, resourceAttributeField),
+            List.of(),
+            List.of(attributes, resourceAttributes),
+            List.of()
+        );
+        assertEquals(attributeField.fieldType(), lookup.get("foo"));
+    }
+
+    public void testNoRootAliasForPassThroughFieldOnConflictingField() {
+        MockFieldMapper attributeFoo = new MockFieldMapper("attributes.foo");
+        MockFieldMapper foo = new MockFieldMapper("foo");
+        PassThroughObjectMapper attributes = createPassThroughMapper("attributes", Map.of("foo", attributeFoo), 0);
+
+        FieldTypeLookup lookup = new FieldTypeLookup(List.of(foo, attributeFoo), List.of(), List.of(attributes), List.of());
+        assertEquals(foo.fieldType(), lookup.get("foo"));
+    }
 }

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

@@ -47,7 +47,7 @@ public class MappingLookupTests extends ESTestCase {
             new MetadataFieldMapper[0],
             Collections.emptyMap()
         );
-        return MappingLookup.fromMappers(mapping, fieldMappers, objectMappers, emptyList());
+        return MappingLookup.fromMappers(mapping, fieldMappers, objectMappers);
     }
 
     public void testOnlyRuntimeField() {

+ 92 - 11
server/src/test/java/org/elasticsearch/index/mapper/PassThroughObjectMapperTests.java

@@ -8,16 +8,20 @@
 
 package org.elasticsearch.index.mapper;
 
+import org.elasticsearch.common.Explicit;
+
 import java.io.IOException;
+import java.util.List;
+import java.util.Map;
 
-import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.instanceOf;
 
 public class PassThroughObjectMapperTests extends MapperServiceTestCase {
 
     public void testSimpleKeyword() throws IOException {
         MapperService mapperService = createMapperService(mapping(b -> {
-            b.startObject("labels").field("type", "passthrough");
+            b.startObject("labels").field("type", "passthrough").field("priority", "0");
             {
                 b.startObject("properties");
                 b.startObject("dim").field("type", "keyword").endObject();
@@ -32,7 +36,7 @@ public class PassThroughObjectMapperTests extends MapperServiceTestCase {
 
     public void testKeywordDimension() throws IOException {
         MapperService mapperService = createMapperService(mapping(b -> {
-            b.startObject("labels").field("type", "passthrough").field("time_series_dimension", "true");
+            b.startObject("labels").field("type", "passthrough").field("priority", "0").field("time_series_dimension", "true");
             {
                 b.startObject("properties");
                 b.startObject("dim").field("type", "keyword").endObject();
@@ -45,9 +49,50 @@ public class PassThroughObjectMapperTests extends MapperServiceTestCase {
         assertTrue(((KeywordFieldMapper) mapper).fieldType().isDimension());
     }
 
+    public void testMissingPriority() throws IOException {
+        MapperException e = expectThrows(MapperException.class, () -> createMapperService(mapping(b -> {
+            b.startObject("labels").field("type", "passthrough");
+            {
+                b.startObject("properties");
+                b.startObject("dim").field("type", "keyword").endObject();
+                b.endObject();
+            }
+            b.endObject();
+        })));
+        assertThat(e.getMessage(), containsString("Pass-through object [labels] is missing a non-negative value for parameter [priority]"));
+    }
+
+    public void testNegativePriority() throws IOException {
+        MapperException e = expectThrows(MapperException.class, () -> createMapperService(mapping(b -> {
+            b.startObject("labels").field("type", "passthrough").field("priority", "-1");
+            {
+                b.startObject("properties");
+                b.startObject("dim").field("type", "keyword").endObject();
+                b.endObject();
+            }
+            b.endObject();
+        })));
+        assertThat(e.getMessage(), containsString("Pass-through object [labels] is missing a non-negative value for parameter [priority]"));
+    }
+
+    public void testPriorityParamSet() throws IOException {
+        MapperService mapperService = createMapperService(mapping(b -> {
+            b.startObject("labels").field("type", "passthrough").field("priority", "10");
+            {
+                b.startObject("properties");
+                b.startObject("dim").field("type", "keyword").endObject();
+                b.endObject();
+            }
+            b.endObject();
+        }));
+        Mapper mapper = mapperService.mappingLookup().objectMappers().get("labels");
+        assertThat(mapper, instanceOf(PassThroughObjectMapper.class));
+        assertEquals(10, ((PassThroughObjectMapper) mapper).priority());
+    }
+
     public void testDynamic() throws IOException {
         MapperService mapperService = createMapperService(mapping(b -> {
-            b.startObject("labels").field("type", "passthrough").field("dynamic", "true");
+            b.startObject("labels").field("type", "passthrough").field("priority", "0").field("dynamic", "true");
             {
                 b.startObject("properties");
                 b.startObject("dim").field("type", "keyword").endObject();
@@ -61,7 +106,7 @@ public class PassThroughObjectMapperTests extends MapperServiceTestCase {
 
     public void testEnabled() throws IOException {
         MapperService mapperService = createMapperService(mapping(b -> {
-            b.startObject("labels").field("type", "passthrough").field("enabled", "false");
+            b.startObject("labels").field("type", "passthrough").field("priority", "0").field("enabled", "false");
             {
                 b.startObject("properties");
                 b.startObject("dim").field("type", "keyword").endObject();
@@ -92,7 +137,7 @@ public class PassThroughObjectMapperTests extends MapperServiceTestCase {
 
     public void testAddSubobjectAutoFlatten() throws IOException {
         MapperService mapperService = createMapperService(mapping(b -> {
-            b.startObject("labels").field("type", "passthrough").field("time_series_dimension", "true");
+            b.startObject("labels").field("type", "passthrough").field("priority", "0").field("time_series_dimension", "true");
             {
                 b.startObject("properties");
                 {
@@ -116,19 +161,55 @@ public class PassThroughObjectMapperTests extends MapperServiceTestCase {
 
     public void testWithoutMappers() throws IOException {
         MapperService mapperService = createMapperService(mapping(b -> {
-            b.startObject("labels").field("type", "passthrough");
+            b.startObject("labels").field("type", "passthrough").field("priority", "1");
             {
                 b.startObject("properties");
                 b.startObject("dim").field("type", "keyword").endObject();
                 b.endObject();
             }
             b.endObject();
-            b.startObject("shallow").field("type", "passthrough");
+            b.startObject("shallow").field("type", "passthrough").field("priority", "2");
             b.endObject();
         }));
 
-        var labels = mapperService.mappingLookup().objectMappers().get("labels");
-        var shallow = mapperService.mappingLookup().objectMappers().get("shallow");
-        assertThat(labels.withoutMappers().toString(), equalTo(shallow.toString().replace("shallow", "labels")));
+        assertEquals("passthrough", mapperService.mappingLookup().objectMappers().get("labels").typeName());
+        assertEquals("passthrough", mapperService.mappingLookup().objectMappers().get("shallow").typeName());
+    }
+
+    public void testCheckForDuplicatePrioritiesEmpty() throws IOException {
+        PassThroughObjectMapper.checkForDuplicatePriorities(List.of());
+    }
+
+    private PassThroughObjectMapper create(String name, int priority) {
+        return new PassThroughObjectMapper(
+            name,
+            name,
+            Explicit.EXPLICIT_TRUE,
+            ObjectMapper.Dynamic.FALSE,
+            Map.of(),
+            Explicit.EXPLICIT_FALSE,
+            priority
+        );
+    }
+
+    public void testCheckForDuplicatePrioritiesOneElement() throws IOException {
+        PassThroughObjectMapper.checkForDuplicatePriorities(List.of(create("foo", 0)));
+        PassThroughObjectMapper.checkForDuplicatePriorities(List.of(create("foo", 10)));
+    }
+
+    public void testCheckForDuplicatePrioritiesManyValidElements() throws IOException {
+        PassThroughObjectMapper.checkForDuplicatePriorities(
+            List.of(create("foo", 1), create("bar", 2), create("baz", 3), create("bar", 4))
+        );
+    }
+
+    public void testCheckForDuplicatePrioritiesManyElementsDuplicatePriority() throws IOException {
+        MapperException e = expectThrows(
+            MapperException.class,
+            () -> PassThroughObjectMapper.checkForDuplicatePriorities(
+                List.of(create("foo", 1), create("bar", 1), create("baz", 3), create("bar", 4))
+            )
+        );
+        assertThat(e.getMessage(), containsString("Pass-through object [bar] has a conflicting param [priority=1] with object [foo]"));
     }
 }

+ 0 - 262
server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java

@@ -8,11 +8,9 @@
 
 package org.elasticsearch.index.mapper;
 
-import org.elasticsearch.common.Explicit;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.xcontent.XContentHelper;
 import org.elasticsearch.core.CheckedConsumer;
-import org.elasticsearch.index.IndexVersion;
 import org.elasticsearch.index.mapper.MapperService.MergeReason;
 import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xcontent.XContentFactory;
@@ -20,8 +18,6 @@ import org.elasticsearch.xcontent.XContentFactory;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
 
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
@@ -346,264 +342,6 @@ public class RootObjectMapperTests extends MapperServiceTestCase {
         assertEquals("Failed to parse mapping: unknown parameter [unsupported] on runtime field [field] of type [keyword]", e.getMessage());
     }
 
-    public void testPassThroughObjectWithAliases() throws IOException {
-        MapperService mapperService = createMapperService(mapping(b -> {
-            b.startObject("labels").field("type", "passthrough");
-            {
-                b.startObject("properties");
-                b.startObject("dim").field("type", "keyword").endObject();
-                b.endObject();
-            }
-            b.endObject();
-        }));
-        assertThat(mapperService.mappingLookup().getMapper("dim"), instanceOf(FieldAliasMapper.class));
-        assertThat(mapperService.mappingLookup().getMapper("labels.dim"), instanceOf(KeywordFieldMapper.class));
-    }
-
-    public void testPassThroughObjectNested() throws IOException {
-        MapperService mapperService = createMapperService(mapping(b -> {
-            b.startObject("resource").field("type", "object");
-            {
-                b.startObject("properties");
-                {
-                    b.startObject("attributes").field("type", "passthrough");
-                    {
-                        b.startObject("properties");
-                        b.startObject("dim").field("type", "keyword").endObject();
-                        b.endObject();
-                    }
-                    b.endObject();
-                }
-                b.endObject();
-            }
-            b.endObject();
-            b.startObject("attributes").field("type", "passthrough");
-            {
-                b.startObject("properties");
-                b.startObject("another.dim").field("type", "keyword").endObject();
-                b.endObject();
-            }
-            b.endObject();
-        }));
-        assertThat(mapperService.mappingLookup().getMapper("dim"), instanceOf(FieldAliasMapper.class));
-        assertThat(mapperService.mappingLookup().getMapper("resource.attributes.dim"), instanceOf(KeywordFieldMapper.class));
-        assertThat(mapperService.mappingLookup().objectMappers().get("another").getMapper("dim"), instanceOf(FieldAliasMapper.class));
-        assertThat(mapperService.mappingLookup().getMapper("attributes.another.dim"), instanceOf(KeywordFieldMapper.class));
-    }
-
-    public void testPassThroughObjectNestedWithDuplicateNames() throws IOException {
-        MapperService mapperService = createMapperService(mapping(b -> {
-            b.startObject("resource").field("type", "object");
-            {
-                b.startObject("properties");
-                {
-                    b.startObject("attributes").field("type", "passthrough");
-                    {
-                        b.startObject("properties");
-                        b.startObject("dim").field("type", "keyword").endObject();
-                        b.startObject("more.attributes.another.dimA").field("type", "keyword").endObject();
-                        b.startObject("more.attributes.another.dimB").field("type", "keyword").endObject();
-                        b.endObject();
-                    }
-                    b.endObject();
-                }
-                b.endObject();
-            }
-            b.endObject();
-            b.startObject("attributes").field("type", "passthrough");
-            {
-                b.startObject("properties");
-                b.startObject("another.dim").field("type", "keyword").endObject();
-                b.startObject("more.attributes.another.dimC").field("type", "keyword").endObject();
-                b.startObject("more.attributes.another.dimD").field("type", "keyword").endObject();
-                b.endObject();
-            }
-            b.endObject();
-        }));
-
-        assertThat(mapperService.mappingLookup().getMapper("dim"), instanceOf(FieldAliasMapper.class));
-        assertThat(mapperService.mappingLookup().getMapper("resource.attributes.dim"), instanceOf(KeywordFieldMapper.class));
-        assertThat(
-            mapperService.mappingLookup().objectMappers().get("more.attributes.another").getMapper("dimA"),
-            instanceOf(FieldAliasMapper.class)
-        );
-        assertThat(
-            mapperService.mappingLookup().getMapper("resource.attributes.more.attributes.another.dimA"),
-            instanceOf(KeywordFieldMapper.class)
-        );
-        assertThat(
-            mapperService.mappingLookup().objectMappers().get("more.attributes.another").getMapper("dimB"),
-            instanceOf(FieldAliasMapper.class)
-        );
-        assertThat(
-            mapperService.mappingLookup().getMapper("resource.attributes.more.attributes.another.dimB"),
-            instanceOf(KeywordFieldMapper.class)
-        );
-
-        assertThat(mapperService.mappingLookup().objectMappers().get("another").getMapper("dim"), instanceOf(FieldAliasMapper.class));
-        assertThat(mapperService.mappingLookup().getMapper("attributes.another.dim"), instanceOf(KeywordFieldMapper.class));
-        assertThat(
-            mapperService.mappingLookup().objectMappers().get("more.attributes.another").getMapper("dimC"),
-            instanceOf(FieldAliasMapper.class)
-        );
-        assertThat(
-            mapperService.mappingLookup().getMapper("attributes.more.attributes.another.dimC"),
-            instanceOf(KeywordFieldMapper.class)
-        );
-        assertThat(
-            mapperService.mappingLookup().objectMappers().get("more.attributes.another").getMapper("dimD"),
-            instanceOf(FieldAliasMapper.class)
-        );
-        assertThat(
-            mapperService.mappingLookup().getMapper("attributes.more.attributes.another.dimD"),
-            instanceOf(KeywordFieldMapper.class)
-        );
-    }
-
-    public void testPassThroughObjectNestedWithConflictingNames() throws IOException {
-        MapperParsingException e = expectThrows(MapperParsingException.class, () -> createMapperService(mapping(b -> {
-            b.startObject("resource").field("type", "object");
-            {
-                b.startObject("properties");
-                {
-                    b.startObject("attributes").field("type", "passthrough");
-                    {
-                        b.startObject("properties");
-                        b.startObject("dim").field("type", "keyword").endObject();
-                        b.startObject("resource.attributes.another.dim").field("type", "keyword").endObject();
-                        b.endObject();
-                    }
-                    b.endObject();
-                }
-                b.endObject();
-            }
-            b.endObject();
-        })));
-        assertEquals(
-            "Failed to parse mapping: Conflicting objects created during alias generation for pass-through field: [resource]",
-            e.getMessage()
-        );
-    }
-
-    public void testAliasMappersCreatesAlias() throws Exception {
-        var context = MapperBuilderContext.root(false, false);
-        Map<String, Mapper> aliases = new RootObjectMapper.Builder("root", Explicit.EXPLICIT_FALSE).getAliasMappers(
-            Map.of(
-                "labels",
-                new PassThroughObjectMapper(
-                    "labels",
-                    "labels",
-                    Explicit.EXPLICIT_TRUE,
-                    ObjectMapper.Dynamic.FALSE,
-                    Map.of("host", new KeywordFieldMapper.Builder("host", IndexVersion.current()).build(context)),
-                    Explicit.EXPLICIT_FALSE
-                )
-            ),
-            context
-        );
-        assertEquals(1, aliases.size());
-        assertThat(aliases.get("host"), instanceOf(FieldAliasMapper.class));
-    }
-
-    public void testAliasMappersCreatesAliasNested() throws Exception {
-        var context = MapperBuilderContext.root(false, false);
-        Map<String, Mapper> aliases = new RootObjectMapper.Builder("root", Explicit.EXPLICIT_FALSE).getAliasMappers(
-            Map.of(
-                "outer",
-                new ObjectMapper(
-                    "outer",
-                    "outer",
-                    Explicit.EXPLICIT_TRUE,
-                    Explicit.EXPLICIT_TRUE,
-                    Explicit.EXPLICIT_FALSE,
-                    ObjectMapper.Dynamic.FALSE,
-                    Map.of(
-                        "inner",
-                        new PassThroughObjectMapper(
-                            "inner",
-                            "outer.inner",
-                            Explicit.EXPLICIT_TRUE,
-                            ObjectMapper.Dynamic.FALSE,
-                            Map.of("host", new KeywordFieldMapper.Builder("host", IndexVersion.current()).build(context)),
-                            Explicit.EXPLICIT_FALSE
-                        )
-                    )
-                )
-            ),
-            context
-        );
-        assertEquals(1, aliases.size());
-        assertThat(aliases.get("host"), instanceOf(FieldAliasMapper.class));
-    }
-
-    public void testAliasMappersExitsInDeepNesting() throws Exception {
-        var context = MapperBuilderContext.root(false, false);
-        Map<String, Mapper> aliases = new HashMap<>();
-        var objectIntermediates = new HashMap<String, ObjectMapper.Builder>(1);
-        var objectIntermediatesFullPath = new HashMap<String, ObjectMapper.Builder>(1);
-        new RootObjectMapper.Builder("root", Explicit.EXPLICIT_FALSE).getAliasMappers(
-            Map.of(
-                "labels",
-                new PassThroughObjectMapper(
-                    "labels",
-                    "labels",
-                    Explicit.EXPLICIT_TRUE,
-                    ObjectMapper.Dynamic.FALSE,
-                    Map.of("host", new KeywordFieldMapper.Builder("host", IndexVersion.current()).build(context)),
-                    Explicit.EXPLICIT_FALSE
-                )
-            ),
-            Map.of(),
-            aliases,
-            objectIntermediates,
-            objectIntermediatesFullPath,
-            context,
-            1_000_000
-        );
-        assertTrue(aliases.isEmpty());
-    }
-
-    public void testAliasMappersCreatesNoAliasForRegularObject() throws Exception {
-        var context = MapperBuilderContext.root(false, false);
-        Map<String, Mapper> aliases = new RootObjectMapper.Builder("root", Explicit.EXPLICIT_FALSE).getAliasMappers(
-            Map.of(
-                "labels",
-                new ObjectMapper(
-                    "labels",
-                    "labels",
-                    Explicit.EXPLICIT_TRUE,
-                    Explicit.EXPLICIT_FALSE,
-                    Explicit.EXPLICIT_FALSE,
-                    ObjectMapper.Dynamic.FALSE,
-                    Map.of("host", new KeywordFieldMapper.Builder("host", IndexVersion.current()).build(context))
-                )
-            ),
-            context
-        );
-        assertTrue(aliases.isEmpty());
-    }
-
-    public void testAliasMappersConflictingField() throws Exception {
-        var context = MapperBuilderContext.root(false, false);
-        Map<String, Mapper> aliases = new RootObjectMapper.Builder("root", Explicit.EXPLICIT_FALSE).getAliasMappers(
-            Map.of(
-                "labels",
-                new PassThroughObjectMapper(
-                    "labels",
-                    "labels",
-                    Explicit.EXPLICIT_TRUE,
-                    ObjectMapper.Dynamic.FALSE,
-                    Map.of("host", new KeywordFieldMapper.Builder("host", IndexVersion.current()).build(context)),
-                    Explicit.EXPLICIT_FALSE
-                ),
-                "host",
-                new KeywordFieldMapper.Builder("host", IndexVersion.current()).build(context)
-            ),
-            context
-        );
-        assertTrue(aliases.isEmpty());
-    }
-
     public void testEmptyType() throws Exception {
         String mapping = Strings.toString(
             XContentFactory.jsonBuilder()

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

@@ -296,7 +296,7 @@ public class SearchExecutionContextTests extends ESTestCase {
             new MetadataFieldMapper[0],
             Collections.emptyMap()
         );
-        return MappingLookup.fromMappers(mapping, mappers, Collections.emptyList(), Collections.emptyList());
+        return MappingLookup.fromMappers(mapping, mappers, Collections.emptyList());
     }
 
     public void testSearchRequestRuntimeFields() {

+ 4 - 5
server/src/test/java/org/elasticsearch/indices/IndicesRequestCacheTests.java

@@ -203,7 +203,7 @@ public class IndicesRequestCacheTests extends ESTestCase {
     public void testCacheDifferentMapping() throws Exception {
         IndicesRequestCache cache = new IndicesRequestCache(Settings.EMPTY);
         MappingLookup.CacheKey mappingKey1 = MappingLookup.EMPTY.cacheKey();
-        MappingLookup.CacheKey mappingKey2 = MappingLookup.fromMappers(Mapping.EMPTY, emptyList(), emptyList(), emptyList()).cacheKey();
+        MappingLookup.CacheKey mappingKey2 = MappingLookup.fromMappers(Mapping.EMPTY, emptyList(), emptyList()).cacheKey();
         AtomicBoolean indexShard = new AtomicBoolean(true);
         ShardRequestCache requestCacheStats = new ShardRequestCache();
         Directory dir = newDirectory();
@@ -363,14 +363,13 @@ public class IndicesRequestCacheTests extends ESTestCase {
 
         writer.updateDocument(new Term("id", "0"), newDoc(0, "bar"));
         DirectoryReader secondReader = ElasticsearchDirectoryReader.wrap(DirectoryReader.open(writer), new ShardId("foo", "bar", 1));
-        MappingLookup.CacheKey secondMappingKey = MappingLookup.fromMappers(Mapping.EMPTY, emptyList(), emptyList(), emptyList())
-            .cacheKey();
+        MappingLookup.CacheKey secondMappingKey = MappingLookup.fromMappers(Mapping.EMPTY, emptyList(), emptyList()).cacheKey();
         TestEntity secondEntity = new TestEntity(requestCacheStats, indexShard);
         Loader secondLoader = new Loader(secondReader, 0);
 
         writer.updateDocument(new Term("id", "0"), newDoc(0, "baz"));
         DirectoryReader thirdReader = ElasticsearchDirectoryReader.wrap(DirectoryReader.open(writer), new ShardId("foo", "bar", 1));
-        MappingLookup.CacheKey thirdMappingKey = MappingLookup.fromMappers(Mapping.EMPTY, emptyList(), emptyList(), emptyList()).cacheKey();
+        MappingLookup.CacheKey thirdMappingKey = MappingLookup.fromMappers(Mapping.EMPTY, emptyList(), emptyList()).cacheKey();
         AtomicBoolean differentIdentity = new AtomicBoolean(true);
         TestEntity thirdEntity = new TestEntity(requestCacheStats, differentIdentity);
         Loader thirdLoader = new Loader(thirdReader, 0);
@@ -506,7 +505,7 @@ public class IndicesRequestCacheTests extends ESTestCase {
         AtomicBoolean trueBoolean = new AtomicBoolean(true);
         AtomicBoolean falseBoolean = new AtomicBoolean(false);
         MappingLookup.CacheKey mKey1 = MappingLookup.EMPTY.cacheKey();
-        MappingLookup.CacheKey mKey2 = MappingLookup.fromMappers(Mapping.EMPTY, emptyList(), emptyList(), emptyList()).cacheKey();
+        MappingLookup.CacheKey mKey2 = MappingLookup.fromMappers(Mapping.EMPTY, emptyList(), emptyList()).cacheKey();
         Directory dir = newDirectory();
         IndexWriterConfig config = newIndexWriterConfig();
         IndexWriter writer = new IndexWriter(dir, config);

+ 1 - 1
server/src/test/java/org/elasticsearch/search/suggest/AbstractSuggestionBuilderTestCase.java

@@ -168,7 +168,7 @@ public abstract class AbstractSuggestionBuilderTestCase<SB extends SuggestionBui
                 invocation -> new TestTemplateService.MockTemplateScript.Factory(((Script) invocation.getArguments()[0]).getIdOrCode())
             );
             List<FieldMapper> mappers = Collections.singletonList(new MockFieldMapper(fieldType));
-            MappingLookup lookup = MappingLookup.fromMappers(Mapping.EMPTY, mappers, emptyList(), emptyList());
+            MappingLookup lookup = MappingLookup.fromMappers(Mapping.EMPTY, mappers, emptyList());
             SearchExecutionContext mockContext = new SearchExecutionContext(
                 0,
                 0,

+ 1 - 1
test/framework/src/main/java/org/elasticsearch/cluster/metadata/DataStreamTestHelper.java

@@ -671,7 +671,7 @@ public final class DataStreamTestHelper {
                 new MetadataFieldMapper[] { dtfm },
                 Collections.emptyMap()
             );
-            mappingLookup = MappingLookup.fromMappers(mapping, List.of(dtfm, dateFieldMapper), List.of(), List.of());
+            mappingLookup = MappingLookup.fromMappers(mapping, List.of(dtfm, dateFieldMapper), List.of());
         }
         IndicesService indicesService = mockIndicesServices(mappingLookup);
 

+ 4 - 0
test/framework/src/main/java/org/elasticsearch/index/mapper/MockFieldMapper.java

@@ -24,6 +24,10 @@ public class MockFieldMapper extends FieldMapper {
         this(findSimpleName(fieldType.name()), fieldType, MultiFields.empty(), CopyTo.empty());
     }
 
+    public MockFieldMapper(MappedFieldType fieldType, String simpleName) {
+        super(simpleName, fieldType, MultiFields.empty(), CopyTo.empty(), false, null);
+    }
+
     public MockFieldMapper(String fullName, MappedFieldType fieldType, MultiFields multifields, CopyTo copyTo) {
         super(findSimpleName(fullName), fieldType, multifields, copyTo, false, null);
     }

+ 3 - 2
test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java

@@ -348,7 +348,8 @@ public abstract class AggregatorTestCase extends ESTestCase {
             // Alias all fields to <name>-alias to test aliases
             Arrays.stream(fieldTypes)
                 .map(ft -> new FieldAliasMapper(ft.name() + "-alias", ft.name() + "-alias", ft.name()))
-                .collect(toList())
+                .collect(toList()),
+            List.of()
         );
         BiFunction<MappedFieldType, FieldDataContext, IndexFieldData<?>> fieldDataBuilder = (fieldType, context) -> fieldType
             .fielddataBuilder(
@@ -461,7 +462,7 @@ public abstract class AggregatorTestCase extends ESTestCase {
          * of stuff.
          */
         SearchExecutionContext subContext = spy(searchExecutionContext);
-        MappingLookup disableNestedLookup = MappingLookup.fromMappers(Mapping.EMPTY, Set.of(), Set.of(), Set.of());
+        MappingLookup disableNestedLookup = MappingLookup.fromMappers(Mapping.EMPTY, Set.of(), Set.of());
         doReturn(new NestedDocuments(disableNestedLookup, bitsetFilterCache::getBitSetProducer, indexSettings.getIndexVersionCreated()))
             .when(subContext)
             .getNestedDocuments();

+ 1 - 1
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/DocumentSubsetBitsetCacheTests.java

@@ -632,7 +632,7 @@ public class DocumentSubsetBitsetCacheTests extends ESTestCase {
             types.add(new MockFieldMapper(new KeywordFieldMapper.KeywordFieldType("dne-" + i)));
         }
 
-        MappingLookup mappingLookup = MappingLookup.fromMappers(Mapping.EMPTY, types, emptyList(), emptyList());
+        MappingLookup mappingLookup = MappingLookup.fromMappers(Mapping.EMPTY, types, emptyList());
 
         final Client client = mock(Client.class);
         when(client.settings()).thenReturn(Settings.EMPTY);

+ 1 - 1
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexReaderWrapperIntegrationTests.java

@@ -344,6 +344,6 @@ public class SecurityIndexReaderWrapperIntegrationTests extends AbstractBuilderT
 
     private static MappingLookup createMappingLookup(List<MappedFieldType> concreteFields) {
         List<FieldMapper> mappers = concreteFields.stream().map(MockFieldMapper::new).collect(Collectors.toList());
-        return MappingLookup.fromMappers(Mapping.EMPTY, mappers, emptyList(), emptyList());
+        return MappingLookup.fromMappers(Mapping.EMPTY, mappers, emptyList());
     }
 }