Skip to content

Commit 15ff3da

Browse files
authored
Add support for field aliases. (#32172)
* Add basic support for field aliases in index mappings. (#31287) * Allow for aliases when fetching stored fields. (#31411) * Add tests around accessing field aliases in scripts. (#31417) * Add documentation around field aliases. (#31538) * Add validation for field alias mappings. (#31518) * Return both concrete fields and aliases in DocumentFieldMappers#getMapper. (#31671) * Make sure that field-level security is enforced when using field aliases. (#31807) * Add more comprehensive tests for field aliases in queries + aggregations. (#31565) * Remove the deprecated method DocumentFieldMappers#getFieldMapper. (#32148)
1 parent 605dc49 commit 15ff3da

File tree

126 files changed

+4093
-1054
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

126 files changed

+4093
-1054
lines changed

docs/reference/indices/clearcache.asciidoc

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ explicitly by setting `query`, `fielddata` or `request`.
1616

1717
All caches relating to a specific field(s) can also be cleared by
1818
specifying `fields` parameter with a comma delimited list of the
19-
relevant fields.
19+
relevant fields. Note that the provided names must refer to concrete
20+
fields -- objects and field aliases are not supported.
2021

2122
[float]
2223
=== Multi Index

docs/reference/mapping.asciidoc

+4-2
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,10 @@ fields to an existing index with the <<indices-put-mapping,PUT mapping API>>.
124124

125125
Other than where documented, *existing field mappings cannot be
126126
updated*. Changing the mapping would mean invalidating already indexed
127-
documents. Instead, you should create a new index with the correct mappings
128-
and <<docs-reindex,reindex>> your data into that index.
127+
documents. Instead, you should create a new index with the correct mappings
128+
and <<docs-reindex,reindex>> your data into that index. If you only wish
129+
to rename a field and not change its mappings, it may make sense to introduce
130+
an <<alias, `alias`>> field.
129131

130132
[float]
131133
== Example mapping

docs/reference/mapping/types.asciidoc

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ string:: <<text,`text`>> and <<keyword,`keyword`>>
4040

4141
<<parent-join>>:: Defines parent/child relation for documents within the same index
4242

43+
<<alias>>:: Defines an alias to an existing field.
44+
4345
<<feature>>:: Record numeric features to boost hits at query time.
4446

4547
<<feature-vector>>:: Record numeric feature vectors to boost hits at query time.
@@ -58,6 +60,8 @@ the <<analysis-standard-analyzer,`standard` analyzer>>, the
5860
This is the purpose of _multi-fields_. Most datatypes support multi-fields
5961
via the <<multi-fields>> parameter.
6062

63+
include::types/alias.asciidoc[]
64+
6165
include::types/array.asciidoc[]
6266

6367
include::types/binary.asciidoc[]
+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
[[alias]]
2+
=== Alias datatype
3+
4+
An `alias` mapping defines an alternate name for a field in the index.
5+
The alias can be used in place of the target field in <<search, search>> requests,
6+
and selected other APIs like <<search-field-caps, field capabilities>>.
7+
8+
[source,js]
9+
--------------------------------
10+
PUT trips
11+
{
12+
"mappings": {
13+
"_doc": {
14+
"properties": {
15+
"distance": {
16+
"type": "long"
17+
},
18+
"route_length_miles": {
19+
"type": "alias",
20+
"path": "distance" // <1>
21+
},
22+
"transit_mode": {
23+
"type": "keyword"
24+
}
25+
}
26+
}
27+
}
28+
}
29+
30+
GET _search
31+
{
32+
"query": {
33+
"range" : {
34+
"route_length_miles" : {
35+
"gte" : 39
36+
}
37+
}
38+
}
39+
}
40+
--------------------------------
41+
// CONSOLE
42+
43+
<1> The path to the target field. Note that this must be the full path, including any parent
44+
objects (e.g. `object1.object2.field`).
45+
46+
Almost all components of the search request accept field aliases. In particular, aliases can be
47+
used in queries, aggregations, and sort fields, as well as when requesting `docvalue_fields`,
48+
`stored_fields`, suggestions, and highlights. Scripts also support aliases when accessing
49+
field values. Please see the section on <<unsupported-apis, unsupported APIs>> for exceptions.
50+
51+
In some parts of the search request and when requesting field capabilities, field wildcard patterns can be
52+
provided. In these cases, the wildcard pattern will match field aliases in addition to concrete fields:
53+
54+
[source,js]
55+
--------------------------------
56+
GET trips/_field_caps?fields=route_*,transit_mode
57+
--------------------------------
58+
// CONSOLE
59+
// TEST[continued]
60+
61+
[[alias-targets]]
62+
==== Alias targets
63+
64+
There are a few restrictions on the target of an alias:
65+
66+
* The target must be a concrete field, and not an object or another field alias.
67+
* The target field must exist at the time the alias is created.
68+
* If nested objects are defined, a field alias must have the same nested scope as its target.
69+
70+
Additionally, a field alias can only have one target. This means that it is not possible to use a
71+
field alias to query over multiple target fields in a single clause.
72+
73+
[[unsupported-apis]]
74+
==== Unsupported APIs
75+
76+
Writes to field aliases are not supported: attempting to use an alias in an index or update request
77+
will result in a failure. Likewise, aliases cannot be used as the target of `copy_to`.
78+
79+
Because alias names are not present in the document source, aliases cannot be used when performing
80+
source filtering. For example, the following request will return an empty result for `_source`:
81+
82+
[source,js]
83+
--------------------------------
84+
GET /_search
85+
{
86+
"query" : {
87+
"match_all": {}
88+
},
89+
"_source": "route_length_miles"
90+
}
91+
--------------------------------
92+
// CONSOLE
93+
// TEST[continued]
94+
95+
Currently only the search and field capabilities APIs will accept and resolve field aliases.
96+
Other APIs that accept field names, such as <<docs-termvectors, term vectors>>, cannot be used
97+
with field aliases.
98+
99+
Finally, some queries, such as `terms`, `geo_shape`, and `more_like_this`, allow for fetching query
100+
information from an indexed document. Because field aliases aren't supported when fetching documents,
101+
the part of the query that specifies the lookup path cannot refer to a field by its alias.

modules/lang-expression/src/test/java/org/elasticsearch/script/expression/ExpressionTests.java

+54-13
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,52 @@
2020
package org.elasticsearch.script.expression;
2121

2222
import org.elasticsearch.common.settings.Settings;
23-
import org.elasticsearch.index.IndexService;
24-
import org.elasticsearch.index.query.QueryShardContext;
23+
import org.elasticsearch.index.fielddata.AtomicNumericFieldData;
24+
import org.elasticsearch.index.fielddata.IndexNumericFieldData;
25+
import org.elasticsearch.index.fielddata.SortedNumericDoubleValues;
26+
import org.elasticsearch.index.mapper.MapperService;
27+
import org.elasticsearch.index.mapper.NumberFieldMapper.NumberFieldType;
28+
import org.elasticsearch.index.mapper.NumberFieldMapper.NumberType;
2529
import org.elasticsearch.script.ScriptException;
2630
import org.elasticsearch.script.SearchScript;
2731
import org.elasticsearch.search.lookup.SearchLookup;
28-
import org.elasticsearch.test.ESSingleNodeTestCase;
32+
import org.elasticsearch.test.ESTestCase;
2933

34+
import java.io.IOException;
3035
import java.text.ParseException;
3136
import java.util.Collections;
3237

33-
public class ExpressionTests extends ESSingleNodeTestCase {
34-
ExpressionScriptEngine service;
35-
SearchLookup lookup;
38+
import static org.mockito.Matchers.anyInt;
39+
import static org.mockito.Matchers.anyObject;
40+
import static org.mockito.Mockito.mock;
41+
import static org.mockito.Mockito.when;
42+
43+
public class ExpressionTests extends ESTestCase {
44+
private ExpressionScriptEngine service;
45+
private SearchLookup lookup;
3646

3747
@Override
3848
public void setUp() throws Exception {
3949
super.setUp();
40-
IndexService index = createIndex("test", Settings.EMPTY, "type", "d", "type=double");
50+
51+
NumberFieldType fieldType = new NumberFieldType(NumberType.DOUBLE);
52+
MapperService mapperService = mock(MapperService.class);
53+
when(mapperService.fullName("field")).thenReturn(fieldType);
54+
when(mapperService.fullName("alias")).thenReturn(fieldType);
55+
56+
SortedNumericDoubleValues doubleValues = mock(SortedNumericDoubleValues.class);
57+
when(doubleValues.advanceExact(anyInt())).thenReturn(true);
58+
when(doubleValues.nextValue()).thenReturn(2.718);
59+
60+
AtomicNumericFieldData atomicFieldData = mock(AtomicNumericFieldData.class);
61+
when(atomicFieldData.getDoubleValues()).thenReturn(doubleValues);
62+
63+
IndexNumericFieldData fieldData = mock(IndexNumericFieldData.class);
64+
when(fieldData.getFieldName()).thenReturn("field");
65+
when(fieldData.load(anyObject())).thenReturn(atomicFieldData);
66+
4167
service = new ExpressionScriptEngine(Settings.EMPTY);
42-
QueryShardContext shardContext = index.newQueryShardContext(0, null, () -> 0, null);
43-
lookup = new SearchLookup(index.mapperService(), shardContext::getForField, null);
68+
lookup = new SearchLookup(mapperService, ignored -> fieldData, null);
4469
}
4570

4671
private SearchScript.LeafFactory compile(String expression) {
@@ -50,22 +75,38 @@ private SearchScript.LeafFactory compile(String expression) {
5075

5176
public void testNeedsScores() {
5277
assertFalse(compile("1.2").needs_score());
53-
assertFalse(compile("doc['d'].value").needs_score());
78+
assertFalse(compile("doc['field'].value").needs_score());
5479
assertTrue(compile("1/_score").needs_score());
55-
assertTrue(compile("doc['d'].value * _score").needs_score());
80+
assertTrue(compile("doc['field'].value * _score").needs_score());
5681
}
5782

5883
public void testCompileError() {
5984
ScriptException e = expectThrows(ScriptException.class, () -> {
60-
compile("doc['d'].value * *@#)(@$*@#$ + 4");
85+
compile("doc['field'].value * *@#)(@$*@#$ + 4");
6186
});
6287
assertTrue(e.getCause() instanceof ParseException);
6388
}
6489

6590
public void testLinkError() {
6691
ScriptException e = expectThrows(ScriptException.class, () -> {
67-
compile("doc['e'].value * 5");
92+
compile("doc['nonexistent'].value * 5");
6893
});
6994
assertTrue(e.getCause() instanceof ParseException);
7095
}
96+
97+
public void testFieldAccess() throws IOException {
98+
SearchScript script = compile("doc['field'].value").newInstance(null);
99+
script.setDocument(1);
100+
101+
double result = script.runAsDouble();
102+
assertEquals(2.718, result, 0.0);
103+
}
104+
105+
public void testFieldAccessWithFieldAlias() throws IOException {
106+
SearchScript script = compile("doc['alias'].value").newInstance(null);
107+
script.setDocument(1);
108+
109+
double result = script.runAsDouble();
110+
assertEquals(2.718, result, 0.0);
111+
}
71112
}

modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQuery.java

+4
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ Query getCandidateMatchesQuery() {
194194
return candidateMatchesQuery;
195195
}
196196

197+
Query getVerifiedMatchesQuery() {
198+
return verifiedMatchesQuery;
199+
}
200+
197201
// Comparing identity here to avoid being cached
198202
// Note that in theory if the same instance gets used multiple times it could still get cached,
199203
// however since we create a new query instance each time we this query this shouldn't happen and thus

modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -618,13 +618,13 @@ protected Analyzer getWrappedAnalyzer(String fieldName) {
618618
docSearcher.setQueryCache(null);
619619
}
620620

621-
PercolatorFieldMapper percolatorFieldMapper = (PercolatorFieldMapper) docMapper.mappers().getMapper(field);
622-
boolean mapUnmappedFieldsAsString = percolatorFieldMapper.isMapUnmappedFieldAsText();
621+
PercolatorFieldMapper.FieldType pft = (PercolatorFieldMapper.FieldType) fieldType;
622+
String name = this.name != null ? this.name : pft.name();
623623
QueryShardContext percolateShardContext = wrap(context);
624+
PercolateQuery.QueryStore queryStore = createStore(pft.queryBuilderField,
625+
percolateShardContext,
626+
pft.mapUnmappedFieldsAsText);
624627

625-
String name = this.name != null ? this.name : field;
626-
PercolatorFieldMapper.FieldType pft = (PercolatorFieldMapper.FieldType) fieldType;
627-
PercolateQuery.QueryStore queryStore = createStore(pft.queryBuilderField, percolateShardContext, mapUnmappedFieldsAsString);
628628
return pft.percolateQuery(name, queryStore, documents, docSearcher, context.indexVersionCreated());
629629
}
630630

modules/percolator/src/main/java/org/elasticsearch/percolator/PercolatorFieldMapper.java

+10-8
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,19 @@ public PercolatorFieldMapper build(BuilderContext context) {
136136
fieldType.rangeField = rangeFieldMapper.fieldType();
137137
NumberFieldMapper minimumShouldMatchFieldMapper = createMinimumShouldMatchField(context);
138138
fieldType.minimumShouldMatchField = minimumShouldMatchFieldMapper.fieldType();
139+
fieldType.mapUnmappedFieldsAsText = getMapUnmappedFieldAsText(context.indexSettings());
140+
139141
context.path().remove();
140142
setupFieldType(context);
141143
return new PercolatorFieldMapper(name(), fieldType, defaultFieldType, context.indexSettings(),
142144
multiFieldsBuilder.build(this, context), copyTo, queryShardContext, extractedTermsField,
143145
extractionResultField, queryBuilderField, rangeFieldMapper, minimumShouldMatchFieldMapper);
144146
}
145147

148+
private static boolean getMapUnmappedFieldAsText(Settings indexSettings) {
149+
return INDEX_MAP_UNMAPPED_FIELDS_AS_TEXT_SETTING.get(indexSettings);
150+
}
151+
146152
static KeywordFieldMapper createExtractQueryFieldBuilder(String name, BuilderContext context) {
147153
KeywordFieldMapper.Builder queryMetaDataFieldBuilder = new KeywordFieldMapper.Builder(name);
148154
queryMetaDataFieldBuilder.docValues(false);
@@ -195,6 +201,7 @@ static class FieldType extends MappedFieldType {
195201
MappedFieldType minimumShouldMatchField;
196202

197203
RangeFieldMapper.RangeFieldType rangeField;
204+
boolean mapUnmappedFieldsAsText;
198205

199206
FieldType() {
200207
setIndexOptions(IndexOptions.NONE);
@@ -209,6 +216,7 @@ static class FieldType extends MappedFieldType {
209216
queryBuilderField = ref.queryBuilderField;
210217
rangeField = ref.rangeField;
211218
minimumShouldMatchField = ref.minimumShouldMatchField;
219+
mapUnmappedFieldsAsText = ref.mapUnmappedFieldsAsText;
212220
}
213221

214222
@Override
@@ -327,7 +335,6 @@ Tuple<List<BytesRef>, Map<String, List<byte[]>>> extractTermsAndRanges(IndexRead
327335

328336
}
329337

330-
private final boolean mapUnmappedFieldAsText;
331338
private final Supplier<QueryShardContext> queryShardContext;
332339
private KeywordFieldMapper queryTermsField;
333340
private KeywordFieldMapper extractionResultField;
@@ -348,14 +355,9 @@ Tuple<List<BytesRef>, Map<String, List<byte[]>>> extractTermsAndRanges(IndexRead
348355
this.extractionResultField = extractionResultField;
349356
this.queryBuilderField = queryBuilderField;
350357
this.minimumShouldMatchFieldMapper = minimumShouldMatchFieldMapper;
351-
this.mapUnmappedFieldAsText = getMapUnmappedFieldAsText(indexSettings);
352358
this.rangeFieldMapper = rangeFieldMapper;
353359
}
354360

355-
private static boolean getMapUnmappedFieldAsText(Settings indexSettings) {
356-
return INDEX_MAP_UNMAPPED_FIELDS_AS_TEXT_SETTING.get(indexSettings);
357-
}
358-
359361
@Override
360362
public FieldMapper updateFieldType(Map<String, MappedFieldType> fullNameToFieldType) {
361363
PercolatorFieldMapper updated = (PercolatorFieldMapper) super.updateFieldType(fullNameToFieldType);
@@ -402,7 +404,7 @@ public Mapper parse(ParseContext context) throws IOException {
402404

403405
Version indexVersion = context.mapperService().getIndexSettings().getIndexVersionCreated();
404406
createQueryBuilderField(indexVersion, queryBuilderField, queryBuilder, context);
405-
Query query = toQuery(queryShardContext, mapUnmappedFieldAsText, queryBuilder);
407+
Query query = toQuery(queryShardContext, isMapUnmappedFieldAsText(), queryBuilder);
406408
processQuery(query, context);
407409
return null;
408410
}
@@ -522,7 +524,7 @@ protected String contentType() {
522524
}
523525

524526
boolean isMapUnmappedFieldAsText() {
525-
return mapUnmappedFieldAsText;
527+
return ((FieldType) fieldType).mapUnmappedFieldsAsText;
526528
}
527529

528530
/**

modules/percolator/src/test/java/org/elasticsearch/percolator/CandidateQueryTests.java

+2-4
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,7 @@ public void testDuel() throws Exception {
194194
}
195195
Collections.sort(intValues);
196196

197-
MappedFieldType intFieldType = mapperService.documentMapper("type").mappers()
198-
.getMapper("int_field").fieldType();
197+
MappedFieldType intFieldType = mapperService.fullName("int_field");
199198

200199
List<Supplier<Query>> queryFunctions = new ArrayList<>();
201200
queryFunctions.add(MatchNoDocsQuery::new);
@@ -327,8 +326,7 @@ public void testDuel2() throws Exception {
327326
stringValues.add("value2");
328327
stringValues.add("value3");
329328

330-
MappedFieldType intFieldType = mapperService.documentMapper("type").mappers()
331-
.getMapper("int_field").fieldType();
329+
MappedFieldType intFieldType = mapperService.fullName("int_field");
332330
List<int[]> ranges = new ArrayList<>();
333331
ranges.add(new int[]{-5, 5});
334332
ranges.add(new int[]{0, 10});

0 commit comments

Comments
 (0)