Skip to content

Commit e00db23

Browse files
authored
Add a soft limit for the number of requested doc-value fields (#26574)
Requesting to many docvalue_fields in a search request can potentially be costly because it might incur a per-field per-document seek. This change introduces a soft limit on the number of fields that can be retrieved. The setting can be changed per index using the `index.max_docvalue_fields_search` setting. Relates to #26390
1 parent a34db4e commit e00db23

File tree

7 files changed

+102
-2
lines changed

7 files changed

+102
-2
lines changed

core/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ public final class IndexScopedSettings extends AbstractScopedSettings {
111111
IndexSettings.INDEX_REFRESH_INTERVAL_SETTING,
112112
IndexSettings.MAX_RESULT_WINDOW_SETTING,
113113
IndexSettings.MAX_INNER_RESULT_WINDOW_SETTING,
114+
IndexSettings.MAX_DOCVALUE_FIELDS_SEARCH_SETTING,
114115
IndexSettings.MAX_RESCORE_WINDOW_SETTING,
115116
IndexSettings.MAX_ADJACENCY_MATRIX_FILTERS_SETTING,
116117
IndexSettings.INDEX_TRANSLOG_SYNC_INTERVAL_SETTING,

core/src/main/java/org/elasticsearch/index/IndexSettings.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ public final class IndexSettings {
9898
*/
9999
public static final Setting<Integer> MAX_INNER_RESULT_WINDOW_SETTING =
100100
Setting.intSetting("index.max_inner_result_window", 100, 1, Property.Dynamic, Property.IndexScope);
101+
/**
102+
* Index setting describing the maximum value of allowed `docvalue_fields`that can be retrieved
103+
* per search request. The default maximum of 100 is defensive for the reason that retrieving
104+
* doc values might incur a per-field per-document seek.
105+
*/
106+
public static final Setting<Integer> MAX_DOCVALUE_FIELDS_SEARCH_SETTING =
107+
Setting.intSetting("index.max_docvalue_fields_search", 100, 0, Property.Dynamic, Property.IndexScope);
101108
/**
102109
* Index setting describing the maximum size of the rescore window. Defaults to {@link #MAX_RESULT_WINDOW_SETTING}
103110
* because they both do the same thing: control the size of the heap of hits.
@@ -221,6 +228,7 @@ public final class IndexSettings {
221228
private volatile int maxInnerResultWindow;
222229
private volatile int maxAdjacencyMatrixFilters;
223230
private volatile int maxRescoreWindow;
231+
private volatile int maxDocvalueFields;
224232
private volatile boolean TTLPurgeDisabled;
225233
/**
226234
* The maximum number of refresh listeners allows on this shard.
@@ -322,6 +330,7 @@ public IndexSettings(final IndexMetaData indexMetaData, final Settings nodeSetti
322330
maxInnerResultWindow = scopedSettings.get(MAX_INNER_RESULT_WINDOW_SETTING);
323331
maxAdjacencyMatrixFilters = scopedSettings.get(MAX_ADJACENCY_MATRIX_FILTERS_SETTING);
324332
maxRescoreWindow = scopedSettings.get(MAX_RESCORE_WINDOW_SETTING);
333+
maxDocvalueFields = scopedSettings.get(MAX_DOCVALUE_FIELDS_SEARCH_SETTING);
325334
TTLPurgeDisabled = scopedSettings.get(INDEX_TTL_DISABLE_PURGE_SETTING);
326335
maxRefreshListeners = scopedSettings.get(MAX_REFRESH_LISTENERS_PER_SHARD);
327336
maxSlicesPerScroll = scopedSettings.get(MAX_SLICES_PER_SCROLL);
@@ -351,6 +360,7 @@ public IndexSettings(final IndexMetaData indexMetaData, final Settings nodeSetti
351360
scopedSettings.addSettingsUpdateConsumer(MAX_INNER_RESULT_WINDOW_SETTING, this::setMaxInnerResultWindow);
352361
scopedSettings.addSettingsUpdateConsumer(MAX_ADJACENCY_MATRIX_FILTERS_SETTING, this::setMaxAdjacencyMatrixFilters);
353362
scopedSettings.addSettingsUpdateConsumer(MAX_RESCORE_WINDOW_SETTING, this::setMaxRescoreWindow);
363+
scopedSettings.addSettingsUpdateConsumer(MAX_DOCVALUE_FIELDS_SEARCH_SETTING, this::setMaxDocvalueFields);
354364
scopedSettings.addSettingsUpdateConsumer(INDEX_WARMER_ENABLED_SETTING, this::setEnableWarmer);
355365
scopedSettings.addSettingsUpdateConsumer(INDEX_GC_DELETES_SETTING, this::setGCDeletes);
356366
scopedSettings.addSettingsUpdateConsumer(INDEX_TRANSLOG_FLUSH_THRESHOLD_SIZE_SETTING, this::setTranslogFlushThresholdSize);
@@ -607,6 +617,17 @@ private void setMaxRescoreWindow(int maxRescoreWindow) {
607617
this.maxRescoreWindow = maxRescoreWindow;
608618
}
609619

620+
/**
621+
* Returns the maximum number of allowed docvalue_fields to retrieve in a search request
622+
*/
623+
public int getMaxDocvalueFields() {
624+
return this.maxDocvalueFields;
625+
}
626+
627+
private void setMaxDocvalueFields(int maxDocvalueFields) {
628+
this.maxDocvalueFields = maxDocvalueFields;
629+
}
630+
610631
/**
611632
* Returns the GC deletes cycle in milliseconds.
612633
*/

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
import org.elasticsearch.index.Index;
4444
import org.elasticsearch.index.IndexService;
4545
import org.elasticsearch.index.IndexSettings;
46-
import org.elasticsearch.index.MergeSchedulerConfig;
4746
import org.elasticsearch.index.engine.Engine;
4847
import org.elasticsearch.index.query.InnerHitContextBuilder;
4948
import org.elasticsearch.index.query.MatchAllQueryBuilder;
@@ -771,6 +770,13 @@ private void parseSource(DefaultSearchContext context, SearchSourceBuilder sourc
771770
context.fetchSourceContext(source.fetchSource());
772771
}
773772
if (source.docValueFields() != null) {
773+
int maxAllowedDocvalueFields = context.mapperService().getIndexSettings().getMaxDocvalueFields();
774+
if (source.docValueFields().size() > maxAllowedDocvalueFields) {
775+
throw new IllegalArgumentException(
776+
"Trying to retrieve too many docvalue_fields. Must be less than or equal to: [" + maxAllowedDocvalueFields
777+
+ "] but was [" + source.docValueFields().size() + "]. This limit can be set by changing the ["
778+
+ IndexSettings.MAX_DOCVALUE_FIELDS_SEARCH_SETTING.getKey() + "] index level setting.");
779+
}
774780
context.docValueFieldsContext(new DocValueFieldsContext(source.docValueFields()));
775781
}
776782
if (source.highlighter() != null) {

core/src/test/java/org/elasticsearch/index/IndexSettingsTests.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,22 @@ public void testMaxInnerResultWindow() {
310310
assertEquals(IndexSettings.MAX_INNER_RESULT_WINDOW_SETTING.get(Settings.EMPTY).intValue(), settings.getMaxInnerResultWindow());
311311
}
312312

313+
public void testMaxDocvalueFields() {
314+
IndexMetaData metaData = newIndexMeta("index", Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)
315+
.put(IndexSettings.MAX_DOCVALUE_FIELDS_SEARCH_SETTING.getKey(), 200).build());
316+
IndexSettings settings = new IndexSettings(metaData, Settings.EMPTY);
317+
assertEquals(200, settings.getMaxDocvalueFields());
318+
settings.updateIndexMetaData(
319+
newIndexMeta("index", Settings.builder().put(IndexSettings.MAX_DOCVALUE_FIELDS_SEARCH_SETTING.getKey(), 50).build()));
320+
assertEquals(50, settings.getMaxDocvalueFields());
321+
settings.updateIndexMetaData(newIndexMeta("index", Settings.EMPTY));
322+
assertEquals(IndexSettings.MAX_DOCVALUE_FIELDS_SEARCH_SETTING.get(Settings.EMPTY).intValue(), settings.getMaxDocvalueFields());
323+
324+
metaData = newIndexMeta("index", Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT).build());
325+
settings = new IndexSettings(metaData, Settings.EMPTY);
326+
assertEquals(IndexSettings.MAX_DOCVALUE_FIELDS_SEARCH_SETTING.get(Settings.EMPTY).intValue(), settings.getMaxDocvalueFields());
327+
}
328+
313329
public void testMaxAdjacencyMatrixFiltersSetting() {
314330
IndexMetaData metaData = newIndexMeta("index", Settings.builder()
315331
.put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)

core/src/test/java/org/elasticsearch/search/SearchServiceTests.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import org.elasticsearch.action.ActionListener;
2626
import org.elasticsearch.action.index.IndexResponse;
2727
import org.elasticsearch.action.search.SearchPhaseExecutionException;
28-
import org.elasticsearch.action.search.SearchRequest;
2928
import org.elasticsearch.action.search.SearchResponse;
3029
import org.elasticsearch.action.search.SearchTask;
3130
import org.elasticsearch.action.search.SearchType;
@@ -262,6 +261,35 @@ public void testTimeout() throws IOException {
262261

263262
}
264263

264+
/**
265+
* test that getting more than the allowed number of docvalue_fields throws an exception
266+
*/
267+
public void testMaxDocvalueFieldsSearch() throws IOException {
268+
createIndex("index");
269+
final SearchService service = getInstanceFromNode(SearchService.class);
270+
final IndicesService indicesService = getInstanceFromNode(IndicesService.class);
271+
final IndexService indexService = indicesService.indexServiceSafe(resolveIndex("index"));
272+
final IndexShard indexShard = indexService.getShard(0);
273+
274+
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
275+
// adding the maximum allowed number of docvalue_fields to retrieve
276+
for (int i = 0; i < indexService.getIndexSettings().getMaxDocvalueFields(); i++) {
277+
searchSourceBuilder.docValueField("field" + i);
278+
}
279+
try (SearchContext context = service.createContext(new ShardSearchLocalRequest(indexShard.shardId(), 1, SearchType.DEFAULT,
280+
searchSourceBuilder, new String[0], false, new AliasFilter(null, Strings.EMPTY_ARRAY), 1.0f), null)) {
281+
assertNotNull(context);
282+
searchSourceBuilder.docValueField("one_field_too_much");
283+
IllegalArgumentException ex = expectThrows(IllegalArgumentException.class,
284+
() -> service.createContext(new ShardSearchLocalRequest(indexShard.shardId(), 1, SearchType.DEFAULT,
285+
searchSourceBuilder, new String[0], false, new AliasFilter(null, Strings.EMPTY_ARRAY), 1.0f), null));
286+
assertEquals(
287+
"Trying to retrieve too many docvalue_fields. Must be less than or equal to: [100] but was [101]. "
288+
+ "This limit can be set by changing the [index.max_docvalue_fields_search] index level setting.",
289+
ex.getMessage());
290+
}
291+
}
292+
265293
public static class FailOnRewriteQueryPlugin extends Plugin implements SearchPlugin {
266294
@Override
267295
public List<QuerySpec<?>> getQueries() {

docs/reference/index-modules.asciidoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ specific index module:
133133
requests take heap memory and time proportional to
134134
`max(window_size, from + size)` and this limits that memory.
135135

136+
`index.max_docvalue_fields_search`::
137+
138+
The maximum number of `docvalue_fields` that are allowed in a query.
139+
Defaults to `100`. Doc-value fields are costly since they might incur
140+
a per-field per-document seek.
141+
136142
`index.blocks.read_only`::
137143

138144
Set to `true` to make the index and index metadata read only, `false` to

rest-api-spec/src/main/resources/rest-api-spec/test/search/30_limits.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,25 @@ setup:
5050
match_all: {}
5151
query_weight: 1
5252
rescore_query_weight: 2
53+
54+
---
55+
"Docvalues_fields size limit":
56+
- skip:
57+
version: " - 6.99.99"
58+
reason: soft limit for docvalue_fields only available as of 7.0.0
59+
60+
- do:
61+
indices.create:
62+
index: test_2
63+
body:
64+
settings:
65+
index.max_docvalue_fields_search: 2
66+
67+
- do:
68+
catch: /Trying to retrieve too many docvalue_fields\. Must be less than or equal to[:] \[2\] but was \[3\]\. This limit can be set by changing the \[index.max_docvalue_fields_search\] index level setting\./
69+
search:
70+
index: test_2
71+
body:
72+
query:
73+
match_all: {}
74+
docvalue_fields: ["one", "two", "three"]

0 commit comments

Comments
 (0)