Skip to content

Commit 7c6fb64

Browse files
author
Christoph Büscher
committed
Support fetching flattened subfields (elastic#70916)
Currently the `fields` API fetches the root flattened field and returns it in a structured way in the response. In addition this change makes it possible to directly query subfields. However, requesting flattened subfields via wildcard patterns is not possible. Closes elastic#70605
1 parent 976cf4b commit 7c6fb64

File tree

9 files changed

+237
-33
lines changed

9 files changed

+237
-33
lines changed

docs/reference/mapping/types/flattened.asciidoc

+70
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,76 @@ lexicographically.
121121
Flattened object fields currently cannot be stored. It is not possible to
122122
specify the <<mapping-store, `store`>> parameter in the mapping.
123123

124+
[[search-fields-flattened]]
125+
==== Retrieving flattened fields
126+
127+
Field values and concrete subfields can be retrieved using the
128+
<<search-fields-param,fields parameter>>. content. Since the `flattened` field maps an
129+
entire object with potentially many subfields as a single field, the response contains
130+
the unaltered structure from `_source`.
131+
132+
Single subfields, however, can be fetched by specifying them explicitly in the request.
133+
This only works for concrete paths, but not using wildcards:
134+
135+
[source,console]
136+
--------------------------------------------------
137+
PUT my-index-000001
138+
{
139+
"mappings": {
140+
"properties": {
141+
"flattened_field": {
142+
"type": "flattened"
143+
}
144+
}
145+
}
146+
}
147+
148+
PUT my-index-000001/_doc/1?refresh=true
149+
{
150+
"flattened_field" : {
151+
"subfield" : "value"
152+
}
153+
}
154+
155+
POST my-index-000001/_search
156+
{
157+
"fields": ["flattened_field.subfield"],
158+
"_source": false
159+
}
160+
--------------------------------------------------
161+
162+
[source,console-result]
163+
----
164+
{
165+
"took": 2,
166+
"timed_out": false,
167+
"_shards": {
168+
"total": 1,
169+
"successful": 1,
170+
"skipped": 0,
171+
"failed": 0
172+
},
173+
"hits": {
174+
"total": {
175+
"value": 1,
176+
"relation": "eq"
177+
},
178+
"max_score": 1.0,
179+
"hits": [{
180+
"_index": "my-index-000001",
181+
"_id": "1",
182+
"_score": 1.0,
183+
"fields": {
184+
"flattened_field.subfield" : [ "value" ]
185+
}
186+
}]
187+
}
188+
}
189+
----
190+
// TESTRESPONSE[s/"took": 2/"took": $body.took/]
191+
// TESTRESPONSE[s/"max_score" : 1.0/"max_score" : $body.hits.max_score/]
192+
// TESTRESPONSE[s/"_score" : 1.0/"_score" : $body.hits.hits.0._score/]
193+
124194
[[flattened-params]]
125195
==== Parameters for flattened object fields
126196

rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/340_flattened.yml

+57
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,60 @@ setup:
180180
- length: { hits.hits: 1 }
181181
- length: { hits.hits.0.fields: 1 }
182182
- match: { hits.hits.0.fields.flattened: [ { "some_field": "some_value" } ] }
183+
184+
---
185+
"Test fetching flattened subfields via fields option":
186+
- do:
187+
indices.create:
188+
index: test
189+
body:
190+
mappings:
191+
properties:
192+
flattened:
193+
type: flattened
194+
195+
- do:
196+
index:
197+
index: test
198+
id: 1
199+
body:
200+
flattened:
201+
some_field: some_value
202+
some_fields:
203+
- value1
204+
- value2
205+
refresh: true
206+
207+
- do:
208+
search:
209+
index: test
210+
body:
211+
fields: [ { "field" : "flattened.some_field" } ]
212+
213+
- length: { hits.hits.0.fields: 1 }
214+
- match: { hits.hits.0.fields.flattened\.some_field: [ "some_value" ] }
215+
216+
- do:
217+
search:
218+
index: test
219+
body:
220+
fields: [ { "field" : "flattened.some_fields" } ]
221+
222+
- length: { hits.hits.0.fields: 1 }
223+
- match: { hits.hits.0.fields.flattened\.some_fields: [ "value1", "value2" ] }
224+
225+
- do:
226+
search:
227+
index: test
228+
body:
229+
fields: [ { "field" : "flattened.some*" } ]
230+
231+
- is_false: hits.hits.0.fields
232+
233+
- do:
234+
search:
235+
index: test
236+
body:
237+
fields: [ { "field" : "flattened.non_existing_field" } ]
238+
239+
- is_false: hits.hits.0.fields

server/src/main/java/org/elasticsearch/index/mapper/FieldTypeLookup.java

+4
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ Set<String> sourcePaths(String field) {
138138
if (fullNameToFieldType.isEmpty()) {
139139
return org.elasticsearch.common.collect.Set.of();
140140
}
141+
if (dynamicKeyLookup.get(field) != null) {
142+
return Collections.singleton(field);
143+
}
144+
141145
String resolvedField = field;
142146
int lastDotIndex = field.lastIndexOf('.');
143147
if (lastDotIndex > 0) {

server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldMapper.java

+13-13
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555

5656
import java.io.IOException;
5757
import java.util.Arrays;
58-
import java.util.Collections;
5958
import java.util.List;
6059
import java.util.Map;
6160
import java.util.function.Supplier;
@@ -167,17 +166,19 @@ public FlattenedFieldMapper build(ContentPath contentPath) {
167166
*/
168167
public static final class KeyedFlattenedFieldType extends StringFieldType {
169168
private final String key;
169+
private final String rootName;
170170

171-
public KeyedFlattenedFieldType(String name, boolean indexed, boolean hasDocValues, String key,
171+
KeyedFlattenedFieldType(String rootName, boolean indexed, boolean hasDocValues, String key,
172172
boolean splitQueriesOnWhitespace, Map<String, String> meta) {
173-
super(name, indexed, false, hasDocValues,
173+
super(rootName + KEYED_FIELD_SUFFIX, indexed, false, hasDocValues,
174174
splitQueriesOnWhitespace ? TextSearchInfo.WHITESPACE_MATCH_ONLY : TextSearchInfo.SIMPLE_MATCH_ONLY,
175175
meta);
176176
this.key = key;
177+
this.rootName = rootName;
177178
}
178179

179-
private KeyedFlattenedFieldType(String name, String key, RootFlattenedFieldType ref) {
180-
this(name, ref.isSearchable(), ref.hasDocValues(), key, ref.splitQueriesOnWhitespace, ref.meta());
180+
private KeyedFlattenedFieldType(String rootName, String key, RootFlattenedFieldType ref) {
181+
this(rootName, ref.isSearchable(), ref.hasDocValues(), key, ref.splitQueriesOnWhitespace, ref.meta());
181182
}
182183

183184
@Override
@@ -267,8 +268,11 @@ public IndexFieldData.Builder fielddataBuilder(String fullyQualifiedIndexName, S
267268

268269
@Override
269270
public ValueFetcher valueFetcher(SearchExecutionContext context, String format) {
270-
// This is an internal field but it can match a field pattern so we return an empty list.
271-
return lookup -> Collections.emptyList();
271+
if (format != null) {
272+
throw new IllegalArgumentException("Field [" + rootName + "." + key + "] of type [" + typeName() +
273+
"] doesn't support formats.");
274+
}
275+
return SourceValueFetcher.identity(rootName + "." + key, context, format);
272276
}
273277
}
274278

@@ -432,7 +436,7 @@ private FlattenedFieldMapper(String simpleName,
432436
Builder builder) {
433437
super(simpleName, mappedFieldType, Lucene.KEYWORD_ANALYZER, CopyTo.empty());
434438
this.builder = builder;
435-
this.fieldParser = new FlattenedFieldParser(mappedFieldType.name(), keyedFieldName(),
439+
this.fieldParser = new FlattenedFieldParser(mappedFieldType.name(), mappedFieldType.name() + KEYED_FIELD_SUFFIX,
436440
mappedFieldType, builder.depthLimit.get(), builder.ignoreAbove.get(), builder.nullValue.get());
437441
}
438442

@@ -456,11 +460,7 @@ public RootFlattenedFieldType fieldType() {
456460

457461
@Override
458462
public KeyedFlattenedFieldType keyedFieldType(String key) {
459-
return new KeyedFlattenedFieldType(keyedFieldName(), key, fieldType());
460-
}
461-
462-
public String keyedFieldName() {
463-
return mappedFieldType.name() + KEYED_FIELD_SUFFIX;
463+
return new KeyedFlattenedFieldType(name(), key, fieldType());
464464
}
465465

466466
@Override

server/src/test/java/org/elasticsearch/index/mapper/FieldTypeLookupTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ public void testFlattenedLookup() {
198198
String searchFieldName = fieldName + "." + objectKey;
199199

200200
MappedFieldType searchFieldType = lookup.get(searchFieldName);
201-
assertEquals(mapper.keyedFieldName(), searchFieldType.name());
201+
assertEquals(mapper.keyedFieldType(objectKey).name(), searchFieldType.name());
202202
assertThat(searchFieldType, Matchers.instanceOf(FlattenedFieldMapper.KeyedFlattenedFieldType.class));
203203

204204
FlattenedFieldMapper.KeyedFlattenedFieldType keyedFieldType = (FlattenedFieldMapper.KeyedFlattenedFieldType) searchFieldType;
@@ -219,7 +219,7 @@ public void testFlattenedLookupWithAlias() {
219219
String searchFieldName = aliasName + "." + objectKey;
220220

221221
MappedFieldType searchFieldType = lookup.get(searchFieldName);
222-
assertEquals(mapper.keyedFieldName(), searchFieldType.name());
222+
assertEquals(mapper.keyedFieldType(objectKey).name(), searchFieldType.name());
223223
assertThat(searchFieldType, Matchers.instanceOf(FlattenedFieldMapper.KeyedFlattenedFieldType.class));
224224

225225
FlattenedFieldMapper.KeyedFlattenedFieldType keyedFieldType = (FlattenedFieldMapper.KeyedFlattenedFieldType) searchFieldType;

server/src/test/java/org/elasticsearch/index/mapper/flattened/FlattenedIndexFieldDataTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,13 @@ public void testGlobalFieldDataCaching() throws IOException {
4646
indexService.mapperService());
4747

4848
FlattenedFieldMapper fieldMapper = new FlattenedFieldMapper.Builder("json").build(new ContentPath(1));
49+
KeyedFlattenedFieldType fieldType1 = fieldMapper.keyedFieldType("key");
4950

5051
AtomicInteger onCacheCalled = new AtomicInteger();
5152
ifdService.setListener(new IndexFieldDataCache.Listener() {
5253
@Override
5354
public void onCache(ShardId shardId, String fieldName, Accountable ramUsage) {
54-
assertEquals(fieldMapper.keyedFieldName(), fieldName);
55+
assertEquals(fieldType1.name(), fieldName);
5556
onCacheCalled.incrementAndGet();
5657
}
5758
});
@@ -71,7 +72,6 @@ public void onCache(ShardId shardId, String fieldName, Accountable ramUsage) {
7172
new ShardId("test", "_na_", 1));
7273

7374
// Load global field data for subfield 'key'.
74-
KeyedFlattenedFieldType fieldType1 = fieldMapper.keyedFieldType("key");
7575
IndexFieldData<?> ifd1 = ifdService.getForField(fieldType1, "test", () -> {
7676
throw new UnsupportedOperationException("search lookup not available");
7777
});

server/src/test/java/org/elasticsearch/index/mapper/flattened/KeyedFlattenedFieldTypeTests.java

+35-11
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,20 @@
2020
import org.elasticsearch.common.lucene.search.AutomatonQueries;
2121
import org.elasticsearch.common.unit.Fuzziness;
2222
import org.elasticsearch.index.mapper.FieldTypeTestCase;
23+
import org.elasticsearch.index.mapper.ValueFetcher;
2324
import org.elasticsearch.index.mapper.flattened.FlattenedFieldMapper.KeyedFlattenedFieldType;
25+
import org.elasticsearch.index.query.SearchExecutionContext;
26+
import org.elasticsearch.search.lookup.SourceLookup;
2427

2528
import java.io.IOException;
2629
import java.util.ArrayList;
2730
import java.util.Collections;
2831
import java.util.List;
2932
import java.util.Map;
3033

34+
import static org.mockito.Mockito.mock;
35+
import static org.mockito.Mockito.when;
36+
3137
public class KeyedFlattenedFieldTypeTests extends FieldTypeTestCase {
3238

3339
private static KeyedFlattenedFieldType createFieldType() {
@@ -50,23 +56,22 @@ public void testIndexedValueForSearch() {
5056
public void testTermQuery() {
5157
KeyedFlattenedFieldType ft = createFieldType();
5258

53-
Query expected = new TermQuery(new Term("field", "key\0value"));
59+
Query expected = new TermQuery(new Term(ft.name(), "key\0value"));
5460
assertEquals(expected, ft.termQuery("value", null));
5561

56-
expected = AutomatonQueries.caseInsensitiveTermQuery(new Term("field", "key\0value"));
62+
expected = AutomatonQueries.caseInsensitiveTermQuery(new Term(ft.name(), "key\0value"));
5763
assertEquals(expected, ft.termQueryCaseInsensitive("value", null));
5864

59-
KeyedFlattenedFieldType unsearchable = new KeyedFlattenedFieldType("field", false, true, "key",
60-
false, Collections.emptyMap());
65+
KeyedFlattenedFieldType unsearchable = new KeyedFlattenedFieldType("field", false, true, "key", false, Collections.emptyMap());
6166
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
6267
() -> unsearchable.termQuery("field", null));
63-
assertEquals("Cannot search on field [field] since it is not indexed.", e.getMessage());
68+
assertEquals("Cannot search on field [" + ft.name() + "] since it is not indexed.", e.getMessage());
6469
}
6570

6671
public void testTermsQuery() {
6772
KeyedFlattenedFieldType ft = createFieldType();
6873

69-
Query expected = new TermInSetQuery("field",
74+
Query expected = new TermInSetQuery(ft.name(),
7075
new BytesRef("key\0value1"),
7176
new BytesRef("key\0value2"));
7277

@@ -81,17 +86,17 @@ public void testTermsQuery() {
8186
public void testExistsQuery() {
8287
KeyedFlattenedFieldType ft = createFieldType();
8388

84-
Query expected = new PrefixQuery(new Term("field", "key\0"));
89+
Query expected = new PrefixQuery(new Term(ft.name(), "key\0"));
8590
assertEquals(expected, ft.existsQuery(null));
8691
}
8792

8893
public void testPrefixQuery() {
8994
KeyedFlattenedFieldType ft = createFieldType();
9095

91-
Query expected = new PrefixQuery(new Term("field", "key\0val"));
96+
Query expected = new PrefixQuery(new Term(ft.name(), "key\0val"));
9297
assertEquals(expected, ft.prefixQuery("val", MultiTermQuery.CONSTANT_SCORE_REWRITE, false, MOCK_CONTEXT));
9398

94-
expected = AutomatonQueries.caseInsensitivePrefixQuery(new Term("field", "key\0vAl"));
99+
expected = AutomatonQueries.caseInsensitivePrefixQuery(new Term(ft.name(), "key\0vAl"));
95100
assertEquals(expected, ft.prefixQuery("vAl", MultiTermQuery.CONSTANT_SCORE_REWRITE, true, MOCK_CONTEXT));
96101

97102
ElasticsearchException ee = expectThrows(ElasticsearchException.class,
@@ -111,12 +116,12 @@ public void testFuzzyQuery() {
111116
public void testRangeQuery() {
112117
KeyedFlattenedFieldType ft = createFieldType();
113118

114-
TermRangeQuery expected = new TermRangeQuery("field",
119+
TermRangeQuery expected = new TermRangeQuery(ft.name(),
115120
new BytesRef("key\0lower"),
116121
new BytesRef("key\0upper"), false, false);
117122
assertEquals(expected, ft.rangeQuery("lower", "upper", false, false, MOCK_CONTEXT));
118123

119-
expected = new TermRangeQuery("field",
124+
expected = new TermRangeQuery(ft.name(),
120125
new BytesRef("key\0lower"),
121126
new BytesRef("key\0upper"), true, true);
122127
assertEquals(expected, ft.rangeQuery("lower", "upper", true, true, MOCK_CONTEXT));
@@ -160,4 +165,23 @@ public void testFetchIsEmpty() throws IOException {
160165
assertEquals(Collections.emptyList(), fetchSourceValue(ft, sourceValue));
161166
assertEquals(Collections.emptyList(), fetchSourceValue(ft, null));
162167
}
168+
169+
public void testFetchSourceValue() throws IOException {
170+
KeyedFlattenedFieldType ft = createFieldType();
171+
Map<String, Object> sourceValue = Collections.singletonMap("key", "value");
172+
173+
SearchExecutionContext searchExecutionContext = mock(SearchExecutionContext.class);
174+
when(searchExecutionContext.sourcePath("field.key")).thenReturn(Collections.singleton("field.key"));
175+
176+
ValueFetcher fetcher = ft.valueFetcher(searchExecutionContext, null);
177+
SourceLookup lookup = new SourceLookup();
178+
lookup.setSource(Collections.singletonMap("field", sourceValue));
179+
180+
assertEquals(Collections.singletonList("value"), fetcher.fetchValues(lookup));
181+
lookup.setSource(Collections.singletonMap("field", null));
182+
assertEquals(Collections.emptyList(), fetcher.fetchValues(lookup));
183+
184+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> ft.valueFetcher(searchExecutionContext, "format"));
185+
assertEquals("Field [field.key] of type [flattened] doesn't support formats.", e.getMessage());
186+
}
163187
}

0 commit comments

Comments
 (0)