Skip to content

Commit e8877d6

Browse files
authored
[8.16] Allow for queries on _tier to skip shards during coordinator rewrite (elastic#114990) (elastic#115513)
* Allow for queries on _tier to skip shards during coordinator rewrite (elastic#114990) The `_tier` metadata field was not used on the coordinator when rewriting queries in order to exclude shards that don't match. This lead to queries in the following form to continue to report failures even though the only unavailable shards were in the tier that was excluded from search (frozen tier in this example): ``` POST testing/_search { "query": { "bool": { "must_not": [ { "term": { "_tier": "data_frozen" } } ] } } } ``` This PR addresses this by having the queries that can execute on `_tier` (term, match, query string, simple query string, prefix, wildcard) execute a coordinator rewrite to exclude the indices that don't match the `_tier` query **before** attempting to reach to the shards (shards, that might not be available and raise errors). Fixes elastic#114910 * Don't use getFirst * Test compile
1 parent 340c422 commit e8877d6

File tree

17 files changed

+594
-38
lines changed

17 files changed

+594
-38
lines changed

docs/changelog/114990.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 114990
2+
summary: Allow for querries on `_tier` to skip shards in the `can_match` phase
3+
area: Search
4+
type: bug
5+
issues:
6+
- 114910

server/src/main/java/org/elasticsearch/index/query/CoordinatorRewriteContext.java

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,23 @@
99

1010
package org.elasticsearch.index.query;
1111

12+
import org.apache.lucene.search.Query;
1213
import org.elasticsearch.client.internal.Client;
1314
import org.elasticsearch.cluster.metadata.DataStream;
1415
import org.elasticsearch.cluster.metadata.IndexMetadata;
16+
import org.elasticsearch.common.Strings;
17+
import org.elasticsearch.common.regex.Regex;
1518
import org.elasticsearch.core.Nullable;
19+
import org.elasticsearch.index.mapper.ConstantFieldType;
1620
import org.elasticsearch.index.mapper.MappedFieldType;
1721
import org.elasticsearch.index.mapper.MappingLookup;
22+
import org.elasticsearch.index.mapper.ValueFetcher;
1823
import org.elasticsearch.index.shard.IndexLongFieldRange;
1924
import org.elasticsearch.indices.DateFieldRangeInfo;
2025
import org.elasticsearch.xcontent.XContentParserConfiguration;
2126

2227
import java.util.Collections;
28+
import java.util.Map;
2329
import java.util.function.LongSupplier;
2430

2531
/**
@@ -30,20 +36,57 @@
3036
* and skip the shards that don't hold queried data. See IndexMetadata for more details.
3137
*/
3238
public class CoordinatorRewriteContext extends QueryRewriteContext {
39+
40+
public static final String TIER_FIELD_NAME = "_tier";
41+
42+
private static final ConstantFieldType TIER_FIELD_TYPE = new ConstantFieldType(TIER_FIELD_NAME, Map.of()) {
43+
@Override
44+
public ValueFetcher valueFetcher(SearchExecutionContext context, String format) {
45+
throw new UnsupportedOperationException("fetching field values is not supported on the coordinator node");
46+
}
47+
48+
@Override
49+
public String typeName() {
50+
return TIER_FIELD_NAME;
51+
}
52+
53+
@Override
54+
protected boolean matches(String pattern, boolean caseInsensitive, QueryRewriteContext context) {
55+
if (caseInsensitive) {
56+
pattern = Strings.toLowercaseAscii(pattern);
57+
}
58+
59+
String tierPreference = context.getTierPreference();
60+
if (tierPreference == null) {
61+
return false;
62+
}
63+
return Regex.simpleMatch(pattern, tierPreference);
64+
}
65+
66+
@Override
67+
public Query existsQuery(SearchExecutionContext context) {
68+
throw new UnsupportedOperationException("field exists query is not supported on the coordinator node");
69+
}
70+
};
71+
3372
private final DateFieldRangeInfo dateFieldRangeInfo;
73+
private final String tier;
3474

3575
/**
3676
* Context for coordinator search rewrites based on time ranges for the @timestamp field and/or 'event.ingested' field
77+
*
3778
* @param parserConfig
3879
* @param client
3980
* @param nowInMillis
4081
* @param dateFieldRangeInfo range and field type info for @timestamp and 'event.ingested'
82+
* @param tier the configured data tier (via the _tier_preference setting) for the index
4183
*/
4284
public CoordinatorRewriteContext(
4385
XContentParserConfiguration parserConfig,
4486
Client client,
4587
LongSupplier nowInMillis,
46-
DateFieldRangeInfo dateFieldRangeInfo
88+
DateFieldRangeInfo dateFieldRangeInfo,
89+
String tier
4790
) {
4891
super(
4992
parserConfig,
@@ -63,10 +106,12 @@ public CoordinatorRewriteContext(
63106
null
64107
);
65108
this.dateFieldRangeInfo = dateFieldRangeInfo;
109+
this.tier = tier;
66110
}
67111

68112
/**
69-
* @param fieldName Must be one of DataStream.TIMESTAMP_FIELD_FIELD or IndexMetadata.EVENT_INGESTED_FIELD_NAME
113+
* @param fieldName Must be one of DataStream.TIMESTAMP_FIELD_FIELD, IndexMetadata.EVENT_INGESTED_FIELD_NAME, or
114+
* DataTierFiledMapper.NAME
70115
* @return MappedField with type for the field. Returns null if fieldName is not one of the allowed field names.
71116
*/
72117
@Nullable
@@ -75,6 +120,8 @@ public MappedFieldType getFieldType(String fieldName) {
75120
return dateFieldRangeInfo.timestampFieldType();
76121
} else if (IndexMetadata.EVENT_INGESTED_FIELD_NAME.equals(fieldName)) {
77122
return dateFieldRangeInfo.eventIngestedFieldType();
123+
} else if (TIER_FIELD_NAME.equals(fieldName)) {
124+
return TIER_FIELD_TYPE;
78125
} else {
79126
return null;
80127
}
@@ -99,4 +146,18 @@ public IndexLongFieldRange getFieldRange(String fieldName) {
99146
public CoordinatorRewriteContext convertToCoordinatorRewriteContext() {
100147
return this;
101148
}
149+
150+
@Override
151+
public String getTierPreference() {
152+
// dominant branch first (tier preference is configured)
153+
return tier.isEmpty() == false ? tier : null;
154+
}
155+
156+
/**
157+
* We're holding on to the index tier in the context as otherwise we'd need
158+
* to re-parse it from the index settings when evaluating the _tier field.
159+
*/
160+
public String tier() {
161+
return tier;
162+
}
102163
}

server/src/main/java/org/elasticsearch/index/query/CoordinatorRewriteContextProvider.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ public CoordinatorRewriteContext getCoordinatorRewriteContext(Index index) {
5252
return null;
5353
}
5454
DateFieldRangeInfo dateFieldRangeInfo = mappingSupplier.apply(index);
55+
// we've now added a coordinator rewrite based on the _tier field so the requirement
56+
// for the timestamps fields to be present is artificial (we could do a coordinator
57+
// rewrite only based on the _tier field) and we might decide to remove this artificial
58+
// limitation to enable coordinator rewrites based on _tier for hot and warm indices
59+
// (currently the _tier coordinator rewrite is only available for mounted and partially mounted
60+
// indices)
5561
if (dateFieldRangeInfo == null) {
5662
return null;
5763
}
@@ -74,7 +80,8 @@ public CoordinatorRewriteContext getCoordinatorRewriteContext(Index index) {
7480
parserConfig,
7581
client,
7682
nowInMillis,
77-
new DateFieldRangeInfo(timestampFieldType, timestampRange, dateFieldRangeInfo.eventIngestedFieldType(), eventIngestedRange)
83+
new DateFieldRangeInfo(timestampFieldType, timestampRange, dateFieldRangeInfo.eventIngestedFieldType(), eventIngestedRange),
84+
indexMetadata.getTierPreference().isEmpty() == false ? indexMetadata.getTierPreference().get(0) : ""
7885
);
7986
}
8087
}

server/src/main/java/org/elasticsearch/index/query/PrefixQueryBuilder.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.elasticsearch.common.io.stream.StreamInput;
2121
import org.elasticsearch.common.io.stream.StreamOutput;
2222
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
23+
import org.elasticsearch.core.Nullable;
2324
import org.elasticsearch.index.IndexSettings;
2425
import org.elasticsearch.index.mapper.ConstantFieldType;
2526
import org.elasticsearch.index.mapper.MappedFieldType;
@@ -189,11 +190,24 @@ public String getWriteableName() {
189190
}
190191

191192
@Override
192-
protected QueryBuilder doIndexMetadataRewrite(QueryRewriteContext context) throws IOException {
193+
protected QueryBuilder doIndexMetadataRewrite(QueryRewriteContext context) {
193194
MappedFieldType fieldType = context.getFieldType(this.fieldName);
194195
if (fieldType == null) {
195196
return new MatchNoneQueryBuilder("The \"" + getName() + "\" query is against a field that does not exist");
196-
} else if (fieldType instanceof ConstantFieldType constantFieldType) {
197+
}
198+
return maybeRewriteBasedOnConstantFields(fieldType, context);
199+
}
200+
201+
@Override
202+
protected QueryBuilder doCoordinatorRewrite(CoordinatorRewriteContext coordinatorRewriteContext) {
203+
MappedFieldType fieldType = coordinatorRewriteContext.getFieldType(this.fieldName);
204+
// we don't rewrite a null field type to `match_none` on the coordinator because the coordinator has access
205+
// to only a subset of fields see {@link CoordinatorRewriteContext#getFieldType}
206+
return maybeRewriteBasedOnConstantFields(fieldType, coordinatorRewriteContext);
207+
}
208+
209+
private QueryBuilder maybeRewriteBasedOnConstantFields(@Nullable MappedFieldType fieldType, QueryRewriteContext context) {
210+
if (fieldType instanceof ConstantFieldType constantFieldType) {
197211
// This logic is correct for all field types, but by only applying it to constant
198212
// fields we also have the guarantee that it doesn't perform I/O, which is important
199213
// since rewrites might happen on a network thread.

server/src/main/java/org/elasticsearch/index/query/QueryRewriteContext.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111
import org.elasticsearch.action.ActionListener;
1212
import org.elasticsearch.action.ResolvedIndices;
1313
import org.elasticsearch.client.internal.Client;
14+
import org.elasticsearch.cluster.routing.allocation.DataTier;
15+
import org.elasticsearch.common.Strings;
1416
import org.elasticsearch.common.collect.Iterators;
1517
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
1618
import org.elasticsearch.common.regex.Regex;
19+
import org.elasticsearch.common.settings.Settings;
1720
import org.elasticsearch.common.util.concurrent.CountDown;
1821
import org.elasticsearch.core.Nullable;
1922
import org.elasticsearch.index.Index;
@@ -406,4 +409,22 @@ public ResolvedIndices getResolvedIndices() {
406409
public PointInTimeBuilder getPointInTimeBuilder() {
407410
return pit;
408411
}
412+
413+
/**
414+
* Retrieve the first tier preference from the index setting. If the setting is not
415+
* present, then return null.
416+
*/
417+
@Nullable
418+
public String getTierPreference() {
419+
Settings settings = getIndexSettings().getSettings();
420+
String value = DataTier.TIER_PREFERENCE_SETTING.get(settings);
421+
422+
if (Strings.hasText(value) == false) {
423+
return null;
424+
}
425+
426+
// Tier preference can be a comma-delimited list of tiers, ordered by preference
427+
// It was decided we should only test the first of these potentially multiple preferences.
428+
return value.split(",")[0].trim();
429+
}
409430
}

server/src/main/java/org/elasticsearch/index/query/TermQueryBuilder.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.elasticsearch.common.ParsingException;
1818
import org.elasticsearch.common.io.stream.StreamInput;
1919
import org.elasticsearch.common.io.stream.StreamOutput;
20+
import org.elasticsearch.core.Nullable;
2021
import org.elasticsearch.index.mapper.ConstantFieldType;
2122
import org.elasticsearch.index.mapper.MappedFieldType;
2223
import org.elasticsearch.xcontent.ParseField;
@@ -170,11 +171,24 @@ protected void addExtraXContent(XContentBuilder builder, Params params) throws I
170171
}
171172

172173
@Override
173-
protected QueryBuilder doIndexMetadataRewrite(QueryRewriteContext context) throws IOException {
174+
protected QueryBuilder doIndexMetadataRewrite(QueryRewriteContext context) {
174175
MappedFieldType fieldType = context.getFieldType(this.fieldName);
175176
if (fieldType == null) {
176177
return new MatchNoneQueryBuilder("The \"" + getName() + "\" query is against a field that does not exist");
177-
} else if (fieldType instanceof ConstantFieldType constantFieldType) {
178+
}
179+
return maybeRewriteBasedOnConstantFields(fieldType, context);
180+
}
181+
182+
@Override
183+
protected QueryBuilder doCoordinatorRewrite(CoordinatorRewriteContext coordinatorRewriteContext) {
184+
MappedFieldType fieldType = coordinatorRewriteContext.getFieldType(this.fieldName);
185+
// we don't rewrite a null field type to `match_none` on the coordinator because the coordinator has access
186+
// to only a subset of fields see {@link CoordinatorRewriteContext#getFieldType}
187+
return maybeRewriteBasedOnConstantFields(fieldType, coordinatorRewriteContext);
188+
}
189+
190+
private QueryBuilder maybeRewriteBasedOnConstantFields(@Nullable MappedFieldType fieldType, QueryRewriteContext context) {
191+
if (fieldType instanceof ConstantFieldType constantFieldType) {
178192
// This logic is correct for all field types, but by only applying it to constant
179193
// fields we also have the guarantee that it doesn't perform I/O, which is important
180194
// since rewrites might happen on a network thread.

server/src/main/java/org/elasticsearch/index/query/TermsQueryBuilder.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,11 +393,24 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws
393393
}
394394

395395
@Override
396-
protected QueryBuilder doIndexMetadataRewrite(QueryRewriteContext context) throws IOException {
396+
protected QueryBuilder doIndexMetadataRewrite(QueryRewriteContext context) {
397397
MappedFieldType fieldType = context.getFieldType(this.fieldName);
398398
if (fieldType == null) {
399399
return new MatchNoneQueryBuilder("The \"" + getName() + "\" query is against a field that does not exist");
400-
} else if (fieldType instanceof ConstantFieldType constantFieldType) {
400+
}
401+
return maybeRewriteBasedOnConstantFields(fieldType, context);
402+
}
403+
404+
@Override
405+
protected QueryBuilder doCoordinatorRewrite(CoordinatorRewriteContext coordinatorRewriteContext) {
406+
MappedFieldType fieldType = coordinatorRewriteContext.getFieldType(this.fieldName);
407+
// we don't rewrite a null field type to `match_none` on the coordinator because the coordinator has access
408+
// to only a subset of fields see {@link CoordinatorRewriteContext#getFieldType}
409+
return maybeRewriteBasedOnConstantFields(fieldType, coordinatorRewriteContext);
410+
}
411+
412+
private QueryBuilder maybeRewriteBasedOnConstantFields(@Nullable MappedFieldType fieldType, QueryRewriteContext context) {
413+
if (fieldType instanceof ConstantFieldType constantFieldType) {
401414
// This logic is correct for all field types, but by only applying it to constant
402415
// fields we also have the guarantee that it doesn't perform I/O, which is important
403416
// since rewrites might happen on a network thread.

server/src/main/java/org/elasticsearch/index/query/WildcardQueryBuilder.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.elasticsearch.common.io.stream.StreamInput;
2121
import org.elasticsearch.common.io.stream.StreamOutput;
2222
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
23+
import org.elasticsearch.core.Nullable;
2324
import org.elasticsearch.index.mapper.ConstantFieldType;
2425
import org.elasticsearch.index.mapper.MappedFieldType;
2526
import org.elasticsearch.index.query.support.QueryParsers;
@@ -200,11 +201,24 @@ public static WildcardQueryBuilder fromXContent(XContentParser parser) throws IO
200201
}
201202

202203
@Override
203-
protected QueryBuilder doIndexMetadataRewrite(QueryRewriteContext context) throws IOException {
204+
protected QueryBuilder doIndexMetadataRewrite(QueryRewriteContext context) {
204205
MappedFieldType fieldType = context.getFieldType(this.fieldName);
205206
if (fieldType == null) {
206-
return new MatchNoneQueryBuilder("The \"" + getName() + "\" query is against a field that does not exist");
207-
} else if (fieldType instanceof ConstantFieldType constantFieldType) {
207+
return new MatchNoneQueryBuilder("The \"" + getName() + "\" query is against a field that does not exist");
208+
}
209+
return maybeRewriteBasedOnConstantFields(fieldType, context);
210+
}
211+
212+
@Override
213+
protected QueryBuilder doCoordinatorRewrite(CoordinatorRewriteContext coordinatorRewriteContext) {
214+
MappedFieldType fieldType = coordinatorRewriteContext.getFieldType(this.fieldName);
215+
// we don't rewrite a null field type to `match_none` on the coordinator because the coordinator has access
216+
// to only a subset of fields see {@link CoordinatorRewriteContext#getFieldType}
217+
return maybeRewriteBasedOnConstantFields(fieldType, coordinatorRewriteContext);
218+
}
219+
220+
private QueryBuilder maybeRewriteBasedOnConstantFields(@Nullable MappedFieldType fieldType, QueryRewriteContext context) {
221+
if (fieldType instanceof ConstantFieldType constantFieldType) {
208222
// This logic is correct for all field types, but by only applying it to constant
209223
// fields we also have the guarantee that it doesn't perform I/O, which is important
210224
// since rewrites might happen on a network thread.

server/src/test/java/org/elasticsearch/index/query/PrefixQueryBuilderTests.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
import org.apache.lucene.search.Query;
1818
import org.elasticsearch.common.ParsingException;
1919
import org.elasticsearch.core.Strings;
20+
import org.elasticsearch.index.mapper.DateFieldMapper;
2021
import org.elasticsearch.test.AbstractQueryTestCase;
22+
import org.hamcrest.CoreMatchers;
2123
import org.hamcrest.Matchers;
2224

2325
import java.io.IOException;
@@ -175,4 +177,37 @@ public void testMustRewrite() throws IOException {
175177
IllegalStateException e = expectThrows(IllegalStateException.class, () -> queryBuilder.toQuery(context));
176178
assertEquals("Rewrite first", e.getMessage());
177179
}
180+
181+
public void testCoordinatorTierRewriteToMatchAll() throws IOException {
182+
QueryBuilder query = new PrefixQueryBuilder("_tier", "data_fro");
183+
final String timestampFieldName = "@timestamp";
184+
long minTimestamp = 1685714000000L;
185+
long maxTimestamp = 1685715000000L;
186+
final CoordinatorRewriteContext coordinatorRewriteContext = createCoordinatorRewriteContext(
187+
new DateFieldMapper.DateFieldType(timestampFieldName),
188+
minTimestamp,
189+
maxTimestamp,
190+
"data_frozen"
191+
);
192+
193+
QueryBuilder rewritten = query.rewrite(coordinatorRewriteContext);
194+
assertThat(rewritten, CoreMatchers.instanceOf(MatchAllQueryBuilder.class));
195+
}
196+
197+
public void testCoordinatorTierRewriteToMatchNone() throws IOException {
198+
QueryBuilder query = QueryBuilders.boolQuery().mustNot(new PrefixQueryBuilder("_tier", "data_fro"));
199+
final String timestampFieldName = "@timestamp";
200+
long minTimestamp = 1685714000000L;
201+
long maxTimestamp = 1685715000000L;
202+
final CoordinatorRewriteContext coordinatorRewriteContext = createCoordinatorRewriteContext(
203+
new DateFieldMapper.DateFieldType(timestampFieldName),
204+
minTimestamp,
205+
maxTimestamp,
206+
"data_frozen"
207+
);
208+
209+
QueryBuilder rewritten = query.rewrite(coordinatorRewriteContext);
210+
assertThat(rewritten, CoreMatchers.instanceOf(MatchNoneQueryBuilder.class));
211+
}
212+
178213
}

0 commit comments

Comments
 (0)