Skip to content

Commit 0e2dc29

Browse files
committed
Add soft limit on allowed number of script fields in request (#26598)
Requesting to many script_fields in a search request can be costly because of script execution. This change introduces a soft limit on the number of script fields that are allowed per request. The setting can be changed per index using the index.max_script_fields setting. Relates to #26390
1 parent 94d65ea commit 0e2dc29

File tree

8 files changed

+136
-1
lines changed

8 files changed

+136
-1
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_DOCVALUE_FIELDS_SEARCH_SETTING,
114+
IndexSettings.MAX_SCRIPT_FIELDS_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: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,15 @@ public final class IndexSettings {
102102
*/
103103
public static final Setting<Integer> MAX_RESULT_WINDOW_SETTING =
104104
Setting.intSetting("index.max_result_window", 10000, 1, Property.Dynamic, Property.IndexScope);
105+
106+
/**
107+
* Index setting describing the maximum value of allowed `script_fields`that can be retrieved
108+
* per search request. The default maximum of 32 is defensive for the reason that retrieving
109+
* script fields is a costly operation.
110+
*/
111+
public static final Setting<Integer> MAX_SCRIPT_FIELDS_SETTING =
112+
Setting.intSetting("index.max_script_fields", 32, 0, Property.Dynamic, Property.IndexScope);
113+
105114
/**
106115
* Index setting describing the maximum value of allowed `docvalue_fields`that can be retrieved
107116
* per search request. The default maximum of 100 is defensive for the reason that retrieving
@@ -232,6 +241,7 @@ public final class IndexSettings {
232241
private volatile int maxAdjacencyMatrixFilters;
233242
private volatile int maxRescoreWindow;
234243
private volatile int maxDocvalueFields;
244+
private volatile int maxScriptFields;
235245
private volatile boolean TTLPurgeDisabled;
236246
/**
237247
* The maximum number of refresh listeners allows on this shard.
@@ -329,6 +339,7 @@ public IndexSettings(final IndexMetaData indexMetaData, final Settings nodeSetti
329339
maxAdjacencyMatrixFilters = scopedSettings.get(MAX_ADJACENCY_MATRIX_FILTERS_SETTING);
330340
maxRescoreWindow = scopedSettings.get(MAX_RESCORE_WINDOW_SETTING);
331341
maxDocvalueFields = scopedSettings.get(MAX_DOCVALUE_FIELDS_SEARCH_SETTING);
342+
maxScriptFields = scopedSettings.get(MAX_SCRIPT_FIELDS_SETTING);
332343
TTLPurgeDisabled = scopedSettings.get(INDEX_TTL_DISABLE_PURGE_SETTING);
333344
maxRefreshListeners = scopedSettings.get(MAX_REFRESH_LISTENERS_PER_SHARD);
334345
maxSlicesPerScroll = scopedSettings.get(MAX_SLICES_PER_SCROLL);
@@ -358,6 +369,7 @@ public IndexSettings(final IndexMetaData indexMetaData, final Settings nodeSetti
358369
scopedSettings.addSettingsUpdateConsumer(MAX_ADJACENCY_MATRIX_FILTERS_SETTING, this::setMaxAdjacencyMatrixFilters);
359370
scopedSettings.addSettingsUpdateConsumer(MAX_RESCORE_WINDOW_SETTING, this::setMaxRescoreWindow);
360371
scopedSettings.addSettingsUpdateConsumer(MAX_DOCVALUE_FIELDS_SEARCH_SETTING, this::setMaxDocvalueFields);
372+
scopedSettings.addSettingsUpdateConsumer(MAX_SCRIPT_FIELDS_SETTING, this::setMaxScriptFields);
361373
scopedSettings.addSettingsUpdateConsumer(INDEX_WARMER_ENABLED_SETTING, this::setEnableWarmer);
362374
scopedSettings.addSettingsUpdateConsumer(INDEX_GC_DELETES_SETTING, this::setGCDeletes);
363375
scopedSettings.addSettingsUpdateConsumer(INDEX_TRANSLOG_FLUSH_THRESHOLD_SIZE_SETTING, this::setTranslogFlushThresholdSize);
@@ -613,6 +625,17 @@ private void setMaxDocvalueFields(int maxDocvalueFields) {
613625
this.maxDocvalueFields = maxDocvalueFields;
614626
}
615627

628+
/**
629+
* Returns the maximum number of allowed script_fields to retrieve in a search request
630+
*/
631+
public int getMaxScriptFields() {
632+
return this.maxScriptFields;
633+
}
634+
635+
private void setMaxScriptFields(int maxScriptFields) {
636+
this.maxScriptFields = maxScriptFields;
637+
}
638+
616639
/**
617640
* Returns the GC deletes cycle in milliseconds.
618641
*/

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,13 @@ private void parseSource(DefaultSearchContext context, SearchSourceBuilder sourc
755755
}
756756
}
757757
if (source.scriptFields() != null) {
758+
int maxAllowedScriptFields = context.mapperService().getIndexSettings().getMaxScriptFields();
759+
if (source.scriptFields().size() > maxAllowedScriptFields) {
760+
throw new IllegalArgumentException(
761+
"Trying to retrieve too many script_fields. Must be less than or equal to: [" + maxAllowedScriptFields
762+
+ "] but was [" + source.scriptFields().size() + "]. This limit can be set by changing the ["
763+
+ IndexSettings.MAX_SCRIPT_FIELDS_SETTING.getKey() + "] index level setting.");
764+
}
758765
for (org.elasticsearch.search.builder.SearchSourceBuilder.ScriptField field : source.scriptFields()) {
759766
SearchScript.Factory factory = scriptService.compile(field.script(), SearchScript.CONTEXT);
760767
SearchScript.LeafFactory searchScript = factory.newFactory(field.script().getParams(), context.lookup());

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,22 @@ public void testMaxDocvalueFields() {
305305
assertEquals(IndexSettings.MAX_DOCVALUE_FIELDS_SEARCH_SETTING.get(Settings.EMPTY).intValue(), settings.getMaxDocvalueFields());
306306
}
307307

308+
public void testMaxScriptFields() {
309+
IndexMetaData metaData = newIndexMeta("index", Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)
310+
.put(IndexSettings.MAX_SCRIPT_FIELDS_SETTING.getKey(), 100).build());
311+
IndexSettings settings = new IndexSettings(metaData, Settings.EMPTY);
312+
assertEquals(100, settings.getMaxScriptFields());
313+
settings.updateIndexMetaData(
314+
newIndexMeta("index", Settings.builder().put(IndexSettings.MAX_SCRIPT_FIELDS_SETTING.getKey(), 20).build()));
315+
assertEquals(20, settings.getMaxScriptFields());
316+
settings.updateIndexMetaData(newIndexMeta("index", Settings.EMPTY));
317+
assertEquals(IndexSettings.MAX_SCRIPT_FIELDS_SETTING.get(Settings.EMPTY).intValue(), settings.getMaxScriptFields());
318+
319+
metaData = newIndexMeta("index", Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT).build());
320+
settings = new IndexSettings(metaData, Settings.EMPTY);
321+
assertEquals(IndexSettings.MAX_SCRIPT_FIELDS_SETTING.get(Settings.EMPTY).intValue(), settings.getMaxScriptFields());
322+
}
323+
308324
public void testMaxAdjacencyMatrixFiltersSetting() {
309325
IndexMetaData metaData = newIndexMeta("index", Settings.builder()
310326
.put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)

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

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@
4747
import org.elasticsearch.indices.IndicesService;
4848
import org.elasticsearch.plugins.Plugin;
4949
import org.elasticsearch.plugins.SearchPlugin;
50+
import org.elasticsearch.script.MockScriptEngine;
51+
import org.elasticsearch.script.MockScriptPlugin;
52+
import org.elasticsearch.script.Script;
53+
import org.elasticsearch.script.ScriptType;
5054
import org.elasticsearch.search.aggregations.bucket.global.GlobalAggregationBuilder;
5155
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
5256
import org.elasticsearch.search.aggregations.support.ValueType;
@@ -60,11 +64,14 @@
6064

6165
import java.io.IOException;
6266
import java.util.Collection;
67+
import java.util.Collections;
6368
import java.util.List;
69+
import java.util.Map;
6470
import java.util.concurrent.CountDownLatch;
6571
import java.util.concurrent.ExecutionException;
6672
import java.util.concurrent.Semaphore;
6773
import java.util.concurrent.atomic.AtomicBoolean;
74+
import java.util.function.Function;
6875

6976
import static java.util.Collections.singletonList;
7077
import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE;
@@ -83,7 +90,19 @@ protected boolean resetNodeAfterTest() {
8390

8491
@Override
8592
protected Collection<Class<? extends Plugin>> getPlugins() {
86-
return pluginList(FailOnRewriteQueryPlugin.class);
93+
return pluginList(FailOnRewriteQueryPlugin.class, CustomScriptPlugin.class);
94+
}
95+
96+
public static class CustomScriptPlugin extends MockScriptPlugin {
97+
98+
static final String DUMMY_SCRIPT = "dummyScript";
99+
100+
@Override
101+
protected Map<String, Function<Map<String, Object>, Object>> pluginScripts() {
102+
return Collections.singletonMap(DUMMY_SCRIPT, vars -> {
103+
return "dummy";
104+
});
105+
}
87106
}
88107

89108
@Override
@@ -290,6 +309,39 @@ searchSourceBuilder, new String[0], false, new AliasFilter(null, Strings.EMPTY_A
290309
}
291310
}
292311

312+
/**
313+
* test that getting more than the allowed number of script_fields throws an exception
314+
*/
315+
public void testMaxScriptFieldsSearch() throws IOException {
316+
createIndex("index");
317+
final SearchService service = getInstanceFromNode(SearchService.class);
318+
final IndicesService indicesService = getInstanceFromNode(IndicesService.class);
319+
final IndexService indexService = indicesService.indexServiceSafe(resolveIndex("index"));
320+
final IndexShard indexShard = indexService.getShard(0);
321+
322+
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
323+
// adding the maximum allowed number of script_fields to retrieve
324+
int maxScriptFields = indexService.getIndexSettings().getMaxScriptFields();
325+
for (int i = 0; i < maxScriptFields; i++) {
326+
searchSourceBuilder.scriptField("field" + i,
327+
new Script(ScriptType.INLINE, MockScriptEngine.NAME, CustomScriptPlugin.DUMMY_SCRIPT, Collections.emptyMap()));
328+
}
329+
try (SearchContext context = service.createContext(new ShardSearchLocalRequest(indexShard.shardId(), 1, SearchType.DEFAULT,
330+
searchSourceBuilder, new String[0], false, new AliasFilter(null, Strings.EMPTY_ARRAY), 1.0f), null)) {
331+
assertNotNull(context);
332+
searchSourceBuilder.scriptField("anotherScriptField",
333+
new Script(ScriptType.INLINE, MockScriptEngine.NAME, CustomScriptPlugin.DUMMY_SCRIPT, Collections.emptyMap()));
334+
IllegalArgumentException ex = expectThrows(IllegalArgumentException.class,
335+
() -> service.createContext(new ShardSearchLocalRequest(indexShard.shardId(), 1, SearchType.DEFAULT,
336+
searchSourceBuilder, new String[0], false, new AliasFilter(null, Strings.EMPTY_ARRAY), 1.0f), null));
337+
assertEquals(
338+
"Trying to retrieve too many script_fields. Must be less than or equal to: [" + maxScriptFields + "] but was ["
339+
+ (maxScriptFields + 1)
340+
+ "]. This limit can be set by changing the [index.max_script_fields] index level setting.",
341+
ex.getMessage());
342+
}
343+
}
344+
293345
public static class FailOnRewriteQueryPlugin extends Plugin implements SearchPlugin {
294346
@Override
295347
public List<QuerySpec<?>> getQueries() {

docs/reference/index-modules.asciidoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ specific index module:
134134
Defaults to `100`. Doc-value fields are costly since they might incur
135135
a per-field per-document seek.
136136

137+
`index.max_script_fields`::
138+
139+
The maximum number of `script_fields` that are allowed in a query.
140+
Defaults to `32`.
141+
137142
`index.blocks.read_only`::
138143

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

docs/reference/migration/migrate_6_0/search.asciidoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ The deprecated `fielddata_fields` have now been removed. `docvalue_fields` shoul
142142
`docvalue_fields` now have a default upper limit of 100 fields that can be requested.
143143
This limit can be overridden by using the `index.max_docvalue_fields_search` index setting.
144144

145+
==== `script_fields`
146+
147+
`script_fields` now have a default upper limit of 32 script fields that can be requested.
148+
This limit can be overridden by using the `index.max_script_fields` index setting.
149+
145150
==== Inner hits
146151

147152
The source inside a hit of inner hits keeps its full path with respect to the entire source.

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,29 @@ setup:
7272
query:
7373
match_all: {}
7474
docvalue_fields: ["one", "two", "three"]
75+
76+
---
77+
"Script_fields size limit":
78+
- skip:
79+
version: " - 5.99.99"
80+
reason: soft limit for script_fields only available as of 6.0.0
81+
82+
- do:
83+
indices.create:
84+
index: test_3
85+
body:
86+
settings:
87+
index.max_script_fields: 2
88+
89+
- do:
90+
catch: /Trying to retrieve too many script_fields\. Must be less than or equal to[:] \[2\] but was \[3\]\. This limit can be set by changing the \[index.max_script_fields\] index level setting\./
91+
search:
92+
index: test_3
93+
body:
94+
query:
95+
match_all: {}
96+
script_fields: {
97+
"test1" : { "script" : { "lang": "painless", "source": "1" }},
98+
"test2" : { "script" : { "lang": "painless", "source": "1" }},
99+
"test3" : { "script" : { "lang": "painless", "source": "1" }}
100+
}

0 commit comments

Comments
 (0)