Skip to content

Commit ee4fbe4

Browse files
authored
Update MatchPhrase- and TermQueryBuilder to be able to rewrite without a SearchExecutionContext. (#96905)
With this change, both query builders can rewrite without using a search context, because QueryRewriteContext often has all the mapping and other index metadata available. The TermQueryBuilder can with this change resolve to a MatchNoneQueryBuilder without needing a SearchExecutionContext, which during the can_match phase means that no searcher needs to be acquired and therefor avoid making a shard search active and doing a potentially refresh. The AbstractQueryBuilder#doRewrite(...) method is altered to by default attempt a coordination rewrite, then fall back to attempt a search rewrite, then finally fall back to do an index metadata aware rewrite. This is inline with what was discussed here: #96161 (comment) This change was forgotten as part of #96161 and is needed to complete #95776.
1 parent 733d465 commit ee4fbe4

File tree

14 files changed

+175
-82
lines changed

14 files changed

+175
-82
lines changed

server/src/main/java/org/elasticsearch/index/mapper/ConstantFieldType.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.elasticsearch.common.lucene.search.Queries;
1717
import org.elasticsearch.common.regex.Regex;
1818
import org.elasticsearch.core.Nullable;
19+
import org.elasticsearch.index.query.QueryRewriteContext;
1920
import org.elasticsearch.index.query.SearchExecutionContext;
2021

2122
import java.util.Collection;
@@ -44,14 +45,18 @@ public final boolean isAggregatable() {
4445
* Return whether the constant value of this field matches the provided {@code pattern}
4546
* as documented in {@link Regex#simpleMatch}.
4647
*/
47-
protected abstract boolean matches(String pattern, boolean caseInsensitive, SearchExecutionContext context);
48+
protected abstract boolean matches(String pattern, boolean caseInsensitive, QueryRewriteContext context);
4849

4950
private static String valueToString(Object value) {
5051
return value instanceof BytesRef ? ((BytesRef) value).utf8ToString() : value.toString();
5152
}
5253

5354
@Override
5455
public final Query termQuery(Object value, SearchExecutionContext context) {
56+
return internalTermQuery(value, context);
57+
}
58+
59+
public final Query internalTermQuery(Object value, QueryRewriteContext context) {
5560
String pattern = valueToString(value);
5661
if (matches(pattern, false, context)) {
5762
return Queries.newMatchAllQuery();
@@ -62,6 +67,10 @@ public final Query termQuery(Object value, SearchExecutionContext context) {
6267

6368
@Override
6469
public final Query termQueryCaseInsensitive(Object value, SearchExecutionContext context) {
70+
return internalTermQueryCaseInsensitive(value, context);
71+
}
72+
73+
public final Query internalTermQueryCaseInsensitive(Object value, QueryRewriteContext context) {
6574
String pattern = valueToString(value);
6675
if (matches(pattern, true, context)) {
6776
return Queries.newMatchAllQuery();

server/src/main/java/org/elasticsearch/index/mapper/IndexFieldMapper.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.elasticsearch.index.fielddata.IndexFieldData;
1717
import org.elasticsearch.index.fielddata.ScriptDocValues;
1818
import org.elasticsearch.index.fielddata.plain.ConstantIndexFieldData;
19+
import org.elasticsearch.index.query.QueryRewriteContext;
1920
import org.elasticsearch.index.query.SearchExecutionContext;
2021
import org.elasticsearch.script.field.DelegateDocValuesField;
2122
import org.elasticsearch.search.aggregations.support.CoreValuesSourceType;
@@ -49,7 +50,7 @@ public String typeName() {
4950
}
5051

5152
@Override
52-
protected boolean matches(String pattern, boolean caseInsensitive, SearchExecutionContext context) {
53+
protected boolean matches(String pattern, boolean caseInsensitive, QueryRewriteContext context) {
5354
if (caseInsensitive) {
5455
// Thankfully, all index names are lower-cased so we don't have to pass a case_insensitive mode flag
5556
// down to all the index name-matching logic. We just lower-case the search string

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,10 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws
299299
if (sec != null) {
300300
return doSearchRewrite(sec);
301301
}
302+
final QueryRewriteContext context = queryRewriteContext.convertToIndexMetadataContext();
303+
if (context != null) {
304+
return doIndexMetadataRewrite(context);
305+
}
302306
return this;
303307
}
304308

@@ -321,6 +325,18 @@ protected QueryBuilder doSearchRewrite(final SearchExecutionContext searchExecut
321325
return this;
322326
}
323327

328+
/**
329+
* Optional rewrite logic that only needs access to index level metadata and services (e.g. index settings and mappings)
330+
* on the data node, but not the shard / Lucene index.
331+
* The can_match phase can use this logic to early terminate a search without doing any search related i/o.
332+
*
333+
* @param context an {@link QueryRewriteContext} instance that has access the mappings and other index metadata
334+
* @return A {@link QueryBuilder} representing the rewritten query, that could be used to determine whether this query yields result.
335+
*/
336+
protected QueryBuilder doIndexMetadataRewrite(final QueryRewriteContext context) throws IOException {
337+
return this;
338+
}
339+
324340
/**
325341
* For internal usage only!
326342
*

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

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -157,24 +157,26 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep
157157
}
158158

159159
@Override
160-
protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException {
161-
SearchExecutionContext sec = queryRewriteContext.convertToSearchExecutionContext();
162-
if (sec == null) {
163-
return this;
164-
}
160+
protected QueryBuilder doSearchRewrite(SearchExecutionContext searchExecutionContext) throws IOException {
161+
return doIndexMetadataRewrite(searchExecutionContext);
162+
}
163+
164+
@Override
165+
protected QueryBuilder doIndexMetadataRewrite(QueryRewriteContext context) throws IOException {
165166
// If we're using the default keyword analyzer then we can rewrite this to a TermQueryBuilder
166167
// and possibly shortcut
167168
// If we're using a keyword analyzer then we can rewrite this to a TermQueryBuilder
168169
// and possibly shortcut
169-
NamedAnalyzer configuredAnalyzer = configuredAnalyzer(sec);
170+
NamedAnalyzer configuredAnalyzer = configuredAnalyzer(context);
170171
if (configuredAnalyzer != null && configuredAnalyzer.analyzer() instanceof KeywordAnalyzer) {
171172
TermQueryBuilder termQueryBuilder = new TermQueryBuilder(fieldName, value);
172-
return termQueryBuilder.rewrite(sec);
173+
return termQueryBuilder.rewrite(context);
174+
} else {
175+
return this;
173176
}
174-
return this;
175177
}
176178

177-
private NamedAnalyzer configuredAnalyzer(SearchExecutionContext context) {
179+
private NamedAnalyzer configuredAnalyzer(QueryRewriteContext context) {
178180
if (analyzer != null) {
179181
return context.getIndexAnalyzers().get(analyzer);
180182
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,13 @@ public CoordinatorRewriteContext convertToCoordinatorRewriteContext() {
141141
return null;
142142
}
143143

144+
/**
145+
* @return an {@link QueryRewriteContext} instance that is aware of the mapping and other index metadata or <code>null</code> otherwise.
146+
*/
147+
public QueryRewriteContext convertToIndexMetadataContext() {
148+
return mapperService != null ? this : null;
149+
}
150+
144151
/**
145152
* Returns the {@link MappedFieldType} for the provided field name.
146153
* If the field is not mapped, the behaviour depends on the index.query.parse.allow_unmapped_fields setting, which defaults to true.
@@ -258,4 +265,21 @@ public void onFailure(Exception e) {
258265
public Index getFullyQualifiedIndex() {
259266
return fullyQualifiedIndex;
260267
}
268+
269+
/**
270+
* Returns the index settings for this context. This might return null if the
271+
* context has not index scope.
272+
*/
273+
public IndexSettings getIndexSettings() {
274+
return indexSettings;
275+
}
276+
277+
/**
278+
* Given an index pattern, checks whether it matches against the current shard. The pattern
279+
* may represent a fully qualified index name if the search targets remote shards.
280+
*/
281+
public boolean indexMatches(String pattern) {
282+
assert indexNameMatcher != null;
283+
return indexNameMatcher.test(pattern);
284+
}
261285
}

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

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -468,15 +468,6 @@ public IndexVersion indexVersionCreated() {
468468
return indexSettings.getIndexVersionCreated();
469469
}
470470

471-
/**
472-
* Given an index pattern, checks whether it matches against the current shard. The pattern
473-
* may represent a fully qualified index name if the search targets remote shards.
474-
*/
475-
public boolean indexMatches(String pattern) {
476-
assert indexNameMatcher != null;
477-
return indexNameMatcher.test(pattern);
478-
}
479-
480471
public boolean indexSortedOnField(String field) {
481472
IndexSortConfig indexSortConfig = indexSettings.getIndexSortConfig();
482473
return indexSortConfig.hasPrimarySortOnField(field);
@@ -607,14 +598,6 @@ public final SearchExecutionContext convertToSearchExecutionContext() {
607598
return this;
608599
}
609600

610-
/**
611-
* Returns the index settings for this context. This might return null if the
612-
* context has not index scope.
613-
*/
614-
public IndexSettings getIndexSettings() {
615-
return indexSettings;
616-
}
617-
618601
/** Return the current {@link IndexReader}, or {@code null} if no index reader is available,
619602
* for instance if this rewrite context is used to index queries (percolation).
620603
*/

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

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -168,33 +168,35 @@ protected void addExtraXContent(XContentBuilder builder, Params params) throws I
168168
}
169169

170170
@Override
171-
protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException {
172-
SearchExecutionContext context = queryRewriteContext.convertToSearchExecutionContext();
173-
if (context != null) {
174-
MappedFieldType fieldType = context.getFieldType(this.fieldName);
175-
if (fieldType == null) {
176-
return new MatchNoneQueryBuilder();
177-
} else if (fieldType instanceof ConstantFieldType) {
178-
// This logic is correct for all field types, but by only applying it to constant
179-
// fields we also have the guarantee that it doesn't perform I/O, which is important
180-
// since rewrites might happen on a network thread.
181-
Query query = null;
182-
if (caseInsensitive) {
183-
query = fieldType.termQueryCaseInsensitive(value, context);
184-
} else {
185-
query = fieldType.termQuery(value, context);
186-
}
171+
protected QueryBuilder doSearchRewrite(SearchExecutionContext searchExecutionContext) throws IOException {
172+
return doIndexMetadataRewrite(searchExecutionContext);
173+
}
187174

188-
if (query instanceof MatchAllDocsQuery) {
189-
return new MatchAllQueryBuilder();
190-
} else if (query instanceof MatchNoDocsQuery) {
191-
return new MatchNoneQueryBuilder();
192-
} else {
193-
assert false : "Constant fields must produce match-all or match-none queries, got " + query;
194-
}
175+
@Override
176+
protected QueryBuilder doIndexMetadataRewrite(QueryRewriteContext context) throws IOException {
177+
MappedFieldType fieldType = context.getFieldType(this.fieldName);
178+
if (fieldType == null) {
179+
return new MatchNoneQueryBuilder();
180+
} else if (fieldType instanceof ConstantFieldType constantFieldType) {
181+
// This logic is correct for all field types, but by only applying it to constant
182+
// fields we also have the guarantee that it doesn't perform I/O, which is important
183+
// since rewrites might happen on a network thread.
184+
Query query;
185+
if (caseInsensitive) {
186+
query = constantFieldType.internalTermQueryCaseInsensitive(value, context);
187+
} else {
188+
query = constantFieldType.internalTermQuery(value, context);
189+
}
190+
191+
if (query instanceof MatchAllDocsQuery) {
192+
return new MatchAllQueryBuilder();
193+
} else if (query instanceof MatchNoDocsQuery) {
194+
return new MatchNoneQueryBuilder();
195+
} else {
196+
assert false : "Constant fields must produce match-all or match-none queries, got " + query;
195197
}
196198
}
197-
return super.doRewrite(queryRewriteContext);
199+
return this;
198200
}
199201

200202
@Override

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

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -192,36 +192,40 @@ public void testParseFailsWithMultipleFields() throws IOException {
192192

193193
public void testRewriteToTermQueries() throws IOException {
194194
QueryBuilder queryBuilder = new MatchPhraseQueryBuilder(KEYWORD_FIELD_NAME, "value");
195-
SearchExecutionContext context = createSearchExecutionContext();
196-
QueryBuilder rewritten = queryBuilder.rewrite(context);
197-
assertThat(rewritten, instanceOf(TermQueryBuilder.class));
198-
TermQueryBuilder tqb = (TermQueryBuilder) rewritten;
199-
assertEquals(KEYWORD_FIELD_NAME, tqb.fieldName);
200-
assertEquals(new BytesRef("value"), tqb.value);
195+
for (QueryRewriteContext context : new QueryRewriteContext[] { createSearchExecutionContext(), createQueryRewriteContext() }) {
196+
QueryBuilder rewritten = queryBuilder.rewrite(context);
197+
assertThat(rewritten, instanceOf(TermQueryBuilder.class));
198+
TermQueryBuilder tqb = (TermQueryBuilder) rewritten;
199+
assertEquals(KEYWORD_FIELD_NAME, tqb.fieldName);
200+
assertEquals(new BytesRef("value"), tqb.value);
201+
}
201202
}
202203

203204
public void testRewriteToTermQueryWithAnalyzer() throws IOException {
204205
MatchPhraseQueryBuilder queryBuilder = new MatchPhraseQueryBuilder(TEXT_FIELD_NAME, "value");
205206
queryBuilder.analyzer("keyword");
206-
SearchExecutionContext context = createSearchExecutionContext();
207-
QueryBuilder rewritten = queryBuilder.rewrite(context);
208-
assertThat(rewritten, instanceOf(TermQueryBuilder.class));
209-
TermQueryBuilder tqb = (TermQueryBuilder) rewritten;
210-
assertEquals(TEXT_FIELD_NAME, tqb.fieldName);
211-
assertEquals(new BytesRef("value"), tqb.value);
207+
for (QueryRewriteContext context : new QueryRewriteContext[] { createSearchExecutionContext(), createQueryRewriteContext() }) {
208+
QueryBuilder rewritten = queryBuilder.rewrite(context);
209+
assertThat(rewritten, instanceOf(TermQueryBuilder.class));
210+
TermQueryBuilder tqb = (TermQueryBuilder) rewritten;
211+
assertEquals(TEXT_FIELD_NAME, tqb.fieldName);
212+
assertEquals(new BytesRef("value"), tqb.value);
213+
}
212214
}
213215

214216
public void testRewriteIndexQueryToMatchNone() throws IOException {
215217
QueryBuilder query = new MatchPhraseQueryBuilder("_index", "does_not_exist");
216-
SearchExecutionContext searchExecutionContext = createSearchExecutionContext();
217-
QueryBuilder rewritten = query.rewrite(searchExecutionContext);
218-
assertThat(rewritten, instanceOf(MatchNoneQueryBuilder.class));
218+
for (QueryRewriteContext context : new QueryRewriteContext[] { createSearchExecutionContext(), createQueryRewriteContext() }) {
219+
QueryBuilder rewritten = query.rewrite(context);
220+
assertThat(rewritten, instanceOf(MatchNoneQueryBuilder.class));
221+
}
219222
}
220223

221224
public void testRewriteIndexQueryToNotMatchNone() throws IOException {
222225
QueryBuilder query = new MatchPhraseQueryBuilder("_index", getIndex().getName());
223-
SearchExecutionContext searchExecutionContext = createSearchExecutionContext();
224-
QueryBuilder rewritten = query.rewrite(searchExecutionContext);
225-
assertThat(rewritten, instanceOf(MatchAllQueryBuilder.class));
226+
for (QueryRewriteContext context : new QueryRewriteContext[] { createSearchExecutionContext(), createQueryRewriteContext() }) {
227+
QueryBuilder rewritten = query.rewrite(context);
228+
assertThat(rewritten, instanceOf(MatchAllQueryBuilder.class));
229+
}
226230
}
227231
}

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -205,16 +205,18 @@ public void testParseAndSerializeBigInteger() throws IOException {
205205

206206
public void testRewriteIndexQueryToMatchNone() throws IOException {
207207
TermQueryBuilder query = QueryBuilders.termQuery("_index", "does_not_exist");
208-
SearchExecutionContext searchExecutionContext = createSearchExecutionContext();
209-
QueryBuilder rewritten = query.rewrite(searchExecutionContext);
210-
assertThat(rewritten, instanceOf(MatchNoneQueryBuilder.class));
208+
for (QueryRewriteContext context : new QueryRewriteContext[] { createSearchExecutionContext(), createQueryRewriteContext() }) {
209+
QueryBuilder rewritten = query.rewrite(context);
210+
assertThat(rewritten, instanceOf(MatchNoneQueryBuilder.class));
211+
}
211212
}
212213

213214
public void testRewriteIndexQueryToNotMatchNone() throws IOException {
214215
TermQueryBuilder query = QueryBuilders.termQuery("_index", getIndex().getName());
215-
SearchExecutionContext searchExecutionContext = createSearchExecutionContext();
216-
QueryBuilder rewritten = query.rewrite(searchExecutionContext);
217-
assertThat(rewritten, instanceOf(MatchAllQueryBuilder.class));
216+
for (QueryRewriteContext context : new QueryRewriteContext[] { createSearchExecutionContext(), createQueryRewriteContext() }) {
217+
QueryBuilder rewritten = query.rewrite(context);
218+
assertThat(rewritten, instanceOf(MatchAllQueryBuilder.class));
219+
}
218220
}
219221

220222
@Override

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,13 +1027,15 @@ public void testCanMatch() throws Exception {
10271027

10281028
CountDownLatch latch = new CountDownLatch(1);
10291029
SearchShardTask task = new SearchShardTask(123L, "", "", "", null, Collections.emptyMap());
1030-
assertEquals(7, numWrapInvocations.get());
1031-
service.executeQueryPhase(request, task, new ActionListener<SearchPhaseResult>() {
1030+
// Because the foo field used in alias filter is unmapped the term query builder rewrite can resolve to a match no docs query,
1031+
// without acquiring a searcher and that means the wrapper is not called
1032+
assertEquals(5, numWrapInvocations.get());
1033+
service.executeQueryPhase(request, task, new ActionListener<>() {
10321034
@Override
10331035
public void onResponse(SearchPhaseResult searchPhaseResult) {
10341036
try {
10351037
// make sure that the wrapper is called when the query is actually executed
1036-
assertEquals(8, numWrapInvocations.get());
1038+
assertEquals(6, numWrapInvocations.get());
10371039
} finally {
10381040
latch.countDown();
10391041
}

0 commit comments

Comments
 (0)