Skip to content

Commit e2ebaed

Browse files
committed
Add basic support for field aliases in index mappings. (#31287)
* Add basic support for field aliases through a new top-level mapper type. * Add tests for queries, aggregations, sorting, and fetching doc values. * Make sure we properly handle wildcard fields in query string queries. * Allow for aliases when requesting suggestions. * Allow for aliases when requesting highlights. * Add a test for field capabilities.
1 parent cfb3014 commit e2ebaed

27 files changed

+1285
-60
lines changed

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ public DocumentMapper(MapperService mapperService, Mapping mapping) {
138138
newFieldMappers.add(metadataMapper);
139139
}
140140
}
141-
MapperUtils.collect(this.mapping.root, newObjectMappers, newFieldMappers);
141+
MapperUtils.collect(this.mapping.root,
142+
newObjectMappers, newFieldMappers, new ArrayList<>());
142143

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

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

+7-2
Original file line numberDiff line numberDiff line change
@@ -459,13 +459,18 @@ private static ParseContext nestedContext(ParseContext context, ObjectMapper map
459459
private static void parseObjectOrField(ParseContext context, Mapper mapper) throws IOException {
460460
if (mapper instanceof ObjectMapper) {
461461
parseObjectOrNested(context, (ObjectMapper) mapper);
462-
} else {
463-
FieldMapper fieldMapper = (FieldMapper)mapper;
462+
} else if (mapper instanceof FieldMapper) {
463+
FieldMapper fieldMapper = (FieldMapper) mapper;
464464
Mapper update = fieldMapper.parse(context);
465465
if (update != null) {
466466
context.addDynamicMapper(update);
467467
}
468468
parseCopyFields(context, fieldMapper.copyTo().copyToFields());
469+
} else if (mapper instanceof FieldAliasMapper) {
470+
throw new IllegalArgumentException("Cannot write to a field alias [" + mapper.name() + "].");
471+
} else {
472+
throw new IllegalStateException("The provided mapper [" + mapper.name() + "] has an unrecognized type [" +
473+
mapper.getClass().getSimpleName() + "].");
469474
}
470475
}
471476

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) {
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

+28-9
Original file line numberDiff line numberDiff line change
@@ -35,30 +35,36 @@
3535
*/
3636
class FieldTypeLookup implements Iterable<MappedFieldType> {
3737

38-
/** Full field name to field type */
3938
final CopyOnWriteHashMap<String, MappedFieldType> fullNameToFieldType;
39+
private final CopyOnWriteHashMap<String, String> aliasToConcreteName;
4040

41-
/** Create a new empty instance. */
4241
FieldTypeLookup() {
4342
fullNameToFieldType = new CopyOnWriteHashMap<>();
43+
aliasToConcreteName = new CopyOnWriteHashMap<>();
4444
}
4545

46-
private FieldTypeLookup(CopyOnWriteHashMap<String, MappedFieldType> fullName) {
47-
this.fullNameToFieldType = fullName;
46+
private FieldTypeLookup(CopyOnWriteHashMap<String, MappedFieldType> fullNameToFieldType,
47+
CopyOnWriteHashMap<String, String> aliasToConcreteName) {
48+
this.fullNameToFieldType = fullNameToFieldType;
49+
this.aliasToConcreteName = aliasToConcreteName;
4850
}
4951

5052
/**
5153
* Return a new instance that contains the union of this instance and the field types
52-
* from the provided fields. If a field already exists, the field type will be updated
53-
* to use the new mappers field type.
54+
* from the provided mappers. If a field already exists, its field type will be updated
55+
* to use the new type from the given field mapper. Similarly if an alias already
56+
* exists, it will be updated to reference the field type from the new mapper.
5457
*/
55-
public FieldTypeLookup copyAndAddAll(String type, Collection<FieldMapper> fieldMappers) {
58+
public FieldTypeLookup copyAndAddAll(String type,
59+
Collection<FieldMapper> fieldMappers,
60+
Collection<FieldAliasMapper> fieldAliasMappers) {
5661
Objects.requireNonNull(type, "type must not be null");
5762
if (MapperService.DEFAULT_MAPPING.equals(type)) {
5863
throw new IllegalArgumentException("Default mappings should not be added to the lookup");
5964
}
6065

6166
CopyOnWriteHashMap<String, MappedFieldType> fullName = this.fullNameToFieldType;
67+
CopyOnWriteHashMap<String, String> aliases = this.aliasToConcreteName;
6268

6369
for (FieldMapper fieldMapper : fieldMappers) {
6470
MappedFieldType fieldType = fieldMapper.fieldType();
@@ -75,7 +81,14 @@ public FieldTypeLookup copyAndAddAll(String type, Collection<FieldMapper> fieldM
7581
}
7682
}
7783
}
78-
return new FieldTypeLookup(fullName);
84+
85+
for (FieldAliasMapper fieldAliasMapper : fieldAliasMappers) {
86+
String aliasName = fieldAliasMapper.name();
87+
String fieldName = fieldAliasMapper.path();
88+
aliases = aliases.copyAndPut(aliasName, fieldName);
89+
}
90+
91+
return new FieldTypeLookup(fullName, aliases);
7992
}
8093

8194
/**
@@ -92,7 +105,8 @@ private void checkCompatibility(MappedFieldType existingFieldType, MappedFieldTy
92105

93106
/** Returns the field for the given field */
94107
public MappedFieldType get(String field) {
95-
return fullNameToFieldType.get(field);
108+
String resolvedField = aliasToConcreteName.getOrDefault(field, field);
109+
return fullNameToFieldType.get(resolvedField);
96110
}
97111

98112
/**
@@ -105,6 +119,11 @@ public Collection<String> simpleMatchToFullName(String pattern) {
105119
fields.add(fieldType.name());
106120
}
107121
}
122+
for (String aliasName : aliasToConcreteName.keySet()) {
123+
if (Regex.simpleMatch(pattern, aliasName)) {
124+
fields.add(aliasName);
125+
}
126+
}
108127
return fields;
109128
}
110129

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

+4-4
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;
@@ -395,15 +394,16 @@ private synchronized Map<String, DocumentMapper> internalMerge(@Nullable Documen
395394
// check basic sanity of the new mapping
396395
List<ObjectMapper> objectMappers = new ArrayList<>();
397396
List<FieldMapper> fieldMappers = new ArrayList<>();
397+
List<FieldAliasMapper> fieldAliasMappers = new ArrayList<>();
398398
Collections.addAll(fieldMappers, newMapper.mapping().metadataMappers);
399-
MapperUtils.collect(newMapper.mapping().root(), objectMappers, fieldMappers);
399+
MapperUtils.collect(newMapper.mapping().root(), objectMappers, fieldMappers, fieldAliasMappers);
400400
checkFieldUniqueness(newMapper.type(), objectMappers, fieldMappers, fullPathObjectMappers, fieldTypes);
401401
checkObjectsCompatibility(objectMappers, fullPathObjectMappers);
402402
checkPartitionedIndexConstraints(newMapper);
403403

404404
// update lookup data-structures
405405
// this will in particular make sure that the merged fields are compatible with other types
406-
fieldTypes = fieldTypes.copyAndAddAll(newMapper.type(), fieldMappers);
406+
fieldTypes = fieldTypes.copyAndAddAll(newMapper.type(), fieldMappers, fieldAliasMappers);
407407

408408
for (ObjectMapper objectMapper : objectMappers) {
409409
if (fullPathObjectMappers == this.fullPathObjectMappers) {
@@ -482,7 +482,7 @@ private boolean assertMappersShareSameFieldType() {
482482
if (mapper != null) {
483483
List<FieldMapper> fieldMappers = new ArrayList<>();
484484
Collections.addAll(fieldMappers, mapper.mapping().metadataMappers);
485-
MapperUtils.collect(mapper.root(), new ArrayList<>(), fieldMappers);
485+
MapperUtils.collect(mapper.root(), new ArrayList<>(), fieldMappers, new ArrayList<>());
486486
for (FieldMapper fieldMapper : fieldMappers) {
487487
assert fieldMapper.fieldType() == fieldTypes.get(fieldMapper.name()) : fieldMapper.name();
488488
}

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

+14-3
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

+14-9
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;
@@ -167,23 +168,27 @@ public static Map<String, Float> resolveMappingField(QueryShardContext context,
167168
if (fieldSuffix != null && context.fieldMapper(fieldName + fieldSuffix) != null) {
168169
fieldName = fieldName + fieldSuffix;
169170
}
170-
FieldMapper mapper = getFieldMapper(context.getMapperService(), fieldName);
171-
if (mapper == null) {
172-
// Unmapped fields are not ignored
173-
fields.put(fieldOrPattern, weight);
174-
continue;
175-
}
176-
if (acceptMetadataField == false && mapper instanceof MetadataFieldMapper) {
177-
// Ignore metadata fields
171+
172+
MappedFieldType fieldType = context.getMapperService().fullName(fieldName);
173+
if (fieldType == null) {
174+
// Note that we don't ignore unmapped fields.
175+
fields.put(fieldName, weight);
178176
continue;
179177
}
178+
180179
// Ignore fields that are not in the allowed mapper types. Some
181180
// types do not support term queries, and thus we cannot generate
182181
// a special query for them.
183-
String mappingType = mapper.fieldType().typeName();
182+
String mappingType = fieldType.typeName();
184183
if (acceptAllTypes == false && ALLOWED_QUERY_MAPPER_TYPES.contains(mappingType) == false) {
185184
continue;
186185
}
186+
187+
// Ignore metadata fields.
188+
FieldMapper mapper = getFieldMapper(context.getMapperService(), fieldName);
189+
if (acceptMetadataField == false && mapper instanceof MetadataFieldMapper) {
190+
continue;
191+
}
187192
fields.put(fieldName, weight);
188193
}
189194
checkForTooManyFields(fields);

server/src/main/java/org/elasticsearch/indices/IndicesModule.java

+3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.elasticsearch.index.mapper.BooleanFieldMapper;
3737
import org.elasticsearch.index.mapper.CompletionFieldMapper;
3838
import org.elasticsearch.index.mapper.DateFieldMapper;
39+
import org.elasticsearch.index.mapper.FieldAliasMapper;
3940
import org.elasticsearch.index.mapper.FieldNamesFieldMapper;
4041
import org.elasticsearch.index.mapper.GeoPointFieldMapper;
4142
import org.elasticsearch.index.mapper.GeoShapeFieldMapper;
@@ -129,7 +130,9 @@ private Map<String, Mapper.TypeParser> getMappers(List<MapperPlugin> mapperPlugi
129130
mappers.put(ObjectMapper.CONTENT_TYPE, new ObjectMapper.TypeParser());
130131
mappers.put(ObjectMapper.NESTED_CONTENT_TYPE, new ObjectMapper.TypeParser());
131132
mappers.put(CompletionFieldMapper.CONTENT_TYPE, new CompletionFieldMapper.TypeParser());
133+
mappers.put(FieldAliasMapper.CONTENT_TYPE, new FieldAliasMapper.TypeParser());
132134
mappers.put(GeoPointFieldMapper.CONTENT_TYPE, new GeoPointFieldMapper.TypeParser());
135+
133136
if (ShapesAvailability.JTS_AVAILABLE && ShapesAvailability.SPATIAL4J_AVAILABLE) {
134137
mappers.put(GeoShapeFieldMapper.CONTENT_TYPE, new GeoShapeFieldMapper.TypeParser());
135138
}

server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightPhase.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public void hitExecute(SearchContext context, HitContext hitContext) {
100100
if (highlightQuery == null) {
101101
highlightQuery = context.parsedQuery().query();
102102
}
103-
HighlighterContext highlighterContext = new HighlighterContext(fieldName,
103+
HighlighterContext highlighterContext = new HighlighterContext(fieldType.name(),
104104
field, fieldType, context, hitContext, highlightQuery);
105105

106106
if ((highlighter.canHighlight(fieldType) == false) && fieldNameContainsWildcards) {
@@ -109,7 +109,11 @@ public void hitExecute(SearchContext context, HitContext hitContext) {
109109
}
110110
HighlightField highlightField = highlighter.highlight(highlighterContext);
111111
if (highlightField != null) {
112-
highlightFields.put(highlightField.name(), highlightField);
112+
// Note that we make sure to use the original field name in the response. This is because the
113+
// original field could be an alias, and highlighter implementations may instead reference the
114+
// concrete field it points to.
115+
highlightFields.put(fieldName,
116+
new HighlightField(fieldName, highlightField.fragments()));
113117
}
114118
}
115119
}

0 commit comments

Comments
 (0)