Skip to content

Commit 3fd380c

Browse files
committed
Add basic support for field aliases in index mappings. (elastic#31287)
1 parent f137d22 commit 3fd380c

File tree

27 files changed

+1311
-70
lines changed

27 files changed

+1311
-70
lines changed

server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ public DocumentMapper(MapperService mapperService, Mapping mapping) {
143143
newFieldMappers.add(metadataMapper);
144144
}
145145
}
146-
MapperUtils.collect(this.mapping.root, newObjectMappers, newFieldMappers);
146+
MapperUtils.collect(this.mapping.root,
147+
newObjectMappers, newFieldMappers, new ArrayList<>());
147148

148149
final IndexAnalyzers indexAnalyzers = mapperService.getIndexAnalyzers();
149150
this.fieldMappers = new DocumentFieldMappers(newFieldMappers,

server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -476,13 +476,18 @@ private static ParseContext nestedContext(ParseContext context, ObjectMapper map
476476
private static void parseObjectOrField(ParseContext context, Mapper mapper) throws IOException {
477477
if (mapper instanceof ObjectMapper) {
478478
parseObjectOrNested(context, (ObjectMapper) mapper);
479-
} else {
480-
FieldMapper fieldMapper = (FieldMapper)mapper;
479+
} else if (mapper instanceof FieldMapper) {
480+
FieldMapper fieldMapper = (FieldMapper) mapper;
481481
Mapper update = fieldMapper.parse(context);
482482
if (update != null) {
483483
context.addDynamicMapper(update);
484484
}
485485
parseCopyFields(context, fieldMapper.copyTo().copyToFields());
486+
} else if (mapper instanceof FieldAliasMapper) {
487+
throw new IllegalArgumentException("Cannot write to a field alias [" + mapper.name() + "].");
488+
} else {
489+
throw new IllegalStateException("The provided mapper [" + mapper.name() + "] has an unrecognized type [" +
490+
mapper.getClass().getSimpleName() + "].");
486491
}
487492
}
488493

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.index.mapper;
21+
22+
import org.elasticsearch.common.xcontent.XContentBuilder;
23+
import org.elasticsearch.common.xcontent.support.XContentMapValues;
24+
25+
import java.io.IOException;
26+
import java.util.Collections;
27+
import java.util.Iterator;
28+
import java.util.Map;
29+
30+
/**
31+
* A mapper for field aliases.
32+
*
33+
* A field alias has no concrete field mappings of its own, but instead points to another field by
34+
* its path. Once defined, an alias can be used in place of the concrete field name in search requests.
35+
*/
36+
public final class FieldAliasMapper extends Mapper {
37+
public static final String CONTENT_TYPE = "alias";
38+
39+
public static class Names {
40+
public static final String PATH = "path";
41+
}
42+
43+
private final String name;
44+
private final String path;
45+
46+
public FieldAliasMapper(String simpleName,
47+
String name,
48+
String path) {
49+
super(simpleName);
50+
this.name = name;
51+
this.path = path;
52+
}
53+
54+
@Override
55+
public String name() {
56+
return name;
57+
}
58+
59+
public String path() {
60+
return path;
61+
}
62+
63+
@Override
64+
public Mapper merge(Mapper mergeWith, boolean updateAllTypes) {
65+
if (!(mergeWith instanceof FieldAliasMapper)) {
66+
throw new IllegalArgumentException("Cannot merge a field alias mapping ["
67+
+ name() + "] with a mapping that is not for a field alias.");
68+
}
69+
return mergeWith;
70+
}
71+
72+
@Override
73+
public Mapper updateFieldType(Map<String, MappedFieldType> fullNameToFieldType) {
74+
return this;
75+
}
76+
77+
@Override
78+
public Iterator<Mapper> iterator() {
79+
return Collections.emptyIterator();
80+
}
81+
82+
@Override
83+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
84+
return builder.startObject(simpleName())
85+
.field("type", CONTENT_TYPE)
86+
.field(Names.PATH, path)
87+
.endObject();
88+
}
89+
90+
public static class TypeParser implements Mapper.TypeParser {
91+
@Override
92+
public Mapper.Builder parse(String name, Map<String, Object> node, ParserContext parserContext)
93+
throws MapperParsingException {
94+
FieldAliasMapper.Builder builder = new FieldAliasMapper.Builder(name);
95+
Object pathField = node.remove(Names.PATH);
96+
String path = XContentMapValues.nodeStringValue(pathField, null);
97+
if (path == null) {
98+
throw new MapperParsingException("The [path] property must be specified for field [" + name + "].");
99+
}
100+
return builder.path(path);
101+
}
102+
}
103+
104+
public static class Builder extends Mapper.Builder<FieldAliasMapper.Builder, FieldAliasMapper> {
105+
private String name;
106+
private String path;
107+
108+
protected Builder(String name) {
109+
super(name);
110+
this.name = name;
111+
}
112+
113+
public String name() {
114+
return this.name;
115+
}
116+
117+
public Builder path(String path) {
118+
this.path = path;
119+
return this;
120+
}
121+
122+
public FieldAliasMapper build(BuilderContext context) {
123+
String fullName = context.path().pathAsText(name);
124+
return new FieldAliasMapper(name, fullName, path);
125+
}
126+
}
127+
}

server/src/main/java/org/elasticsearch/index/mapper/FieldTypeLookup.java

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,26 +36,35 @@
3636
*/
3737
class FieldTypeLookup implements Iterable<MappedFieldType> {
3838

39-
/** Full field name to field type */
39+
/**
40+
* A mapping from concrete field name to field type.
41+
**/
4042
final CopyOnWriteHashMap<String, MappedFieldType> fullNameToFieldType;
43+
private final CopyOnWriteHashMap<String, String> aliasToConcreteName;
4144

42-
/** Full field name to types containing a mapping for this full name. */
43-
final CopyOnWriteHashMap<String, Set<String>> fullNameToTypes;
45+
/**
46+
* Full field name to types containing a mapping for this full name. Note that
47+
* this map contains aliases as well as concrete fields.
48+
**/
49+
private final CopyOnWriteHashMap<String, Set<String>> fullNameToTypes;
4450

45-
/** Create a new empty instance. */
4651
FieldTypeLookup() {
4752
fullNameToFieldType = new CopyOnWriteHashMap<>();
53+
aliasToConcreteName = new CopyOnWriteHashMap<>();
4854
fullNameToTypes = new CopyOnWriteHashMap<>();
4955
}
5056

51-
private FieldTypeLookup(
52-
CopyOnWriteHashMap<String, MappedFieldType> fullName,
53-
CopyOnWriteHashMap<String, Set<String>> fullNameToTypes) {
54-
this.fullNameToFieldType = fullName;
57+
private FieldTypeLookup(CopyOnWriteHashMap<String, MappedFieldType> fullNameToFieldType,
58+
CopyOnWriteHashMap<String, String> aliasToConcreteName,
59+
CopyOnWriteHashMap<String, Set<String>> fullNameToTypes) {
60+
this.fullNameToFieldType = fullNameToFieldType;
61+
this.aliasToConcreteName = aliasToConcreteName;
5562
this.fullNameToTypes = fullNameToTypes;
5663
}
5764

58-
private static CopyOnWriteHashMap<String, Set<String>> addType(CopyOnWriteHashMap<String, Set<String>> map, String key, String type) {
65+
private static CopyOnWriteHashMap<String, Set<String>> addType(CopyOnWriteHashMap<String, Set<String>> map,
66+
String key,
67+
String type) {
5968
Set<String> types = map.get(key);
6069
if (types == null) {
6170
return map.copyAndPut(key, Collections.singleton(type));
@@ -74,16 +83,21 @@ private static CopyOnWriteHashMap<String, Set<String>> addType(CopyOnWriteHashMa
7483

7584
/**
7685
* Return a new instance that contains the union of this instance and the field types
77-
* from the provided fields. If a field already exists, the field type will be updated
78-
* to use the new mappers field type.
86+
* from the provided mappers. If a field already exists, its field type will be updated
87+
* to use the new type from the given field mapper. Similarly if an alias already
88+
* exists, it will be updated to reference the field type from the new mapper.
7989
*/
80-
public FieldTypeLookup copyAndAddAll(String type, Collection<FieldMapper> fieldMappers, boolean updateAllTypes) {
90+
public FieldTypeLookup copyAndAddAll(String type,
91+
Collection<FieldMapper> fieldMappers,
92+
Collection<FieldAliasMapper> fieldAliasMappers,
93+
boolean updateAllTypes) {
8194
Objects.requireNonNull(type, "type must not be null");
8295
if (MapperService.DEFAULT_MAPPING.equals(type)) {
8396
throw new IllegalArgumentException("Default mappings should not be added to the lookup");
8497
}
8598

8699
CopyOnWriteHashMap<String, MappedFieldType> fullName = this.fullNameToFieldType;
100+
CopyOnWriteHashMap<String, String> aliases = this.aliasToConcreteName;
87101
CopyOnWriteHashMap<String, Set<String>> fullNameToTypes = this.fullNameToTypes;
88102

89103
for (FieldMapper fieldMapper : fieldMappers) {
@@ -96,10 +110,16 @@ public FieldTypeLookup copyAndAddAll(String type, Collection<FieldMapper> fieldM
96110
if (fieldType.equals(fullNameFieldType) == false) {
97111
fullName = fullName.copyAndPut(fieldType.name(), fieldMapper.fieldType());
98112
}
99-
100113
fullNameToTypes = addType(fullNameToTypes, fieldType.name(), type);
101114
}
102-
return new FieldTypeLookup(fullName, fullNameToTypes);
115+
116+
for (FieldAliasMapper fieldAliasMapper : fieldAliasMappers) {
117+
String aliasName = fieldAliasMapper.name();
118+
String fieldName = fieldAliasMapper.path();
119+
aliases = aliases.copyAndPut(aliasName, fieldName);
120+
}
121+
122+
return new FieldTypeLookup(fullName, aliases, fullNameToTypes);
103123
}
104124

105125
private static boolean beStrict(String type, Set<String> types, boolean updateAllTypes) {
@@ -134,11 +154,15 @@ private void checkCompatibility(String type, FieldMapper fieldMapper, boolean up
134154

135155
/** Returns the field for the given field */
136156
public MappedFieldType get(String field) {
137-
return fullNameToFieldType.get(field);
157+
String resolvedField = aliasToConcreteName.getOrDefault(field, field);
158+
return fullNameToFieldType.get(resolvedField);
138159
}
139160

140-
/** Get the set of types that have a mapping for the given field. */
141-
public Set<String> getTypes(String field) {
161+
/**
162+
* Get the set of types that have a mapping for the given field.
163+
* Note: this method is only visible for testing.
164+
**/
165+
protected Set<String> getTypes(String field) {
142166
Set<String> types = fullNameToTypes.get(field);
143167
if (types == null) {
144168
types = Collections.emptySet();
@@ -156,6 +180,11 @@ public Collection<String> simpleMatchToFullName(String pattern) {
156180
fields.add(fieldType.name());
157181
}
158182
}
183+
for (String aliasName : aliasToConcreteName.keySet()) {
184+
if (Regex.simpleMatch(pattern, aliasName)) {
185+
fields.add(aliasName);
186+
}
187+
}
159188
return fields;
160189
}
161190

server/src/main/java/org/elasticsearch/index/mapper/MapperService.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
import com.carrotsearch.hppc.ObjectHashSet;
2323
import com.carrotsearch.hppc.cursors.ObjectCursor;
24-
2524
import org.apache.logging.log4j.message.ParameterizedMessage;
2625
import org.apache.lucene.analysis.Analyzer;
2726
import org.apache.lucene.analysis.DelegatingAnalyzerWrapper;
@@ -428,15 +427,16 @@ private synchronized Map<String, DocumentMapper> internalMerge(@Nullable Documen
428427
// check basic sanity of the new mapping
429428
List<ObjectMapper> objectMappers = new ArrayList<>();
430429
List<FieldMapper> fieldMappers = new ArrayList<>();
430+
List<FieldAliasMapper> fieldAliasMappers = new ArrayList<>();
431431
Collections.addAll(fieldMappers, newMapper.mapping().metadataMappers);
432-
MapperUtils.collect(newMapper.mapping().root(), objectMappers, fieldMappers);
432+
MapperUtils.collect(newMapper.mapping().root(), objectMappers, fieldMappers, fieldAliasMappers);
433433
checkFieldUniqueness(newMapper.type(), objectMappers, fieldMappers, fullPathObjectMappers, fieldTypes);
434434
checkObjectsCompatibility(objectMappers, updateAllTypes, fullPathObjectMappers);
435435
checkPartitionedIndexConstraints(newMapper);
436436

437437
// update lookup data-structures
438438
// this will in particular make sure that the merged fields are compatible with other types
439-
fieldTypes = fieldTypes.copyAndAddAll(newMapper.type(), fieldMappers, updateAllTypes);
439+
fieldTypes = fieldTypes.copyAndAddAll(newMapper.type(), fieldMappers, fieldAliasMappers, updateAllTypes);
440440

441441
for (ObjectMapper objectMapper : objectMappers) {
442442
if (fullPathObjectMappers == this.fullPathObjectMappers) {
@@ -540,7 +540,7 @@ private boolean assertMappersShareSameFieldType() {
540540
for (DocumentMapper mapper : docMappers(false)) {
541541
List<FieldMapper> fieldMappers = new ArrayList<>();
542542
Collections.addAll(fieldMappers, mapper.mapping().metadataMappers);
543-
MapperUtils.collect(mapper.root(), new ArrayList<>(), fieldMappers);
543+
MapperUtils.collect(mapper.root(), new ArrayList<>(), fieldMappers, new ArrayList<>());
544544
for (FieldMapper fieldMapper : fieldMappers) {
545545
assert fieldMapper.fieldType() == fieldTypes.get(fieldMapper.name()) : fieldMapper.name();
546546
}

server/src/main/java/org/elasticsearch/index/mapper/MapperUtils.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,28 @@
2424
enum MapperUtils {
2525
;
2626

27-
/** Split mapper and its descendants into object and field mappers. */
28-
public static void collect(Mapper mapper, Collection<ObjectMapper> objectMappers, Collection<FieldMapper> fieldMappers) {
27+
/**
28+
* Splits the provided mapper and its descendants into object, field, and field alias mappers.
29+
*/
30+
public static void collect(Mapper mapper, Collection<ObjectMapper> objectMappers,
31+
Collection<FieldMapper> fieldMappers,
32+
Collection<FieldAliasMapper> fieldAliasMappers) {
2933
if (mapper instanceof RootObjectMapper) {
3034
// root mapper isn't really an object mapper
3135
} else if (mapper instanceof ObjectMapper) {
3236
objectMappers.add((ObjectMapper)mapper);
3337
} else if (mapper instanceof FieldMapper) {
3438
fieldMappers.add((FieldMapper)mapper);
39+
} else if (mapper instanceof FieldAliasMapper) {
40+
fieldAliasMappers.add((FieldAliasMapper) mapper);
41+
} else {
42+
throw new IllegalStateException("Unrecognized mapper type [" +
43+
mapper.getClass().getSimpleName() + "].");
3544
}
45+
46+
3647
for (Mapper child : mapper) {
37-
collect(child, objectMappers, fieldMappers);
48+
collect(child, objectMappers, fieldMappers, fieldAliasMappers);
3849
}
3950
}
4051
}

server/src/main/java/org/elasticsearch/index/search/QueryParserHelper.java

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.elasticsearch.index.mapper.FieldMapper;
2626
import org.elasticsearch.index.mapper.IpFieldMapper;
2727
import org.elasticsearch.index.mapper.KeywordFieldMapper;
28+
import org.elasticsearch.index.mapper.MappedFieldType;
2829
import org.elasticsearch.index.mapper.MapperService;
2930
import org.elasticsearch.index.mapper.MetadataFieldMapper;
3031
import org.elasticsearch.index.mapper.NumberFieldMapper;
@@ -165,23 +166,27 @@ public static Map<String, Float> resolveMappingField(QueryShardContext context,
165166
if (fieldSuffix != null && context.fieldMapper(fieldName + fieldSuffix) != null) {
166167
fieldName = fieldName + fieldSuffix;
167168
}
168-
FieldMapper mapper = getFieldMapper(context.getMapperService(), fieldName);
169-
if (mapper == null) {
170-
// Unmapped fields are not ignored
171-
fields.put(fieldOrPattern, weight);
172-
continue;
173-
}
174-
if (acceptMetadataField == false && mapper instanceof MetadataFieldMapper) {
175-
// Ignore metadata fields
169+
170+
MappedFieldType fieldType = context.getMapperService().fullName(fieldName);
171+
if (fieldType == null) {
172+
// Note that we don't ignore unmapped fields.
173+
fields.put(fieldName, weight);
176174
continue;
177175
}
176+
178177
// Ignore fields that are not in the allowed mapper types. Some
179178
// types do not support term queries, and thus we cannot generate
180179
// a special query for them.
181-
String mappingType = mapper.fieldType().typeName();
180+
String mappingType = fieldType.typeName();
182181
if (acceptAllTypes == false && ALLOWED_QUERY_MAPPER_TYPES.contains(mappingType) == false) {
183182
continue;
184183
}
184+
185+
// Ignore metadata fields.
186+
FieldMapper mapper = getFieldMapper(context.getMapperService(), fieldName);
187+
if (acceptMetadataField == false && mapper instanceof MetadataFieldMapper) {
188+
continue;
189+
}
185190
fields.put(fieldName, weight);
186191
}
187192
return fields;

0 commit comments

Comments
 (0)