Browse Source

Consolidate nested mapper information into NestedLookup (#75677)

Nested mapping information is currently accessed by looking through
a list of ObjectMappers and then reconstructing relationships by checking
field names against possible parents. This is implemented in several
different places throughout the codebase.

Now that nested object mappers are separate from generic ObjectMapper,
we can instead centralise this information into a single NestedLookup class
built from the mappings.
Alan Woodward 3 years ago
parent
commit
3157d1ca92
36 changed files with 414 additions and 352 deletions
  1. 2 2
      benchmarks/src/main/java/org/elasticsearch/benchmark/search/aggregations/AggConstructionContentionBenchmark.java
  2. 3 2
      modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java
  3. 5 1
      server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/NestedIT.java
  4. 14 17
      server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesFetcher.java
  5. 4 3
      server/src/main/java/org/elasticsearch/index/cache/bitset/BitsetFilterCache.java
  6. 1 1
      server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java
  7. 2 2
      server/src/main/java/org/elasticsearch/index/mapper/FieldAliasMapper.java
  8. 2 2
      server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java
  9. 1 1
      server/src/main/java/org/elasticsearch/index/mapper/MapperService.java
  10. 8 59
      server/src/main/java/org/elasticsearch/index/mapper/MappingLookup.java
  11. 114 0
      server/src/main/java/org/elasticsearch/index/mapper/NestedLookup.java
  12. 0 1
      server/src/main/java/org/elasticsearch/index/mapper/NestedObjectMapper.java
  13. 10 15
      server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java
  14. 3 30
      server/src/main/java/org/elasticsearch/index/query/SearchExecutionContext.java
  15. 15 34
      server/src/main/java/org/elasticsearch/index/search/NestedHelper.java
  16. 4 2
      server/src/main/java/org/elasticsearch/search/DefaultSearchContext.java
  17. 20 14
      server/src/main/java/org/elasticsearch/search/NestedDocuments.java
  18. 4 17
      server/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregationBuilder.java
  19. 3 9
      server/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/ReverseNestedAggregationBuilder.java
  20. 5 5
      server/src/main/java/org/elasticsearch/search/aggregations/support/AggregationContext.java
  21. 2 6
      server/src/main/java/org/elasticsearch/search/fetch/subphase/FieldFetcher.java
  22. 8 13
      server/src/main/java/org/elasticsearch/search/sort/FieldSortBuilder.java
  23. 2 8
      server/src/main/java/org/elasticsearch/search/sort/SortBuilder.java
  24. 84 0
      server/src/test/java/org/elasticsearch/index/mapper/NestedLookupTests.java
  25. 10 10
      server/src/test/java/org/elasticsearch/index/mapper/NestedObjectMapperTests.java
  26. 10 9
      server/src/test/java/org/elasticsearch/index/query/ExistsQueryBuilderTests.java
  27. 2 1
      server/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java
  28. 38 70
      server/src/test/java/org/elasticsearch/index/search/NestedHelperTests.java
  29. 2 1
      server/src/test/java/org/elasticsearch/search/fetch/subphase/FieldFetcherTests.java
  30. 7 3
      server/src/test/java/org/elasticsearch/search/sort/AbstractSortTestCase.java
  31. 2 3
      server/src/test/java/org/elasticsearch/search/sort/GeoDistanceSortBuilderTests.java
  32. 2 2
      server/src/test/java/org/elasticsearch/search/sort/NestedSortBuilderTests.java
  33. 14 6
      test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java
  34. 4 2
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/DocumentPermissions.java
  35. 1 1
      x-pack/plugin/vectors/src/main/java/org/elasticsearch/xpack/vectors/mapper/DenseVectorFieldMapper.java
  36. 6 0
      x-pack/plugin/wildcard/src/test/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapperTests.java

+ 2 - 2
benchmarks/src/main/java/org/elasticsearch/benchmark/search/aggregations/AggConstructionContentionBenchmark.java

@@ -28,9 +28,9 @@ import org.elasticsearch.index.cache.bitset.BitsetFilterCache;
 import org.elasticsearch.index.fielddata.IndexFieldData;
 import org.elasticsearch.index.fielddata.IndexFieldDataCache;
 import org.elasticsearch.index.mapper.MappedFieldType;
+import org.elasticsearch.index.mapper.NestedLookup;
 import org.elasticsearch.index.mapper.NumberFieldMapper;
 import org.elasticsearch.index.mapper.NumberFieldMapper.NumberType;
-import org.elasticsearch.index.mapper.ObjectMapper;
 import org.elasticsearch.index.query.QueryBuilder;
 import org.elasticsearch.index.query.support.NestedScope;
 import org.elasticsearch.indices.breaker.CircuitBreakerService;
@@ -277,7 +277,7 @@ public class AggConstructionContentionBenchmark {
         }
 
         @Override
-        public ObjectMapper getObjectMapper(String path) {
+        public NestedLookup nestedLookup() {
             throw new UnsupportedOperationException();
         }
 

+ 3 - 2
modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java

@@ -51,6 +51,7 @@ import org.elasticsearch.index.fielddata.IndexFieldDataCache;
 import org.elasticsearch.index.mapper.LuceneDocument;
 import org.elasticsearch.index.mapper.MappedFieldType;
 import org.elasticsearch.index.mapper.MapperService;
+import org.elasticsearch.index.mapper.NestedLookup;
 import org.elasticsearch.index.mapper.ParsedDocument;
 import org.elasticsearch.index.mapper.SourceToParse;
 import org.elasticsearch.index.query.AbstractQueryBuilder;
@@ -517,9 +518,9 @@ public class PercolateQueryBuilder extends AbstractQueryBuilder<PercolateQueryBu
         final IndexSearcher docSearcher;
         final boolean excludeNestedDocuments;
         if (docs.size() > 1 || docs.get(0).docs().size() > 1) {
-            assert docs.size() != 1 || context.hasNested();
+            assert docs.size() != 1 || context.nestedLookup() != NestedLookup.EMPTY;
             docSearcher = createMultiDocumentSearcher(analyzer, docs);
-            excludeNestedDocuments = context.hasNested()
+            excludeNestedDocuments = context.nestedLookup() != NestedLookup.EMPTY
                 && docs.stream().map(ParsedDocument::docs).mapToInt(List::size).anyMatch(size -> size > 1);
         } else {
             MemoryIndex memoryIndex = MemoryIndex.fromDocument(docs.get(0).rootDoc(), analyzer, true, false);

+ 5 - 1
server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/NestedIT.java

@@ -9,7 +9,6 @@ package org.elasticsearch.search.aggregations.bucket;
 
 import org.apache.lucene.search.join.ScoreMode;
 import org.elasticsearch.action.index.IndexRequestBuilder;
-import org.elasticsearch.action.search.SearchPhaseExecutionException;
 import org.elasticsearch.action.search.SearchRequestBuilder;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.common.settings.Settings;
@@ -361,6 +360,10 @@ public class NestedIT extends ESIntegTestCase {
         assertThat(nested.getDocCount(), is(0L));
     }
 
+    // TODO previously we would detect if you tried to do a nested agg on a non-nested object field,
+    // but ignore things if you tried to do a nested agg on any other field. We should probably
+    // decide which behaviour we want and do the same in both cases.
+    /*
     public void testNestedOnObjectField() throws Exception {
         try {
             client().prepareSearch("idx").setQuery(matchAllQuery()).addAggregation(nested("object_field", "incorrect")).get();
@@ -369,6 +372,7 @@ public class NestedIT extends ESIntegTestCase {
             assertThat(e.toString(), containsString("[nested] nested path [incorrect] is not nested"));
         }
     }
+    */
 
     // Test based on: https://github.com/elastic/elasticsearch/issues/9280
     public void testParentFilterResolvedCorrectly() throws Exception {

+ 14 - 17
server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesFetcher.java

@@ -11,7 +11,6 @@ package org.elasticsearch.action.fieldcaps;
 import org.elasticsearch.index.IndexService;
 import org.elasticsearch.index.engine.Engine;
 import org.elasticsearch.index.mapper.MappedFieldType;
-import org.elasticsearch.index.mapper.ObjectMapper;
 import org.elasticsearch.index.mapper.RuntimeField;
 import org.elasticsearch.index.query.MatchAllQueryBuilder;
 import org.elasticsearch.index.query.QueryBuilder;
@@ -107,22 +106,20 @@ class FieldCapabilitiesFetcher {
                         // checks if the parent field contains sub-fields
                         if (searchExecutionContext.getFieldType(parentField) == null) {
                             // no field type, it must be an object field
-                            ObjectMapper mapper = searchExecutionContext.getObjectMapper(parentField);
-                            // Composite runtime fields do not have a mapped type for the root - check for null
-                            if (mapper != null) {
-                                String type = mapper.isNested() ? "nested" : "object";
-                                IndexFieldCapabilities fieldCap = new IndexFieldCapabilities(
-                                    parentField,
-                                    type,
-                                    false,
-                                    false,
-                                    false,
-                                    false,
-                                    null,
-                                    Collections.emptyMap()
-                                );
-                                responseMap.put(parentField, fieldCap);
-                            }
+                            String type = searchExecutionContext.nestedLookup().getNestedMappers().get(parentField) != null
+                                ? "nested"
+                                : "object";
+                            IndexFieldCapabilities fieldCap = new IndexFieldCapabilities(
+                                parentField,
+                                type,
+                                false,
+                                false,
+                                false,
+                                false,
+                                null,
+                                Collections.emptyMap()
+                            );
+                            responseMap.put(parentField, fieldCap);
                         }
                         dotIndex = parentField.lastIndexOf('.');
                     }

+ 4 - 3
server/src/main/java/org/elasticsearch/index/cache/bitset/BitsetFilterCache.java

@@ -38,7 +38,7 @@ import org.elasticsearch.index.IndexWarmer;
 import org.elasticsearch.index.IndexWarmer.TerminationHandle;
 import org.elasticsearch.index.mapper.MapperService;
 import org.elasticsearch.index.mapper.MappingLookup;
-import org.elasticsearch.index.mapper.NestedObjectMapper;
+import org.elasticsearch.index.mapper.NestedLookup;
 import org.elasticsearch.index.shard.IndexShard;
 import org.elasticsearch.index.shard.ShardId;
 import org.elasticsearch.index.shard.ShardUtils;
@@ -238,9 +238,10 @@ public final class BitsetFilterCache extends AbstractIndexComponent
             final Set<Query> warmUp = new HashSet<>();
             final MapperService mapperService = indexShard.mapperService();
             MappingLookup lookup = mapperService.mappingLookup();
-            if (lookup.hasNested()) {
+            NestedLookup nestedLookup = lookup.nestedLookup();
+            if (nestedLookup != NestedLookup.EMPTY) {
                 warmUp.add(Queries.newNonNestedFilter());
-                lookup.getNestedParentMappers().stream().map(NestedObjectMapper::nestedTypeFilter).forEach(warmUp::add);
+                warmUp.addAll(nestedLookup.getNestedParentFilters().values());
             }
 
             final CountDownLatch latch = new CountDownLatch(reader.leaves().size() * warmUp.size());

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

@@ -95,7 +95,7 @@ public class DocumentMapper {
                 );
             }
         }
-        if (settings.getIndexSortConfig().hasIndexSort() && mappers().hasNested()) {
+        if (settings.getIndexSortConfig().hasIndexSort() && mappers().nestedLookup() != NestedLookup.EMPTY) {
             throw new IllegalArgumentException("cannot have nested fields when index sort is activated");
         }
         List<String> routingPaths = settings.getIndexMetadata().getRoutingPaths();

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

@@ -94,8 +94,8 @@ public final class FieldAliasMapper extends Mapper {
                 "Invalid [path] value [" + path + "] for field alias [" + name() + "]: an alias cannot refer to another alias."
             );
         }
-        String aliasScope = mappers.getNestedParent(name);
-        String pathScope = mappers.getNestedParent(path);
+        String aliasScope = mappers.nestedLookup().getNestedParent(name);
+        String pathScope = mappers.nestedLookup().getNestedParent(path);
 
         if (Objects.equals(aliasScope, pathScope) == false) {
             StringBuilder message = new StringBuilder(

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

@@ -340,7 +340,7 @@ public abstract class FieldMapper extends Mapper implements Cloneable {
                 throw new IllegalArgumentException("[copy_to] may not be used to copy from a multi-field: [" + this.name() + "]");
             }
 
-            final String sourceScope = mappers.getNestedParent(this.name());
+            final String sourceScope = mappers.nestedLookup().getNestedParent(this.name());
             for (String copyTo : this.copyTo().copyToFields()) {
                 if (mappers.isMultiField(copyTo)) {
                     throw new IllegalArgumentException("[copy_to] may not be used to copy to a multi-field: [" + copyTo + "]");
@@ -349,7 +349,7 @@ public abstract class FieldMapper extends Mapper implements Cloneable {
                     throw new IllegalArgumentException("Cannot copy to field [" + copyTo + "] since it is mapped as an object");
                 }
 
-                final String targetScope = mappers.getNestedParent(copyTo);
+                final String targetScope = mappers.nestedLookup().getNestedParent(copyTo);
                 checkNestedScopeCompatibility(sourceScope, targetScope);
             }
         }

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

@@ -176,7 +176,7 @@ public class MapperService extends AbstractIndexComponent implements Closeable {
     }
 
     public boolean hasNested() {
-        return mappingLookup().hasNested();
+        return mappingLookup().nestedLookup() != NestedLookup.EMPTY;
     }
 
     public IndexAnalyzers getIndexAnalyzers() {

+ 8 - 59
server/src/main/java/org/elasticsearch/index/mapper/MappingLookup.java

@@ -49,7 +49,7 @@ public final class MappingLookup {
     /** Full field name to mapper */
     private final Map<String, Mapper> fieldMappers;
     private final Map<String, ObjectMapper> objectMappers;
-    private final boolean hasNested;
+    private final NestedLookup nestedLookup;
     private final FieldTypeLookup fieldTypeLookup;
     private final FieldTypeLookup indexTimeLookup;  // for index-time scripts, a lookup that does not include runtime fields
     private final Map<String, NamedAnalyzer> indexAnalyzersMap = new HashMap<>();
@@ -133,16 +133,16 @@ public final class MappingLookup {
         Map<String, Mapper> fieldMappers = new HashMap<>();
         Map<String, ObjectMapper> objects = new HashMap<>();
 
-        boolean hasNested = false;
+        List<NestedObjectMapper> nestedMappers = new ArrayList<>();
         for (ObjectMapper mapper : objectMappers) {
             if (objects.put(mapper.fullPath(), mapper) != null) {
                 throw new MapperParsingException("Object mapper [" + mapper.fullPath() + "] is defined more than once");
             }
             if (mapper.isNested()) {
-                hasNested = true;
+                nestedMappers.add((NestedObjectMapper) mapper);
             }
         }
-        this.hasNested = hasNested;
+        this.nestedLookup = NestedLookup.build(nestedMappers);
 
         for (FieldMapper mapper : mappers) {
             if (objects.containsKey(mapper.name())) {
@@ -327,14 +327,14 @@ public final class MappingLookup {
         }
     }
 
-    public boolean hasNested() {
-        return hasNested;
-    }
-
     public Map<String, ObjectMapper> objectMappers() {
         return objectMappers;
     }
 
+    public NestedLookup nestedLookup() {
+        return nestedLookup;
+    }
+
     public boolean isMultiField(String field) {
         String sourceParent = parentObject(field);
         return sourceParent != null && fieldMappers.containsKey(sourceParent);
@@ -344,23 +344,6 @@ public final class MappingLookup {
         return objectMappers.containsKey(field);
     }
 
-    /**
-     * Given a nested object path, returns the path to its nested parent
-     *
-     * In particular, if a nested field `foo` contains an object field
-     * `bar.baz`, then calling this method with `foo.bar.baz` will return
-     * the path `foo`, skipping over the object-but-not-nested `foo.bar`
-     */
-    public String getNestedParent(String path) {
-        for (String parentPath = parentObject(path); parentPath != null; parentPath = parentObject(parentPath)) {
-            ObjectMapper objectMapper = objectMappers.get(parentPath);
-            if (objectMapper != null && objectMapper.isNested()) {
-                return parentPath;
-            }
-        }
-        return null;
-    }
-
     private static String parentObject(String field) {
         int lastDot = field.lastIndexOf('.');
         if (lastDot == -1) {
@@ -455,40 +438,6 @@ public final class MappingLookup {
         return mapping;
     }
 
-    /**
-     * Returns all nested object mappers
-     */
-    public List<NestedObjectMapper> getNestedMappers() {
-        List<NestedObjectMapper> childMappers = new ArrayList<>();
-        for (ObjectMapper mapper : objectMappers().values()) {
-            if (mapper.isNested() == false) {
-                continue;
-            }
-            childMappers.add((NestedObjectMapper) mapper);
-        }
-        return childMappers;
-    }
-
-    /**
-     * Returns all nested object mappers which contain further nested object mappers
-     *
-     * Used by BitSetProducerWarmer
-     */
-    public List<NestedObjectMapper> getNestedParentMappers() {
-        List<NestedObjectMapper> parents = new ArrayList<>();
-        for (ObjectMapper mapper : objectMappers().values()) {
-            String nestedParentPath = getNestedParent(mapper.fullPath());
-            if (nestedParentPath == null) {
-                continue;
-            }
-            ObjectMapper parent = objectMappers().get(nestedParentPath);
-            if (parent.isNested()) {
-                parents.add((NestedObjectMapper) parent);
-            }
-        }
-        return parents;
-    }
-
     /**
      * Check if the provided {@link MappedFieldType} shadows a dimension
      * or metric field.

+ 114 - 0
server/src/main/java/org/elasticsearch/index/mapper/NestedLookup.java

@@ -0,0 +1,114 @@
+/*
+ * 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.Query;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Holds information about nested mappings
+ */
+public interface NestedLookup {
+
+    /**
+     * @return a map of all nested object mappers in the current mapping
+     */
+    Map<String, NestedObjectMapper> getNestedMappers();
+
+    /**
+     * @return filters for nested objects that contain further nested mappers
+     */
+    Map<String, Query> getNestedParentFilters();
+
+    /**
+     * Given a nested object path, returns the path to its nested parent
+     *
+     * In particular, if a nested field `foo` contains an object field
+     * `bar.baz`, then calling this method with `foo.bar.baz` will return
+     * the path `foo`, skipping over the object-but-not-nested `foo.bar`
+     *
+     * @param path the path to resolve
+     */
+    String getNestedParent(String path);
+
+    /**
+     * A NestedLookup for a mapping with no nested mappers
+     */
+    NestedLookup EMPTY = new NestedLookup() {
+        @Override
+        public Map<String, NestedObjectMapper> getNestedMappers() {
+            return Collections.emptyMap();
+        }
+
+        @Override
+        public Map<String, Query> getNestedParentFilters() {
+            return Collections.emptyMap();
+        }
+
+        @Override
+        public String getNestedParent(String path) {
+            return null;
+        }
+    };
+
+    /**
+     * Construct a NestedLookup from a list of NestedObjectMappers
+     * @param mappers   the nested mappers to build a lookup over
+     */
+    static NestedLookup build(List<NestedObjectMapper> mappers) {
+        if (mappers == null || mappers.isEmpty()) {
+            return NestedLookup.EMPTY;
+        }
+        mappers = mappers.stream().sorted(Comparator.comparing(ObjectMapper::name)).collect(Collectors.toList());
+        Map<String, Query> parentFilters = new HashMap<>();
+        Map<String, NestedObjectMapper> mappersByName = new HashMap<>();
+        NestedObjectMapper previous = null;
+        for (NestedObjectMapper mapper : mappers) {
+            mappersByName.put(mapper.name(), mapper);
+            if (previous != null) {
+                if (mapper.name().startsWith(previous.name() + ".")) {
+                    parentFilters.put(previous.name(), previous.nestedTypeFilter());
+                }
+            }
+            previous = mapper;
+        }
+        List<String> nestedPathNames = mappers.stream().map(NestedObjectMapper::name).collect(Collectors.toList());
+        return new NestedLookup() {
+
+            @Override
+            public Map<String, NestedObjectMapper> getNestedMappers() {
+                return mappersByName;
+            }
+
+            @Override
+            public Map<String, Query> getNestedParentFilters() {
+                return parentFilters;
+            }
+
+            @Override
+            public String getNestedParent(String path) {
+                String parent = null;
+                for (String parentPath : nestedPathNames) {
+                    if (path.startsWith(parentPath + ".")) {
+                        // path names are ordered so this will give us the
+                        // parent with the longest path
+                        parent = parentPath;
+                    }
+                }
+                return parent;
+            }
+        };
+    }
+}

+ 0 - 1
server/src/main/java/org/elasticsearch/index/mapper/NestedObjectMapper.java

@@ -174,7 +174,6 @@ public class NestedObjectMapper extends ObjectMapper {
         }
         NestedObjectMapper mergeWithObject = (NestedObjectMapper) mergeWith;
         NestedObjectMapper toMerge = (NestedObjectMapper) clone();
-
         if (reason == MapperService.MergeReason.INDEX_TEMPLATE) {
             if (mergeWithObject.includeInParent.explicit()) {
                 toMerge.includeInParent = mergeWithObject.includeInParent;

+ 10 - 15
server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java

@@ -32,7 +32,6 @@ import org.elasticsearch.common.lucene.Lucene;
 import org.elasticsearch.common.lucene.search.Queries;
 import org.elasticsearch.common.lucene.search.TopDocsAndMaxScore;
 import org.elasticsearch.index.mapper.NestedObjectMapper;
-import org.elasticsearch.index.mapper.ObjectMapper;
 import org.elasticsearch.index.search.ESToParentBlockJoinQuery;
 import org.elasticsearch.index.search.NestedHelper;
 import org.elasticsearch.search.SearchHit;
@@ -263,17 +262,14 @@ public class NestedQueryBuilder extends AbstractQueryBuilder<NestedQueryBuilder>
             );
         }
 
-        ObjectMapper nestedObjectMapper = context.getObjectMapper(path);
-        if (nestedObjectMapper == null) {
+        NestedObjectMapper mapper = context.nestedLookup().getNestedMappers().get(path);
+        if (mapper == null) {
             if (ignoreUnmapped) {
                 return new MatchNoDocsQuery();
             } else {
                 throw new IllegalStateException("[" + NAME + "] failed to find nested object under path [" + path + "]");
             }
         }
-        if (nestedObjectMapper.isNested() == false) {
-            throw new IllegalStateException("[" + NAME + "] nested object under path [" + path + "] is not of nested type");
-        }
         final BitSetProducer parentFilter;
         Query innerQuery;
         NestedObjectMapper objectMapper = context.nestedScope().getObjectMapper();
@@ -284,7 +280,7 @@ public class NestedQueryBuilder extends AbstractQueryBuilder<NestedQueryBuilder>
         }
 
         try {
-            context.nestedScope().nextLevel((NestedObjectMapper) nestedObjectMapper);
+            context.nestedScope().nextLevel(mapper);
             innerQuery = this.query.toQuery(context);
         } finally {
             context.nestedScope().previousLevel();
@@ -292,9 +288,9 @@ public class NestedQueryBuilder extends AbstractQueryBuilder<NestedQueryBuilder>
 
         // ToParentBlockJoinQuery requires that the inner query only matches documents
         // in its child space
-        NestedHelper nestedHelper = new NestedHelper(context::getObjectMapper, context::isFieldMapped);
+        NestedHelper nestedHelper = new NestedHelper(context.nestedLookup(), context::isFieldMapped);
         if (nestedHelper.mightMatchNonNestedDocs(innerQuery, path)) {
-            innerQuery = Queries.filtered(innerQuery, ((NestedObjectMapper) nestedObjectMapper).nestedTypeFilter());
+            innerQuery = Queries.filtered(innerQuery, mapper.nestedTypeFilter());
         }
 
         return new ESToParentBlockJoinQuery(innerQuery, parentFilter, scoreMode, objectMapper == null ? null : objectMapper.fullPath());
@@ -342,22 +338,21 @@ public class NestedQueryBuilder extends AbstractQueryBuilder<NestedQueryBuilder>
         @Override
         protected void doBuild(SearchContext parentSearchContext, InnerHitsContext innerHitsContext) throws IOException {
             SearchExecutionContext searchExecutionContext = parentSearchContext.getSearchExecutionContext();
-            ObjectMapper objectMapper = searchExecutionContext.getObjectMapper(path);
-            if (objectMapper == null || objectMapper.isNested() == false) {
+            NestedObjectMapper nestedMapper = searchExecutionContext.nestedLookup().getNestedMappers().get(path);
+            if (nestedMapper == null) {
                 if (innerHitBuilder.isIgnoreUnmapped() == false) {
                     throw new IllegalStateException("[" + query.getName() + "] no mapping found for type [" + path + "]");
                 } else {
                     return;
                 }
             }
-            NestedObjectMapper nestedObjectMapper = (NestedObjectMapper) objectMapper;
-            String name = innerHitBuilder.getName() != null ? innerHitBuilder.getName() : nestedObjectMapper.fullPath();
-            NestedObjectMapper parentObjectMapper = searchExecutionContext.nestedScope().nextLevel(nestedObjectMapper);
+            String name = innerHitBuilder.getName() != null ? innerHitBuilder.getName() : nestedMapper.fullPath();
+            NestedObjectMapper parentObjectMapper = searchExecutionContext.nestedScope().nextLevel(nestedMapper);
             NestedInnerHitSubContext nestedInnerHits = new NestedInnerHitSubContext(
                 name,
                 parentSearchContext,
                 parentObjectMapper,
-                nestedObjectMapper
+                nestedMapper
             );
             setupInnerHitsContext(searchExecutionContext, nestedInnerHits);
             searchExecutionContext.nestedScope().previousLevel();

+ 3 - 30
server/src/main/java/org/elasticsearch/index/query/SearchExecutionContext.java

@@ -28,7 +28,6 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
 import org.elasticsearch.common.lucene.search.Queries;
 import org.elasticsearch.common.regex.Regex;
 import org.elasticsearch.core.CheckedFunction;
-import org.elasticsearch.core.Nullable;
 import org.elasticsearch.index.Index;
 import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.IndexSortConfig;
@@ -44,8 +43,7 @@ import org.elasticsearch.index.mapper.MapperParsingException;
 import org.elasticsearch.index.mapper.MapperService;
 import org.elasticsearch.index.mapper.MappingLookup;
 import org.elasticsearch.index.mapper.MappingParserContext;
-import org.elasticsearch.index.mapper.NestedObjectMapper;
-import org.elasticsearch.index.mapper.ObjectMapper;
+import org.elasticsearch.index.mapper.NestedLookup;
 import org.elasticsearch.index.mapper.ParsedDocument;
 import org.elasticsearch.index.mapper.RuntimeField;
 import org.elasticsearch.index.mapper.SourceToParse;
@@ -305,18 +303,14 @@ public class SearchExecutionContext extends QueryRewriteContext {
         return mapperService.documentParser().parseDocument(source, mappingLookup);
     }
 
-    public boolean hasNested() {
-        return mappingLookup.hasNested();
+    public NestedLookup nestedLookup() {
+        return mappingLookup.nestedLookup();
     }
 
     public boolean hasMappings() {
         return mappingLookup.hasMappings();
     }
 
-    public List<NestedObjectMapper> nestedMappings() {
-        return mappingLookup.getNestedMappers();
-    }
-
     /**
      * Returns the names of all mapped fields that match a given pattern
      *
@@ -376,16 +370,6 @@ public class SearchExecutionContext extends QueryRewriteContext {
         return fieldType == null ? mappingLookup.getFieldType(name) : fieldType;
     }
 
-    /**
-     *
-     * @param name name of the object
-     * @return can be null e.g. if field is root of a composite runtime field
-     */
-    @Nullable
-    public ObjectMapper getObjectMapper(String name) {
-        return mappingLookup.objectMappers().get(name);
-    }
-
     public boolean isMetadataField(String field) {
         return mapperService.isMetadataField(field);
     }
@@ -706,17 +690,6 @@ public class SearchExecutionContext extends QueryRewriteContext {
         return mappingLookup.cacheKey();
     }
 
-    /**
-     * Given a nested object path, returns the path to its nested parent
-     *
-     * In particular, if a nested field `foo` contains an object field
-     * `bar.baz`, then calling this method with `foo.bar.baz` will return
-     * the path `foo`, skipping over the object-but-not-nested `foo.bar`
-     */
-    public String getNestedParent(String nestedPath) {
-        return mappingLookup.getNestedParent(nestedPath);
-    }
-
     public NestedDocuments getNestedDocuments() {
         return new NestedDocuments(mappingLookup, bitsetFilterCache::getBitSetProducer);
     }

+ 15 - 34
server/src/main/java/org/elasticsearch/index/search/NestedHelper.java

@@ -21,21 +21,20 @@ import org.apache.lucene.search.PointRangeQuery;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.search.TermInSetQuery;
 import org.apache.lucene.search.TermQuery;
+import org.elasticsearch.index.mapper.NestedLookup;
 import org.elasticsearch.index.mapper.NestedObjectMapper;
-import org.elasticsearch.index.mapper.ObjectMapper;
 
-import java.util.function.Function;
 import java.util.function.Predicate;
 
 /** Utility class to filter parent and children clauses when building nested
  * queries. */
 public final class NestedHelper {
 
-    private final Function<String, ObjectMapper> objectMapperLookup;
+    private final NestedLookup nestedLookup;
     private final Predicate<String> isMappedFieldPredicate;
 
-    public NestedHelper(Function<String, ObjectMapper> objectMapperLookup, Predicate<String> isMappedFieldPredicate) {
-        this.objectMapperLookup = objectMapperLookup;
+    public NestedHelper(NestedLookup nestedLookup, Predicate<String> isMappedFieldPredicate) {
+        this.nestedLookup = nestedLookup;
         this.isMappedFieldPredicate = isMappedFieldPredicate;
     }
 
@@ -102,13 +101,7 @@ public final class NestedHelper {
             // field does not exist
             return false;
         }
-        for (String parent = parentObject(field); parent != null; parent = parentObject(parent)) {
-            ObjectMapper mapper = objectMapperLookup.apply(parent);
-            if (mapper != null && mapper.isNested()) {
-                return true;
-            }
-        }
-        return false;
+        return nestedLookup.getNestedParent(field) != null;
     }
 
     /** Returns true if the given query might match parent documents or documents
@@ -171,30 +164,18 @@ public final class NestedHelper {
         if (isMappedFieldPredicate.test(field) == false) {
             return false;
         }
-        for (String parent = parentObject(field); parent != null; parent = parentObject(parent)) {
-            ObjectMapper mapper = objectMapperLookup.apply(parent);
-            if (mapper != null && mapper.isNested()) {
-                NestedObjectMapper nestedMapper = (NestedObjectMapper) mapper;
-                if (mapper.fullPath().equals(nestedPath)) {
-                    // If the mapper does not include in its parent or in the root object then
-                    // the query might only match nested documents with the given path
-                    return nestedMapper.isIncludeInParent() || nestedMapper.isIncludeInRoot();
-                } else {
-                    // the first parent nested mapper does not have the expected path
-                    // It might be misconfiguration or a sub nested mapper
-                    return true;
-                }
-            }
+        String nestedParent = nestedLookup.getNestedParent(field);
+        if (nestedParent == null || nestedParent.startsWith(nestedPath) == false) {
+            // the field is not a sub field of the nested path
+            return true;
         }
-        return true; // the field is not a sub field of the nested path
-    }
-
-    public static String parentObject(String field) {
-        int lastDot = field.lastIndexOf('.');
-        if (lastDot == -1) {
-            return null;
+        NestedObjectMapper nestedMapper = nestedLookup.getNestedMappers().get(nestedParent);
+        // If the mapper does not include in its parent or in the root object then
+        // the query might only match nested documents with the given path
+        if (nestedParent.equals(nestedPath)) {
+            return nestedMapper.isIncludeInParent() || nestedMapper.isIncludeInRoot();
         }
-        return field.substring(0, lastDot);
+        return true;
     }
 
 }

+ 4 - 2
server/src/main/java/org/elasticsearch/search/DefaultSearchContext.java

@@ -24,6 +24,7 @@ import org.elasticsearch.index.IndexService;
 import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.cache.bitset.BitsetFilterCache;
 import org.elasticsearch.index.engine.Engine;
+import org.elasticsearch.index.mapper.NestedLookup;
 import org.elasticsearch.index.query.AbstractQueryBuilder;
 import org.elasticsearch.index.query.ParsedQuery;
 import org.elasticsearch.index.query.QueryBuilder;
@@ -274,8 +275,9 @@ final class DefaultSearchContext extends SearchContext {
     @Override
     public Query buildFilteredQuery(Query query) {
         List<Query> filters = new ArrayList<>();
-        NestedHelper nestedHelper = new NestedHelper(searchExecutionContext::getObjectMapper, searchExecutionContext::isFieldMapped);
-        if (searchExecutionContext.hasNested()
+        NestedLookup nestedLookup = searchExecutionContext.nestedLookup();
+        NestedHelper nestedHelper = new NestedHelper(nestedLookup, searchExecutionContext::isFieldMapped);
+        if (nestedLookup != NestedLookup.EMPTY
             && nestedHelper.mightMatchNestedDocs(query)
             && (aliasFilter == null || nestedHelper.mightMatchNestedDocs(aliasFilter))) {
             filters.add(Queries.newNonNestedFilter());

+ 20 - 14
server/src/main/java/org/elasticsearch/search/NestedDocuments.java

@@ -20,6 +20,7 @@ import org.apache.lucene.search.join.BitSetProducer;
 import org.apache.lucene.util.BitSet;
 import org.elasticsearch.common.lucene.search.Queries;
 import org.elasticsearch.index.mapper.MappingLookup;
+import org.elasticsearch.index.mapper.NestedLookup;
 import org.elasticsearch.index.mapper.NestedObjectMapper;
 
 import java.io.IOException;
@@ -34,9 +35,8 @@ public class NestedDocuments {
 
     private final Map<String, BitSetProducer> parentObjectFilters = new HashMap<>();
     private final Map<String, Weight> childObjectFilters = new HashMap<>();
-    private final Map<String, NestedObjectMapper> childObjectMappers = new HashMap<>();
     private final BitSetProducer parentDocumentFilter;
-    private final MappingLookup mappingLookup;
+    private final NestedLookup nestedLookup;
 
     /**
      * Create a new NestedDocuments object for an index
@@ -44,17 +44,14 @@ public class NestedDocuments {
      * @param filterProducer    a function to build BitSetProducers from filter queries
      */
     public NestedDocuments(MappingLookup mappingLookup, Function<Query, BitSetProducer> filterProducer) {
-        this.mappingLookup = mappingLookup;
-        if (mappingLookup.hasNested() == false) {
+        this.nestedLookup = mappingLookup.nestedLookup();
+        if (this.nestedLookup == NestedLookup.EMPTY) {
             this.parentDocumentFilter = null;
         } else {
             this.parentDocumentFilter = filterProducer.apply(Queries.newNonNestedFilter());
-            for (NestedObjectMapper mapper : mappingLookup.getNestedParentMappers()) {
-                parentObjectFilters.put(mapper.name(), filterProducer.apply(mapper.nestedTypeFilter()));
-            }
-            for (NestedObjectMapper mapper : mappingLookup.getNestedMappers()) {
-                childObjectFilters.put(mapper.name(), null);
-                childObjectMappers.put(mapper.name(), mapper);
+            nestedLookup.getNestedParentFilters().forEach((k, v) -> parentObjectFilters.put(k, filterProducer.apply(v)));
+            for (String nestedPath : nestedLookup.getNestedMappers().keySet()) {
+                childObjectFilters.put(nestedPath, null);
             }
         }
     }
@@ -70,12 +67,12 @@ public class NestedDocuments {
     }
 
     private Weight getNestedChildWeight(LeafReaderContext ctx, String path) throws IOException {
-        if (childObjectFilters.containsKey(path) == false || childObjectMappers.containsKey(path) == false) {
+        if (childObjectFilters.containsKey(path) == false) {
             throw new IllegalStateException("Cannot find object mapper for path " + path);
         }
         if (childObjectFilters.get(path) == null) {
             IndexSearcher searcher = new IndexSearcher(ReaderUtil.getTopLevelContext(ctx));
-            NestedObjectMapper childMapper = childObjectMappers.get(path);
+            NestedObjectMapper childMapper = nestedLookup.getNestedMappers().get(path);
             childObjectFilters.put(
                 path,
                 searcher.createWeight(searcher.rewrite(childMapper.nestedTypeFilter()), ScoreMode.COMPLETE_NO_SCORES, 1)
@@ -168,7 +165,7 @@ public class NestedDocuments {
             int parentNameLength;
             String path = findObjectPath(doc);
             while (path != null) {
-                String parent = mappingLookup.getNestedParent(path);
+                String parent = nestedLookup.getNestedParent(path);
                 // We have to pull a new scorer for each document here, because we advance from
                 // the last parent which will be behind the doc
                 Scorer childScorer = getNestedChildWeight(ctx, path).scorer(ctx);
@@ -181,7 +178,16 @@ public class NestedDocuments {
                     parentNameLength = 0;
                 } else {
                     if (objectFilters.containsKey(parent) == false) {
-                        throw new IllegalStateException("Cannot find parent mapper for path " + path + " in doc " + doc);
+                        throw new IllegalStateException(
+                            "Cannot find parent mapper "
+                                + parent
+                                + " for path "
+                                + path
+                                + " in doc "
+                                + doc
+                                + " - known parents are "
+                                + objectFilters.keySet()
+                        );
                     }
                     parentBitSet = objectFilters.get(parent);
                     parentNameLength = parent.length() + 1;

+ 4 - 17
server/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregationBuilder.java

@@ -12,10 +12,8 @@ import org.elasticsearch.common.ParsingException;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.index.mapper.NestedObjectMapper;
-import org.elasticsearch.index.mapper.ObjectMapper;
 import org.elasticsearch.search.aggregations.AbstractAggregationBuilder;
 import org.elasticsearch.search.aggregations.AggregationBuilder;
-import org.elasticsearch.search.aggregations.AggregationExecutionException;
 import org.elasticsearch.search.aggregations.AggregatorFactories.Builder;
 import org.elasticsearch.search.aggregations.AggregatorFactory;
 import org.elasticsearch.search.aggregations.support.AggregationContext;
@@ -84,26 +82,15 @@ public class NestedAggregationBuilder extends AbstractAggregationBuilder<NestedA
     @Override
     protected AggregatorFactory doBuild(AggregationContext context, AggregatorFactory parent, Builder subFactoriesBuilder)
         throws IOException {
-        ObjectMapper childObjectMapper = context.getObjectMapper(path);
-        if (childObjectMapper == null) {
+        NestedObjectMapper nestedMapper = context.nestedLookup().getNestedMappers().get(path);
+        if (nestedMapper == null) {
             // in case the path has been unmapped:
             return new NestedAggregatorFactory(name, null, null, context, parent, subFactoriesBuilder, metadata);
         }
 
-        if (childObjectMapper.isNested() == false) {
-            throw new AggregationExecutionException("[nested] nested path [" + path + "] is not nested");
-        }
         try {
-            NestedObjectMapper parentObjectMapper = context.nestedScope().nextLevel((NestedObjectMapper) childObjectMapper);
-            return new NestedAggregatorFactory(
-                name,
-                parentObjectMapper,
-                (NestedObjectMapper) childObjectMapper,
-                context,
-                parent,
-                subFactoriesBuilder,
-                metadata
-            );
+            NestedObjectMapper parentObjectMapper = context.nestedScope().nextLevel(nestedMapper);
+            return new NestedAggregatorFactory(name, parentObjectMapper, nestedMapper, context, parent, subFactoriesBuilder, metadata);
         } finally {
             context.nestedScope().previousLevel();
         }

+ 3 - 9
server/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/ReverseNestedAggregationBuilder.java

@@ -12,11 +12,9 @@ import org.elasticsearch.common.ParsingException;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.index.mapper.NestedObjectMapper;
-import org.elasticsearch.index.mapper.ObjectMapper;
 import org.elasticsearch.index.query.support.NestedScope;
 import org.elasticsearch.search.aggregations.AbstractAggregationBuilder;
 import org.elasticsearch.search.aggregations.AggregationBuilder;
-import org.elasticsearch.search.aggregations.AggregationExecutionException;
 import org.elasticsearch.search.aggregations.AggregatorFactories.Builder;
 import org.elasticsearch.search.aggregations.AggregatorFactory;
 import org.elasticsearch.search.aggregations.support.AggregationContext;
@@ -91,19 +89,15 @@ public class ReverseNestedAggregationBuilder extends AbstractAggregationBuilder<
             throw new IllegalArgumentException("Reverse nested aggregation [" + name + "] can only be used inside a [nested] aggregation");
         }
 
-        ObjectMapper parentObjectMapper = null;
+        NestedObjectMapper nestedMapper = null;
         if (path != null) {
-            parentObjectMapper = context.getObjectMapper(path);
-            if (parentObjectMapper == null) {
+            nestedMapper = context.nestedLookup().getNestedMappers().get(path);
+            if (nestedMapper == null) {
                 return new ReverseNestedAggregatorFactory(name, true, null, context, parent, subFactoriesBuilder, metadata);
             }
-            if (parentObjectMapper.isNested() == false) {
-                throw new AggregationExecutionException("[reverse_nested] nested path [" + path + "] is not nested");
-            }
         }
 
         NestedScope nestedScope = context.nestedScope();
-        NestedObjectMapper nestedMapper = (NestedObjectMapper) parentObjectMapper;
         try {
             nestedScope.nextLevel(nestedMapper);
             return new ReverseNestedAggregatorFactory(name, false, nestedMapper, context, parent, subFactoriesBuilder, metadata);

+ 5 - 5
server/src/main/java/org/elasticsearch/search/aggregations/support/AggregationContext.java

@@ -25,7 +25,7 @@ import org.elasticsearch.index.analysis.NamedAnalyzer;
 import org.elasticsearch.index.cache.bitset.BitsetFilterCache;
 import org.elasticsearch.index.fielddata.IndexFieldData;
 import org.elasticsearch.index.mapper.MappedFieldType;
-import org.elasticsearch.index.mapper.ObjectMapper;
+import org.elasticsearch.index.mapper.NestedLookup;
 import org.elasticsearch.index.query.QueryBuilder;
 import org.elasticsearch.index.query.Rewriteable;
 import org.elasticsearch.index.query.SearchExecutionContext;
@@ -206,9 +206,9 @@ public abstract class AggregationContext implements Releasable {
     public abstract Optional<SortAndFormats> buildSort(List<SortBuilder<?>> sortBuilders) throws IOException;
 
     /**
-     * Find an {@link ObjectMapper}.
+     * Get the {@link NestedLookup} of this index
      */
-    public abstract ObjectMapper getObjectMapper(String path);
+    public abstract NestedLookup nestedLookup();
 
     /**
      * Access the nested scope. Stay away from this unless you are dealing with nested.
@@ -474,8 +474,8 @@ public abstract class AggregationContext implements Releasable {
         }
 
         @Override
-        public ObjectMapper getObjectMapper(String path) {
-            return context.getObjectMapper(path);
+        public NestedLookup nestedLookup() {
+            return context.nestedLookup();
         }
 
         @Override

+ 2 - 6
server/src/main/java/org/elasticsearch/search/fetch/subphase/FieldFetcher.java

@@ -16,7 +16,6 @@ import org.elasticsearch.common.xcontent.support.XContentMapValues;
 import org.elasticsearch.core.Nullable;
 import org.elasticsearch.index.mapper.MappedFieldType;
 import org.elasticsearch.index.mapper.NestedValueFetcher;
-import org.elasticsearch.index.mapper.ObjectMapper;
 import org.elasticsearch.index.mapper.ValueFetcher;
 import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.search.lookup.SourceLookup;
@@ -24,7 +23,6 @@ import org.elasticsearch.search.lookup.SourceLookup;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -45,9 +43,7 @@ public class FieldFetcher {
     private static final int AUTOMATON_MAX_DETERMINIZED_STATES = 100000;
 
     public static FieldFetcher create(SearchExecutionContext context, Collection<FieldAndFormat> fieldAndFormats) {
-        Set<String> nestedMappingPaths = context.hasNested()
-            ? context.nestedMappings().stream().map(ObjectMapper::name).collect(Collectors.toSet())
-            : Collections.emptySet();
+        Set<String> nestedMappingPaths = context.nestedLookup().getNestedMappers().keySet();
         return create(context, fieldAndFormats, nestedMappingPaths, "");
     }
 
@@ -268,7 +264,7 @@ public class FieldFetcher {
     private static Set<String> getParentPaths(Set<String> nestedPathsInScope, SearchExecutionContext context) {
         Set<String> parentPaths = new HashSet<>();
         for (String candidate : nestedPathsInScope) {
-            String nestedParent = context.getNestedParent(candidate);
+            String nestedParent = context.nestedLookup().getNestedParent(candidate);
             // if the candidate has no nested parent itself, its a minimal parent path
             // if the candidate has a parent which is out of scope this means it minimal itself
             if (nestedParent == null || nestedPathsInScope.contains(nestedParent) == false) {

+ 8 - 13
server/src/main/java/org/elasticsearch/search/sort/FieldSortBuilder.java

@@ -30,9 +30,9 @@ import org.elasticsearch.index.fielddata.IndexNumericFieldData.NumericType;
 import org.elasticsearch.index.mapper.DateFieldMapper.DateFieldType;
 import org.elasticsearch.index.mapper.KeywordFieldMapper;
 import org.elasticsearch.index.mapper.MappedFieldType;
+import org.elasticsearch.index.mapper.NestedLookup;
 import org.elasticsearch.index.mapper.NestedObjectMapper;
 import org.elasticsearch.index.mapper.NumberFieldMapper.NumberFieldType;
-import org.elasticsearch.index.mapper.ObjectMapper;
 import org.elasticsearch.index.query.QueryBuilder;
 import org.elasticsearch.index.query.QueryRewriteContext;
 import org.elasticsearch.index.query.QueryShardException;
@@ -55,7 +55,6 @@ import java.util.function.Function;
 
 import static org.elasticsearch.index.mapper.DateFieldMapper.Resolution.MILLISECONDS;
 import static org.elasticsearch.index.mapper.DateFieldMapper.Resolution.NANOSECONDS;
-import static org.elasticsearch.index.search.NestedHelper.parentObject;
 import static org.elasticsearch.search.sort.NestedSortBuilder.NESTED_FIELD;
 
 /**
@@ -660,17 +659,13 @@ public class FieldSortBuilder extends SortBuilder<FieldSortBuilder> {
             // already in nested context
             return;
         }
-        for (String parent = parentObject(field); parent != null; parent = parentObject(parent)) {
-            ObjectMapper parentMapper = context.getObjectMapper(parent);
-            if (parentMapper != null && parentMapper.isNested()) {
-                NestedObjectMapper parentNested = (NestedObjectMapper) parentMapper;
-                if (parentNested.isIncludeInRoot() == false) {
-                    throw new QueryShardException(
-                        context,
-                        "it is mandatory to set the [nested] context on the nested sort field: [" + field + "]."
-                    );
-                }
-            }
+        NestedLookup nestedLookup = context.nestedLookup();
+        String nestedParent = nestedLookup.getNestedParent(field);
+        if (nestedParent != null && nestedLookup.getNestedMappers().get(nestedParent).isIncludeInRoot() == false) {
+            throw new QueryShardException(
+                context,
+                "it is mandatory to set the [nested] context on the nested sort field: [" + field + "]."
+            );
         }
     }
 

+ 2 - 8
server/src/main/java/org/elasticsearch/search/sort/SortBuilder.java

@@ -20,7 +20,6 @@ import org.elasticsearch.common.util.BigArrays;
 import org.elasticsearch.core.RestApiVersion;
 import org.elasticsearch.index.fielddata.IndexFieldData.XFieldComparatorSource.Nested;
 import org.elasticsearch.index.mapper.NestedObjectMapper;
-import org.elasticsearch.index.mapper.ObjectMapper;
 import org.elasticsearch.index.query.QueryBuilder;
 import org.elasticsearch.index.query.QueryShardException;
 import org.elasticsearch.index.query.Rewriteable;
@@ -210,15 +209,10 @@ public abstract class SortBuilder<T extends SortBuilder<T>> implements NamedWrit
         NestedSortBuilder nestedNestedSort = nestedSort.getNestedSort();
 
         // verify our nested path
-        ObjectMapper objectMapper = context.getObjectMapper(nestedPath);
-
-        if (objectMapper == null) {
+        NestedObjectMapper nestedObjectMapper = context.nestedLookup().getNestedMappers().get(nestedPath);
+        if (nestedObjectMapper == null) {
             throw new QueryShardException(context, "[nested] failed to find nested object under path [" + nestedPath + "]");
         }
-        if (objectMapper.isNested() == false) {
-            throw new QueryShardException(context, "[nested] nested object under path [" + nestedPath + "] is not of nested type");
-        }
-        NestedObjectMapper nestedObjectMapper = (NestedObjectMapper) objectMapper;
         NestedObjectMapper parentMapper = context.nestedScope().getObjectMapper();
 
         // get our child query, potentially applying a users filter

+ 84 - 0
server/src/test/java/org/elasticsearch/index/mapper/NestedLookupTests.java

@@ -0,0 +1,84 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.index.mapper;
+
+import org.elasticsearch.Version;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.hasSize;
+
+public class NestedLookupTests extends MapperServiceTestCase {
+
+    public void testMultiLevelParents() throws IOException {
+
+        String mapping = "{\n"
+            + "  \"_doc\": {\n"
+            + "    \"properties\" : {\n"
+            + "      \"SWufZ\" : {\n"
+            + "        \"type\" : \"nested\",\n"
+            + "        \"properties\" : {\n"
+            + "          \"ZCPoX\" : {\n"
+            + "            \"type\" : \"keyword\"\n"
+            + "          },\n"
+            + "          \"NnUDX\" : {\n"
+            + "            \"properties\" : {\n"
+            + "              \"dljyS\" : {\n"
+            + "                \"type\" : \"nested\",\n"
+            + "                \"properties\" : {\n"
+            + "                  \"JYmZZ\" : {\n"
+            + "                    \"type\" : \"keyword\"\n"
+            + "                  },\n"
+            + "                  \"EvbGO\" : {\n"
+            + "                    \"type\" : \"nested\",\n"
+            + "                    \"properties\" : {\n"
+            + "                      \"LAgoT\" : {\n"
+            + "                        \"type\" : \"keyword\"\n"
+            + "                      }\n"
+            + "                    }\n"
+            + "                  }\n"
+            + "                }\n"
+            + "              }\n"
+            + "            }\n"
+            + "          }\n"
+            + "        }\n"
+            + "      }\n"
+            + "    }\n"
+            + "  }\n"
+            + "}";
+
+        MapperService mapperService = createMapperService(mapping);
+
+        NestedLookup lookup = mapperService.mappingLookup().nestedLookup();
+        assertEquals("SWufZ.NnUDX.dljyS", lookup.getNestedParent("SWufZ.NnUDX.dljyS.EvbGO"));
+        assertThat(lookup.getNestedParentFilters().keySet(), hasSize(2));
+
+    }
+
+    private static NestedObjectMapper buildMapper(String name) {
+        return new NestedObjectMapper.Builder(name, Version.CURRENT).build(MapperBuilderContext.ROOT);
+    }
+
+    public void testAllParentFilters() {
+        List<NestedObjectMapper> mappers = List.of(
+            buildMapper("a.b"),
+            buildMapper("a.d"),
+            buildMapper("a.b.c.d.e"),
+            buildMapper("a.b.d"),
+            buildMapper("a"),
+            buildMapper("a.b.c.d")
+        );
+
+        NestedLookup lookup = NestedLookup.build(mappers);
+        assertThat(lookup.getNestedParentFilters().keySet(), containsInAnyOrder("a", "a.b", "a.b.c.d"));
+    }
+
+}

+ 10 - 10
server/src/test/java/org/elasticsearch/index/mapper/NestedObjectMapperTests.java

@@ -59,7 +59,7 @@ public class NestedObjectMapperTests extends MapperServiceTestCase {
 
         DocumentMapper docMapper = createDocumentMapper(mapping(b -> b.startObject("nested1").field("type", "nested").endObject()));
 
-        assertThat(docMapper.mappers().hasNested(), equalTo(true));
+        assertNotEquals(NestedLookup.EMPTY, docMapper.mappers().nestedLookup());
         ObjectMapper mapper = docMapper.mappers().objectMappers().get("nested1");
         assertThat(mapper, instanceOf(NestedObjectMapper.class));
         NestedObjectMapper nested1Mapper = (NestedObjectMapper) mapper;
@@ -136,7 +136,7 @@ public class NestedObjectMapperTests extends MapperServiceTestCase {
             b.endObject();
         }));
 
-        assertThat(docMapper.mappers().hasNested(), equalTo(true));
+        assertNotEquals(NestedLookup.EMPTY, docMapper.mappers().nestedLookup());
         ObjectMapper mapper1 = docMapper.mappers().objectMappers().get("nested1");
         assertThat(mapper1, instanceOf(NestedObjectMapper.class));
         NestedObjectMapper nested1Mapper = (NestedObjectMapper) mapper1;
@@ -226,7 +226,7 @@ public class NestedObjectMapperTests extends MapperServiceTestCase {
             b.endObject();
         }));
 
-        assertThat(docMapper.mappers().hasNested(), equalTo(true));
+        assertNotEquals(NestedLookup.EMPTY, docMapper.mappers().nestedLookup());
         ObjectMapper mapper1 = docMapper.mappers().objectMappers().get("nested1");
         assertThat(mapper1, instanceOf(NestedObjectMapper.class));
         NestedObjectMapper nested1Mapper = (NestedObjectMapper) mapper1;
@@ -317,7 +317,7 @@ public class NestedObjectMapperTests extends MapperServiceTestCase {
             b.endObject();
         }));
 
-        assertThat(docMapper.mappers().hasNested(), equalTo(true));
+        assertNotEquals(NestedLookup.EMPTY, docMapper.mappers().nestedLookup());
         ObjectMapper mapper1 = docMapper.mappers().objectMappers().get("nested1");
         assertThat(mapper1, instanceOf(NestedObjectMapper.class));
         NestedObjectMapper nested1Mapper = (NestedObjectMapper) mapper1;
@@ -407,11 +407,11 @@ public class NestedObjectMapperTests extends MapperServiceTestCase {
             b.endObject();
         }));
 
-        assertEquals("nested1", docMapper.mappers().getNestedParent("nested1.nested2"));
-        assertNull(docMapper.mappers().getNestedParent("nonexistent"));
-        assertNull(docMapper.mappers().getNestedParent("nested1"));
+        assertEquals("nested1", docMapper.mappers().nestedLookup().getNestedParent("nested1.nested2"));
+        assertNull(docMapper.mappers().nestedLookup().getNestedParent("nonexistent"));
+        assertNull(docMapper.mappers().nestedLookup().getNestedParent("nested1"));
 
-        assertThat(docMapper.mappers().hasNested(), equalTo(true));
+        assertNotEquals(NestedLookup.EMPTY, docMapper.mappers().nestedLookup());
         ObjectMapper mapper1 = docMapper.mappers().objectMappers().get("nested1");
         assertThat(mapper1, instanceOf(NestedObjectMapper.class));
         NestedObjectMapper nested1Mapper = (NestedObjectMapper) mapper1;
@@ -745,7 +745,7 @@ public class NestedObjectMapperTests extends MapperServiceTestCase {
             b.endObject();
         }));
 
-        assertThat(docMapper.mappers().hasNested(), equalTo(true));
+        assertNotEquals(NestedLookup.EMPTY, docMapper.mappers().nestedLookup());
         ObjectMapper nested1Mapper = docMapper.mappers().objectMappers().get("nested1");
         assertThat(nested1Mapper, instanceOf(NestedObjectMapper.class));
         assertThat(nested1Mapper.dynamic(), equalTo(Dynamic.STRICT));
@@ -974,7 +974,7 @@ public class NestedObjectMapperTests extends MapperServiceTestCase {
             mapping(b -> b.startObject("nested1").field("type", "nested").endObject())
         );
 
-        assertThat(docMapper.mappers().hasNested(), equalTo(true));
+        assertNotEquals(NestedLookup.EMPTY, docMapper.mappers().nestedLookup());
         ObjectMapper mapper = docMapper.mappers().objectMappers().get("nested1");
         assertThat(mapper, instanceOf(NestedObjectMapper.class));
 

+ 10 - 9
server/src/test/java/org/elasticsearch/index/query/ExistsQueryBuilderTests.java

@@ -20,9 +20,8 @@ import org.elasticsearch.common.regex.Regex;
 import org.elasticsearch.test.AbstractQueryTestCase;
 
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collection;
-import java.util.List;
+import java.util.Collections;
 
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.instanceOf;
@@ -49,8 +48,11 @@ public class ExistsQueryBuilderTests extends AbstractQueryTestCase<ExistsQueryBu
     protected void doAssertLuceneQuery(ExistsQueryBuilder queryBuilder, Query query, SearchExecutionContext context) throws IOException {
         String fieldPattern = queryBuilder.fieldName();
         Collection<String> fields = context.getMatchingFieldNames(fieldPattern);
-        if (fields.size() == 0 && Regex.isSimpleMatchPattern(fieldPattern) == false && context.getObjectMapper(fieldPattern) != null) {
-            fields = context.getMatchingFieldNames(fieldPattern + ".*");
+        if (fields.size() == 0 && Regex.isSimpleMatchPattern(fieldPattern) == false) {
+            if (context.getMatchingFieldNames(fieldPattern + ".*").isEmpty() == false) {
+                // we're an object field
+                fields = Collections.singleton(fieldPattern);
+            }
         }
         if (fields.size() == 0) {
             assertThat(fieldPattern, query, instanceOf(MatchNoDocsQuery.class));
@@ -58,14 +60,13 @@ public class ExistsQueryBuilderTests extends AbstractQueryTestCase<ExistsQueryBu
             assertThat(query, instanceOf(ConstantScoreQuery.class));
             ConstantScoreQuery constantScoreQuery = (ConstantScoreQuery) query;
             String field = expectedFieldName(fields.iterator().next());
-            if (context.getObjectMapper(field) != null) {
+            if (context.getFieldType(field) == null) {
+                // not a leaf field, so we're doing an object exists query
                 assertThat(constantScoreQuery.getQuery(), instanceOf(BooleanQuery.class));
                 BooleanQuery booleanQuery = (BooleanQuery) constantScoreQuery.getQuery();
-                List<String> childFields = new ArrayList<>();
-                context.getObjectMapper(field).forEach(mapper -> childFields.add(mapper.name()));
+                Collection<String> childFields = context.getMatchingFieldNames(field + ".*");
                 assertThat(booleanQuery.clauses().size(), equalTo(childFields.size()));
-                for (int i = 0; i < childFields.size(); i++) {
-                    BooleanClause booleanClause = booleanQuery.clauses().get(i);
+                for (BooleanClause booleanClause : booleanQuery) {
                     assertThat(booleanClause.getOccur(), equalTo(BooleanClause.Occur.SHOULD));
                 }
             } else if (context.getFieldType(field).hasDocValues()) {

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

@@ -21,6 +21,7 @@ import org.elasticsearch.common.compress.CompressedXContent;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.mapper.MapperService;
+import org.elasticsearch.index.mapper.NestedLookup;
 import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
 import org.elasticsearch.index.search.ESToParentBlockJoinQuery;
 import org.elasticsearch.search.fetch.subphase.InnerHitsContext;
@@ -330,9 +331,9 @@ public class NestedQueryBuilderTests extends AbstractQueryTestCase<NestedQueryBu
 
     public void testBuildIgnoreUnmappedNestQuery() throws Exception {
         SearchExecutionContext searchExecutionContext = mock(SearchExecutionContext.class);
-        when(searchExecutionContext.getObjectMapper("path")).thenReturn(null);
         IndexSettings settings = new IndexSettings(newIndexMeta("index", Settings.EMPTY), Settings.EMPTY);
         when(searchExecutionContext.getIndexSettings()).thenReturn(settings);
+        when(searchExecutionContext.nestedLookup()).thenReturn(NestedLookup.EMPTY);
         SearchContext searchContext = mock(SearchContext.class);
         when(searchContext.getSearchExecutionContext()).thenReturn(searchExecutionContext);
         InnerHitBuilder leafInnerHits = randomNestedInnerHits();

+ 38 - 70
server/src/test/java/org/elasticsearch/index/search/NestedHelperTests.java

@@ -8,94 +8,69 @@
 
 package org.elasticsearch.index.search;
 
-import org.apache.lucene.index.MultiReader;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.search.BooleanClause.Occur;
 import org.apache.lucene.search.BooleanQuery;
-import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.MatchAllDocsQuery;
 import org.apache.lucene.search.MatchNoDocsQuery;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.search.TermQuery;
 import org.apache.lucene.search.join.ScoreMode;
-import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.index.IndexService;
 import org.elasticsearch.index.mapper.MapperService;
+import org.elasticsearch.index.mapper.MapperServiceTestCase;
 import org.elasticsearch.index.query.MatchAllQueryBuilder;
 import org.elasticsearch.index.query.NestedQueryBuilder;
 import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.index.query.TermQueryBuilder;
-import org.elasticsearch.test.ESSingleNodeTestCase;
-import org.elasticsearch.xcontent.XContentBuilder;
-import org.elasticsearch.xcontent.XContentFactory;
 
 import java.io.IOException;
 import java.util.Collections;
 
-import static java.util.Collections.emptyMap;
+import static org.mockito.Mockito.mock;
 
-public class NestedHelperTests extends ESSingleNodeTestCase {
+public class NestedHelperTests extends MapperServiceTestCase {
 
-    IndexService indexService;
     MapperService mapperService;
 
     @Override
     public void setUp() throws Exception {
         super.setUp();
-        XContentBuilder mapping = XContentFactory.jsonBuilder()
-            .startObject()
-            .startObject("_doc")
-            .startObject("properties")
-            .startObject("foo")
-            .field("type", "keyword")
-            .endObject()
-            .startObject("foo2")
-            .field("type", "long")
-            .endObject()
-            .startObject("nested1")
-            .field("type", "nested")
-            .startObject("properties")
-            .startObject("foo")
-            .field("type", "keyword")
-            .endObject()
-            .startObject("foo2")
-            .field("type", "long")
-            .endObject()
-            .endObject()
-            .endObject()
-            .startObject("nested2")
-            .field("type", "nested")
-            .field("include_in_parent", true)
-            .startObject("properties")
-            .startObject("foo")
-            .field("type", "keyword")
-            .endObject()
-            .startObject("foo2")
-            .field("type", "long")
-            .endObject()
-            .endObject()
-            .endObject()
-            .startObject("nested3")
-            .field("type", "nested")
-            .field("include_in_root", true)
-            .startObject("properties")
-            .startObject("foo")
-            .field("type", "keyword")
-            .endObject()
-            .startObject("foo2")
-            .field("type", "long")
-            .endObject()
-            .endObject()
-            .endObject()
-            .endObject()
-            .endObject()
-            .endObject();
-        indexService = createIndex("index", Settings.EMPTY, mapping);
-        mapperService = indexService.mapperService();
+        String mapping = """
+            { "_doc" : {
+              "properties" : {
+                "foo" : { "type" : "keyword" },
+                "foo2" : { "type" : "long" },
+                "nested1" : {
+                  "type" : "nested",
+                  "properties" : {
+                    "foo" : { "type" : "keyword" },
+                    "foo2" : { "type" : "long" }
+                  }
+                },
+                "nested2" : {
+                  "type" : "nested",
+                  "include_in_parent" : true,
+                  "properties": {
+                    "foo" : { "type" : "keyword" },
+                    "foo2" : { "type" : "long" }
+                  }
+                },
+                "nested3" : {
+                  "type" : "nested",
+                  "include_in_root" : true,
+                  "properties": {
+                    "foo" : { "type" : "keyword" },
+                    "foo2" : { "type" : "long" }
+                  }
+                }
+              }
+            } }
+            """;
+        mapperService = createMapperService(mapping);
     }
 
     private static NestedHelper buildNestedHelper(MapperService mapperService) {
-        return new NestedHelper(mapperService.mappingLookup().objectMappers()::get, field -> mapperService.fieldType(field) != null);
+        return new NestedHelper(mapperService.mappingLookup().nestedLookup(), field -> mapperService.fieldType(field) != null);
     }
 
     public void testMatchAll() {
@@ -175,7 +150,7 @@ public class NestedHelperTests extends ESSingleNodeTestCase {
     }
 
     public void testRangeQuery() {
-        SearchExecutionContext context = createSearchContext(indexService).getSearchExecutionContext();
+        SearchExecutionContext context = mock(SearchExecutionContext.class);
         Query rangeQuery = mapperService.fieldType("foo2").rangeQuery(2, 5, true, true, null, null, null, context);
         assertFalse(buildNestedHelper(mapperService).mightMatchNestedDocs(rangeQuery));
         assertTrue(buildNestedHelper(mapperService).mightMatchNonNestedDocs(rangeQuery, "nested1"));
@@ -302,14 +277,7 @@ public class NestedHelperTests extends ESSingleNodeTestCase {
     }
 
     public void testNested() throws IOException {
-        SearchExecutionContext context = indexService.newSearchExecutionContext(
-            0,
-            0,
-            new IndexSearcher(new MultiReader()),
-            () -> 0,
-            null,
-            emptyMap()
-        );
+        SearchExecutionContext context = createSearchExecutionContext(mapperService);
         NestedQueryBuilder queryBuilder = new NestedQueryBuilder("nested1", new MatchAllQueryBuilder(), ScoreMode.Avg);
         ESToParentBlockJoinQuery query = (ESToParentBlockJoinQuery) queryBuilder.toQuery(context);
 

+ 2 - 1
server/src/test/java/org/elasticsearch/search/fetch/subphase/FieldFetcherTests.java

@@ -46,6 +46,7 @@ import static org.elasticsearch.xcontent.ObjectPath.eval;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.hasItems;
+import static org.hamcrest.Matchers.hasSize;
 
 public class FieldFetcherTests extends MapperServiceTestCase {
 
@@ -718,7 +719,7 @@ public class FieldFetcherTests extends MapperServiceTestCase {
             .endObject();
 
         Map<String, DocumentField> fields = fetchFields(mapperService, source, fieldAndFormatList("*", null, false));
-        assertEquals(2, fields.size());
+        assertThat(fields.values(), hasSize(2));
         assertThat(fields.keySet(), containsInAnyOrder("f1", "obj"));
         assertEquals("value1", fields.get("f1").getValue());
         List<Object> obj = fields.get("obj").getValues();

+ 7 - 3
server/src/test/java/org/elasticsearch/search/sort/AbstractSortTestCase.java

@@ -23,9 +23,9 @@ import org.elasticsearch.index.fielddata.IndexFieldData;
 import org.elasticsearch.index.fielddata.IndexFieldDataCache;
 import org.elasticsearch.index.mapper.MappedFieldType;
 import org.elasticsearch.index.mapper.MapperBuilderContext;
+import org.elasticsearch.index.mapper.NestedLookup;
 import org.elasticsearch.index.mapper.NestedObjectMapper;
 import org.elasticsearch.index.mapper.NumberFieldMapper;
-import org.elasticsearch.index.mapper.ObjectMapper;
 import org.elasticsearch.index.query.IdsQueryBuilder;
 import org.elasticsearch.index.query.MatchAllQueryBuilder;
 import org.elasticsearch.index.query.QueryBuilder;
@@ -53,6 +53,7 @@ import org.mockito.Mockito;
 
 import java.io.IOException;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.function.Function;
 import java.util.function.Supplier;
@@ -195,6 +196,9 @@ public abstract class AbstractSortTestCase<T extends SortBuilder<T>> extends EST
             IndexFieldData.Builder builder = fieldType.fielddataBuilder(fieldIndexName, searchLookup);
             return builder.build(new IndexFieldDataCache.None(), null);
         };
+        NestedLookup nestedLookup = NestedLookup.build(
+            List.of(new NestedObjectMapper.Builder("path", Version.CURRENT).build(MapperBuilderContext.ROOT))
+        );
         return new SearchExecutionContext(
             0,
             0,
@@ -223,8 +227,8 @@ public abstract class AbstractSortTestCase<T extends SortBuilder<T>> extends EST
             }
 
             @Override
-            public ObjectMapper getObjectMapper(String name) {
-                return new NestedObjectMapper.Builder(name, Version.CURRENT).build(MapperBuilderContext.ROOT);
+            public NestedLookup nestedLookup() {
+                return nestedLookup;
             }
         };
     }

+ 2 - 3
server/src/test/java/org/elasticsearch/search/sort/GeoDistanceSortBuilderTests.java

@@ -87,8 +87,7 @@ public class GeoDistanceSortBuilderTests extends AbstractSortTestCase<GeoDistanc
             result.validation(randomValueOtherThan(result.validation(), () -> randomFrom(GeoValidationMethod.values())));
         }
         if (randomBoolean()) {
-            // don't fully randomize here, GeoDistanceSort is picky about the filters that are allowed
-            NestedSortBuilder nestedSort = new NestedSortBuilder(randomAlphaOfLengthBetween(3, 10));
+            NestedSortBuilder nestedSort = new NestedSortBuilder("path");
             nestedSort.setFilter(new MatchAllQueryBuilder());
             result.setNestedSort(nestedSort);
         }
@@ -386,7 +385,7 @@ public class GeoDistanceSortBuilderTests extends AbstractSortTestCase<GeoDistanc
         assertEquals(SortField.class, sort.field.getClass()); // descending means the max value should be considered rather than min
 
         builder = new GeoDistanceSortBuilder("random_field_name", new GeoPoint(3.5, 2.1));
-        builder.setNestedSort(new NestedSortBuilder("some_nested_path"));
+        builder.setNestedSort(new NestedSortBuilder("path"));
         sort = builder.build(context);
         assertEquals(SortField.class, sort.field.getClass()); // can't use LatLon optimized sorting with nested fields
 

+ 2 - 2
server/src/test/java/org/elasticsearch/search/sort/NestedSortBuilderTests.java

@@ -73,10 +73,10 @@ public class NestedSortBuilderTests extends ESTestCase {
     }
 
     /**
-     * Create a {@link NestedSortBuilder} with random path and filter of the given depth.
+     * Create a {@link NestedSortBuilder} with random filter of the given depth.
      */
     public static NestedSortBuilder createRandomNestedSort(int depth) {
-        NestedSortBuilder nestedSort = new NestedSortBuilder(randomAlphaOfLengthBetween(3, 10));
+        NestedSortBuilder nestedSort = new NestedSortBuilder("path");
         if (randomBoolean()) {
             nestedSort.setFilter(AbstractSortTestCase.randomNestedFilter());
         }

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

@@ -16,6 +16,7 @@ import org.apache.lucene.index.RandomIndexWriter;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.store.Directory;
+import org.apache.lucene.util.Accountable;
 import org.elasticsearch.Version;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.common.Strings;
@@ -41,6 +42,7 @@ import org.elasticsearch.index.query.QueryBuilder;
 import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.index.query.support.NestedScope;
 import org.elasticsearch.index.shard.IndexShard;
+import org.elasticsearch.index.shard.ShardId;
 import org.elasticsearch.index.similarity.SimilarityService;
 import org.elasticsearch.indices.IndicesModule;
 import org.elasticsearch.indices.breaker.NoneCircuitBreakerService;
@@ -443,7 +445,7 @@ public abstract class MapperServiceTestCase extends ESTestCase {
             }
 
             @Override
-            public ObjectMapper getObjectMapper(String path) {
+            public NestedLookup nestedLookup() {
                 throw new UnsupportedOperationException();
             }
 
@@ -592,11 +594,17 @@ public abstract class MapperServiceTestCase extends ESTestCase {
         IndexSettings indexSettings = new IndexSettings(indexMetadata, Settings.EMPTY);
         final SimilarityService similarityService = new SimilarityService(indexSettings, null, Map.of());
         final long nowInMillis = randomNonNegativeLong();
-        return new SearchExecutionContext(
-            0,
-            0,
-            indexSettings,
-            null,
+        return new SearchExecutionContext(0, 0, indexSettings, new BitsetFilterCache(indexSettings, new BitsetFilterCache.Listener() {
+            @Override
+            public void onCache(ShardId shardId, Accountable accountable) {
+
+            }
+
+            @Override
+            public void onRemoval(ShardId shardId, Accountable accountable) {
+
+            }
+        }),
             (ft, idxName, lookup) -> ft.fielddataBuilder(idxName, lookup)
                 .build(new IndexFieldDataCache.None(), new NoneCircuitBreakerService()),
             mapperService,

+ 4 - 2
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/DocumentPermissions.java

@@ -14,6 +14,7 @@ import org.apache.lucene.search.join.ToChildBlockJoinQuery;
 import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.lucene.search.Queries;
+import org.elasticsearch.index.mapper.NestedLookup;
 import org.elasticsearch.index.query.QueryBuilder;
 import org.elasticsearch.index.query.QueryRewriteContext;
 import org.elasticsearch.index.query.Rewriteable;
@@ -170,8 +171,9 @@ public final class DocumentPermissions implements CacheKey {
                 failIfQueryUsesClient(queryBuilder, context);
                 Query roleQuery = context.toQuery(queryBuilder).query();
                 filter.add(roleQuery, SHOULD);
-                if (context.hasNested()) {
-                    NestedHelper nestedHelper = new NestedHelper(context::getObjectMapper, context::isFieldMapped);
+                NestedLookup nestedLookup = context.nestedLookup();
+                if (nestedLookup != NestedLookup.EMPTY) {
+                    NestedHelper nestedHelper = new NestedHelper(nestedLookup, context::isFieldMapped);
                     if (nestedHelper.mightMatchNestedDocs(roleQuery)) {
                         roleQuery = new BooleanQuery.Builder().add(roleQuery, FILTER).add(Queries.newNonNestedFilter(), FILTER).build();
                     }

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

@@ -502,7 +502,7 @@ public class DenseVectorFieldMapper extends FieldMapper implements PerFieldKnnVe
 
     @Override
     public void doValidate(MappingLookup mappers) {
-        if (indexed && mappers.getNestedParent(name()) != null) {
+        if (indexed && mappers.nestedLookup().getNestedParent(name()) != null) {
             throw new IllegalArgumentException("[" + CONTENT_TYPE + "] fields cannot be indexed if they're" + " within [nested] mappings");
         }
     }

+ 6 - 0
x-pack/plugin/wildcard/src/test/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapperTests.java

@@ -59,6 +59,7 @@ import org.elasticsearch.index.mapper.LuceneDocument;
 import org.elasticsearch.index.mapper.MappedFieldType;
 import org.elasticsearch.index.mapper.MapperBuilderContext;
 import org.elasticsearch.index.mapper.MapperTestCase;
+import org.elasticsearch.index.mapper.NestedLookup;
 import org.elasticsearch.index.mapper.ParsedDocument;
 import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.plugins.Plugin;
@@ -1102,6 +1103,11 @@ public class WildcardFieldMapperTests extends MapperTestCase {
             public MappedFieldType getFieldType(String name) {
                 return provideMappedFieldType(name);
             }
+
+            @Override
+            public NestedLookup nestedLookup() {
+                return NestedLookup.EMPTY;
+            }
         };
     }