-
Notifications
You must be signed in to change notification settings - Fork 25.8k
Add aliases as runtime fields #70065
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 16 commits
83d921d
74b4349
cdeb51a
b9e8e3a
33dbf9c
c7c0db4
c49ed9c
88bed88
fdd27a4
9cbee43
3389a2d
175f442
9151775
94fc104
683d1be
0b4ca87
38ce282
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
| * in compliance with, at your election, the Elastic License 2.0 or the Server | ||
| * Side Public License, v 1. | ||
| */ | ||
|
|
||
| package org.elasticsearch.index.mapper; | ||
|
|
||
| import org.elasticsearch.common.xcontent.XContentBuilder; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.List; | ||
| import java.util.Objects; | ||
| import java.util.function.Function; | ||
|
|
||
| public class AliasRuntimeField implements RuntimeField { | ||
|
|
||
| public static final String CONTENT_TYPE = "alias"; | ||
|
|
||
| private static class Builder extends RuntimeField.Builder { | ||
|
|
||
| final FieldMapper.Parameter<String> path = FieldMapper.Parameter.stringParam( | ||
| "path", | ||
| true, | ||
| initializerNotSupported(), | ||
| null | ||
| ).setValidator( | ||
| s -> { | ||
| if (s == null) { | ||
| throw new MapperParsingException("Missing required parameter [path]"); | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| protected Builder(String name) { | ||
| super(name); | ||
| } | ||
|
|
||
| @Override | ||
| protected List<FieldMapper.Parameter<?>> getParameters() { | ||
| return List.of(path); | ||
| } | ||
|
|
||
| @Override | ||
| protected RuntimeField createRuntimeField(Mapper.TypeParser.ParserContext parserContext) { | ||
| return new AliasRuntimeField(name, path.get()); | ||
| } | ||
| } | ||
|
|
||
| public static final RuntimeField.Parser PARSER = new RuntimeField.Parser(Builder::new); | ||
|
|
||
| private final String name; | ||
| private final String path; | ||
|
|
||
| public AliasRuntimeField(String name, String path) { | ||
| this.name = name; | ||
| this.path = path; | ||
| if (Objects.equals(name, path)) { | ||
| throw new MapperParsingException("Invalid path [" + path + "] for alias [" + path + "]: an alias cannot refer to itself"); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void doXContentBody(XContentBuilder builder, Params params) throws IOException { | ||
| builder.field("path", path); | ||
| } | ||
|
|
||
| @Override | ||
| public MappedFieldType asMappedFieldType(Function<String, MappedFieldType> lookup) { | ||
| MappedFieldType ft = lookup.apply(path); | ||
| if (ft == null) { | ||
| throw new MapperParsingException("Cannot resolve alias [" + name + "]: path [" + path + "] does not exist in mappings"); | ||
| } | ||
| return ft; | ||
| } | ||
|
|
||
| @Override | ||
| public String name() { | ||
| return name; | ||
| } | ||
|
|
||
| @Override | ||
| public String typeName() { | ||
| return CONTENT_TYPE; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,21 +68,29 @@ final class FieldTypeLookup { | |
| } | ||
| this.maxParentPathDots = maxParentPathDots; | ||
|
|
||
| Map<String, MappedFieldType> resolvedRuntimeMappers = new HashMap<>(); | ||
| Map<String, DynamicFieldType> resolvedDynamicMappers = new HashMap<>(); | ||
| for (FieldAliasMapper fieldAliasMapper : fieldAliasMappers) { | ||
| String aliasName = fieldAliasMapper.name(); | ||
| String path = fieldAliasMapper.path(); | ||
| MappedFieldType fieldType = fullNameToFieldType.get(path); | ||
| fullNameToFieldType.put(aliasName, fieldType); | ||
| if (fieldType instanceof DynamicFieldType) { | ||
| dynamicFieldTypes.put(aliasName, (DynamicFieldType) fieldType); | ||
| RuntimeField aliasField = new AliasRuntimeField(aliasName, path); | ||
| MappedFieldType resolved = aliasField.asMappedFieldType(fullNameToFieldType::get); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This means that the internal representation of an alias field as it existed so far is the same as the runtime alias field, but this has no effect on how it gets serialized and sent out throughout the nodes and stored in the cluster state, correct? |
||
| resolvedRuntimeMappers.put(aliasName, resolved); | ||
| if (resolved instanceof DynamicFieldType) { | ||
| resolvedDynamicMappers.put(aliasName, (DynamicFieldType) resolved); | ||
| } | ||
| } | ||
|
|
||
| for (RuntimeField runtimeField : runtimeFields) { | ||
| MappedFieldType runtimeFieldType = runtimeField.asMappedFieldType(); | ||
| //this will override concrete fields with runtime fields that have the same name | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. did you remove this comment on purpose? |
||
| fullNameToFieldType.put(runtimeFieldType.name(), runtimeFieldType); | ||
| MappedFieldType resolved = runtimeField.asMappedFieldType(fullNameToFieldType::get); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thinking out loud: if we use asMappedFieldType only for aliases, given that all existing runtime fields return
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. one more thing: fullNameToFieldType does not include the previously resolved alias field mapper, so we are sure that an alias runtime field never points to an alias field defined under properties? |
||
| resolvedRuntimeMappers.put(runtimeField.name(), resolved); | ||
| if (resolved instanceof DynamicFieldType) { | ||
| resolvedDynamicMappers.put(runtimeField.name(), (DynamicFieldType) resolved); | ||
| } | ||
| } | ||
|
|
||
| this.fullNameToFieldType.putAll(resolvedRuntimeMappers); | ||
| this.dynamicFieldTypes.putAll(resolvedDynamicMappers); | ||
| } | ||
|
|
||
| private static int dotCount(String path) { | ||
|
|
@@ -103,6 +111,9 @@ MappedFieldType get(String field) { | |
| if (fieldType != null) { | ||
| return fieldType; | ||
| } | ||
|
|
||
| // If the mapping contains fields that support dynamic sub-key lookup, check | ||
| // if this could correspond to a keyed field of the form 'path_to_field.path_to_key'. | ||
| return getDynamicField(field); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,7 +54,7 @@ default XContentBuilder toXContent(XContentBuilder builder, Params params) throw | |
| * Exposes the {@link MappedFieldType} backing this runtime field, used to execute queries, run aggs etc. | ||
| * @return the {@link MappedFieldType} backing this runtime field | ||
| */ | ||
| MappedFieldType asMappedFieldType(); | ||
| MappedFieldType asMappedFieldType(Function<String, MappedFieldType> lookup); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For my knowledge, will only runtime aliases override this method? Or do we have other plans to use it? Also small comment, we could add javadoc for
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Back when I added the method, I thought we were going to use it for the "object" runtime field, possibly changing the signature to return a list of field types. It turns out I won't need it though, as we went for using the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not super happy about this method. I added it as I thought we needed it for the object runtime field, but it turns out we won't use it and we have so far all implementors returning Question: do we need the lookup to be dynamic or could it be done at creation time? We still rebuild all mappings at every mapping update don't we?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
After thinking about this more, having some method like this makes sense to me in general. It seems like As for the function argument, so far I don't see a cleaner approach. To me it's not too strange that runtime fields have access to the concrete field types here. @romseygeek pointed out that we've brainstormed new types of runtime fields that refer to concrete fields -- such as one that combines the contents of multiple text fields like a virtual |
||
|
|
||
| /** | ||
| * For runtime fields the {@link RuntimeField.Parser} returns directly the {@link MappedFieldType}. | ||
|
|
@@ -103,6 +103,10 @@ private void validate() { | |
| throw new IllegalArgumentException("runtime field [" + name + "] does not support [copy_to]"); | ||
| } | ||
| } | ||
|
|
||
| protected static <T> Function<FieldMapper, T> initializerNotSupported() { | ||
| return mapper -> { throw new UnsupportedOperationException(); }; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -394,8 +394,10 @@ public boolean isFieldMapped(String name) { | |
| } | ||
|
|
||
| private MappedFieldType fieldType(String name) { | ||
| MappedFieldType fieldType = runtimeMappings.get(name); | ||
| return fieldType == null ? mappingLookup.getFieldType(name) : fieldType; | ||
| if (runtimeMappings.containsKey(name)) { | ||
| return runtimeMappings.get(name); | ||
| } | ||
| return mappingLookup.getFieldType(name); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we gain anything with this change? |
||
| } | ||
|
|
||
| public ObjectMapper getObjectMapper(String name) { | ||
|
|
@@ -664,14 +666,15 @@ private static Map<String, MappedFieldType> parseRuntimeMappings(Map<String, Obj | |
| if (runtimeMappings.isEmpty()) { | ||
| return Collections.emptyMap(); | ||
| } | ||
| Map<String, RuntimeField> runtimeFields = RuntimeField.parseRuntimeFields(new HashMap<>(runtimeMappings), | ||
| mapperService.parserContext(), false); | ||
| Map<String, MappedFieldType> runtimeFieldTypes = new HashMap<>(); | ||
| Map<String, RuntimeField> runtimeFields | ||
| = RuntimeField.parseRuntimeFields(new HashMap<>(runtimeMappings), mapperService.parserContext(), false); | ||
| MappingLookup lookup = mapperService.mappingLookup(); | ||
| Map<String, MappedFieldType> resolvedFields = new HashMap<>(); | ||
|
|
||
| for (RuntimeField runtimeField : runtimeFields.values()) { | ||
| MappedFieldType fieldType = runtimeField.asMappedFieldType(); | ||
| runtimeFieldTypes.put(fieldType.name(), fieldType); | ||
| resolvedFields.put(runtimeField.name(), runtimeField.asMappedFieldType(lookup::getFieldType)); | ||
| } | ||
| return Collections.unmodifiableMap(runtimeFieldTypes); | ||
| return resolvedFields; | ||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| /* | ||
| * 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.index.mapper.flattened.FlattenedFieldMapper; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| public class RuntimeAliasTests extends MapperServiceTestCase { | ||
|
|
||
| public void testSimpleAlias() throws IOException { | ||
| MapperService mapperService = createMapperService(topMapping(b -> { | ||
| b.startObject("runtime"); | ||
| { | ||
| b.startObject("alias-to-field").field("type", "alias").field("path", "field").endObject(); | ||
| } | ||
| b.endObject(); | ||
| b.startObject("properties"); | ||
| { | ||
| b.startObject("field").field("type", "keyword").endObject(); | ||
| } | ||
| b.endObject(); | ||
| })); | ||
|
|
||
| MappedFieldType aliased = mapperService.mappingLookup().getFieldType("alias-to-field"); | ||
| assertEquals("field", aliased.name()); | ||
| assertEquals(KeywordFieldMapper.KeywordFieldType.class, aliased.getClass()); | ||
| } | ||
|
|
||
| public void testInvalidAlias() { | ||
| Exception e = expectThrows(MapperParsingException.class, () -> createMapperService(topMapping(b -> { | ||
| b.startObject("runtime"); | ||
| { | ||
| b.startObject("alias-to-field").field("type", "alias").field("path", "field").endObject(); | ||
| } | ||
| b.endObject(); | ||
| }))); | ||
| assertEquals("Cannot resolve alias [alias-to-field]: path [field] does not exist in mappings", e.getMessage()); | ||
| } | ||
|
|
||
| public void testDynamicLookup() throws IOException { | ||
jtibshirani marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| MapperService mapperService = createMapperService(topMapping(b -> { | ||
| b.startObject("runtime"); | ||
| { | ||
| b.startObject("dynamic-alias").field("type", "alias").field("path", "flattened").endObject(); | ||
| } | ||
| b.endObject(); | ||
| b.startObject("properties"); | ||
| { | ||
| b.startObject("flattened").field("type", "flattened").endObject(); | ||
| } | ||
| b.endObject(); | ||
| })); | ||
|
|
||
| MappedFieldType dynamic = mapperService.fieldType("flattened.key"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small comment, we could check the subfield paths too ( |
||
| assertEquals("flattened._keyed", dynamic.name()); | ||
| MappedFieldType aliased = mapperService.fieldType("dynamic-alias.key"); | ||
| assertNotNull(aliased); | ||
| assertEquals("flattened._keyed", aliased.name()); | ||
| FlattenedFieldMapper.KeyedFlattenedFieldType keyed = (FlattenedFieldMapper.KeyedFlattenedFieldType) aliased; | ||
| assertEquals("key", keyed.key()); | ||
| } | ||
|
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.