Skip to content

Commit c0e1fc1

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 74a3bf6 commit c0e1fc1

File tree

6 files changed

+315
-49
lines changed

6 files changed

+315
-49
lines changed

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

+45-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ class FieldTypeLookup implements Iterable<MappedFieldType> {
3737

3838
private final Map<String, MappedFieldType> fullNameToFieldType = new HashMap<>();
3939
private final Map<String, String> aliasToConcreteName = new HashMap<>();
40+
41+
/**
42+
* A map from field name to all fields whose content has been copied into it
43+
* through copy_to. A field only be present in the map if some other field
44+
* has listed it as a target of copy_to.
45+
*
46+
* For convenience, the set of copied fields includes the field itself.
47+
*/
48+
private final Map<String, Set<String>> fieldToCopiedFields = new HashMap<>();
4049
private final DynamicKeyFieldTypeLookup dynamicKeyLookup;
4150

4251
FieldTypeLookup() {
@@ -45,7 +54,6 @@ class FieldTypeLookup implements Iterable<MappedFieldType> {
4554

4655
FieldTypeLookup(Collection<FieldMapper> fieldMappers,
4756
Collection<FieldAliasMapper> fieldAliasMappers) {
48-
4957
Map<String, DynamicKeyFieldMapper> dynamicKeyMappers = new HashMap<>();
5058

5159
for (FieldMapper fieldMapper : fieldMappers) {
@@ -55,6 +63,17 @@ class FieldTypeLookup implements Iterable<MappedFieldType> {
5563
if (fieldMapper instanceof DynamicKeyFieldMapper) {
5664
dynamicKeyMappers.put(fieldName, (DynamicKeyFieldMapper) fieldMapper);
5765
}
66+
67+
for (String targetField : fieldMapper.copyTo().copyToFields()) {
68+
Set<String> sourcePath = fieldToCopiedFields.get(targetField);
69+
if (sourcePath == null) {
70+
fieldToCopiedFields.put(targetField, Set.of(targetField, fieldName));
71+
} else if (sourcePath.contains(fieldName) == false) {
72+
Set<String> newSourcePath = new HashSet<>(sourcePath);
73+
newSourcePath.add(fieldName);
74+
fieldToCopiedFields.put(targetField, Collections.unmodifiableSet(newSourcePath));
75+
}
76+
}
5877
}
5978

6079
for (FieldAliasMapper fieldAliasMapper : fieldAliasMappers) {
@@ -99,6 +118,31 @@ public Set<String> simpleMatchToFullName(String pattern) {
99118
return fields;
100119
}
101120

121+
/**
122+
* Given a field, returns its possible paths in the _source.
123+
*
124+
* For most fields, the source path is the same as the field itself. However
125+
* there are some exceptions:
126+
* - The 'source path' for a field alias is its target field.
127+
* - For a multi-field, the source path is the parent field.
128+
* - One field's content could have been copied to another through copy_to.
129+
*/
130+
public Set<String> sourcePaths(String field) {
131+
String resolvedField = aliasToConcreteName.getOrDefault(field, field);
132+
133+
int lastDotIndex = resolvedField.lastIndexOf('.');
134+
if (lastDotIndex > 0) {
135+
String parentField = resolvedField.substring(0, lastDotIndex);
136+
if (fullNameToFieldType.containsKey(parentField)) {
137+
resolvedField = parentField;
138+
}
139+
}
140+
141+
return fieldToCopiedFields.containsKey(resolvedField)
142+
? fieldToCopiedFields.get(resolvedField)
143+
: Set.of(resolvedField);
144+
}
145+
102146
@Override
103147
public Iterator<MappedFieldType> iterator() {
104148
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
@@ -569,6 +569,14 @@ public Set<String> simpleMatchToFullName(String pattern) {
569569
return fieldTypes.simpleMatchToFullName(pattern);
570570
}
571571

572+
/**
573+
* Given a field name, returns its possible paths in the _source. For example,
574+
* the 'source path' for a multi-field is the path to its parent field.
575+
*/
576+
public Set<String> sourcePath(String fullName) {
577+
return fieldTypes.sourcePaths(fullName);
578+
}
579+
572580
/**
573581
* Returns all mapped field types.
574582
*/

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

+55
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525
import java.util.Collection;
2626
import java.util.Collections;
2727
import java.util.Iterator;
28+
import java.util.Set;
2829

2930
import static java.util.Collections.emptyList;
31+
import static java.util.Collections.singletonList;
3032

3133
public class FieldTypeLookupTests extends ESTestCase {
3234

@@ -77,6 +79,59 @@ public void testSimpleMatchToFullName() {
7779
assertTrue(names.contains("barometer"));
7880
}
7981

82+
public void testSourcePathWithMultiFields() {
83+
Mapper.BuilderContext context = new Mapper.BuilderContext(
84+
MockFieldMapper.DEFAULT_SETTINGS, new ContentPath());
85+
86+
MockFieldMapper field = new MockFieldMapper.Builder("field")
87+
.addMultiField(new MockFieldMapper.Builder("field.subfield1"))
88+
.addMultiField(new MockFieldMapper.Builder("field.subfield2"))
89+
.build(context);
90+
91+
FieldTypeLookup lookup = new FieldTypeLookup(singletonList(field), emptyList());
92+
93+
assertEquals(Set.of("field"), lookup.sourcePaths("field"));
94+
assertEquals(Set.of("field"), lookup.sourcePaths("field.subfield1"));
95+
assertEquals(Set.of("field"), lookup.sourcePaths("field.subfield2"));
96+
}
97+
98+
public void testSourcePathWithAliases() {
99+
Mapper.BuilderContext context = new Mapper.BuilderContext(
100+
MockFieldMapper.DEFAULT_SETTINGS, new ContentPath());
101+
102+
MockFieldMapper field = new MockFieldMapper.Builder("field")
103+
.addMultiField(new MockFieldMapper.Builder("field.subfield"))
104+
.build(context);
105+
106+
FieldAliasMapper alias1 = new FieldAliasMapper("alias1", "alias1", "field");
107+
FieldAliasMapper alias2 = new FieldAliasMapper("alias2", "alias2", "field.subfield");
108+
109+
FieldTypeLookup lookup = new FieldTypeLookup(Arrays.asList(field), Arrays.asList(alias1, alias2));
110+
111+
assertEquals(Set.of("field"), lookup.sourcePaths("alias1"));
112+
assertEquals(Set.of("field"), lookup.sourcePaths("alias2"));
113+
}
114+
115+
public void testSourcePathsWithCopyTo() {
116+
Mapper.BuilderContext context = new Mapper.BuilderContext(
117+
MockFieldMapper.DEFAULT_SETTINGS, new ContentPath());
118+
119+
MockFieldMapper field = new MockFieldMapper.Builder("field")
120+
.addMultiField(new MockFieldMapper.Builder("field.subfield1"))
121+
.build(context);
122+
123+
MockFieldMapper otherField = new MockFieldMapper.Builder("other_field")
124+
.copyTo(new FieldMapper.CopyTo.Builder()
125+
.add("field")
126+
.build())
127+
.build(context);
128+
129+
FieldTypeLookup lookup = new FieldTypeLookup(Arrays.asList(field, otherField), emptyList());
130+
131+
assertEquals(Set.of("other_field", "field"), lookup.sourcePaths("field"));
132+
assertEquals(Set.of("other_field", "field"), lookup.sourcePaths("field.subfield1"));
133+
}
134+
80135
public void testIteratorImmutable() {
81136
MockFieldMapper f1 = new MockFieldMapper("foo");
82137
FieldTypeLookup lookup = new FieldTypeLookup(Collections.singletonList(f1), emptyList());

0 commit comments

Comments
 (0)