From 5ec8c174be0728d89d133d49f4ff5d0a5c3bcb1f Mon Sep 17 00:00:00 2001 From: Julie Tibshirani Date: Thu, 6 Feb 2020 11:26:54 -0800 Subject: [PATCH] Add support for source_path to the field caps API. --- .../org/elasticsearch/client/SearchIT.java | 6 +- .../test/field_caps/30_source_path.yml | 76 ++++++++++ .../action/fieldcaps/FieldCapabilities.java | 138 +++++++++++++++++- .../fieldcaps/IndexFieldCapabilities.java | 29 +++- .../TransportFieldCapabilitiesAction.java | 4 +- ...TransportFieldCapabilitiesIndexAction.java | 5 +- .../index/mapper/FieldTypeLookup.java | 43 +++++- .../index/mapper/MapperService.java | 4 + .../FieldCapabilitiesResponseTests.java | 5 +- .../fieldcaps/FieldCapabilitiesTests.java | 48 ++++-- .../MergedFieldCapabilitiesResponseTests.java | 7 +- .../index/mapper/FieldTypeLookupTests.java | 67 +++++++++ .../search/fieldcaps/FieldCapabilitiesIT.java | 40 ++++- .../index/mapper/MockFieldMapper.java | 21 +++ .../ExtractedFieldsDetectorTests.java | 3 +- .../analysis/index/IndexResolverTests.java | 11 +- 16 files changed, 461 insertions(+), 46 deletions(-) create mode 100644 rest-api-spec/src/main/resources/rest-api-spec/test/field_caps/30_source_path.yml diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java index b4b8787f86418..e1029cc7b6895 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java @@ -1229,11 +1229,11 @@ public void testFieldCaps() throws IOException { assertEquals(2, ratingResponse.size()); FieldCapabilities expectedKeywordCapabilities = new FieldCapabilities( - "rating", "keyword", true, true, new String[]{"index2"}, null, null, Collections.emptyMap()); + "rating", "keyword", true, true, new String[]{"index2"}, null, null, Collections.emptyMap(), Collections.emptyList()); assertEquals(expectedKeywordCapabilities, ratingResponse.get("keyword")); FieldCapabilities expectedLongCapabilities = new FieldCapabilities( - "rating", "long", true, true, new String[]{"index1"}, null, null, Collections.emptyMap()); + "rating", "long", true, true, new String[]{"index1"}, null, null, Collections.emptyMap(), Collections.emptyList()); assertEquals(expectedLongCapabilities, ratingResponse.get("long")); // Check the capabilities for the 'field' field. @@ -1242,7 +1242,7 @@ public void testFieldCaps() throws IOException { assertEquals(1, fieldResponse.size()); FieldCapabilities expectedTextCapabilities = new FieldCapabilities( - "field", "text", true, false, null, null, null, Collections.emptyMap()); + "field", "text", true, false, null, null, null, Collections.emptyMap(), Collections.emptyList()); assertEquals(expectedTextCapabilities, fieldResponse.get("text")); } diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/field_caps/30_source_path.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/field_caps/30_source_path.yml new file mode 100644 index 0000000000000..e1b85e1b25cf9 --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/field_caps/30_source_path.yml @@ -0,0 +1,76 @@ +--- +setup: + - skip: + version: " - 7.99.99" + reason: "source_path is currently only supported on 8.0." + + - do: + indices.create: + index: index1 + body: + mappings: + properties: + field: + type: text + new_field: + type: alias + path: field + copy_to_field1: + type: text + copy_to: field + + - do: + indices.create: + index: index2 + body: + mappings: + properties: + field: + type: text + new_field: + type: alias + path: field + copy_to_field2: + type: text + copy_to: field + + - do: + indices.create: + index: index3 + body: + mappings: + properties: + field: + type: text + new_field: + type: text + copy_to_field2: + type: text + copy_to: field + +--- +"Merge source path information across multiple indices": + - do: + field_caps: + index: index1 + fields: ["field", "new_field"] + + - match: {"fields.field.text.source_path.0.indices": ["index1"]} + - match: {"fields.field.text.source_path.0.paths": ["copy_to_field1"]} + + - match: {"fields.new_field.text.source_path.0.indices": ["index1"]} + - match: {"fields.new_field.text.source_path.0.paths": ["field"]} + + - do: + field_caps: + index: index1,index2,index3 + fields: ["field", "new_field"] + + - match: {"fields.field.text.source_path.0.indices": ["index1"]} + - match: {"fields.field.text.source_path.0.paths": ["copy_to_field1"]} + + - match: {"fields.field.text.source_path.1.indices": ["index2", "index3"]} + - match: {"fields.field.text.source_path.1.paths": ["copy_to_field2"]} + + - match: {"fields.new_field.text.source_path.0.indices": ["index1", "index2"]} + - match: {"fields.new_field.text.source_path.0.paths": ["field"]} diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java index 751276812bddc..6c889cc3749ab 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java @@ -41,6 +41,8 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.function.Function; import java.util.stream.Collectors; @@ -56,6 +58,8 @@ public class FieldCapabilities implements Writeable, ToXContentObject { private static final ParseField NON_SEARCHABLE_INDICES_FIELD = new ParseField("non_searchable_indices"); private static final ParseField NON_AGGREGATABLE_INDICES_FIELD = new ParseField("non_aggregatable_indices"); private static final ParseField META_FIELD = new ParseField("meta"); + private static final ParseField SOURCE_PATH_FIELD = new ParseField("source_path"); + private final String name; private final String type; @@ -67,6 +71,7 @@ public class FieldCapabilities implements Writeable, ToXContentObject { private final String[] nonAggregatableIndices; private final Map> meta; + private final List sourcePath; /** * Constructor for a set of indices. @@ -81,13 +86,15 @@ public class FieldCapabilities implements Writeable, ToXContentObject { * @param nonAggregatableIndices The list of indices where this field is not aggregatable, * or null if the field is aggregatable in all indices. * @param meta Merged metadata across indices. + * @param sourcePath Merged source paths across indices. */ public FieldCapabilities(String name, String type, boolean isSearchable, boolean isAggregatable, String[] indices, String[] nonSearchableIndices, String[] nonAggregatableIndices, - Map> meta) { + Map> meta, + List sourcePath) { this.name = name; this.type = type; this.isSearchable = isSearchable; @@ -96,6 +103,7 @@ public FieldCapabilities(String name, String type, this.nonSearchableIndices = nonSearchableIndices; this.nonAggregatableIndices = nonAggregatableIndices; this.meta = Objects.requireNonNull(meta); + this.sourcePath = sourcePath; } FieldCapabilities(StreamInput in) throws IOException { @@ -111,6 +119,11 @@ public FieldCapabilities(String name, String type, } else { meta = Collections.emptyMap(); } + if (in.getVersion().onOrAfter(Version.V_8_0_0)) { + sourcePath = in.readList(SourcePath::new); + } else { + sourcePath = Collections.emptyList(); + } } @Override @@ -125,6 +138,9 @@ public void writeTo(StreamOutput out) throws IOException { if (out.getVersion().onOrAfter(Version.V_7_6_0)) { out.writeMap(meta, StreamOutput::writeString, (o, set) -> o.writeCollection(set, StreamOutput::writeString)); } + if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + out.writeCollection(sourcePath, (o, path) -> path.writeTo(o)); + } } @Override @@ -153,6 +169,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } builder.endObject(); } + if (sourcePath.isEmpty() == false) { + builder.field(SOURCE_PATH_FIELD.getPreferredName(), sourcePath); + } builder.endObject(); return builder; } @@ -172,7 +191,8 @@ public static FieldCapabilities fromXContent(String name, XContentParser parser) a[3] != null ? ((List) a[3]).toArray(new String[0]) : null, a[4] != null ? ((List) a[4]).toArray(new String[0]) : null, a[5] != null ? ((List) a[5]).toArray(new String[0]) : null, - a[6] != null ? ((Map>) a[6]) : Collections.emptyMap())); + a[6] != null ? ((Map>) a[6]) : Collections.emptyMap(), + a[7] != null ? ((List) a[7]): Collections.emptyList())); static { PARSER.declareString(ConstructingObjectParser.constructorArg(), TYPE_FIELD); @@ -183,6 +203,8 @@ public static FieldCapabilities fromXContent(String name, XContentParser parser) PARSER.declareStringArray(ConstructingObjectParser.optionalConstructorArg(), NON_AGGREGATABLE_INDICES_FIELD); PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (parser, context) -> parser.map(HashMap::new, p -> Set.copyOf(p.list())), META_FIELD); + PARSER.declareObjectArray(ConstructingObjectParser.optionalConstructorArg(), + (parser, context) -> SourcePath.fromXContent(parser), SOURCE_PATH_FIELD); } /** @@ -244,6 +266,13 @@ public Map> meta() { return meta; } + /** + * A merged list of source paths across indices. + */ + public List sourcePath() { + return sourcePath; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -256,12 +285,13 @@ public boolean equals(Object o) { Arrays.equals(indices, that.indices) && Arrays.equals(nonSearchableIndices, that.nonSearchableIndices) && Arrays.equals(nonAggregatableIndices, that.nonAggregatableIndices) && - Objects.equals(meta, that.meta); + Objects.equals(meta, that.meta) && + Objects.equals(sourcePath, that.sourcePath); } @Override public int hashCode() { - int result = Objects.hash(name, type, isSearchable, isAggregatable, meta); + int result = Objects.hash(name, type, isSearchable, isAggregatable, meta, sourcePath); result = 31 * result + Arrays.hashCode(indices); result = 31 * result + Arrays.hashCode(nonSearchableIndices); result = 31 * result + Arrays.hashCode(nonAggregatableIndices); @@ -281,6 +311,10 @@ static class Builder { private List indiceList; private Map> meta; + // Maps from a sorted list of paths to index names. This allows us to detect when multiple + // indices have the same source path information, and combine them into a single entry. + private Map, Set> sourcePaths; + Builder(String name, String type) { this.name = name; this.type = type; @@ -288,20 +322,28 @@ static class Builder { this.isAggregatable = true; this.indiceList = new ArrayList<>(); this.meta = new HashMap<>(); + this.sourcePaths = new HashMap<>(); } /** * Collect the field capabilities for an index. */ - void add(String index, boolean search, boolean agg, Map meta) { + void add(String index, boolean search, boolean agg, Map meta, Set sourcePath) { IndexCaps indexCaps = new IndexCaps(index, search, agg); indiceList.add(indexCaps); this.isSearchable &= search; this.isAggregatable &= agg; + for (Map.Entry entry : meta.entrySet()) { this.meta.computeIfAbsent(entry.getKey(), key -> new HashSet<>()) .add(entry.getValue()); } + + if (sourcePath.isEmpty() == false) { + SortedSet sortedPaths = Collections.unmodifiableSortedSet(new TreeSet<>(sourcePath)); + this.sourcePaths.computeIfAbsent(sortedPaths, key -> new HashSet<>()) + .add(index); + } } List getIndices() { @@ -347,11 +389,95 @@ FieldCapabilities build(boolean withIndices) { nonAggregatableIndices = null; } final Function>, Set> entryValueFunction = Map.Entry::getValue; + Map> immutableMeta = meta.entrySet().stream() .collect(Collectors.toUnmodifiableMap( Map.Entry::getKey, entryValueFunction.andThen(Set::copyOf))); + + List immutableSourcePath = sourcePaths.entrySet().stream() + .map(entry -> new SourcePath( + entry.getValue().toArray(new String[0]), + entry.getKey().toArray(new String[0]))) + .sorted(Comparator.comparing(sourcePath -> sourcePath.indices()[0])) + .collect(Collectors.toList()); + return new FieldCapabilities(name, type, isSearchable, isAggregatable, - indices, nonSearchableIndices, nonAggregatableIndices, immutableMeta); + indices, nonSearchableIndices, nonAggregatableIndices, + immutableMeta, immutableSourcePath); + } + } + + public static class SourcePath implements Writeable, ToXContentObject { + private static final ParseField INDICES_FIELD = new ParseField("indices"); + private static final ParseField PATHS_FIELD = new ParseField("paths"); + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "source_path", true, + (a, name) -> new SourcePath( + ((List) a[0]).toArray(new String[0]), + ((List) a[1]).toArray(new String[0]))); + + static { + PARSER.declareStringArray(ConstructingObjectParser.constructorArg(), INDICES_FIELD); + PARSER.declareStringArray(ConstructingObjectParser.constructorArg(), PATHS_FIELD); + } + + private final String[] indices; + private final String[] paths; + + public SourcePath(String[] indices, String[] paths) { + this.indices = Arrays.copyOf(indices, indices.length); + this.paths = Arrays.copyOf(paths, paths.length); + Arrays.sort(this.indices); + Arrays.sort(this.paths); + } + + SourcePath(StreamInput in) throws IOException { + this.indices = in.readStringArray(); + this.paths = in.readStringArray(); + } + + public String[] indices() { + return indices; + } + + public String[] paths() { + return paths; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeStringArray(indices); + out.writeStringArray(paths); + } + + public static SourcePath fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject() + .field(INDICES_FIELD.getPreferredName(), indices) + .field(PATHS_FIELD.getPreferredName(), paths) + .endObject(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SourcePath that = (SourcePath) o; + return Arrays.equals(indices, that.indices) && + Arrays.equals(paths, that.paths); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(indices); + result = 31 * result + Arrays.hashCode(paths); + return result; } } diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/IndexFieldCapabilities.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/IndexFieldCapabilities.java index 7948e6dd8b516..59ec7f8f44b97 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/IndexFieldCapabilities.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/IndexFieldCapabilities.java @@ -25,6 +25,7 @@ import org.elasticsearch.common.io.stream.Writeable; import java.io.IOException; +import java.util.Collections; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -40,6 +41,7 @@ public class IndexFieldCapabilities implements Writeable { private final boolean isSearchable; private final boolean isAggregatable; private final Map meta; + private final Set sourcePath; /** * @param name The name of the field. @@ -47,16 +49,19 @@ public class IndexFieldCapabilities implements Writeable { * @param isSearchable Whether this field is indexed for search. * @param isAggregatable Whether this field can be aggregated on. * @param meta Metadata about the field. + * @param sourcePath The paths in _source that contain this field's values. */ IndexFieldCapabilities(String name, String type, boolean isSearchable, boolean isAggregatable, - Map meta) { + Map meta, + Set sourcePath) { this.name = name; this.type = type; this.isSearchable = isSearchable; this.isAggregatable = isAggregatable; this.meta = meta; + this.sourcePath = sourcePath; } IndexFieldCapabilities(StreamInput in) throws IOException { @@ -77,11 +82,15 @@ public class IndexFieldCapabilities implements Writeable { Map.Entry::getKey, entry -> entry.getValue().iterator().next())); } + + this.sourcePath = in.getVersion().onOrAfter(Version.V_8_0_0) + ? in.readSet(StreamInput::readString) + : Collections.emptySet(); } @Override public void writeTo(StreamOutput out) throws IOException { - if (out.getVersion().onOrAfter(Version.V_7_7_0)) { + if (out.getVersion().onOrAfter(Version.V_7_7_0)) { out.writeString(name); out.writeString(type); out.writeBoolean(isSearchable); @@ -92,9 +101,14 @@ public void writeTo(StreamOutput out) throws IOException { Map> wrappedMeta = meta.entrySet().stream().collect(Collectors.toMap( Map.Entry::getKey, entry -> Set.of(entry.getValue()))); - FieldCapabilities fieldCaps = new FieldCapabilities(name, type, isSearchable, isAggregatable, null, null, null, wrappedMeta); + FieldCapabilities fieldCaps = new FieldCapabilities(name, type, isSearchable, isAggregatable, + null, null, null, wrappedMeta, Collections.emptyList()); fieldCaps.writeTo(out); } + + if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + out.writeStringCollection(sourcePath); + } } public String getName() { @@ -117,6 +131,10 @@ public Map meta() { return meta; } + public Set sourcePath() { + return sourcePath; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -126,11 +144,12 @@ public boolean equals(Object o) { isAggregatable == that.isAggregatable && Objects.equals(name, that.name) && Objects.equals(type, that.type) && - Objects.equals(meta, that.meta); + Objects.equals(meta, that.meta) && + Objects.equals(sourcePath, that.sourcePath); } @Override public int hashCode() { - return Objects.hash(name, type, isSearchable, isAggregatable, meta); + return Objects.hash(name, type, isSearchable, isAggregatable, meta, sourcePath); } } diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java index 7022249c67c41..5b5c733f0f5b4 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java @@ -176,7 +176,7 @@ private void addUnmappedFields(String[] indices, String field, Map> resp Map typeMap = responseMapBuilder.computeIfAbsent(field, f -> new HashMap<>()); FieldCapabilities.Builder builder = typeMap.computeIfAbsent(fieldCap.getType(), key -> new FieldCapabilities.Builder(field, key)); - builder.add(indexName, fieldCap.isSearchable(), fieldCap.isAggregatable(), fieldCap.meta()); + builder.add(indexName, fieldCap.isSearchable(), fieldCap.isAggregatable(), fieldCap.meta(), fieldCap.sourcePath()); } } } diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesIndexAction.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesIndexAction.java index 52c73dedccf9a..5f7071e9dcc1c 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesIndexAction.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesIndexAction.java @@ -90,8 +90,9 @@ protected FieldCapabilitiesIndexResponse shardOperation(final FieldCapabilitiesI if (ft != null) { if (indicesService.isMetaDataField(mapperService.getIndexSettings().getIndexVersionCreated(), field) || fieldPredicate.test(ft.name())) { + Set sourcePath = mapperService.sourcePath(field); IndexFieldCapabilities fieldCap = new IndexFieldCapabilities(field, ft.typeName(), - ft.isSearchable(), ft.isAggregatable(), ft.meta()); + ft.isSearchable(), ft.isAggregatable(), ft.meta(), sourcePath); responseMap.put(field, fieldCap); } else { continue; @@ -110,7 +111,7 @@ protected FieldCapabilitiesIndexResponse shardOperation(final FieldCapabilitiesI ObjectMapper mapper = mapperService.getObjectMapper(parentField); String type = mapper.nested().isNested() ? "nested" : "object"; IndexFieldCapabilities fieldCap = new IndexFieldCapabilities(parentField, type, - false, false, Collections.emptyMap()); + false, false, Collections.emptyMap(), Collections.emptySet()); responseMap.put(parentField, fieldCap); } dotIndex = parentField.lastIndexOf('.'); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/FieldTypeLookup.java b/server/src/main/java/org/elasticsearch/index/mapper/FieldTypeLookup.java index 2a444fa4f5987..b60dd5995cac8 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/FieldTypeLookup.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/FieldTypeLookup.java @@ -24,6 +24,7 @@ import org.elasticsearch.common.regex.Regex; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -38,20 +39,24 @@ class FieldTypeLookup implements Iterable { final CopyOnWriteHashMap fullNameToFieldType; private final CopyOnWriteHashMap aliasToConcreteName; + private final CopyOnWriteHashMap> fieldToSourcePath; private final DynamicKeyFieldTypeLookup dynamicKeyLookup; FieldTypeLookup() { fullNameToFieldType = new CopyOnWriteHashMap<>(); aliasToConcreteName = new CopyOnWriteHashMap<>(); + fieldToSourcePath = new CopyOnWriteHashMap<>(); dynamicKeyLookup = new DynamicKeyFieldTypeLookup(); } private FieldTypeLookup(CopyOnWriteHashMap fullNameToFieldType, CopyOnWriteHashMap aliasToConcreteName, + CopyOnWriteHashMap> fieldToSourcePath, DynamicKeyFieldTypeLookup dynamicKeyLookup) { this.fullNameToFieldType = fullNameToFieldType; this.aliasToConcreteName = aliasToConcreteName; + this.fieldToSourcePath = fieldToSourcePath; this.dynamicKeyLookup = dynamicKeyLookup; } @@ -66,6 +71,7 @@ public FieldTypeLookup copyAndAddAll(Collection fieldMappers, CopyOnWriteHashMap fullName = this.fullNameToFieldType; CopyOnWriteHashMap aliases = this.aliasToConcreteName; + CopyOnWriteHashMap> sourcePaths = this.fieldToSourcePath; Map dynamicKeyMappers = new HashMap<>(); for (FieldMapper fieldMapper : fieldMappers) { @@ -80,6 +86,17 @@ public FieldTypeLookup copyAndAddAll(Collection fieldMappers, if (fieldMapper instanceof DynamicKeyFieldMapper) { dynamicKeyMappers.put(fieldName, (DynamicKeyFieldMapper) fieldMapper); } + + for (String targetField : fieldMapper.copyTo().copyToFields()) { + Set sourcePath = sourcePaths.get(targetField); + if (sourcePath == null) { + sourcePaths = sourcePaths.copyAndPut(targetField, Collections.singleton(fieldName)); + } else if (sourcePath.contains(fieldName) == false) { + Set newSourcePath = new HashSet<>(sourcePath); + newSourcePath.add(fieldName); + sourcePaths = sourcePaths.copyAndPut(targetField, Collections.unmodifiableSet(newSourcePath)); + } + } } for (FieldAliasMapper fieldAliasMapper : fieldAliasMappers) { @@ -93,7 +110,7 @@ public FieldTypeLookup copyAndAddAll(Collection fieldMappers, } DynamicKeyFieldTypeLookup newDynamicKeyLookup = this.dynamicKeyLookup.copyAndAddAll(dynamicKeyMappers, aliases); - return new FieldTypeLookup(fullName, aliases, newDynamicKeyLookup); + return new FieldTypeLookup(fullName, aliases, sourcePaths, newDynamicKeyLookup); } /** @@ -129,6 +146,30 @@ public Set simpleMatchToFullName(String pattern) { return fields; } + public Set sourcePath(String field) { + String targetField = aliasToConcreteName.get(field); + if (targetField != null) { + return Collections.singleton(targetField); + } + + String parentField = getParentField(field); + if (parentField != null) { + return Collections.singleton(parentField); + } + + return fieldToSourcePath.getOrDefault(field, new HashSet<>()); + } + + private String getParentField(String field) { + int lastDotIndex = field.lastIndexOf('.'); + if (lastDotIndex < 0) { + return null; + } + + String parentField = field.substring(0, lastDotIndex); + return fullNameToFieldType.containsKey(parentField) ? parentField : null; + } + @Override public Iterator iterator() { Iterator concreteFieldTypes = fullNameToFieldType.values().iterator(); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java index 1bf66bec2372b..e9ca752e29603 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java @@ -602,6 +602,10 @@ public Set simpleMatchToFullName(String pattern) { return fieldTypes.simpleMatchToFullName(pattern); } + public Set sourcePath(String fullName) { + return fieldTypes.sourcePath(fullName); + } + /** * Returns all mapped field types. */ diff --git a/server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponseTests.java b/server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponseTests.java index 18f4f2a85d5e4..16752a196bf49 100644 --- a/server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponseTests.java @@ -27,6 +27,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import static com.carrotsearch.randomizedtesting.RandomizedTest.randomAsciiLettersOfLength; @@ -74,8 +75,10 @@ private static IndexFieldCapabilities randomFieldCaps(String fieldName) { break; } + Set sourcePath = randomBoolean() ? Collections.emptySet() : Set.of("field1", "field2"); + return new IndexFieldCapabilities(fieldName, randomAlphaOfLengthBetween(5, 20), - randomBoolean(), randomBoolean(), meta); + randomBoolean(), randomBoolean(), meta, sourcePath); } @Override diff --git a/server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesTests.java b/server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesTests.java index 6776d66c9df72..f0f3d7fd24ec0 100644 --- a/server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesTests.java +++ b/server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.action.fieldcaps; +import org.elasticsearch.action.fieldcaps.FieldCapabilities.SourcePath; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractSerializingTestCase; @@ -26,6 +27,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Set; @@ -51,9 +53,9 @@ protected Writeable.Reader instanceReader() { public void testBuilder() { FieldCapabilities.Builder builder = new FieldCapabilities.Builder("field", "type"); - builder.add("index1", true, false, Collections.emptyMap()); - builder.add("index2", true, false, Collections.emptyMap()); - builder.add("index3", true, false, Collections.emptyMap()); + builder.add("index1", true, false, Collections.emptyMap(), Collections.emptySet()); + builder.add("index2", true, false, Collections.emptyMap(), Collections.emptySet()); + builder.add("index3", true, false, Collections.emptyMap(), Collections.emptySet()); { FieldCapabilities cap1 = builder.build(false); @@ -63,6 +65,7 @@ public void testBuilder() { assertNull(cap1.nonSearchableIndices()); assertNull(cap1.nonAggregatableIndices()); assertEquals(Collections.emptyMap(), cap1.meta()); + assertEquals(Collections.emptyList(), cap1.sourcePath()); FieldCapabilities cap2 = builder.build(true); assertThat(cap2.isSearchable(), equalTo(true)); @@ -72,12 +75,13 @@ public void testBuilder() { assertNull(cap2.nonSearchableIndices()); assertNull(cap2.nonAggregatableIndices()); assertEquals(Collections.emptyMap(), cap2.meta()); + assertEquals(Collections.emptyList(), cap2.sourcePath()); } builder = new FieldCapabilities.Builder("field", "type"); - builder.add("index1", false, true, Collections.emptyMap()); - builder.add("index2", true, false, Collections.emptyMap()); - builder.add("index3", false, false, Collections.emptyMap()); + builder.add("index1", false, true, Collections.emptyMap(), Collections.emptySet()); + builder.add("index2", true, false, Collections.emptyMap(), Collections.emptySet()); + builder.add("index3", false, false, Collections.emptyMap(), Collections.emptySet()); { FieldCapabilities cap1 = builder.build(false); assertThat(cap1.isSearchable(), equalTo(false)); @@ -86,6 +90,7 @@ public void testBuilder() { assertThat(cap1.nonSearchableIndices(), equalTo(new String[]{"index1", "index3"})); assertThat(cap1.nonAggregatableIndices(), equalTo(new String[]{"index2", "index3"})); assertEquals(Collections.emptyMap(), cap1.meta()); + assertEquals(Collections.emptyList(), cap1.sourcePath()); FieldCapabilities cap2 = builder.build(true); assertThat(cap2.isSearchable(), equalTo(false)); @@ -95,12 +100,13 @@ public void testBuilder() { assertThat(cap2.nonSearchableIndices(), equalTo(new String[]{"index1", "index3"})); assertThat(cap2.nonAggregatableIndices(), equalTo(new String[]{"index2", "index3"})); assertEquals(Collections.emptyMap(), cap2.meta()); + assertEquals(Collections.emptyList(), cap2.sourcePath()); } builder = new FieldCapabilities.Builder("field", "type"); - builder.add("index1", true, true, Collections.emptyMap()); - builder.add("index2", true, true, Map.of("foo", "bar")); - builder.add("index3", true, true, Map.of("foo", "quux")); + builder.add("index1", true, true, Collections.emptyMap(), Set.of("field1", "field2")); + builder.add("index2", true, true, Map.of("foo", "bar"), Collections.emptySet()); + builder.add("index3", true, true, Map.of("foo", "quux"), Set.of("field2", "field1")); { FieldCapabilities cap1 = builder.build(false); assertThat(cap1.isSearchable(), equalTo(true)); @@ -109,6 +115,7 @@ public void testBuilder() { assertNull(cap1.nonSearchableIndices()); assertNull(cap1.nonAggregatableIndices()); assertEquals(Map.of("foo", Set.of("bar", "quux")), cap1.meta()); + assertEquals(List.of(new SourcePath(new String[]{"index1", "index3"}, new String[]{"field1", "field2"})), cap1.sourcePath()); FieldCapabilities cap2 = builder.build(true); assertThat(cap2.isSearchable(), equalTo(true)); @@ -118,6 +125,7 @@ public void testBuilder() { assertNull(cap2.nonSearchableIndices()); assertNull(cap2.nonAggregatableIndices()); assertEquals(Map.of("foo", Set.of("bar", "quux")), cap2.meta()); + assertEquals(List.of(new SourcePath(new String[]{"index1", "index3"}, new String[]{"field1", "field2"})), cap2.sourcePath()); } } @@ -157,9 +165,16 @@ static FieldCapabilities randomFieldCaps(String fieldName) { break; } + List sourcePaths = Collections.emptyList(); + if (randomBoolean()) { + sourcePaths = List.of( + new SourcePath(new String[]{"index1, index2"}, new String[]{"field"}), + new SourcePath(new String[]{"index3"}, new String[]{"other_field"})); + } + return new FieldCapabilities(fieldName, randomAlphaOfLengthBetween(5, 20), randomBoolean(), randomBoolean(), - indices, nonSearchableIndices, nonAggregatableIndices, meta); + indices, nonSearchableIndices, nonAggregatableIndices, meta, sourcePaths); } @Override @@ -172,6 +187,7 @@ protected FieldCapabilities mutateInstance(FieldCapabilities instance) { String[] nonSearchableIndices = instance.nonSearchableIndices(); String[] nonAggregatableIndices = instance.nonAggregatableIndices(); Map> meta = instance.meta(); + List sourcePaths = instance.sourcePath(); switch (between(0, 7)) { case 0: name += randomAlphaOfLengthBetween(1, 10); @@ -236,9 +252,19 @@ protected FieldCapabilities mutateInstance(FieldCapabilities instance) { } meta = newMeta; break; + case 8: + if (sourcePaths.isEmpty()) { + sourcePaths = List.of( + new SourcePath(new String[]{"index1, index2"}, new String[]{"field"}), + new SourcePath(new String[]{"index3"}, new String[]{"other_field"})); + } else { + sourcePaths = Collections.emptyList(); + } + break; default: throw new AssertionError(); } - return new FieldCapabilities(name, type, isSearchable, isAggregatable, indices, nonSearchableIndices, nonAggregatableIndices, meta); + return new FieldCapabilities(name, type, isSearchable, isAggregatable, + indices, nonSearchableIndices, nonAggregatableIndices, meta, sourcePaths); } } diff --git a/server/src/test/java/org/elasticsearch/action/fieldcaps/MergedFieldCapabilitiesResponseTests.java b/server/src/test/java/org/elasticsearch/action/fieldcaps/MergedFieldCapabilitiesResponseTests.java index b98e9445fb9bb..4749e6368326a 100644 --- a/server/src/test/java/org/elasticsearch/action/fieldcaps/MergedFieldCapabilitiesResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/fieldcaps/MergedFieldCapabilitiesResponseTests.java @@ -152,19 +152,20 @@ public void testEmptyResponse() throws IOException { private static FieldCapabilitiesResponse createSimpleResponse() { Map titleCapabilities = new HashMap<>(); - titleCapabilities.put("text", new FieldCapabilities("title", "text", true, false, null, null, null, Collections.emptyMap())); + titleCapabilities.put("text", new FieldCapabilities("title", "text", true, false, null, null, null, + Collections.emptyMap(), Collections.emptyList())); Map ratingCapabilities = new HashMap<>(); ratingCapabilities.put("long", new FieldCapabilities("rating", "long", true, false, new String[]{"index1", "index2"}, null, - new String[]{"index1"}, Collections.emptyMap())); + new String[]{"index1"}, Collections.emptyMap(), Collections.emptyList())); ratingCapabilities.put("keyword", new FieldCapabilities("rating", "keyword", false, true, new String[]{"index3", "index4"}, new String[]{"index4"}, - null, Collections.emptyMap())); + null, Collections.emptyMap(), Collections.emptyList())); Map> responses = new HashMap<>(); responses.put("title", titleCapabilities); diff --git a/server/src/test/java/org/elasticsearch/index/mapper/FieldTypeLookupTests.java b/server/src/test/java/org/elasticsearch/index/mapper/FieldTypeLookupTests.java index 6fcf2f70df79e..197566ac94760 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/FieldTypeLookupTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/FieldTypeLookupTests.java @@ -30,6 +30,7 @@ import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.Set; import static java.util.Collections.emptyList; @@ -150,6 +151,72 @@ public void testSimpleMatchToFullName() { assertTrue(names.contains("barometer")); } + public void testSourcePathsWithMultiFields() { + MappedFieldType ft = new MockFieldMapper.FakeFieldType(); + Mapper.BuilderContext context = new Mapper.BuilderContext( + MockFieldMapper.dummySettings, new ContentPath()); + + MockFieldMapper field = new MockFieldMapper.Builder("field", ft, ft) + .addMultiField(new MockFieldMapper.Builder("field.subfield1", ft, ft)) + .addMultiField(new MockFieldMapper.Builder("field.subfield2", ft, ft)) + .build(context); + + FieldTypeLookup lookup = new FieldTypeLookup(); + lookup = lookup.copyAndAddAll(newList(field), emptyList()); + + assertTrue(lookup.sourcePath("field").isEmpty()); + assertEquals(Set.of("field"), lookup.sourcePath("field.subfield1")); + assertEquals(Set.of("field"), lookup.sourcePath("field.subfield2")); + } + + public void testSourcePathsWithCopyTo() { + MappedFieldType ft = new MockFieldMapper.FakeFieldType(); + Mapper.BuilderContext context = new Mapper.BuilderContext( + MockFieldMapper.dummySettings, new ContentPath()); + + MockFieldMapper field = new MockFieldMapper.Builder("field", ft, ft) + .addMultiField(new MockFieldMapper.Builder("field.subfield1", ft, ft)) + .build(context); + + MockFieldMapper otherField = new MockFieldMapper.Builder("other_field", ft, ft) + .copyTo(new FieldMapper.CopyTo.Builder() + .add("field") + .add("field.subfield1") + .build()) + .build(context); + + MockFieldMapper anotherField = new MockFieldMapper.Builder("another_field", ft, ft) + .copyTo(new FieldMapper.CopyTo.Builder() + .add("field") + .build()) + .build(context); + + FieldTypeLookup lookup = new FieldTypeLookup(); + lookup = lookup.copyAndAddAll(newList(field, otherField, anotherField), emptyList()); + + assertEquals(Set.of("other_field", "another_field"), lookup.sourcePath("field")); + assertEquals(Set.of("field"), lookup.sourcePath("field.subfield1")); + } + + public void testSourcePathsWithAliases() { + MappedFieldType ft = new MockFieldMapper.FakeFieldType(); + Mapper.BuilderContext context = new Mapper.BuilderContext( + MockFieldMapper.dummySettings, new ContentPath()); + + MockFieldMapper field = new MockFieldMapper.Builder("field", ft, ft) + .addMultiField(new MockFieldMapper.Builder("field.subfield", ft, ft)) + .build(context); + + FieldAliasMapper alias1 = new FieldAliasMapper("alias1", "alias1", "field"); + FieldAliasMapper alias2 = new FieldAliasMapper("alias2", "alias2", "field.subfield"); + + FieldTypeLookup lookup = new FieldTypeLookup(); + lookup = lookup.copyAndAddAll(newList(field), newList(alias1, alias2)); + + assertEquals(Set.of("field"), lookup.sourcePath("alias1")); + assertEquals(Set.of("field.subfield"), lookup.sourcePath("alias2")); + } + public void testIteratorImmutable() { MockFieldMapper f1 = new MockFieldMapper("foo"); FieldTypeLookup lookup = new FieldTypeLookup(); diff --git a/server/src/test/java/org/elasticsearch/search/fieldcaps/FieldCapabilitiesIT.java b/server/src/test/java/org/elasticsearch/search/fieldcaps/FieldCapabilitiesIT.java index ffd6944df445a..3588ee35118ad 100644 --- a/server/src/test/java/org/elasticsearch/search/fieldcaps/FieldCapabilitiesIT.java +++ b/server/src/test/java/org/elasticsearch/search/fieldcaps/FieldCapabilitiesIT.java @@ -20,6 +20,7 @@ package org.elasticsearch.search.fieldcaps; import org.elasticsearch.action.fieldcaps.FieldCapabilities; +import org.elasticsearch.action.fieldcaps.FieldCapabilities.SourcePath; import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; @@ -31,6 +32,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.function.Predicate; @@ -120,22 +122,24 @@ public void testFieldAlias() { assertTrue(distance.containsKey("double")); assertEquals( new FieldCapabilities("distance", "double", true, true, new String[] {"old_index"}, null, null, - Collections.emptyMap()), + Collections.emptyMap(), Collections.emptyList()), distance.get("double")); assertTrue(distance.containsKey("text")); assertEquals( new FieldCapabilities("distance", "text", true, false, new String[] {"new_index"}, null, null, - Collections.emptyMap()), + Collections.emptyMap(), Collections.emptyList()), distance.get("text")); // Check the capabilities for the 'route_length_miles' alias. Map routeLength = response.getField("route_length_miles"); assertEquals(1, routeLength.size()); + SourcePath expectedRoutePath = new SourcePath(new String[]{"old_index"}, new String[]{"distance"}); assertTrue(routeLength.containsKey("double")); assertEquals( - new FieldCapabilities("route_length_miles", "double", true, true, null, null, null, Collections.emptyMap()), + new FieldCapabilities("route_length_miles", "double", true, true, null, null, null, + Collections.emptyMap(), List.of(expectedRoutePath)), routeLength.get("double")); } @@ -161,6 +165,29 @@ public void testFieldAliasFilteringWithWildcard() { assertTrue(response.get().containsKey("distance")); } + public void testSourcePath() { + FieldCapabilitiesResponse response = client().prepareFieldCaps().setFields("route_length_miles", "distance").get(); + assertIndices(response, "old_index", "new_index"); + + // The alias field 'route_field_miles' should have 'distance' as its source path. + Map routeResponse = response.getField("route_length_miles"); + assertNotNull(routeResponse); + + FieldCapabilities routeFieldCaps = routeResponse.get("double"); + assertNotNull(routeFieldCaps); + + SourcePath expectedRoutePath = new SourcePath(new String[]{"old_index"}, new String[]{"distance"}); + assertEquals(List.of(expectedRoutePath), routeFieldCaps.sourcePath()); + + // The concrete field 'distance' should have an empty source path. + Map distanceResponse = response.getField("distance"); + assertNotNull(distanceResponse); + + FieldCapabilities distanceFieldCaps = distanceResponse.get("double"); + assertNotNull(distanceFieldCaps); + assertEquals(List.of(), distanceFieldCaps.sourcePath()); + } + public void testWithUnmapped() { FieldCapabilitiesResponse response = client().prepareFieldCaps() .setFields("new_field", "old_field") @@ -177,21 +204,22 @@ public void testWithUnmapped() { assertTrue(oldField.containsKey("long")); assertEquals( new FieldCapabilities("old_field", "long", true, true, new String[] {"old_index"}, null, null, - Collections.emptyMap()), + Collections.emptyMap(), Collections.emptyList()), oldField.get("long")); assertTrue(oldField.containsKey("unmapped")); assertEquals( new FieldCapabilities("old_field", "unmapped", false, false, new String[] {"new_index"}, null, null, - Collections.emptyMap()), + Collections.emptyMap(), Collections.emptyList()), oldField.get("unmapped")); Map newField = response.getField("new_field"); assertEquals(1, newField.size()); + SourcePath expectedSourcePath = new SourcePath(new String[]{"old_index"}, new String[]{"old_field"}); assertTrue(newField.containsKey("long")); assertEquals( - new FieldCapabilities("new_field", "long", true, true, null, null, null, Collections.emptyMap()), + new FieldCapabilities("new_field", "long", true, true, null, null, null, Collections.emptyMap(), List.of(expectedSourcePath)), newField.get("long")); } diff --git a/test/framework/src/main/java/org/elasticsearch/index/mapper/MockFieldMapper.java b/test/framework/src/main/java/org/elasticsearch/index/mapper/MockFieldMapper.java index b374a6b40346b..7772b303d573f 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/mapper/MockFieldMapper.java +++ b/test/framework/src/main/java/org/elasticsearch/index/mapper/MockFieldMapper.java @@ -44,6 +44,14 @@ public MockFieldMapper(String fullName, MappedFieldType fieldType) { MultiFields.empty(), new CopyTo.Builder().build()); } + public MockFieldMapper(String fullName, + MappedFieldType fieldType, + MultiFields multifields, + CopyTo copyTo) { + super(findSimpleName(fullName), setName(fullName, fieldType), setName(fullName, fieldType), dummySettings, + multifields, copyTo); + } + static MappedFieldType setName(String fullName, MappedFieldType fieldType) { fieldType.setName(fullName); return fieldType; @@ -90,4 +98,17 @@ protected String contentType() { @Override protected void parseCreateField(ParseContext context, List list) throws IOException { } + + public static class Builder extends FieldMapper.Builder { + protected Builder(String name, MappedFieldType fieldType, MappedFieldType defaultFieldType) { + super(name, fieldType, defaultFieldType); + builder = this; + } + + @Override + public MockFieldMapper build(BuilderContext context) { + MultiFields multiFields = multiFieldsBuilder.build(this, context); + return new MockFieldMapper(name(), fieldType, multiFields, copyTo); + } + } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java index 49a302a498b82..070ea3fdc8f58 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorTests.java @@ -968,7 +968,8 @@ private MockFieldCapsResponseBuilder addNonAggregatableField(String field, Strin private MockFieldCapsResponseBuilder addField(String field, boolean isAggregatable, String... types) { Map caps = new HashMap<>(); for (String type : types) { - caps.put(type, new FieldCapabilities(field, type, true, isAggregatable, null, null, null, Collections.emptyMap())); + caps.put(type, new FieldCapabilities(field, type, true, isAggregatable, null, null, null, + Collections.emptyMap(), Collections.emptyList())); } fieldCaps.put(field, caps); return this; diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/index/IndexResolverTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/index/IndexResolverTests.java index b53d1cbdf8133..ada5ff1becc10 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/index/IndexResolverTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/index/IndexResolverTests.java @@ -244,9 +244,9 @@ public void testMergeIncompatibleCapabilitiesOfObjectFields() throws Exception { Map multi = new HashMap<>(); multi.put("long", new FieldCapabilities(fieldName, "long", true, true, new String[] { "one-index" }, null, null, - Collections.emptyMap())); + Collections.emptyMap(), Collections.emptyList())); multi.put("text", new FieldCapabilities(fieldName, "text", true, false, new String[] { "another-index" }, null, null, - Collections.emptyMap())); + Collections.emptyMap(), Collections.emptyList())); fieldCaps.put(fieldName, multi); @@ -317,7 +317,7 @@ public void testMultipleCompatibleIndicesWithDifferentFields() { public void testIndexWithNoMapping() { Map> versionFC = singletonMap("_version", singletonMap("_index", new FieldCapabilities("_version", "_version", false, false, - null, null, null, Collections.emptyMap()))); + null, null, null, Collections.emptyMap(), Collections.emptyList()))); assertTrue(mergedMappings("*", new String[] { "empty" }, versionFC).isValid()); } @@ -392,7 +392,7 @@ private static class UpdateableFieldCapabilities extends FieldCapabilities { List nonAggregatableIndices = new ArrayList<>(); UpdateableFieldCapabilities(String name, String type, boolean isSearchable, boolean isAggregatable) { - super(name, type, isSearchable, isAggregatable, null, null, null, Collections.emptyMap()); + super(name, type, isSearchable, isAggregatable, null, null, null, Collections.emptyMap(), Collections.emptyList()); } @Override @@ -426,7 +426,8 @@ private static void assertEqualsMaps(Map left, Map right) { private void addFieldCaps(Map> fieldCaps, String name, String type, boolean isSearchable, boolean isAggregatable) { Map cap = new HashMap<>(); - cap.put(type, new FieldCapabilities(name, type, isSearchable, isAggregatable, null, null, null, Collections.emptyMap())); + cap.put(type, new FieldCapabilities(name, type, isSearchable, isAggregatable, null, null, null, + Collections.emptyMap(), Collections.emptyList())); fieldCaps.put(name, cap); }