Skip to content

Commit 8d64f60

Browse files
committed
Resolve field aliases and multi-fields. (#55889)
This commit adds the capability to `FieldTypeLookup` to retrieve a field's paths in the _source. When retrieving a field's values, we consult these source paths to make sure we load the relevant values. This allows us to handle requests for field aliases and multi-fields. We also retrieve values that were copied into the field through copy_to. To me this is what users would expect out of the API, and it's consistent with what comes back from `docvalues_fields` and `stored_fields`. However it does add some complexity, and was not something flagged as important from any of the clients I spoke to about this API. I'm looking for feedback on this point. Relates to #55363.
1 parent bb11df9 commit 8d64f60

File tree

6 files changed

+318
-49
lines changed

6 files changed

+318
-49
lines changed

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

+51-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.elasticsearch.common.regex.Regex;
2525

2626
import java.util.Collection;
27+
import java.util.Collections;
2728
import java.util.HashMap;
2829
import java.util.HashSet;
2930
import java.util.Iterator;
@@ -38,20 +39,32 @@ class FieldTypeLookup implements Iterable<MappedFieldType> {
3839

3940
final CopyOnWriteHashMap<String, MappedFieldType> fullNameToFieldType;
4041
private final CopyOnWriteHashMap<String, String> aliasToConcreteName;
42+
43+
/**
44+
* A map from field name to all fields whose content has been copied into it
45+
* through copy_to. A field only be present in the map if some other field
46+
* has listed it as a target of copy_to.
47+
*
48+
* For convenience, the set of copied fields includes the field itself.
49+
*/
50+
private final CopyOnWriteHashMap<String, Set<String>> fieldToCopiedFields;
4151
private final DynamicKeyFieldTypeLookup dynamicKeyLookup;
4252

4353

4454
FieldTypeLookup() {
4555
fullNameToFieldType = new CopyOnWriteHashMap<>();
4656
aliasToConcreteName = new CopyOnWriteHashMap<>();
57+
fieldToCopiedFields = new CopyOnWriteHashMap<>();
4758
dynamicKeyLookup = new DynamicKeyFieldTypeLookup();
4859
}
4960

5061
private FieldTypeLookup(CopyOnWriteHashMap<String, MappedFieldType> fullNameToFieldType,
5162
CopyOnWriteHashMap<String, String> aliasToConcreteName,
63+
CopyOnWriteHashMap<String, Set<String>> fieldToCopiedFields,
5264
DynamicKeyFieldTypeLookup dynamicKeyLookup) {
5365
this.fullNameToFieldType = fullNameToFieldType;
5466
this.aliasToConcreteName = aliasToConcreteName;
67+
this.fieldToCopiedFields = fieldToCopiedFields;
5568
this.dynamicKeyLookup = dynamicKeyLookup;
5669
}
5770

@@ -66,6 +79,7 @@ public FieldTypeLookup copyAndAddAll(Collection<FieldMapper> fieldMappers,
6679

6780
CopyOnWriteHashMap<String, MappedFieldType> fullName = this.fullNameToFieldType;
6881
CopyOnWriteHashMap<String, String> aliases = this.aliasToConcreteName;
82+
CopyOnWriteHashMap<String, Set<String>> sourcePaths = this.fieldToCopiedFields;
6983
Map<String, DynamicKeyFieldMapper> dynamicKeyMappers = new HashMap<>();
7084

7185
for (FieldMapper fieldMapper : fieldMappers) {
@@ -80,6 +94,17 @@ public FieldTypeLookup copyAndAddAll(Collection<FieldMapper> fieldMappers,
8094
if (fieldMapper instanceof DynamicKeyFieldMapper) {
8195
dynamicKeyMappers.put(fieldName, (DynamicKeyFieldMapper) fieldMapper);
8296
}
97+
98+
for (String targetField : fieldMapper.copyTo().copyToFields()) {
99+
Set<String> sourcePath = sourcePaths.get(targetField);
100+
if (sourcePath == null) {
101+
sourcePaths = sourcePaths.copyAndPut(targetField, Set.of(targetField, fieldName));
102+
} else if (sourcePath.contains(fieldName) == false) {
103+
Set<String> newSourcePath = new HashSet<>(sourcePath);
104+
newSourcePath.add(fieldName);
105+
sourcePaths = sourcePaths.copyAndPut(targetField, Collections.unmodifiableSet(newSourcePath));
106+
}
107+
}
83108
}
84109

85110
for (FieldAliasMapper fieldAliasMapper : fieldAliasMappers) {
@@ -93,7 +118,7 @@ public FieldTypeLookup copyAndAddAll(Collection<FieldMapper> fieldMappers,
93118
}
94119

95120
DynamicKeyFieldTypeLookup newDynamicKeyLookup = this.dynamicKeyLookup.copyAndAddAll(dynamicKeyMappers, aliases);
96-
return new FieldTypeLookup(fullName, aliases, newDynamicKeyLookup);
121+
return new FieldTypeLookup(fullName, aliases, sourcePaths, newDynamicKeyLookup);
97122
}
98123

99124
/**
@@ -129,6 +154,31 @@ public Set<String> simpleMatchToFullName(String pattern) {
129154
return fields;
130155
}
131156

157+
/**
158+
* Given a field, returns its possible paths in the _source.
159+
*
160+
* For most fields, the source path is the same as the field itself. However
161+
* there are some exceptions:
162+
* - The 'source path' for a field alias is its target field.
163+
* - For a multi-field, the source path is the parent field.
164+
* - One field's content could have been copied to another through copy_to.
165+
*/
166+
public Set<String> sourcePaths(String field) {
167+
String resolvedField = aliasToConcreteName.getOrDefault(field, field);
168+
169+
int lastDotIndex = resolvedField.lastIndexOf('.');
170+
if (lastDotIndex > 0) {
171+
String parentField = resolvedField.substring(0, lastDotIndex);
172+
if (fullNameToFieldType.containsKey(parentField)) {
173+
resolvedField = parentField;
174+
}
175+
}
176+
177+
return fieldToCopiedFields.containsKey(resolvedField)
178+
? fieldToCopiedFields.get(resolvedField)
179+
: Set.of(resolvedField);
180+
}
181+
132182
@Override
133183
public Iterator<MappedFieldType> iterator() {
134184
Iterator<MappedFieldType> concreteFieldTypes = fullNameToFieldType.values().iterator();

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

+8
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,14 @@ public Set<String> simpleMatchToFullName(String pattern) {
580580
return fieldTypes.simpleMatchToFullName(pattern);
581581
}
582582

583+
/**
584+
* Given a field name, returns its possible paths in the _source. For example,
585+
* the 'source path' for a multi-field is the path to its parent field.
586+
*/
587+
public Set<String> sourcePath(String fullName) {
588+
return fieldTypes.sourcePaths(fullName);
589+
}
590+
583591
/**
584592
* Returns all mapped field types.
585593
*/

server/src/main/java/org/elasticsearch/search/fetch/subphase/FieldValueRetriever.java

+51-19
Original file line numberDiff line numberDiff line change
@@ -21,50 +21,72 @@
2121

2222
import org.elasticsearch.common.document.DocumentField;
2323
import org.elasticsearch.common.xcontent.support.XContentMapValues;
24-
import org.elasticsearch.index.mapper.DocumentMapper;
24+
import org.elasticsearch.index.mapper.MappedFieldType;
2525
import org.elasticsearch.index.mapper.MapperService;
2626
import org.elasticsearch.search.lookup.SourceLookup;
2727

28+
import java.util.ArrayList;
2829
import java.util.Collection;
2930
import java.util.HashMap;
3031
import java.util.HashSet;
3132
import java.util.List;
3233
import java.util.Map;
3334
import java.util.Set;
3435

36+
/**
37+
* A helper class to {@link FetchFieldsPhase} that's initialized with a list of field patterns to fetch.
38+
* Then given a specific document, it can retrieve the corresponding fields from the document's source.
39+
*/
3540
public class FieldValueRetriever {
36-
private final Set<String> fields;
41+
private final List<FieldContext> fieldContexts;
42+
private final Set<String> sourcePaths;
3743

3844
public static FieldValueRetriever create(MapperService mapperService,
3945
Collection<String> fieldPatterns) {
40-
Set<String> fields = new HashSet<>();
41-
DocumentMapper documentMapper = mapperService.documentMapper();
46+
List<FieldContext> fields = new ArrayList<>();
47+
Set<String> sourcePaths = new HashSet<>();
4248

4349
for (String fieldPattern : fieldPatterns) {
44-
if (documentMapper.objectMappers().containsKey(fieldPattern)) {
45-
continue;
46-
}
4750
Collection<String> concreteFields = mapperService.simpleMatchToFullName(fieldPattern);
48-
fields.addAll(concreteFields);
51+
for (String field : concreteFields) {
52+
MappedFieldType fieldType = mapperService.fieldType(field);
53+
54+
if (fieldType != null) {
55+
Set<String> sourcePath = mapperService.sourcePath(field);
56+
fields.add(new FieldContext(field, sourcePath));
57+
sourcePaths.addAll(sourcePath);
58+
}
59+
}
4960
}
50-
return new FieldValueRetriever(fields);
61+
62+
return new FieldValueRetriever(fields, sourcePaths);
5163
}
5264

53-
private FieldValueRetriever(Set<String> fields) {
54-
this.fields = fields;
65+
private FieldValueRetriever(List<FieldContext> fieldContexts, Set<String> sourcePaths) {
66+
this.fieldContexts = fieldContexts;
67+
this.sourcePaths = sourcePaths;
5568
}
5669

5770
@SuppressWarnings("unchecked")
5871
public Map<String, DocumentField> retrieve(SourceLookup sourceLookup) {
5972
Map<String, DocumentField> result = new HashMap<>();
60-
Map<String, Object> sourceValues = extractValues(sourceLookup, this.fields);
73+
Map<String, Object> sourceValues = extractValues(sourceLookup, sourcePaths);
74+
75+
for (FieldContext fieldContext : fieldContexts) {
76+
String field = fieldContext.fieldName;
77+
Set<String> sourcePath = fieldContext.sourcePath;
6178

62-
for (Map.Entry<String, Object> entry : sourceValues.entrySet()) {
63-
String field = entry.getKey();
64-
Object value = entry.getValue();
65-
List<Object> values = value instanceof List
66-
? (List<Object>) value
67-
: List.of(value);
79+
List<Object> values = new ArrayList<>();
80+
for (String path : sourcePath) {
81+
Object value = sourceValues.get(path);
82+
if (value != null) {
83+
if (value instanceof List) {
84+
values.addAll((List<Object>) value);
85+
} else {
86+
values.add(value);
87+
}
88+
}
89+
}
6890
result.put(field, new DocumentField(field, values));
6991
}
7092
return result;
@@ -74,7 +96,7 @@ public Map<String, DocumentField> retrieve(SourceLookup sourceLookup) {
7496
* For each of the provided paths, return its value in the source. Note that in contrast with
7597
* {@link SourceLookup#extractRawValues}, array and object values can be returned.
7698
*/
77-
private static Map<String, Object> extractValues(SourceLookup sourceLookup, Collection<String> paths) {
99+
private static Map<String, Object> extractValues(SourceLookup sourceLookup, Set<String> paths) {
78100
Map<String, Object> result = new HashMap<>(paths.size());
79101
for (String path : paths) {
80102
Object value = XContentMapValues.extractValue(path, sourceLookup);
@@ -84,4 +106,14 @@ private static Map<String, Object> extractValues(SourceLookup sourceLookup, Coll
84106
}
85107
return result;
86108
}
109+
110+
private static class FieldContext {
111+
final String fieldName;
112+
final Set<String> sourcePath;
113+
114+
FieldContext(String fieldName, Set<String> sourcePath) {
115+
this.fieldName = fieldName;
116+
this.sourcePath = sourcePath;
117+
}
118+
}
87119
}

server/src/test/java/org/elasticsearch/index/mapper/FieldTypeLookupTests.java

+60
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.util.Collection;
3131
import java.util.Iterator;
3232
import java.util.List;
33+
import java.util.Set;
3334

3435
import static java.util.Collections.emptyList;
3536

@@ -150,6 +151,65 @@ public void testSimpleMatchToFullName() {
150151
assertTrue(names.contains("barometer"));
151152
}
152153

154+
public void testSourcePathWithMultiFields() {
155+
MappedFieldType ft = new MockFieldMapper.FakeFieldType();
156+
Mapper.BuilderContext context = new Mapper.BuilderContext(
157+
MockFieldMapper.dummySettings, new ContentPath());
158+
159+
MockFieldMapper field = new MockFieldMapper.Builder("field", ft, ft)
160+
.addMultiField(new MockFieldMapper.Builder("field.subfield1", ft, ft))
161+
.addMultiField(new MockFieldMapper.Builder("field.subfield2", ft, ft))
162+
.build(context);
163+
164+
FieldTypeLookup lookup = new FieldTypeLookup();
165+
lookup = lookup.copyAndAddAll(newList(field), emptyList());
166+
167+
assertEquals(Set.of("field"), lookup.sourcePaths("field"));
168+
assertEquals(Set.of("field"), lookup.sourcePaths("field.subfield1"));
169+
assertEquals(Set.of("field"), lookup.sourcePaths("field.subfield2"));
170+
}
171+
172+
public void testSourcePathWithAliases() {
173+
MappedFieldType ft = new MockFieldMapper.FakeFieldType();
174+
Mapper.BuilderContext context = new Mapper.BuilderContext(
175+
MockFieldMapper.dummySettings, new ContentPath());
176+
177+
MockFieldMapper field = new MockFieldMapper.Builder("field", ft, ft)
178+
.addMultiField(new MockFieldMapper.Builder("field.subfield", ft, ft))
179+
.build(context);
180+
181+
FieldAliasMapper alias1 = new FieldAliasMapper("alias1", "alias1", "field");
182+
FieldAliasMapper alias2 = new FieldAliasMapper("alias2", "alias2", "field.subfield");
183+
184+
FieldTypeLookup lookup = new FieldTypeLookup();
185+
lookup = lookup.copyAndAddAll(newList(field), newList(alias1, alias2));
186+
187+
assertEquals(Set.of("field"), lookup.sourcePaths("alias1"));
188+
assertEquals(Set.of("field"), lookup.sourcePaths("alias2"));
189+
}
190+
191+
public void testSourcePathsWithCopyTo() {
192+
MappedFieldType ft = new MockFieldMapper.FakeFieldType();
193+
Mapper.BuilderContext context = new Mapper.BuilderContext(
194+
MockFieldMapper.dummySettings, new ContentPath());
195+
196+
MockFieldMapper field = new MockFieldMapper.Builder("field", ft, ft)
197+
.addMultiField(new MockFieldMapper.Builder("field.subfield1", ft, ft))
198+
.build(context);
199+
200+
MockFieldMapper otherField = new MockFieldMapper.Builder("other_field", ft, ft)
201+
.copyTo(new FieldMapper.CopyTo.Builder()
202+
.add("field")
203+
.build())
204+
.build(context);
205+
206+
FieldTypeLookup lookup = new FieldTypeLookup();
207+
lookup = lookup.copyAndAddAll(newList(field, otherField), emptyList());
208+
209+
assertEquals(Set.of("other_field", "field"), lookup.sourcePaths("field"));
210+
assertEquals(Set.of("other_field", "field"), lookup.sourcePaths("field.subfield1"));
211+
}
212+
153213
public void testIteratorImmutable() {
154214
MockFieldMapper f1 = new MockFieldMapper("foo");
155215
FieldTypeLookup lookup = new FieldTypeLookup();

0 commit comments

Comments
 (0)