Skip to content

Commit 37265db

Browse files
author
Christoph Büscher
committed
Support unmapped fields in search 'fields' option
Currently, the 'fields' option only supports fetching mapped fields. Since 'fields' is meant to be the central place to retrieve document content, it should allow for loading unmapped values. This change adds implementation and tests for this addition. Closes elastic#63690
1 parent c219e17 commit 37265db

File tree

8 files changed

+334
-18
lines changed

8 files changed

+334
-18
lines changed

rest-api-spec/src/main/resources/rest-api-spec/test/search/330_fetch_fields.yml

+66
Original file line numberDiff line numberDiff line change
@@ -295,3 +295,69 @@ setup:
295295
- is_true: hits.hits.0._id
296296
- match: { hits.hits.0.fields.count: [2] }
297297
- is_false: hits.hits.0.fields.count_without_dv
298+
---
299+
Test unmapped field:
300+
- skip:
301+
version: ' - 7.99.99'
302+
reason: support isn't yet backported
303+
- do:
304+
indices.create:
305+
index: test
306+
body:
307+
mappings:
308+
dynamic: false
309+
properties:
310+
f1:
311+
type: keyword
312+
f2:
313+
type: object
314+
enabled: false
315+
f3:
316+
type: object
317+
- do:
318+
index:
319+
index: test
320+
id: 1
321+
refresh: true
322+
body:
323+
f1: some text
324+
f2:
325+
a: foo
326+
b: bar
327+
f3:
328+
c: baz
329+
f4: some other text
330+
- do:
331+
search:
332+
index: test
333+
body:
334+
fields:
335+
- f1
336+
- f4
337+
- match:
338+
hits.hits.0.fields.f1:
339+
- some text
340+
- match:
341+
hits.hits.0.fields.f4:
342+
- some other text
343+
- do:
344+
search:
345+
index: test
346+
body:
347+
fields:
348+
- f*
349+
- match:
350+
hits.hits.0.fields.f1:
351+
- some text
352+
- match:
353+
hits.hits.0.fields.f2\.a:
354+
- foo
355+
- match:
356+
hits.hits.0.fields.f2\.b:
357+
- bar
358+
- match:
359+
hits.hits.0.fields.f3\.c:
360+
- baz
361+
- match:
362+
hits.hits.0.fields.f4:
363+
- some other text

