Skip to content

Support unmapped fields in search 'fields' option #65386

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Dec 1, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,92 @@ no dedicated array type, and any field could contain multiple values. The
a specific order. See the mapping documentation on <<array, arrays>> for more
background.

[discrete]
[[retrieve-unmapped-fields]]
==== Retrieving unmapped fields

By default, the `fields` parameter returns only values of mapped fields. However,
Elasticsearch allows storing fields in `_source` that are unmapped, for example by
setting <<dynamic-field-mapping,Dynamic field mapping>> to `false` or by using an
object field with `enabled: false`, thereby disabling parsing and indexing of its content.

Fields in such an object can be retrieved from `_source` using the `include_unmapped` option
in the `fieldS` section:

[source,console]
----
PUT my-index-000001
{
"mappings": {
"enabled": false <1>
}
}

PUT my-index-000001/_doc/1?refresh=true
{
"user_id": "kimchy",
"session_data": {
"object": {
"some_field": "some_value"
}
}
}

POST my-index-000001/_search
{
"fields": [
"user_id",
{
"field": "session_data.object.*",
"include_unmapped" : true <2>
}
],
"_source": false
}
----

<1> Disable all mappings.
<2> Include unmapped fields matching this field pattern.

The response will contain fields results under the `session_data.object.*` path even if the
fields are unmapped, but will not contain `user_id` since it is unmapped but the `include_unmapped`
flag hasn't been set to `true for that field pattern.

[source,console-result]
----
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "my-index-000001",
"_id" : "1",
"_score" : 1.0,
"fields" : {
"session_data.object.some_field": [
"some_value"
]
}
}
]
}
}
----
// TESTRESPONSE[s/"took" : 2/"took": $body.took/]
// TESTRESPONSE[s/"max_score" : 1.0/"max_score" : $body.hits.max_score/]
// TESTRESPONSE[s/"_score" : 1.0/"_score" : $body.hits.hits.0._score/]

