Skip to content

Commit d20b754

Browse files
author
Christoph Büscher
authored
Support unmapped fields in search 'fields' option (#65386) (#65705)
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 feature. Closes #63690
1 parent e7b8757 commit d20b754

File tree

13 files changed

+687
-49
lines changed

13 files changed

+687
-49
lines changed

docs/reference/search/search-your-data/retrieve-selected-fields.asciidoc

+86
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,93 @@ no dedicated array type, and any field could contain multiple values. The
168168
a specific order. See the mapping documentation on <<array, arrays>> for more
169169
background.
170170

171+
[discrete]
172+
[[retrieve-unmapped-fields]]
173+
==== Retrieving unmapped fields
174+
175+
By default, the `fields` parameter returns only values of mapped fields. However,
176+
Elasticsearch allows storing fields in `_source` that are unmapped, for example by
177+
setting <<dynamic-field-mapping,Dynamic field mapping>> to `false` or by using an
178+
object field with `enabled: false`, thereby disabling parsing and indexing of its content.
171179

180+
Fields in such an object can be retrieved from `_source` using the `include_unmapped` option
181+
in the `fields` section:
182+
183+
[source,console]
184+
----
185+
PUT my-index-000001
186+
{
187+
"mappings": {
188+
"enabled": false <1>
189+
}
190+
}
191+
192+
PUT my-index-000001/_doc/1?refresh=true
193+
{
194+
"user_id": "kimchy",
195+
"session_data": {
196+
"object": {
197+
"some_field": "some_value"
198+
}
199+
}
200+
}
201+
202+
POST my-index-000001/_search
203+
{
204+
"fields": [
205+
"user_id",
206+
{
207+
"field": "session_data.object.*",
208+
"include_unmapped" : true <2>
209+
}
210+
],
211+
"_source": false
212+
}
213+
----
214+
215+
<1> Disable all mappings.
216+
<2> Include unmapped fields matching this field pattern.
217+
218+
The response will contain fields results under the `session_data.object.*` path even if the
219+
fields are unmapped, but will not contain `user_id` since it is unmapped but the `include_unmapped`
220+
flag hasn't been set to `true` for that field pattern.
221+
222+
[source,console-result]
223+
----
224+
{
225+
"took" : 2,
226+
"timed_out" : false,
227+
"_shards" : {
228+
"total" : 1,
229+
"successful" : 1,
230+
"skipped" : 0,
231+
"failed" : 0
232+
},
233+
"hits" : {
234+
"total" : {
235+
"value" : 1,
236+
"relation" : "eq"
237+
},
238+
"max_score" : 1.0,
239+
"hits" : [
240+
{
241+
"_index" : "my-index-000001",
242+
"_id" : "1",
243+
"_score" : 1.0,
244+
"_type" : "_doc",
245+
"fields" : {
246+
"session_data.object.some_field": [
247+
"some_value"
248+
]
249+
}
250+
}
251+
]
252+
}
253+
}
254+
----
255+
// TESTRESPONSE[s/"took" : 2/"took": $body.took/]
256+
// TESTRESPONSE[s/"max_score" : 1.0/"max_score" : $body.hits.max_score/]
257+
// TESTRESPONSE[s/"_score" : 1.0/"_score" : $body.hits.hits.0._score/]
172258

173259
[discrete]
174260
[[docvalue-fields]]

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

+107
Original file line numberDiff line numberDiff line change
@@ -295,3 +295,110 @@ 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.10.99'
302+
reason: support was introduced in 7.11
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+
- { "field" : "f4", "include_unmapped" : true }
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+
- { "field" : "f*", "include_unmapped" : true }
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
364+
---
365+
Test unmapped fields inside disabled objects:
366+
- skip:
367+
version: ' - 7.10.99'
368+
reason: support was introduced in 7.11
369+
- do:
370+
indices.create:
371+
index: test
372+
body:
373+
mappings:
374+
properties:
375+
f1:
376+
type: object
377+
enabled: false
378+
- do:
379+
index:
380+
index: test
381+
id: 1
382+
refresh: true
383+
body:
384+
f1:
385+
- some text
386+
- a: b
387+
-
388+
- 1
389+
- 2
390+
- 3
391+
- do:
392+
search:
393+
index: test
394+
body:
395+
fields: [ { "field" : "*", "include_unmapped" : true } ]
396+
- match:
397+
hits.hits.0.fields.f1:
398+
- 1
399+
- 2
400+
- 3
401+
- some text
402+
- match:
403+
hits.hits.0.fields.f1\.a:
404+
- b

server/src/main/java/org/elasticsearch/action/search/ExpandSearchPhase.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ private SearchSourceBuilder buildExpandSearchSourceBuilder(InnerHitBuilder optio
138138
}
139139
}
140140
if (options.getFetchFields() != null) {
141-
options.getFetchFields().forEach(ff -> groupSource.fetchField(ff.field, ff.format));
141+
options.getFetchFields().forEach(ff -> groupSource.fetchField(ff));
142142
}
143143
if (options.getDocValueFields() != null) {
144144
options.getDocValueFields().forEach(ff -> groupSource.docValueField(ff.field, ff.format));

server/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java

+6-5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.elasticsearch.search.builder.PointInTimeBuilder;
3333
import org.elasticsearch.search.builder.SearchSourceBuilder;
3434
import org.elasticsearch.search.collapse.CollapseBuilder;
35+
import org.elasticsearch.search.fetch.subphase.FieldAndFormat;
3536
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
3637
import org.elasticsearch.search.rescore.RescorerBuilder;
3738
import org.elasticsearch.search.slice.SliceBuilder;
@@ -321,18 +322,18 @@ public SearchRequestBuilder addDocValueField(String name) {
321322
* @param name The field to load
322323
*/
323324
public SearchRequestBuilder addFetchField(String name) {
324-
sourceBuilder().fetchField(name, null);
325+
sourceBuilder().fetchField(new FieldAndFormat(name, null, null));
325326
return this;
326327
}
327328

328329
/**
329330
* Adds a field to load and return. The field must be present in the document _source.
330331
*
331-
* @param name The field to load
332-
* @param format an optional format string used when formatting values, for example a date format.
332+
* @param fetchField a {@link FieldAndFormat} specifying the field pattern, optional format (for example a date format) and
333+
* whether this field pattern sould also include unmapped fields
333334
*/
334-
public SearchRequestBuilder addFetchField(String name, String format) {
335-
sourceBuilder().fetchField(name, format);
335+
public SearchRequestBuilder addFetchField(FieldAndFormat fetchField) {
336+
sourceBuilder().fetchField(fetchField);
336337
return this;
337338
}
338339

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

+11-1
Original file line numberDiff line numberDiff line change
@@ -424,10 +424,20 @@ public InnerHitBuilder addFetchField(String name) {
424424
* @param format an optional format string used when formatting values, for example a date format.
425425
*/
426426
public InnerHitBuilder addFetchField(String name, @Nullable String format) {
427+
return addFetchField(name, format, null);
428+
}
429+
430+
/**
431+
* Adds a field to load and return as part of the search request.
432+
* @param name the field name.
433+
* @param format an optional format string used when formatting values, for example a date format.
434+
* @param includeUnmapped whether unmapped fields should be returned as well
435+
*/
436+
public InnerHitBuilder addFetchField(String name, @Nullable String format, Boolean includeUnmapped) {
427437
if (fetchFields == null || fetchFields.isEmpty()) {
428438
fetchFields = new ArrayList<>();
429439
}
430-
fetchFields.add(new FieldAndFormat(name, format));
440+
fetchFields.add(new FieldAndFormat(name, format, includeUnmapped));
431441
return this;
432442
}
433443

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -452,22 +452,22 @@ public List<FieldAndFormat> docValueFields() {
452452
/**
453453
* Adds a field to load and return as part of the search request.
454454
*/
455-
public TopHitsAggregationBuilder fetchField(String field, String format) {
456-
if (field == null) {
455+
public TopHitsAggregationBuilder fetchField(FieldAndFormat fieldAndFormat) {
456+
if (fieldAndFormat == null) {
457457
throw new IllegalArgumentException("[fields] must not be null: [" + name + "]");
458458
}
459459
if (fetchFields == null) {
460460
fetchFields = new ArrayList<>();
461461
}
462-
fetchFields.add(new FieldAndFormat(field, format));
462+
fetchFields.add(fieldAndFormat);
463463
return this;
464464
}
465465

466466
/**
467467
* Adds a field to load and return as part of the search request.
468468
*/
469469
public TopHitsAggregationBuilder fetchField(String field) {
470-
return fetchField(field, null);
470+
return fetchField(new FieldAndFormat(field, null, null));
471471
}
472472

473473
/**
@@ -802,7 +802,7 @@ public static TopHitsAggregationBuilder parse(String aggregationName, XContentPa
802802
} else if (SearchSourceBuilder.FETCH_FIELDS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
803803
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
804804
FieldAndFormat ff = FieldAndFormat.fromXContent(parser);
805-
factory.fetchField(ff.field, ff.format);
805+
factory.fetchField(ff);
806806
}
807807
} else if (SearchSourceBuilder.SORT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
808808
List<SortBuilder<?>> sorts = SortBuilder.fromXContent(parser);

server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java

+4-5
Original file line numberDiff line numberDiff line change
@@ -904,19 +904,18 @@ public List<FieldAndFormat> fetchFields() {
904904
* Adds a field to load and return as part of the search request.
905905
*/
906906
public SearchSourceBuilder fetchField(String name) {
907-
return fetchField(name, null);
907+
return fetchField(new FieldAndFormat(name, null, null));
908908
}
909909

910910
/**
911911
* Adds a field to load and return as part of the search request.
912-
* @param name the field name.
913-
* @param format an optional format string used when formatting values, for example a date format.
912+
* @param fetchField defining the field name, optional format and optional inclusion of unmapped fields
914913
*/
915-
public SearchSourceBuilder fetchField(String name, @Nullable String format) {
914+
public SearchSourceBuilder fetchField(FieldAndFormat fetchField) {
916915
if (fetchFields == null) {
917916
fetchFields = new ArrayList<>();
918917
}
919-
fetchFields.add(new FieldAndFormat(name, format));
918+
fetchFields.add(fetchField);
920919
return this;
921920
}
922921

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public FetchDocValuesContext(QueryShardContext shardContext, List<FieldAndFormat
4545
Collection<String> fieldNames = shardContext.simpleMatchToIndexNames(field.field);
4646
for (String fieldName : fieldNames) {
4747
if (shardContext.isFieldMapped(fieldName)) {
48-
fields.add(new FieldAndFormat(fieldName, field.format));
48+
fields.add(new FieldAndFormat(fieldName, field.format, field.includeUnmapped));
4949
}
5050
}
5151
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public FetchSubPhaseProcessor getProcessor(FetchContext fetchContext) {
5454
}
5555

5656
FieldFetcher fieldFetcher = FieldFetcher.create(fetchContext.getQueryShardContext(), searchLookup, fetchFieldsContext.fields());
57+
5758
return new FetchSubPhaseProcessor() {
5859
@Override
5960
public void setNextReader(LeafReaderContext readerContext) {

0 commit comments

Comments
 (0)