Skip to content

Commit 54cfce4

Browse files
piergmkertal
andauthored
Flag in _field_caps to return only fields with values in index (#103651)
We are adding a query parameter to the field_caps api in order to filter out fields with no values. The parameter is called `include_empty_fields` and defaults to true, and if set to false it will filter out from the field_caps response all the fields that has no value in the index. We keep track of FieldInfos during refresh in order to know which field has value in an index. We added also a system property `es.field_caps_empty_fields_filter` in order to disable this feature if needed. --------- Co-authored-by: Matthias Wilhelm <[email protected]>
1 parent 5c1e3e2 commit 54cfce4

File tree

35 files changed

+1130
-64
lines changed

35 files changed

+1130
-64
lines changed

docs/changelog/103651.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 103651
2+
summary: Flag in `_field_caps` to return only fields with values in index
3+
area: Search
4+
type: enhancement
5+
issues: []

docs/reference/search/field-caps.asciidoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=index-ignore-unavailab
7777
(Optional, Boolean) If `true`, unmapped fields that are mapped in one index but not in another are included in the response. Fields that don't have any mapping are never included.
7878
Defaults to `false`.
7979

80+
`include_empty_fields`::
81+
(Optional, Boolean) If `false`, fields that never had a value in any shards are not included in the response. Fields that are not empty are always included. This flag does not consider deletions and updates. If a field was non-empty and all the documents containing that field were deleted or the field was removed by updates, it will still be returned even if the flag is `false`.
82+
Defaults to `true`.
83+
8084
`filters`::
8185
(Optional, string) Comma-separated list of filters to apply to the response.
8286
+

modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/RankFeatureFieldMapper.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
package org.elasticsearch.index.mapper.extras;
1010

1111
import org.apache.lucene.document.FeatureField;
12+
import org.apache.lucene.index.FieldInfo;
13+
import org.apache.lucene.index.FieldInfos;
1214
import org.apache.lucene.index.Term;
1315
import org.apache.lucene.search.Query;
1416
import org.apache.lucene.search.TermQuery;
@@ -38,6 +40,7 @@
3840
*/
3941
public class RankFeatureFieldMapper extends FieldMapper {
4042

43+
public static final String NAME = "_feature";
4144
public static final String CONTENT_TYPE = "rank_feature";
4245

4346
private static RankFeatureFieldType ft(FieldMapper in) {
@@ -128,7 +131,17 @@ public boolean positiveScoreImpact() {
128131

129132
@Override
130133
public Query existsQuery(SearchExecutionContext context) {
131-
return new TermQuery(new Term("_feature", name()));
134+
return new TermQuery(new Term(NAME, name()));
135+
}
136+
137+
@Override
138+
public boolean fieldHasValue(FieldInfos fieldInfos) {
139+
for (FieldInfo fieldInfo : fieldInfos) {
140+
if (fieldInfo.getName().equals(NAME)) {
141+
return true;
142+
}
143+
}
144+
return false;
132145
}
133146

134147
@Override
@@ -208,7 +221,7 @@ protected void parseCreateField(DocumentParserContext context) throws IOExceptio
208221
value = 1 / value;
209222
}
210223

211-
context.doc().addWithKey(name(), new FeatureField("_feature", name(), value));
224+
context.doc().addWithKey(name(), new FeatureField(NAME, name(), value));
212225
}
213226

214227
private static Float objectToFloat(Object value) {

modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/RankFeatureMetaFieldMapper.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
package org.elasticsearch.index.mapper.extras;
1010

11+
import org.apache.lucene.index.FieldInfo;
12+
import org.apache.lucene.index.FieldInfos;
1113
import org.apache.lucene.search.Query;
1214
import org.elasticsearch.index.mapper.MappedFieldType;
1315
import org.elasticsearch.index.mapper.MetadataFieldMapper;
@@ -35,7 +37,8 @@ public static final class RankFeatureMetaFieldType extends MappedFieldType {
3537

3638
public static final RankFeatureMetaFieldType INSTANCE = new RankFeatureMetaFieldType();
3739

38-
private RankFeatureMetaFieldType() {
40+
// made visible for tests
41+
RankFeatureMetaFieldType() {
3942
super(NAME, false, false, false, TextSearchInfo.NONE, Collections.emptyMap());
4043
}
4144

@@ -54,6 +57,16 @@ public Query existsQuery(SearchExecutionContext context) {
5457
throw new UnsupportedOperationException("Cannot run exists query on [_feature]");
5558
}
5659

60+
@Override
61+
public boolean fieldHasValue(FieldInfos fieldInfos) {
62+
for (FieldInfo fieldInfo : fieldInfos) {
63+
if (fieldInfo.getName().equals(NAME)) {
64+
return true;
65+
}
66+
}
67+
return false;
68+
}
69+
5770
@Override
5871
public Query termQuery(Object value, SearchExecutionContext context) {
5972
throw new UnsupportedOperationException("The [_feature] field may not be queried directly");
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.index.mapper.extras;
10+
11+
import org.elasticsearch.action.fieldcaps.FieldCapabilities;
12+
import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse;
13+
import org.elasticsearch.action.support.ActiveShardCount;
14+
import org.elasticsearch.plugins.Plugin;
15+
import org.elasticsearch.test.ESIntegTestCase;
16+
import org.hamcrest.Matchers;
17+
import org.junit.Before;
18+
19+
import java.util.ArrayList;
20+
import java.util.Collection;
21+
import java.util.Collections;
22+
import java.util.Map;
23+
24+
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
25+
26+
@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST)
27+
public class FieldCapsRankFeatureTests extends ESIntegTestCase {
28+
private final String INDEX = "index-1";
29+
30+
@Override
31+
protected Collection<Class<? extends Plugin>> nodePlugins() {
32+
var plugins = new ArrayList<>(super.nodePlugins());
33+
plugins.add(MapperExtrasPlugin.class);
34+
return plugins;
35+
}
36+
37+
@Before
38+
public void setUpIndices() {
39+
assertAcked(
40+
prepareCreate(INDEX).setWaitForActiveShards(ActiveShardCount.ALL)
41+
.setSettings(indexSettings())
42+
.setMapping("fooRank", "type=rank_feature", "barRank", "type=rank_feature")
43+
);
44+
}
45+
46+
public void testRankFeatureInIndex() {
47+
FieldCapabilitiesResponse response = client().prepareFieldCaps(INDEX).setFields("*").setincludeEmptyFields(false).get();
48+
assertFalse(response.get().containsKey("fooRank"));
49+
assertFalse(response.get().containsKey("barRank"));
50+
prepareIndex(INDEX).setSource("fooRank", 8).setSource("barRank", 8).get();
51+
refresh(INDEX);
52+
53+
response = client().prepareFieldCaps(INDEX).setFields("*").setincludeEmptyFields(false).get();
54+
assertEquals(1, response.getIndices().length);
55+
assertEquals(response.getIndices()[0], INDEX);
56+
assertThat(response.get(), Matchers.hasKey("fooRank"));
57+
// Check the capabilities for the 'fooRank' field.
58+
Map<String, FieldCapabilities> fooRankField = response.getField("fooRank");
59+
assertEquals(1, fooRankField.size());
60+
assertThat(fooRankField, Matchers.hasKey("rank_feature"));
61+
assertEquals(
62+
new FieldCapabilities("fooRank", "rank_feature", false, true, false, null, null, null, Collections.emptyMap()),
63+
fooRankField.get("rank_feature")
64+
);
65+
}
66+
67+
public void testRankFeatureInIndexAfterRestart() throws Exception {
68+
prepareIndex(INDEX).setSource("fooRank", 8).get();
69+
internalCluster().fullRestart();
70+
ensureGreen(INDEX);
71+
72+
FieldCapabilitiesResponse response = client().prepareFieldCaps(INDEX).setFields("*").setincludeEmptyFields(false).get();
73+
74+
assertEquals(1, response.getIndices().length);
75+
assertEquals(response.getIndices()[0], INDEX);
76+
assertThat(response.get(), Matchers.hasKey("fooRank"));
77+
// Check the capabilities for the 'fooRank' field.
78+
Map<String, FieldCapabilities> fooRankField = response.getField("fooRank");
79+
assertEquals(1, fooRankField.size());
80+
assertThat(fooRankField, Matchers.hasKey("rank_feature"));
81+
assertEquals(
82+
new FieldCapabilities("fooRank", "rank_feature", false, true, false, null, null, null, Collections.emptyMap()),
83+
fooRankField.get("rank_feature")
84+
);
85+
}
86+
87+
public void testAllRankFeatureReturnedIfOneIsPresent() {
88+
prepareIndex(INDEX).setSource("fooRank", 8).get();
89+
refresh(INDEX);
90+
91+
FieldCapabilitiesResponse response = client().prepareFieldCaps(INDEX).setFields("*").setincludeEmptyFields(false).get();
92+
93+
assertEquals(1, response.getIndices().length);
94+
assertEquals(response.getIndices()[0], INDEX);
95+
assertThat(response.get(), Matchers.hasKey("fooRank"));
96+
// Check the capabilities for the 'fooRank' field.
97+
Map<String, FieldCapabilities> fooRankField = response.getField("fooRank");
98+
assertEquals(1, fooRankField.size());
99+
assertThat(fooRankField, Matchers.hasKey("rank_feature"));
100+
assertEquals(
101+
new FieldCapabilities("fooRank", "rank_feature", false, true, false, null, null, null, Collections.emptyMap()),
102+
fooRankField.get("rank_feature")
103+
);
104+
assertThat(response.get(), Matchers.hasKey("barRank"));
105+
// Check the capabilities for the 'barRank' field.
106+
Map<String, FieldCapabilities> barRankField = response.getField("barRank");
107+
assertEquals(1, barRankField.size());
108+
assertThat(barRankField, Matchers.hasKey("rank_feature"));
109+
assertEquals(
110+
new FieldCapabilities("barRank", "rank_feature", false, true, false, null, null, null, Collections.emptyMap()),
111+
barRankField.get("rank_feature")
112+
);
113+
}
114+
}

modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/extras/RankFeatureFieldTypeTests.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
package org.elasticsearch.index.mapper.extras;
1010

11+
import org.apache.lucene.index.FieldInfo;
12+
import org.apache.lucene.index.FieldInfos;
1113
import org.elasticsearch.index.mapper.FieldTypeTestCase;
1214
import org.elasticsearch.index.mapper.MappedFieldType;
1315
import org.elasticsearch.index.mapper.MapperBuilderContext;
@@ -19,7 +21,7 @@
1921
public class RankFeatureFieldTypeTests extends FieldTypeTestCase {
2022

2123
public void testIsNotAggregatable() {
22-
MappedFieldType fieldType = new RankFeatureFieldMapper.RankFeatureFieldType("field", Collections.emptyMap(), true, null);
24+
MappedFieldType fieldType = getMappedFieldType();
2325
assertFalse(fieldType.isAggregatable());
2426
}
2527

@@ -32,4 +34,28 @@ public void testFetchSourceValue() throws IOException {
3234
assertEquals(List.of(42.9f), fetchSourceValue(mapper, "42.9"));
3335
assertEquals(List.of(2.0f), fetchSourceValue(mapper, null));
3436
}
37+
38+
@Override
39+
public void testFieldHasValue() {
40+
MappedFieldType fieldType = getMappedFieldType();
41+
FieldInfos fieldInfos = new FieldInfos(new FieldInfo[] { getFieldInfoWithName("_feature") });
42+
assertTrue(fieldType.fieldHasValue(fieldInfos));
43+
}
44+
45+
@Override
46+
public void testFieldHasValueWithEmptyFieldInfos() {
47+
MappedFieldType fieldType = getMappedFieldType();
48+
assertFalse(fieldType.fieldHasValue(FieldInfos.EMPTY));
49+
}
50+
51+
public void testFieldEmptyIfNameIsPresentInFieldInfos() {
52+
MappedFieldType fieldType = getMappedFieldType();
53+
FieldInfos fieldInfos = new FieldInfos(new FieldInfo[] { getFieldInfoWithName("field") });
54+
assertFalse(fieldType.fieldHasValue(fieldInfos));
55+
}
56+
57+
@Override
58+
public MappedFieldType getMappedFieldType() {
59+
return new RankFeatureFieldMapper.RankFeatureFieldType("field", Collections.emptyMap(), true, null);
60+
}
3561
}

modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/extras/RankFeatureMetaFieldMapperTests.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@
88

99
package org.elasticsearch.index.mapper.extras;
1010

11+
import org.apache.lucene.index.FieldInfo;
12+
import org.apache.lucene.index.FieldInfos;
1113
import org.elasticsearch.common.Strings;
1214
import org.elasticsearch.common.bytes.BytesReference;
1315
import org.elasticsearch.common.compress.CompressedXContent;
1416
import org.elasticsearch.index.mapper.DocumentMapper;
1517
import org.elasticsearch.index.mapper.DocumentParsingException;
18+
import org.elasticsearch.index.mapper.MappedFieldType;
1619
import org.elasticsearch.index.mapper.MapperService;
1720
import org.elasticsearch.index.mapper.MapperServiceTestCase;
1821
import org.elasticsearch.index.mapper.Mapping;
@@ -73,4 +76,19 @@ public void testDocumentParsingFailsOnMetaField() throws Exception {
7376
CoreMatchers.containsString("Field [" + rfMetaField + "] is a metadata field and cannot be added inside a document.")
7477
);
7578
}
79+
80+
@Override
81+
public void testFieldHasValue() {
82+
assertTrue(getMappedFieldType().fieldHasValue(new FieldInfos(new FieldInfo[] { getFieldInfoWithName("_feature") })));
83+
}
84+
85+
@Override
86+
public void testFieldHasValueWithEmptyFieldInfos() {
87+
assertFalse(getMappedFieldType().fieldHasValue(FieldInfos.EMPTY));
88+
}
89+
90+
@Override
91+
public MappedFieldType getMappedFieldType() {
92+
return new RankFeatureMetaFieldMapper.RankFeatureMetaFieldType();
93+
}
7694
}

qa/multi-cluster-search/src/test/resources/rest-api-spec/test/multi_cluster/30_field_caps.yml

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@
6565
field_caps:
6666
index: 'my_remote_cluster:some_index_that_doesnt_exist'
6767
fields: [number]
68-
6968
- match: { error.type: "index_not_found_exception" }
7069
- match: { error.reason: "no such index [some_index_that_doesnt_exist]" }
7170

@@ -156,3 +155,38 @@
156155
- length: {fields.number: 1}
157156
- match: {fields.number.long.searchable: true}
158157
- match: {fields.number.long.aggregatable: true}
158+
159+
---
160+
"Field caps with with include_empty_fields false":
161+
- skip:
162+
version: " - 8.12.99"
163+
reason: include_empty_fields has been added in 8.13.0
164+
- do:
165+
indices.create:
166+
index: field_caps_index_5
167+
body:
168+
mappings:
169+
properties:
170+
number:
171+
type: double
172+
empty-baz:
173+
type: text
174+
175+
- do:
176+
index:
177+
index: field_caps_index_5
178+
body: { number: "42", unmapped-bar: "bar" }
179+
180+
- do:
181+
indices.refresh:
182+
index: [field_caps_index_5]
183+
- do:
184+
field_caps:
185+
include_empty_fields: false
186+
index: 'field_caps_index_5,my_remote_cluster:field_*'
187+
fields: '*'
188+
189+
- is_true: fields.number
190+
- is_false: fields.empty-baz
191+
- is_true: fields.unmapped-bar
192+
- is_true: fields._index

qa/multi-cluster-search/src/test/resources/rest-api-spec/test/remote_cluster/10_basic.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@
8282
nested2:
8383
type: keyword
8484
doc_values: false
85+
- do:
86+
index:
87+
index: field_caps_index_1
88+
body: { number: "42", unmapped-bar: "bar" }
8589
- do:
8690
indices.create:
8791
index: test_index

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@
7171
"types": {
7272
"type": "list",
7373
"description":"Only return results for fields that have one of the types in the list"
74+
},
75+
"include_empty_fields": {
76+
"type":"boolean",
77+
"default": true,
78+
"description":"Include empty fields in result"
7479
}
7580
},
7681
"body":{

0 commit comments

Comments
 (0)