[discrete]
[[docvalue-fields]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,3 +295,111 @@ setup:
- is_true: hits.hits.0._id
- match: { hits.hits.0.fields.count: [2] }
- is_false: hits.hits.0.fields.count_without_dv
---
Test unmapped field:
- skip:
version: ' - 7.99.99'
reason: support isn't yet backported
- do:
indices.create:
index: test
body:
mappings:
dynamic: false
properties:
f1:
type: keyword
f2:
type: object
enabled: false
f3:
type: object
- do:
index:
index: test
id: 1
refresh: true
body:
f1: some text
f2:
a: foo
b: bar
f3:
c: baz
f4: some other text
- do:
search:
index: test
body:
fields:
- f1
- { "field" : "f4", "include_unmapped" : true }
- match:
hits.hits.0.fields.f1:
- some text
- match:
hits.hits.0.fields.f4:
- some other text
- do:
search:
index: test
body:
fields:
- { "field" : "f*", "include_unmapped" : true }
- match:
hits.hits.0.fields.f1:
- some text
- match:
hits.hits.0.fields.f2\.a:
- foo
- match:
hits.hits.0.fields.f2\.b:
- bar
- match:
hits.hits.0.fields.f3\.c:
- baz
- match:
hits.hits.0.fields.f4:
- some other text
---
Test unmapped fields inside disabled objects:
- skip:
version: ' - 7.99.99'
reason: support isn't yet backported
- do:
indices.create:
index: test
body:
mappings:
properties:
f1:
type: object
enabled: false
- do:
index:
index: test
id: 1
refresh: true
body:
f1:
- some text
- a: b
-
- 1
- 2
- 3
- do:
search:
index: test
body:
fields: [ { "field" : "*", "include_unmapped" : true } ]
- match:
hits.hits.0.fields.f1:
- some text
-
- 1
- 2
- 3
- match:
hits.hits.0.fields.f1\.a:
- b
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ private SearchSourceBuilder buildExpandSearchSourceBuilder(InnerHitBuilder optio
}
}
if (options.getFetchFields() != null) {
options.getFetchFields().forEach(ff -> groupSource.fetchField(ff.field, ff.format));
options.getFetchFields().forEach(ff -> groupSource.fetchField(ff.field, ff.format, ff.includeUnmapped.orElse(null)));
}
if (options.getDocValueFields() != null) {
options.getDocValueFields().forEach(ff -> groupSource.docValueField(ff.field, ff.format));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ public SearchRequestBuilder addDocValueField(String name) {
* @param name The field to load
*/
public SearchRequestBuilder addFetchField(String name) {
sourceBuilder().fetchField(name, null);
sourceBuilder().fetchField(name, null, null);
return this;
}

Expand All @@ -319,9 +319,10 @@ public SearchRequestBuilder addFetchField(String name) {
*
* @param name The field to load
* @param format an optional format string used when formatting values, for example a date format.
* @param includeUnmapped whether this field pattern should also include unmapped fields
*/
public SearchRequestBuilder addFetchField(String name, String format) {
sourceBuilder().fetchField(name, format);
public SearchRequestBuilder addFetchField(String name, String format, boolean includeUnmapped) {
sourceBuilder().fetchField(name, format, null);
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

import static org.elasticsearch.common.xcontent.XContentParser.Token.END_OBJECT;
Expand Down Expand Up @@ -395,10 +396,20 @@ public InnerHitBuilder addFetchField(String name) {
* @param format an optional format string used when formatting values, for example a date format.
*/
public InnerHitBuilder addFetchField(String name, @Nullable String format) {
return addFetchField(name, format, null);
}

/**
* Adds a field to load and return as part of the search request.
* @param name the field name.
* @param format an optional format string used when formatting values, for example a date format.
* @param includeUnmapped whether unmapped fields should be returned as well
*/
public InnerHitBuilder addFetchField(String name, @Nullable String format, Boolean includeUnmapped) {
if (fetchFields == null || fetchFields.isEmpty()) {
fetchFields = new ArrayList<>();
}
fetchFields.add(new FieldAndFormat(name, format));
fetchFields.add(new FieldAndFormat(name, format, Optional.ofNullable(includeUnmapped)));
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -446,17 +446,24 @@ public List<FieldAndFormat> docValueFields() {
/**
* Adds a field to load and return as part of the search request.
*/
public TopHitsAggregationBuilder fetchField(String field, String format) {
public TopHitsAggregationBuilder fetchField(String field, String format, boolean includeUnmapped) {
if (field == null) {
throw new IllegalArgumentException("[fields] must not be null: [" + name + "]");
}
if (fetchFields == null) {
fetchFields = new ArrayList<>();
}
fetchFields.add(new FieldAndFormat(field, format));
fetchFields.add(new FieldAndFormat(field, format, Optional.of(includeUnmapped)));
return this;
}

/**
* Adds a field to load and return as part of the search request.
*/
public TopHitsAggregationBuilder fetchField(String field, String format) {
return fetchField(field, format, false);
}

/**
* Adds a field to load and return as part of the search request.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import static java.util.Collections.emptyMap;
import static org.elasticsearch.index.query.AbstractQueryBuilder.parseInnerQueryBuilder;
Expand Down Expand Up @@ -868,19 +869,19 @@ public List<FieldAndFormat> fetchFields() {
* Adds a field to load and return as part of the search request.
*/
public SearchSourceBuilder fetchField(String name) {
return fetchField(name, null);
return fetchField(name, null, null);
}

/**
* Adds a field to load and return as part of the search request.
* @param name the field name.
* @param format an optional format string used when formatting values, for example a date format.
*/
public SearchSourceBuilder fetchField(String name, @Nullable String format) {
public SearchSourceBuilder fetchField(String name, @Nullable String format, @Nullable Boolean includeUnmapped) {
if (fetchFields == null) {
fetchFields = new ArrayList<>();
}
fetchFields.add(new FieldAndFormat(name, format));
fetchFields.add(new FieldAndFormat(name, format, Optional.ofNullable(includeUnmapped)));
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public FetchDocValuesContext(QueryShardContext shardContext, List<FieldAndFormat
Collection<String> fieldNames = shardContext.simpleMatchToIndexNames(field.field);
for (String fieldName : fieldNames) {
if (shardContext.isFieldMapped(fieldName)) {
fields.add(new FieldAndFormat(fieldName, field.format));
fields.add(new FieldAndFormat(fieldName, field.format, field.includeUnmapped));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public FetchSubPhaseProcessor getProcessor(FetchContext fetchContext) {
}

FieldFetcher fieldFetcher = FieldFetcher.create(fetchContext.getQueryShardContext(), searchLookup, fetchFieldsContext.fields());

return new FetchSubPhaseProcessor() {
@Override
public void setNextReader(LeafReaderContext readerContext) {
Expand Down
Loading