Skip to content

Commit 1aa61bb

Browse files
committed
Add a simple 'fetch fields' phase. (#55639)
Currently the phase just looks up each field name in the _source and returns its values in the 'fields' section of the response. There are several aspects that need improvement -- this PR just lays out the initial class structure and tests.
1 parent fb2e3f8 commit 1aa61bb

File tree

19 files changed

+527
-19
lines changed

19 files changed

+527
-19
lines changed

rest-api-spec/src/main/resources/rest-api-spec/api/search.json

+4
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@
5656
"type":"string",
5757
"description":"The field to use as default where no field prefix is given in the query string"
5858
},
59+
"fields": {
60+
"type":"list",
61+
"description":"A comma-separated list of fields to retrieve as part of each hit"
62+
},
5963
"explain":{
6064
"type":"boolean",
6165
"description":"Specify whether to return detailed information about score computation as part of a hit"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
setup:
2+
- skip:
3+
version: " - 7.99.99"
4+
reason: "fields retrieval is currently only implemented on master"
5+
- do:
6+
indices.create:
7+
index: test
8+
body:
9+
mappings:
10+
properties:
11+
keyword:
12+
type: keyword
13+
integer_range:
14+
type: integer_range
15+
16+
- do:
17+
index:
18+
index: test
19+
id: 1
20+
body:
21+
keyword: [ "first", "second" ]
22+
integer_range:
23+
gte: 0
24+
lte: 42
25+
26+
- do:
27+
indices.refresh:
28+
index: [ test ]
29+
30+
---
31+
"Test basic field retrieval":
32+
- do:
33+
search:
34+
index: test
35+
body:
36+
fields: [keyword, integer_range]
37+
38+
- is_true: hits.hits.0._id
39+
- is_true: hits.hits.0._source
40+
41+
- match: { hits.hits.0.fields.keyword.0: first }
42+
- match: { hits.hits.0.fields.keyword.1: second }
43+
44+
- match: { hits.hits.0.fields.integer_range.0.gte: 0 }
45+
- match: { hits.hits.0.fields.integer_range.0.lte: 42 }

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

+5
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,11 @@ public SearchRequestBuilder addDocValueField(String name) {
303303
return addDocValueField(name, null);
304304
}
305305

306+
public SearchRequestBuilder addFetchField(String name) {
307+
sourceBuilder().fetchField(name);
308+
return this;
309+
}
310+
306311
/**
307312
* Adds a stored field to load and return (note, it must be stored) as part of the search request.
308313
*/

server/src/main/java/org/elasticsearch/common/document/DocumentField.java

+3-4
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,9 @@ public void writeTo(StreamOutput out) throws IOException {
105105
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
106106
builder.startArray(name);
107107
for (Object value : values) {
108-
// this call doesn't really need to support writing any kind of object.
109-
// Stored fields values are converted using MappedFieldType#valueForDisplay.
110-
// As a result they can either be Strings, Numbers, or Booleans, that's
111-
// all.
108+
// This call doesn't really need to support writing any kind of object, since the values
109+
// here are always serializable to xContent. Each value could be a leaf types like a string,
110+
// number, or boolean, a list of such values, or a map of such values with string keys.
112111
builder.value(value);
113112
}
114113
builder.endArray();

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

+13
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import org.elasticsearch.search.fetch.FetchSearchResult;
5757
import org.elasticsearch.search.fetch.StoredFieldsContext;
5858
import org.elasticsearch.search.fetch.subphase.FetchDocValuesContext;
59+
import org.elasticsearch.search.fetch.subphase.FetchFieldsContext;
5960
import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
6061
import org.elasticsearch.search.fetch.subphase.ScriptFieldsContext;
6162
import org.elasticsearch.search.fetch.subphase.highlight.SearchContextHighlight;
@@ -111,6 +112,7 @@ final class DefaultSearchContext extends SearchContext {
111112
private ScriptFieldsContext scriptFields;
112113
private FetchSourceContext fetchSourceContext;
113114
private FetchDocValuesContext docValuesContext;
115+
private FetchFieldsContext fetchFieldsContext;
114116
private int from = -1;
115117
private int size = -1;
116118
private SortAndFormats sort;
@@ -454,6 +456,17 @@ public SearchContext docValuesContext(FetchDocValuesContext docValuesContext) {
454456
return this;
455457
}
456458

459+
@Override
460+
public FetchFieldsContext fetchFieldsContext() {
461+
return fetchFieldsContext;
462+
}
463+
464+
@Override
465+
public SearchContext fetchFieldsContext(FetchFieldsContext fetchFieldsContext) {
466+
this.fetchFieldsContext = fetchFieldsContext;
467+
return this;
468+
}
469+
457470
@Override
458471
public ContextIndexSearcher searcher() {
459472
return this.searcher;

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

+2
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@
220220
import org.elasticsearch.search.fetch.FetchSubPhase;
221221
import org.elasticsearch.search.fetch.subphase.ExplainPhase;
222222
import org.elasticsearch.search.fetch.subphase.FetchDocValuesPhase;
223+
import org.elasticsearch.search.fetch.subphase.FetchFieldsPhase;
223224
import org.elasticsearch.search.fetch.subphase.FetchScorePhase;
224225
import org.elasticsearch.search.fetch.subphase.FetchSourcePhase;
225226
import org.elasticsearch.search.fetch.subphase.FetchVersionPhase;
@@ -731,6 +732,7 @@ private void registerFetchSubPhases(List<SearchPlugin> plugins) {
731732
registerFetchSubPhase(new FetchDocValuesPhase());
732733
registerFetchSubPhase(new ScriptFieldsPhase());
733734
registerFetchSubPhase(new FetchSourcePhase());
735+
registerFetchSubPhase(new FetchFieldsPhase());
734736
registerFetchSubPhase(new FetchVersionPhase());
735737
registerFetchSubPhase(new SeqNoPrimaryTermPhase());
736738
registerFetchSubPhase(new MatchedQueriesPhase());

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

+4
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
import org.elasticsearch.search.fetch.ScrollQueryFetchSearchResult;
8787
import org.elasticsearch.search.fetch.ShardFetchRequest;
8888
import org.elasticsearch.search.fetch.subphase.FetchDocValuesContext;
89+
import org.elasticsearch.search.fetch.subphase.FetchFieldsContext;
8990
import org.elasticsearch.search.fetch.subphase.ScriptFieldsContext.ScriptField;
9091
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
9192
import org.elasticsearch.search.internal.AliasFilter;
@@ -933,6 +934,9 @@ private void parseSource(DefaultSearchContext context, SearchSourceBuilder sourc
933934
}
934935
context.docValuesContext(new FetchDocValuesContext(docValueFields));
935936
}
937+
if (source.fetchFields() != null) {
938+
context.fetchFieldsContext(new FetchFieldsContext(source.fetchFields()));
939+
}
936940
if (source.highlighter() != null) {
937941
HighlightBuilder highlightBuilder = source.highlighter();
938942
try {

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

+38
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.elasticsearch.search.builder;
2121

2222
import org.elasticsearch.ElasticsearchException;
23+
import org.elasticsearch.Version;
2324
import org.elasticsearch.common.Booleans;
2425
import org.elasticsearch.common.Nullable;
2526
import org.elasticsearch.common.ParseField;
@@ -89,6 +90,7 @@ public final class SearchSourceBuilder implements Writeable, ToXContentObject, R
8990
public static final ParseField _SOURCE_FIELD = new ParseField("_source");
9091
public static final ParseField STORED_FIELDS_FIELD = new ParseField("stored_fields");
9192
public static final ParseField DOCVALUE_FIELDS_FIELD = new ParseField("docvalue_fields");
93+
public static final ParseField FETCH_FIELDS_FIELD = new ParseField("fields");
9294
public static final ParseField SCRIPT_FIELDS_FIELD = new ParseField("script_fields");
9395
public static final ParseField SCRIPT_FIELD = new ParseField("script");
9496
public static final ParseField IGNORE_FAILURE_FIELD = new ParseField("ignore_failure");
@@ -165,6 +167,7 @@ public static HighlightBuilder highlight() {
165167
private List<FieldAndFormat> docValueFields;
166168
private List<ScriptField> scriptFields;
167169
private FetchSourceContext fetchSourceContext;
170+
private List<String> fetchFields;
168171

169172
private AggregatorFactories.Builder aggregations;
170173

@@ -239,6 +242,10 @@ public SearchSourceBuilder(StreamInput in) throws IOException {
239242
sliceBuilder = in.readOptionalWriteable(SliceBuilder::new);
240243
collapse = in.readOptionalWriteable(CollapseBuilder::new);
241244
trackTotalHitsUpTo = in.readOptionalInt();
245+
246+
if (in.getVersion().onOrAfter(Version.V_8_0_0)) {
247+
fetchFields = in.readOptionalStringList();
248+
}
242249
}
243250

244251
@Override
@@ -293,6 +300,10 @@ public void writeTo(StreamOutput out) throws IOException {
293300
out.writeOptionalWriteable(sliceBuilder);
294301
out.writeOptionalWriteable(collapse);
295302
out.writeOptionalInt(trackTotalHitsUpTo);
303+
304+
if (out.getVersion().onOrAfter(Version.V_8_0_0)) {
305+
out.writeOptionalStringCollection(fetchFields);
306+
}
296307
}
297308

298309
/**
@@ -821,6 +832,24 @@ public SearchSourceBuilder docValueField(String name) {
821832
return docValueField(name, null);
822833
}
823834

835+
/**
836+
* Gets the fields to load and return as part of the search request.
837+
*/
838+
public List<String> fetchFields() {
839+
return fetchFields;
840+
}
841+
842+
/**
843+
* Adds a field to load and return as part of the search request.
844+
*/
845+
public SearchSourceBuilder fetchField(String fieldName) {
846+
if (fetchFields == null) {
847+
fetchFields = new ArrayList<>();
848+
}
849+
fetchFields.add(fieldName);
850+
return this;
851+
}
852+
824853
/**
825854
* Adds a script field under the given name with the provided script.
826855
*
@@ -1116,6 +1145,11 @@ public void parseXContent(XContentParser parser, boolean checkTrailingTokens) th
11161145
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
11171146
docValueFields.add(FieldAndFormat.fromXContent(parser));
11181147
}
1148+
} else if (FETCH_FIELDS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
1149+
fetchFields = new ArrayList<>();
1150+
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
1151+
fetchFields.add(parser.text());
1152+
}
11191153
} else if (INDICES_BOOST_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
11201154
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
11211155
indexBoosts.add(new IndexBoost(parser));
@@ -1223,6 +1257,10 @@ public XContentBuilder innerToXContent(XContentBuilder builder, Params params) t
12231257
builder.endArray();
12241258
}
12251259

1260+
if (fetchFields != null) {
1261+
builder.array(FETCH_FIELDS_FIELD.getPreferredName(), fetchFields);
1262+
}
1263+
12261264
if (scriptFields != null) {
12271265
builder.startObject(SCRIPT_FIELDS_FIELD.getPreferredName());
12281266
for (ScriptField scriptField : scriptFields) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.elasticsearch.search.fetch.subphase;
20+
21+
import java.util.List;
22+
23+
/**
24+
* The context needed to retrieve fields.
25+
*/
26+
public class FetchFieldsContext {
27+
28+
private final List<String> fields;
29+
30+
public FetchFieldsContext(List<String> fields) {
31+
this.fields = fields;
32+
}
33+
34+
public List<String> fields() {
35+
return this.fields;
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.search.fetch.subphase;
21+
22+
import org.apache.lucene.index.LeafReaderContext;
23+
import org.apache.lucene.index.ReaderUtil;
24+
import org.elasticsearch.common.document.DocumentField;
25+
import org.elasticsearch.common.xcontent.support.XContentMapValues;
26+
import org.elasticsearch.index.mapper.DocumentMapper;
27+
import org.elasticsearch.search.SearchHit;
28+
import org.elasticsearch.search.fetch.FetchSubPhase;
29+
import org.elasticsearch.search.internal.SearchContext;
30+
import org.elasticsearch.search.lookup.SourceLookup;
31+
32+
import java.util.Collection;
33+
import java.util.HashMap;
34+
import java.util.HashSet;
35+
import java.util.List;
36+
import java.util.Map;
37+
import java.util.Set;
38+
import java.util.function.Function;
39+
40+
/**
41+
* A fetch sub-phase for high-level field retrieval. Given a list of fields, it
42+
* retrieves the field values from _source and returns them as document fields.
43+
*/
44+
public final class FetchFieldsPhase implements FetchSubPhase {
45+
46+
@Override
47+
public void hitsExecute(SearchContext context, SearchHit[] hits) {
48+
hitsExecute(context, hit -> getSourceLookup(context, hit), hits);
49+
}
50+
51+
// Visible for testing.
52+
@SuppressWarnings("unchecked")
53+
void hitsExecute(SearchContext context,
54+
Function<SearchHit, SourceLookup> sourceProvider,
55+
SearchHit[] hits) {
56+
FetchFieldsContext fetchFieldsContext = context.fetchFieldsContext();
57+
if (fetchFieldsContext == null || fetchFieldsContext.fields().isEmpty()) {
58+
return;
59+
}
60+
61+
DocumentMapper documentMapper = context.mapperService().documentMapper();
62+
if (documentMapper.sourceMapper().enabled() == false) {
63+
throw new IllegalArgumentException("Unable to retrieve the requested [fields] since _source is " +
64+
"disabled in the mappings for index [" + context.indexShard().shardId().getIndexName() + "]");
65+
}
66+
67+
Set<String> fields = new HashSet<>();
68+
for (String fieldPattern : context.fetchFieldsContext().fields()) {
69+
if (documentMapper.objectMappers().containsKey(fieldPattern)) {
70+
continue;
71+
}
72+
Collection<String> concreteFields = context.mapperService().simpleMatchToFullName(fieldPattern);
73+
fields.addAll(concreteFields);
74+
}
75+
76+
for (SearchHit hit : hits) {
77+
SourceLookup sourceLookup = sourceProvider.apply(hit);
78+
Map<String, Object> valuesByField = extractValues(sourceLookup, fields);
79+
80+
for (Map.Entry<String, Object> entry : valuesByField.entrySet()) {
81+
String field = entry.getKey();
82+
Object value = entry.getValue();
83+
List<Object> values = value instanceof List
84+
? (List<Object>) value
85+
: List.of(value);
86+
87+
DocumentField documentField = new DocumentField(field, values);
88+
hit.setDocumentField(field, documentField);
89+
}
90+
}
91+
}
92+
93+
private SourceLookup getSourceLookup(SearchContext context, SearchHit hit) {
94+
SourceLookup sourceLookup = context.lookup().source();
95+
int readerIndex = ReaderUtil.subIndex(hit.docId(), context.searcher().getIndexReader().leaves());
96+
LeafReaderContext readerContext = context.searcher().getIndexReader().leaves().get(readerIndex);
97+
sourceLookup.setSegmentAndDocument(readerContext, hit.docId());
98+
return sourceLookup;
99+
}
100+
101+
/**
102+
* For each of the provided paths, return its value in the source. Note that in contrast with
103+
* {@link SourceLookup#extractRawValues}, array and object values can be returned.
104+
*/
105+
private Map<String, Object> extractValues(SourceLookup sourceLookup, Collection<String> paths) {
106+
Map<String, Object> result = new HashMap<>(paths.size());
107+
for (String path : paths) {
108+
Object value = XContentMapValues.extractValue(path, sourceLookup);
109+
if (value != null) {
110+
result.put(path, value);
111+
}
112+
}
113+
return result;
114+
}
115+
}

0 commit comments

Comments
 (0)