server/src/main/java/org/elasticsearch/index/query/InnerHitContextBuilder.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ protected void setupInnerHitsContext(QueryShardContext queryShardContext,
9393
innerHitsContext.docValuesContext(docValuesContext);
9494
}
9595
if (innerHitBuilder.getFetchFields() != null) {
96-
FetchFieldsContext fieldsContext = new FetchFieldsContext(innerHitBuilder.getFetchFields());
96+
FetchFieldsContext fieldsContext = new FetchFieldsContext(innerHitBuilder.getFetchFields(), false);
9797
innerHitsContext.fetchFieldsContext(fieldsContext);
9898
}
9999
if (innerHitBuilder.getScriptFields() != null) {

server/src/main/java/org/elasticsearch/search/SearchService.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -973,7 +973,8 @@ private void parseSource(DefaultSearchContext context, SearchSourceBuilder sourc
973973
context.docValuesContext(docValuesContext);
974974
}
975975
if (source.fetchFields() != null) {
976-
FetchFieldsContext fetchFieldsContext = new FetchFieldsContext(source.fetchFields());
976+
// TODO make "includeUnmapped configurable?
977+
FetchFieldsContext fetchFieldsContext = new FetchFieldsContext(source.fetchFields(), true);
977978
context.fetchFieldsContext(fetchFieldsContext);
978979
}
979980
if (source.highlighter() != null) {

server/src/main/java/org/elasticsearch/search/aggregations/metrics/TopHitsAggregatorFactory.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ public Aggregator createInternal(SearchContext searchContext,
114114
subSearchContext.docValuesContext(docValuesContext);
115115
}
116116
if (fetchFields != null) {
117-
FetchFieldsContext fieldsContext = new FetchFieldsContext(fetchFields);
117+
FetchFieldsContext fieldsContext = new FetchFieldsContext(fetchFields, true);
118118
subSearchContext.fetchFieldsContext(fieldsContext);
119119
}
120120
for (ScriptFieldsContext.ScriptField field : scriptFields) {

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,18 @@
2525
*/
2626
public class FetchFieldsContext {
2727
private final List<FieldAndFormat> fields;
28+
private final boolean includeUnmapped;
2829

29-
public FetchFieldsContext(List<FieldAndFormat> fields) {
30+
public FetchFieldsContext(List<FieldAndFormat> fields, boolean includeUnmapped) {
3031
this.fields = fields;
32+
this.includeUnmapped = includeUnmapped;
3133
}
3234

3335
public List<FieldAndFormat> fields() {
3436
return fields;
3537
}
38+
39+
public boolean includeUnmapped() {
40+
return includeUnmapped;
41+
}
3642
}

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,13 @@ public FetchSubPhaseProcessor getProcessor(FetchContext fetchContext) {
5353
"in the mappings for index [" + fetchContext.getIndexName() + "]");
5454
}
5555

56-
FieldFetcher fieldFetcher = FieldFetcher.create(fetchContext.getQueryShardContext(), searchLookup, fetchFieldsContext.fields());
56+
FieldFetcher fieldFetcher = FieldFetcher.create(
57+
fetchContext.getQueryShardContext(),
58+
searchLookup,
59+
fetchFieldsContext.fields(),
60+
fetchFieldsContext.includeUnmapped()
61+
);
62+
5763
return new FetchSubPhaseProcessor() {
5864
@Override
5965
public void setNextReader(LeafReaderContext readerContext) {

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

+74-4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.apache.lucene.index.LeafReaderContext;
2323
import org.elasticsearch.common.document.DocumentField;
24+
import org.elasticsearch.common.regex.Regex;
2425
import org.elasticsearch.index.mapper.MappedFieldType;
2526
import org.elasticsearch.index.mapper.ValueFetcher;
2627
import org.elasticsearch.index.query.QueryShardContext;
@@ -30,7 +31,9 @@
3031
import java.io.IOException;
3132
import java.util.ArrayList;
3233
import java.util.Collection;
34+
import java.util.Collections;
3335
import java.util.HashMap;
36+
import java.util.HashSet;
3437
import java.util.List;
3538
import java.util.Map;
3639
import java.util.Set;
@@ -42,32 +45,48 @@
4245
public class FieldFetcher {
4346
public static FieldFetcher create(QueryShardContext context,
4447
SearchLookup searchLookup,
45-
Collection<FieldAndFormat> fieldAndFormats) {
48+
Collection<FieldAndFormat> fieldAndFormats,
49+
boolean includeUnmapped) {
4650

4751
List<FieldContext> fieldContexts = new ArrayList<>();
52+
List<String> originalPattern = new ArrayList<>();
53+
List<String> mappedFields = new ArrayList<>();
4854

4955
for (FieldAndFormat fieldAndFormat : fieldAndFormats) {
5056
String fieldPattern = fieldAndFormat.field;
5157
String format = fieldAndFormat.format;
5258

5359
Collection<String> concreteFields = context.simpleMatchToIndexNames(fieldPattern);
60+
originalPattern.add(fieldAndFormat.field);
5461
for (String field : concreteFields) {
5562
MappedFieldType ft = context.getFieldType(field);
5663
if (ft == null || context.isMetadataField(field)) {
5764
continue;
5865
}
5966
ValueFetcher valueFetcher = ft.valueFetcher(context, searchLookup, format);
67+
mappedFields.add(field);
6068
fieldContexts.add(new FieldContext(field, valueFetcher));
6169
}
6270
}
6371

64-
return new FieldFetcher(fieldContexts);
72+
return new FieldFetcher(fieldContexts, originalPattern, mappedFields, includeUnmapped);
6573
}
6674

6775
private final List<FieldContext> fieldContexts;
68-
69-
private FieldFetcher(List<FieldContext> fieldContexts) {
76+
private final List<String> originalPattern;
77+
private final List<String> mappedFields;
78+
private final boolean includeUnmapped;
79+
80+
private FieldFetcher(
81+
List<FieldContext> fieldContexts,
82+
List<String> originalPattern,
83+
List<String> mappedFields,
84+
boolean includeUnmapped
85+
) {
7086
this.fieldContexts = fieldContexts;
87+
this.originalPattern = originalPattern;
88+
this.mappedFields = mappedFields;
89+
this.includeUnmapped = includeUnmapped;
7190
}
7291

7392
public Map<String, DocumentField> fetch(SourceLookup sourceLookup, Set<String> ignoredFields) throws IOException {
@@ -85,9 +104,60 @@ public Map<String, DocumentField> fetch(SourceLookup sourceLookup, Set<String> i
85104
documentFields.put(field, new DocumentField(field, parsedValues));
86105
}
87106
}
107+
if (includeUnmapped) {
108+
// also look up unmapped fields from source
109+
Set<String> allSourcePaths = extractAllLeafPaths(sourceLookup.loadSourceIfNeeded());
110+
for (String fetchFieldPattern : originalPattern) {
111+
if (Regex.isSimpleMatchPattern(fetchFieldPattern) == false) {
112+
// if pattern has no wildcard, simply look up field if not already present
113+
if (allSourcePaths.contains(fetchFieldPattern)) {
114+
addValueFromSource(sourceLookup, fetchFieldPattern, documentFields);
115+
}
116+
} else {
117+
for (String singlePath : allSourcePaths) {
118+
if (Regex.simpleMatch(fetchFieldPattern, singlePath)) {
119+
addValueFromSource(sourceLookup, singlePath, documentFields);
120+
}
121+
}
122+
}
123+
}
124+
}
88125
return documentFields;
89126
}
90127

128+
private void addValueFromSource(SourceLookup sourceLookup, String path, Map<String, DocumentField> documentFields) {
129+
// checking mapped fields here again to avoid adding from _source where some value was already added or ignored on purpose before
130+
if (mappedFields.contains(path) == false) {
131+
Object object = sourceLookup.extractValue(path, null);
132+
DocumentField f;
133+
if (object instanceof List) {
134+
f = new DocumentField(path, (List) object);
135+
} else {
136+
f = new DocumentField(path, Collections.singletonList(object));
137+
}
138+
if (f.getValue() != null) {
139+
documentFields.put(path, f);
140+
}
141+
}
142+
}
143+
144+
static Set<String> extractAllLeafPaths(Map<String, Object> map) {
145+
Set<String> allPaths = new HashSet<>();
146+
collectAllPaths(allPaths, "", map);
147+
return allPaths;
148+
}
149+
150+
private static void collectAllPaths(Set<String> allPaths, String prefix, Map<String, Object> source) {
151+
for (String key : source.keySet()) {
152+
Object value = source.get(key);
153+
if (value instanceof Map) {
154+
collectAllPaths(allPaths, prefix + key + ".", (Map<String, Object>) value);
155+
} else {
156+
allPaths.add(prefix + key);
157+
}
158+
}
159+
}
160+
91161
public void setNextReader(LeafReaderContext readerContext) {
92162
for (FieldContext field : fieldContexts) {
93163
field.valueFetcher.setNextReader(readerContext);

0 commit comments

Comments
 (0)