Skip to content

Commit 5924fe0

Browse files
committed
Add basic support for field aliases through a new top-level mapper type.
1 parent d9a6d69 commit 5924fe0

File tree

10 files changed

+464
-36
lines changed

10 files changed

+464
-36
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
@@ -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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -459,13 +459,17 @@ 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.");
469473
}
470474
}
471475

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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+
public class FieldAliasMapper extends Mapper {
31+
public static final String CONTENT_TYPE = "alias";
32+
33+
public static class Names {
34+
public static final String PATH = "path";
35+
}
36+
37+
private final String name;
38+
private final String path;
39+
40+
public FieldAliasMapper(String simpleName,
41+
String name,
42+
String path) {
43+
super(simpleName);
44+
this.name = name;
45+
this.path = path;
46+
}
47+
48+
@Override
49+
public String name() {
50+
return name;
51+
}
52+
53+
public String path() {
54+
return path;
55+
}
56+
57+
@Override
58+
public Mapper merge(Mapper mergeWith) {
59+
if (!(mergeWith instanceof FieldAliasMapper)) {
60+
throw new IllegalArgumentException("Cannot merge a field alias mapping ["
61+
+ name() + "] with a mapping that is not for a field alias.");
62+
}
63+
return mergeWith;
64+
}
65+
66+
@Override
67+
public Mapper updateFieldType(Map<String, MappedFieldType> fullNameToFieldType) {
68+
return this;
69+
}
70+
71+
@Override
72+
public Iterator<Mapper> iterator() {
73+
return Collections.emptyIterator();
74+
}
75+
76+
@Override
77+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
78+
return builder.startObject(simpleName())
79+
.field("type", CONTENT_TYPE)
80+
.field(Names.PATH, path)
81+
.endObject();
82+
}
83+
84+
public static class TypeParser implements Mapper.TypeParser {
85+
@Override
86+
public Mapper.Builder parse(String name, Map<String, Object> node, ParserContext parserContext)
87+
throws MapperParsingException {
88+
FieldAliasMapper.Builder builder = new FieldAliasMapper.Builder(name);
89+
Object pathField = node.remove(Names.PATH);
90+
String path = XContentMapValues.nodeStringValue(pathField, null);
91+
if (path == null) {
92+
throw new MapperParsingException("The [path] property must be specified.");
93+
}
94+
return builder.path(path);
95+
}
96+
}
97+
98+
public static class Builder extends Mapper.Builder<FieldAliasMapper.Builder, FieldAliasMapper> {
99+
private String name;
100+
private String path;
101+
102+
protected Builder(String name) {
103+
super(name);
104+
this.name = name;
105+
}
106+
107+
public String name() {
108+
return this.name;
109+
}
110+
111+
public Builder path(String path) {
112+
this.path = path;
113+
return this;
114+
}
115+
116+
public FieldAliasMapper build(BuilderContext context) {
117+
String fullName = context.path().pathAsText(name);
118+
return new FieldAliasMapper(name, fullName, path);
119+
}
120+
}
121+
}

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

Lines changed: 30 additions & 9 deletions
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,10 @@ 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.get(field);
109+
return resolvedField == null
110+
? fullNameToFieldType.get(field)
111+
: fullNameToFieldType.get(resolvedField);
96112
}
97113

98114
/**
@@ -105,6 +121,11 @@ public Collection<String> simpleMatchToFullName(String pattern) {
105121
fields.add(fieldType.name());
106122
}
107123
}
124+
for (String aliasName : aliasToConcreteName.keySet()) {
125+
if (Regex.simpleMatch(pattern, aliasName)) {
126+
fields.add(aliasName);
127+
}
128+
}
108129
return fields;
109130
}
110131

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;
@@ -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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,23 @@
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);
3541
}
3642
for (Mapper child : mapper) {
37-
collect(child, objectMappers, fieldMappers);
43+
collect(child, objectMappers, fieldMappers, fieldAliasMappers);
3844
}
3945
}
4046
}

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

Lines changed: 3 additions & 0 deletions
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/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,4 +1389,33 @@ public void testBlankFieldNames() throws Exception {
13891389
client().prepareIndex("idx", "type").setSource(bytes2, XContentType.JSON).get());
13901390
assertThat(ExceptionsHelper.detailedMessage(err), containsString("field name cannot be an empty string"));
13911391
}
1392+
1393+
public void testFieldAlias() throws Exception {
1394+
String mapping = Strings.toString(XContentFactory.jsonBuilder()
1395+
.startObject()
1396+
.startObject("type")
1397+
.startObject("properties")
1398+
.startObject("alias-field")
1399+
.field("type", "alias")
1400+
.field("path", "concrete-field")
1401+
.endObject()
1402+
.startObject("concrete-field")
1403+
.field("type", "keyword")
1404+
.endObject()
1405+
.endObject()
1406+
.endObject()
1407+
.endObject());
1408+
1409+
DocumentMapperParser mapperParser = createIndex("test").mapperService().documentMapperParser();
1410+
DocumentMapper mapper = mapperParser.parse("type", new CompressedXContent(mapping));
1411+
1412+
BytesReference bytes = BytesReference.bytes(XContentFactory.jsonBuilder()
1413+
.startObject()
1414+
.field("alias-field", "value")
1415+
.endObject());
1416+
MapperParsingException exception = expectThrows(MapperParsingException.class,
1417+
() -> mapper.parse(SourceToParse.source("test", "type", "1", bytes, XContentType.JSON)));
1418+
1419+
assertEquals("Cannot write to a field alias [alias-field].", exception.getCause().getMessage());
1420+
}
13921421
}

0 commit comments

Comments
 (0)