Skip to content

Commit 50bdab6

Browse files
committed
Make the fetch fields phase easier to test. (#55756)
This commit pulls out a FieldValueRetriever object, which retrieves specific fields given a document's source. The new object makes it easier to unit test the logic, and will help keep FetchFieldsPhase from growing too complex as we add more functionality.
1 parent 5352ed6 commit 50bdab6

File tree

4 files changed

+173
-142
lines changed

4 files changed

+173
-142
lines changed

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

+51-18
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,34 @@ setup:
22
- skip:
33
version: " - 7.99.99"
44
reason: "fields retrieval is currently only implemented on master"
5+
6+
---
7+
"Test basic field retrieval":
58
- do:
69
indices.create:
7-
index: test
8-
body:
9-
mappings:
10-
properties:
11-
keyword:
12-
type: keyword
13-
integer_range:
14-
type: integer_range
10+
index: test
11+
body:
12+
mappings:
13+
properties:
14+
keyword:
15+
type: keyword
16+
integer_range:
17+
type: integer_range
1518

1619
- do:
1720
index:
18-
index: test
19-
id: 1
20-
body:
21-
keyword: [ "first", "second" ]
22-
integer_range:
23-
gte: 0
24-
lte: 42
21+
index: test
22+
id: 1
23+
body:
24+
keyword: [ "first", "second" ]
25+
integer_range:
26+
gte: 0
27+
lte: 42
2528

2629
- do:
2730
indices.refresh:
28-
index: [ test ]
31+
index: [ test ]
2932

30-
---
31-
"Test basic field retrieval":
3233
- do:
3334
search:
3435
index: test
@@ -43,3 +44,35 @@ setup:
4344

4445
- match: { hits.hits.0.fields.integer_range.0.gte: 0 }
4546
- match: { hits.hits.0.fields.integer_range.0.lte: 42 }
47+
48+
---
49+
"Test disable source":
50+
- do:
51+
indices.create:
52+
index: test
53+
body:
54+
settings:
55+
number_of_shards: 1
56+
mappings:
57+
_source:
58+
enabled: false
59+
properties:
60+
keyword:
61+
type: keyword
62+
63+
- do:
64+
index:
65+
index: test
66+
id: 1
67+
body:
68+
keyword: [ "value" ]
69+
70+
- do:
71+
catch: bad_request
72+
search:
73+
index: test
74+
body:
75+
fields: [keyword]
76+
- match: { error.root_cause.0.type: "illegal_argument_exception" }
77+
- match: { error.root_cause.0.reason: "Unable to retrieve the requested [fields] since _source is disabled
78+
in the mappings for index [test]" }

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

+9-58
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,13 @@
2222
import org.apache.lucene.index.LeafReaderContext;
2323
import org.apache.lucene.index.ReaderUtil;
2424
import org.elasticsearch.common.document.DocumentField;
25-
import org.elasticsearch.common.xcontent.support.XContentMapValues;
2625
import org.elasticsearch.index.mapper.DocumentMapper;
2726
import org.elasticsearch.search.SearchHit;
2827
import org.elasticsearch.search.fetch.FetchSubPhase;
2928
import org.elasticsearch.search.internal.SearchContext;
3029
import org.elasticsearch.search.lookup.SourceLookup;
3130

32-
import java.util.Collection;
33-
import java.util.HashMap;
34-
import java.util.HashSet;
35-
import java.util.List;
3631
import java.util.Map;
37-
import java.util.Set;
38-
import java.util.function.Function;
3932

4033
/**
4134
* A fetch sub-phase for high-level field retrieval. Given a list of fields, it
@@ -45,14 +38,6 @@ public final class FetchFieldsPhase implements FetchSubPhase {
4538

4639
@Override
4740
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) {
5641
FetchFieldsContext fetchFieldsContext = context.fetchFieldsContext();
5742
if (fetchFieldsContext == null || fetchFieldsContext.fields().isEmpty()) {
5843
return;
@@ -64,52 +49,18 @@ void hitsExecute(SearchContext context,
6449
"disabled in the mappings for index [" + context.indexShard().shardId().getIndexName() + "]");
6550
}
6651

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-
}
52+
SourceLookup sourceLookup = context.lookup().source();
53+
FieldValueRetriever fieldValueRetriever = FieldValueRetriever.create(
54+
context.mapperService(),
55+
fetchFieldsContext.fields());
7556

7657
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.setField(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-
}
58+
int readerIndex = ReaderUtil.subIndex(hit.docId(), context.searcher().getIndexReader().leaves());
59+
LeafReaderContext readerContext = context.searcher().getIndexReader().leaves().get(readerIndex);
60+
sourceLookup.setSegmentAndDocument(readerContext, hit.docId());
10061

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-
}
62+
Map<String, DocumentField> fieldValues = fieldValueRetriever.retrieve(sourceLookup);
63+
hit.fields(fieldValues);
11264
}
113-
return result;
11465
}
11566
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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.elasticsearch.common.document.DocumentField;
23+
import org.elasticsearch.common.xcontent.support.XContentMapValues;
24+
import org.elasticsearch.index.mapper.DocumentMapper;
25+
import org.elasticsearch.index.mapper.MapperService;
26+
import org.elasticsearch.search.lookup.SourceLookup;
27+
28+
import java.util.Collection;
29+
import java.util.HashMap;
30+
import java.util.HashSet;
31+
import java.util.List;
32+
import java.util.Map;
33+
import java.util.Set;
34+
35+
public class FieldValueRetriever {
36+
private final Set<String> fields;
37+
38+
public static FieldValueRetriever create(MapperService mapperService,
39+
Collection<String> fieldPatterns) {
40+
Set<String> fields = new HashSet<>();
41+
DocumentMapper documentMapper = mapperService.documentMapper();
42+
43+
for (String fieldPattern : fieldPatterns) {
44+
if (documentMapper.objectMappers().containsKey(fieldPattern)) {
45+
continue;
46+
}
47+
Collection<String> concreteFields = mapperService.simpleMatchToFullName(fieldPattern);
48+
fields.addAll(concreteFields);
49+
}
50+
return new FieldValueRetriever(fields);
51+
}
52+
53+
private FieldValueRetriever(Set<String> fields) {
54+
this.fields = fields;
55+
}
56+
57+
@SuppressWarnings("unchecked")
58+
public Map<String, DocumentField> retrieve(SourceLookup sourceLookup) {
59+
Map<String, DocumentField> result = new HashMap<>();
60+
Map<String, Object> sourceValues = extractValues(sourceLookup, this.fields);
61+
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);
68+
result.put(field, new DocumentField(field, values));
69+
}
70+
return result;
71+
}
72+
73+
/**
74+
* For each of the provided paths, return its value in the source. Note that in contrast with
75+
* {@link SourceLookup#extractRawValues}, array and object values can be returned.
76+
*/
77+
private static Map<String, Object> extractValues(SourceLookup sourceLookup, Collection<String> paths) {
78+
Map<String, Object> result = new HashMap<>(paths.size());
79+
for (String path : paths) {
80+
Object value = XContentMapValues.extractValue(path, sourceLookup);
81+
if (value != null) {
82+
result.put(path, value);
83+
}
84+
}
85+
return result;
86+
}
87+
}

0 commit comments

Comments
 (0)