Skip to content

Commit a8b39ed

Browse files
authored
Add a cluster setting to disallow expensive queries (#51385)
Add a new cluster setting `search.allow_expensive_queries` which by default is `true`. If set to `false`, certain queries that have usually slow performance cannot be executed and an error message is returned. - Queries that need to do linear scans to identify matches: - Script queries - Queries that have a high up-front cost: - Fuzzy queries - Regexp queries - Prefix queries (without index_prefixes enabled - Wildcard queries - Range queries on text and keyword fields - Joining queries - HasParent queries - HasChild queries - ParentId queries - Nested queries - Queries on deprecated 6.x geo shapes (using PrefixTree implementation) - Queries that may have a high per-document cost: - Script score queries - Percolate queries Closes: #29050
1 parent eda30ac commit a8b39ed

File tree

83 files changed

+1396
-219
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+1396
-219
lines changed

docs/reference/mapping/types/geo-shape.asciidoc

+4
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ between index size and a reasonable level of precision of 50m at the
252252
equator. This allows for indexing tens of millions of shapes without
253253
overly bloating the resulting index too much relative to the input size.
254254

255+
[NOTE]
256+
Geo-shape queries on geo-shapes implemented with PrefixTrees will not be executed if
257+
<<query-dsl-allow-expensive-queries, `search.allow_expensive_queries`>> is set to false.
258+
255259
[[input-structure]]
256260
[float]
257261
==== Input Structure

docs/reference/query-dsl.asciidoc

+22-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,27 @@ or to alter their behaviour (such as the
2525

2626
Query clauses behave differently depending on whether they are used in
2727
<<query-filter-context,query context or filter context>>.
28+
29+
[[query-dsl-allow-expensive-queries]]
30+
Allow expensive queries::
31+
Certain types of queries will generally execute slowly due to the way they are implemented, which can affect
32+
the stability of the cluster. Those queries can be categorised as follows:
33+
* Queries that need to do linear scans to identify matches:
34+
** <<query-dsl-script-query, `script queries`>>
35+
* Queries that have a high up-front cost:
36+
** <<query-dsl-fuzzy-query,`fuzzy queries`>>
37+
** <<query-dsl-regexp-query,`regexp queries`>>
38+
** <<query-dsl-prefix-query,`prefix queries`>> without <<index-prefixes, `index_prefixes`>>
39+
** <<query-dsl-wildcard-query, `wildcard queries`>>
40+
** <<query-dsl-range-query, `range queries>> on <<text, `text`>> and <<keyword, `keyword`>> fields
41+
* <<joining-queries, `Joining queries`>>
42+
* Queries on <<prefix-trees, deprecated geo shapes>>
43+
* Queries that may have a high per-document cost:
44+
** <<query-dsl-script-score-query, `script score queries`>>
45+
** <<query-dsl-percolate-query, `percolate queries`>>
46+
47+
The execution of such queries can be prevented by setting the value of the `search.allow_expensive_queries`
48+
setting to `false` (defaults to `true`).
2849
--
2950

3051
include::query-dsl/query_filter_context.asciidoc[]
@@ -51,4 +72,4 @@ include::query-dsl/minimum-should-match.asciidoc[]
5172

5273
include::query-dsl/multi-term-rewrite.asciidoc[]
5374

54-
include::query-dsl/regexp-syntax.asciidoc[]
75+
include::query-dsl/regexp-syntax.asciidoc[]

docs/reference/query-dsl/fuzzy-query.asciidoc

+5-1
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,8 @@ adjacent characters (ab → ba). Defaults to `true`.
9797

9898
`rewrite`::
9999
(Optional, string) Method used to rewrite the query. For valid values and more
100-
information, see the <<query-dsl-multi-term-rewrite, `rewrite` parameter>>.
100+
information, see the <<query-dsl-multi-term-rewrite, `rewrite` parameter>>.
101+
102+
==== Notes
103+
Fuzzy queries will not be executed if <<query-dsl-allow-expensive-queries, `search.allow_expensive_queries`>>
104+
is set to false.

docs/reference/query-dsl/geo-shape-query.asciidoc

+4
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,7 @@ and will not match any documents for this query. This can be useful when
161161
querying multiple indexes which might have different mappings. When set to
162162
`false` (the default value) the query will throw an exception if the field
163163
is not mapped.
164+
165+
==== Notes
166+
Geo-shape queries on geo-shapes implemented with <<prefix-trees, `PrefixTrees`>> will not be executed if
167+
<<query-dsl-allow-expensive-queries, `search.allow_expensive_queries`>> is set to false.

docs/reference/query-dsl/joining-queries.asciidoc

+4-1
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,7 @@ include::has-parent-query.asciidoc[]
2929

3030
include::parent-id-query.asciidoc[]
3131

32-
32+
=== Notes
33+
==== Allow expensive queries
34+
Joining queries will not be executed if <<query-dsl-allow-expensive-queries, `search.allow_expensive_queries`>>
35+
is set to false.

docs/reference/query-dsl/percolate-query.asciidoc

+5
Original file line numberDiff line numberDiff line change
@@ -686,3 +686,8 @@ being percolated, as opposed to a single index as we do in examples. There are a
686686
allows for fields to be stored in a denser, more efficient way.
687687
- Percolate queries do not scale in the same way as other queries, so percolation performance may benefit from using
688688
a different index configuration, like the number of primary shards.
689+
690+
=== Notes
691+
==== Allow expensive queries
692+
Percolate queries will not be executed if <<query-dsl-allow-expensive-queries, `search.allow_expensive_queries`>>
693+
is set to false.

docs/reference/query-dsl/prefix-query.asciidoc

+7-1
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,10 @@ GET /_search
6464
You can speed up prefix queries using the <<index-prefixes,`index_prefixes`>>
6565
mapping parameter. If enabled, {es} indexes prefixes between 2 and 5
6666
characters in a separate field. This lets {es} run prefix queries more
67-
efficiently at the cost of a larger index.
67+
efficiently at the cost of a larger index.
68+
69+
[[prefix-query-allow-expensive-queries]]
70+
===== Allow expensive queries
71+
Prefix queries will not be executed if <<query-dsl-allow-expensive-queries, `search.allow_expensive_queries`>>
72+
is set to false. However, if <<index-prefixes, `index_prefixes`>> are enabled, an optimised query is built which
73+
is not considered slow, and will be executed in spite of this setting.

docs/reference/query-dsl/query-string-query.asciidoc

+6
Original file line numberDiff line numberDiff line change
@@ -537,3 +537,9 @@ The example above creates a boolean query:
537537
`(blended(terms:[field2:this, field1:this]) blended(terms:[field2:that, field1:that]) blended(terms:[field2:thus, field1:thus]))~2`
538538

539539
that matches documents with at least two of the three per-term blended queries.
540+
541+
==== Notes
542+
===== Allow expensive queries
543+
Query string query can be internally be transformed to a <<query-dsl-prefix-query, `prefix query`>> which means
544+
that if the prefix queries are disabled as explained <<prefix-query-allow-expensive-queries, here>> the query will not be
545+
executed and an exception will be thrown.

docs/reference/query-dsl/range-query.asciidoc

+5
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ increases the relevance score.
134134
[[range-query-notes]]
135135
==== Notes
136136

137+
[[ranges-on-text-and-keyword]]
138+
===== Using the `range` query with `text` and `keyword` fields
139+
Range queries on <<text, `text`>> or <<keyword, `keyword`>> files will not be executed if
140+
<<query-dsl-allow-expensive-queries, `search.allow_expensive_queries`>> is set to false.
141+
137142
[[ranges-on-dates]]
138143
===== Using the `range` query with `date` fields
139144

docs/reference/query-dsl/regexp-query.asciidoc

+5
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,8 @@ regular expressions.
8686
`rewrite`::
8787
(Optional, string) Method used to rewrite the query. For valid values and more
8888
information, see the <<query-dsl-multi-term-rewrite, `rewrite` parameter>>.
89+
90+
==== Notes
91+
===== Allow expensive queries
92+
Regexp queries will not be executed if <<query-dsl-allow-expensive-queries, `search.allow_expensive_queries`>>
93+
is set to false.

docs/reference/query-dsl/script-query.asciidoc

+4
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,7 @@ GET /_search
6969
}
7070
}
7171
----
72+
73+
===== Allow expensive queries
74+
Script queries will not be executed if <<query-dsl-allow-expensive-queries, `search.allow_expensive_queries`>>
75+
is set to false.

docs/reference/query-dsl/script-score-query.asciidoc

+4
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,10 @@ and default time zone. Also calculations with `now` are not supported.
221221
<<vector-functions, Functions for vector fields>> are accessible through
222222
`script_score` query.
223223

224+
===== Allow expensive queries
225+
Script score queries will not be executed if <<query-dsl-allow-expensive-queries, `search.allow_expensive_queries`>>
226+
is set to false.
227+
224228
[[script-score-faster-alt]]
225229
===== Faster alternatives
226230
The `script_score` query calculates the score for

docs/reference/query-dsl/wildcard-query.asciidoc

+6-1
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,9 @@ increases the relevance score.
6767

6868
`rewrite`::
6969
(Optional, string) Method used to rewrite the query. For valid values and more information, see the
70-
<<query-dsl-multi-term-rewrite, `rewrite` parameter>>.
70+
<<query-dsl-multi-term-rewrite, `rewrite` parameter>>.
71+
72+
==== Notes
73+
===== Allow expensive queries
74+
Wildcard queries will not be executed if <<query-dsl-allow-expensive-queries, `search.allow_expensive_queries`>>
75+
is set to false.

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

+8-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.apache.lucene.search.TermInSetQuery;
2727
import org.apache.lucene.search.TermQuery;
2828
import org.apache.lucene.util.BytesRef;
29+
import org.elasticsearch.ElasticsearchException;
2930
import org.elasticsearch.index.mapper.SearchAsYouTypeFieldMapper.Defaults;
3031
import org.elasticsearch.index.mapper.SearchAsYouTypeFieldMapper.PrefixFieldType;
3132
import org.elasticsearch.index.mapper.SearchAsYouTypeFieldMapper.SearchAsYouTypeFieldType;
@@ -100,14 +101,19 @@ public void testPrefixQuery() {
100101

101102
// this term should be a length that can be rewriteable to a term query on the prefix field
102103
final String withinBoundsTerm = "foo";
103-
assertThat(fieldType.prefixQuery(withinBoundsTerm, CONSTANT_SCORE_REWRITE, null),
104+
assertThat(fieldType.prefixQuery(withinBoundsTerm, CONSTANT_SCORE_REWRITE, randomMockShardContext()),
104105
equalTo(new ConstantScoreQuery(new TermQuery(new Term(PREFIX_NAME, withinBoundsTerm)))));
105106

106107
// our defaults don't allow a situation where a term can be too small
107108

108109
// this term should be too long to be rewriteable to a term query on the prefix field
109110
final String longTerm = "toolongforourprefixfieldthistermis";
110-
assertThat(fieldType.prefixQuery(longTerm, CONSTANT_SCORE_REWRITE, null),
111+
assertThat(fieldType.prefixQuery(longTerm, CONSTANT_SCORE_REWRITE, MOCK_QSC),
111112
equalTo(new PrefixQuery(new Term(NAME, longTerm))));
113+
114+
ElasticsearchException ee = expectThrows(ElasticsearchException.class,
115+
() -> fieldType.prefixQuery(longTerm, CONSTANT_SCORE_REWRITE, MOCK_QSC_DISALLOW_EXPENSIVE));
116+
assertEquals("[prefix] queries cannot be executed when 'search.allow_expensive_queries' is set to false. " +
117+
"For optimised prefix queries on text fields please enable [index_prefixes].", ee.getMessage());
112118
}
113119
}

modules/parent-join/src/main/java/org/elasticsearch/join/query/HasChildQueryBuilder.java

+8
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.apache.lucene.search.join.JoinUtil;
2828
import org.apache.lucene.search.join.ScoreMode;
2929
import org.apache.lucene.search.similarities.Similarity;
30+
import org.elasticsearch.ElasticsearchException;
3031
import org.elasticsearch.common.ParseField;
3132
import org.elasticsearch.common.ParsingException;
3233
import org.elasticsearch.common.io.stream.StreamInput;
@@ -53,6 +54,8 @@
5354
import java.util.Map;
5455
import java.util.Objects;
5556

57+
import static org.elasticsearch.search.SearchService.ALLOW_EXPENSIVE_QUERIES;
58+
5659
/**
5760
* A query builder for {@code has_child} query.
5861
*/
@@ -295,6 +298,11 @@ public String getWriteableName() {
295298

296299
@Override
297300
protected Query doToQuery(QueryShardContext context) throws IOException {
301+
if (context.allowExpensiveQueries() == false) {
302+
throw new ElasticsearchException("[joining] queries cannot be executed when '" +
303+
ALLOW_EXPENSIVE_QUERIES.getKey() + "' is set to false.");
304+
}
305+
298306
ParentJoinFieldMapper joinFieldMapper = ParentJoinFieldMapper.getMapper(context.getMapperService());
299307
if (joinFieldMapper == null) {
300308
if (ignoreUnmapped) {

modules/parent-join/src/main/java/org/elasticsearch/join/query/HasParentQueryBuilder.java

+8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.apache.lucene.search.MatchNoDocsQuery;
2222
import org.apache.lucene.search.Query;
2323
import org.apache.lucene.search.join.ScoreMode;
24+
import org.elasticsearch.ElasticsearchException;
2425
import org.elasticsearch.common.ParseField;
2526
import org.elasticsearch.common.ParsingException;
2627
import org.elasticsearch.common.io.stream.StreamInput;
@@ -45,6 +46,8 @@
4546
import java.util.Map;
4647
import java.util.Objects;
4748

49+
import static org.elasticsearch.search.SearchService.ALLOW_EXPENSIVE_QUERIES;
50+
4851
/**
4952
* Builder for the 'has_parent' query.
5053
*/
@@ -158,6 +161,11 @@ public boolean ignoreUnmapped() {
158161

159162
@Override
160163
protected Query doToQuery(QueryShardContext context) throws IOException {
164+
if (context.allowExpensiveQueries() == false) {
165+
throw new ElasticsearchException("[joining] queries cannot be executed when '" +
166+
ALLOW_EXPENSIVE_QUERIES.getKey() + "' is set to false.");
167+
}
168+
161169
ParentJoinFieldMapper joinFieldMapper = ParentJoinFieldMapper.getMapper(context.getMapperService());
162170
if (joinFieldMapper == null) {
163171
if (ignoreUnmapped) {

modules/parent-join/src/main/java/org/elasticsearch/join/query/ParentIdQueryBuilder.java

+8
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.apache.lucene.search.BooleanQuery;
2424
import org.apache.lucene.search.MatchNoDocsQuery;
2525
import org.apache.lucene.search.Query;
26+
import org.elasticsearch.ElasticsearchException;
2627
import org.elasticsearch.common.ParseField;
2728
import org.elasticsearch.common.ParsingException;
2829
import org.elasticsearch.common.io.stream.StreamInput;
@@ -38,6 +39,8 @@
3839
import java.io.IOException;
3940
import java.util.Objects;
4041

42+
import static org.elasticsearch.search.SearchService.ALLOW_EXPENSIVE_QUERIES;
43+
4144
public final class ParentIdQueryBuilder extends AbstractQueryBuilder<ParentIdQueryBuilder> {
4245
public static final String NAME = "parent_id";
4346

@@ -153,6 +156,11 @@ public static ParentIdQueryBuilder fromXContent(XContentParser parser) throws IO
153156

154157
@Override
155158
protected Query doToQuery(QueryShardContext context) throws IOException {
159+
if (context.allowExpensiveQueries() == false) {
160+
throw new ElasticsearchException("[joining] queries cannot be executed when '" +
161+
ALLOW_EXPENSIVE_QUERIES.getKey() + "' is set to false.");
162+
}
163+
156164
ParentJoinFieldMapper joinFieldMapper = ParentJoinFieldMapper.getMapper(context.getMapperService());
157165
if (joinFieldMapper == null) {
158166
if (ignoreUnmapped) {

modules/parent-join/src/test/java/org/elasticsearch/join/query/HasChildQueryBuilderTests.java

+16-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
package org.elasticsearch.join.query;
2121

2222
import com.carrotsearch.randomizedtesting.generators.RandomPicks;
23-
2423
import org.apache.lucene.index.Term;
2524
import org.apache.lucene.search.BooleanClause;
2625
import org.apache.lucene.search.BooleanQuery;
@@ -32,6 +31,7 @@
3231
import org.apache.lucene.search.join.ScoreMode;
3332
import org.apache.lucene.search.similarities.PerFieldSimilarityWrapper;
3433
import org.apache.lucene.search.similarities.Similarity;
34+
import org.elasticsearch.ElasticsearchException;
3535
import org.elasticsearch.Version;
3636
import org.elasticsearch.cluster.metadata.IndexMetaData;
3737
import org.elasticsearch.common.Strings;
@@ -70,6 +70,8 @@
7070
import static org.hamcrest.CoreMatchers.equalTo;
7171
import static org.hamcrest.CoreMatchers.instanceOf;
7272
import static org.hamcrest.CoreMatchers.notNullValue;
73+
import static org.mockito.Mockito.mock;
74+
import static org.mockito.Mockito.when;
7375

7476
public class HasChildQueryBuilderTests extends AbstractQueryTestCase<HasChildQueryBuilder> {
7577

@@ -361,5 +363,18 @@ public void testExtractInnerHitBuildersWithDuplicate() {
361363
queryBuilder.innerHit(new InnerHitBuilder("some_name"));
362364
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
363365
() -> InnerHitContextBuilder.extractInnerHits(queryBuilder, Collections.singletonMap("some_name", null)));
366+
assertEquals("[inner_hits] already contains an entry for key [some_name]", e.getMessage());
367+
}
368+
369+
public void testDisallowExpensiveQueries() {
370+
QueryShardContext queryShardContext = mock(QueryShardContext.class);
371+
when(queryShardContext.allowExpensiveQueries()).thenReturn(false);
372+
373+
HasChildQueryBuilder queryBuilder =
374+
hasChildQuery(CHILD_DOC, new TermQueryBuilder("custom_string", "value"), ScoreMode.None);
375+
ElasticsearchException e = expectThrows(ElasticsearchException.class,
376+
() -> queryBuilder.toQuery(queryShardContext));
377+
assertEquals("[joining] queries cannot be executed when 'search.allow_expensive_queries' is set to false.",
378+
e.getMessage());
364379
}
365380
}

modules/parent-join/src/test/java/org/elasticsearch/join/query/HasParentQueryBuilderTests.java

+16
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.apache.lucene.search.MatchNoDocsQuery;
2323
import org.apache.lucene.search.Query;
2424
import org.apache.lucene.search.join.ScoreMode;
25+
import org.elasticsearch.ElasticsearchException;
2526
import org.elasticsearch.Version;
2627
import org.elasticsearch.cluster.metadata.IndexMetaData;
2728
import org.elasticsearch.common.Strings;
@@ -57,6 +58,8 @@
5758
import static org.hamcrest.CoreMatchers.equalTo;
5859
import static org.hamcrest.CoreMatchers.instanceOf;
5960
import static org.hamcrest.CoreMatchers.notNullValue;
61+
import static org.mockito.Mockito.mock;
62+
import static org.mockito.Mockito.when;
6063

6164
public class HasParentQueryBuilderTests extends AbstractQueryTestCase<HasParentQueryBuilder> {
6265
private static final String TYPE = "_doc";
@@ -261,5 +264,18 @@ public void testExtractInnerHitBuildersWithDuplicate() {
261264
queryBuilder.innerHit(new InnerHitBuilder("some_name"));
262265
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
263266
() -> InnerHitContextBuilder.extractInnerHits(queryBuilder, Collections.singletonMap("some_name", null)));
267+
assertEquals("[inner_hits] already contains an entry for key [some_name]", e.getMessage());
268+
}
269+
270+
public void testDisallowExpensiveQueries() {
271+
QueryShardContext queryShardContext = mock(QueryShardContext.class);
272+
when(queryShardContext.allowExpensiveQueries()).thenReturn(false);
273+
274+
HasParentQueryBuilder queryBuilder = new HasParentQueryBuilder(
275+
CHILD_DOC, new WrapperQueryBuilder(new MatchAllQueryBuilder().toString()), false);
276+
ElasticsearchException e = expectThrows(ElasticsearchException.class,
277+
() -> queryBuilder.toQuery(queryShardContext));
278+
assertEquals("[joining] queries cannot be executed when 'search.allow_expensive_queries' is set to false.",
279+
e.getMessage());
264280
}
265281
}

modules/parent-join/src/test/java/org/elasticsearch/join/query/ParentIdQueryBuilderTests.java

+13
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.apache.lucene.search.MatchNoDocsQuery;
2626
import org.apache.lucene.search.Query;
2727
import org.apache.lucene.search.TermQuery;
28+
import org.elasticsearch.ElasticsearchException;
2829
import org.elasticsearch.Version;
2930
import org.elasticsearch.cluster.metadata.IndexMetaData;
3031
import org.elasticsearch.common.Strings;
@@ -48,6 +49,8 @@
4849
import static org.hamcrest.CoreMatchers.equalTo;
4950
import static org.hamcrest.CoreMatchers.instanceOf;
5051
import static org.hamcrest.CoreMatchers.notNullValue;
52+
import static org.mockito.Mockito.mock;
53+
import static org.mockito.Mockito.when;
5154

5255
public class ParentIdQueryBuilderTests extends AbstractQueryTestCase<ParentIdQueryBuilder> {
5356

@@ -154,4 +157,14 @@ public void testIgnoreUnmapped() throws IOException {
154157
assertThat(e.getMessage(), containsString("[" + ParentIdQueryBuilder.NAME + "] no relation found for child [unmapped]"));
155158
}
156159

160+
public void testDisallowExpensiveQueries() {
161+
QueryShardContext queryShardContext = mock(QueryShardContext.class);
162+
when(queryShardContext.allowExpensiveQueries()).thenReturn(false);
163+
164+
ParentIdQueryBuilder queryBuilder = doCreateTestQueryBuilder();
165+
ElasticsearchException e = expectThrows(ElasticsearchException.class,
166+
() -> queryBuilder.toQuery(queryShardContext));
167+
assertEquals("[joining] queries cannot be executed when 'search.allow_expensive_queries' is set to false.",
168+
e.getMessage());
169+
}
157170
}

0 commit comments

Comments
 (0)