Skip to content

Commit b79e03a

Browse files
authored
Improve exists query rewrite. (#97159)
The ExistsQueryBuilder rewrite only depends on information that exists in mappings. Therefor the rewrite can be done as index metadata rewrite. Practically this means the rewrite can occur during can match phase before opening a searcher or doing any potential refresh. The `getMatchingFieldNames(...)` method was moved from SearchExecutionContext to QueryRewriteContext. As an additional change, the QueryRewriteContext#doSearchRewrite(...) method now by default delegates to doIndexMetadataRewrite(...). This is because a search rewrite by default should be compatible with a index metadata rewrite.
1 parent 5e0cf56 commit b79e03a

File tree

8 files changed

+62
-50
lines changed

8 files changed

+62
-50
lines changed

docs/changelog/97159.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 97159
2+
summary: Improve exists query rewrite
3+
area: Search
4+
type: enhancement
5+
issues: []

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ protected QueryBuilder doCoordinatorRewrite(final CoordinatorRewriteContext coor
322322
* @return A {@link QueryBuilder} representing the rewritten query.
323323
*/
324324
protected QueryBuilder doSearchRewrite(final SearchExecutionContext searchExecutionContext) throws IOException {
325-
return this;
325+
return doIndexMetadataRewrite(searchExecutionContext);
326326
}
327327

328328
/**

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,12 @@ public String fieldName() {
6666
}
6767

6868
@Override
69-
protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException {
70-
SearchExecutionContext context = queryRewriteContext.convertToSearchExecutionContext();
71-
if (context != null) {
72-
if (getMappedFields(context, fieldName).isEmpty()) {
73-
return new MatchNoneQueryBuilder();
74-
}
69+
protected QueryBuilder doIndexMetadataRewrite(QueryRewriteContext context) throws IOException {
70+
if (getMappedFields(context, fieldName).isEmpty()) {
71+
return new MatchNoneQueryBuilder();
72+
} else {
73+
return this;
7574
}
76-
return super.doRewrite(queryRewriteContext);
7775
}
7876

7977
@Override
@@ -153,7 +151,7 @@ public static Query newFilter(SearchExecutionContext context, String fieldPatter
153151
return new ConstantScoreQuery(boolFilterBuilder.build());
154152
}
155153

156-
private static Collection<String> getMappedFields(SearchExecutionContext context, String fieldPattern) {
154+
private static Collection<String> getMappedFields(QueryRewriteContext context, String fieldPattern) {
157155
Set<String> matchingFieldNames = context.getMatchingFieldNames(fieldPattern);
158156
if (matchingFieldNames.isEmpty()) {
159157
// might be an object field, so try matching it as an object prefix pattern

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,6 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep
156156
builder.endObject();
157157
}
158158

159-
@Override
160-
protected QueryBuilder doSearchRewrite(SearchExecutionContext searchExecutionContext) throws IOException {
161-
return doIndexMetadataRewrite(searchExecutionContext);
162-
}
163-
164159
@Override
165160
protected QueryBuilder doIndexMetadataRewrite(QueryRewriteContext context) throws IOException {
166161
// If we're using the default keyword analyzer then we can rewrite this to a TermQueryBuilder

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.elasticsearch.action.ActionListener;
1111
import org.elasticsearch.client.internal.Client;
1212
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
13+
import org.elasticsearch.common.regex.Regex;
1314
import org.elasticsearch.common.util.concurrent.CountDown;
1415
import org.elasticsearch.index.Index;
1516
import org.elasticsearch.index.IndexSettings;
@@ -26,9 +27,11 @@
2627

2728
import java.util.ArrayList;
2829
import java.util.Collections;
30+
import java.util.HashSet;
2931
import java.util.List;
3032
import java.util.Map;
3133
import java.util.Objects;
34+
import java.util.Set;
3235
import java.util.function.BiConsumer;
3336
import java.util.function.BooleanSupplier;
3437
import java.util.function.LongSupplier;
@@ -289,4 +292,34 @@ public boolean indexMatches(String pattern) {
289292
assert indexNameMatcher != null;
290293
return indexNameMatcher.test(pattern);
291294
}
295+
296+
/**
297+
* Returns the names of all mapped fields that match a given pattern
298+
*
299+
* All names returned by this method are guaranteed to resolve to a
300+
* MappedFieldType if passed to {@link #getFieldType(String)}
301+
*
302+
* @param pattern the field name pattern
303+
*/
304+
public Set<String> getMatchingFieldNames(String pattern) {
305+
if (runtimeMappings.isEmpty()) {
306+
return mappingLookup.getMatchingFieldNames(pattern);
307+
}
308+
Set<String> matches = new HashSet<>(mappingLookup.getMatchingFieldNames(pattern));
309+
if ("*".equals(pattern)) {
310+
matches.addAll(runtimeMappings.keySet());
311+
} else if (Regex.isSimpleMatchPattern(pattern) == false) {
312+
// no wildcard
313+
if (runtimeMappings.containsKey(pattern)) {
314+
matches.add(pattern);
315+
}
316+
} else {
317+
for (String name : runtimeMappings.keySet()) {
318+
if (Regex.simpleMatch(pattern, name)) {
319+
matches.add(name);
320+
}
321+
}
322+
}
323+
return matches;
324+
}
292325
}

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

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import org.elasticsearch.common.ParsingException;
2525
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
2626
import org.elasticsearch.common.lucene.search.Queries;
27-
import org.elasticsearch.common.regex.Regex;
2827
import org.elasticsearch.common.settings.ClusterSettings;
2928
import org.elasticsearch.core.CheckedFunction;
3029
import org.elasticsearch.index.Index;
@@ -311,36 +310,6 @@ public boolean hasMappings() {
311310
return mappingLookup.hasMappings();
312311
}
313312

314-
/**
315-
* Returns the names of all mapped fields that match a given pattern
316-
*
317-
* All names returned by this method are guaranteed to resolve to a
318-
* MappedFieldType if passed to {@link #getFieldType(String)}
319-
*
320-
* @param pattern the field name pattern
321-
*/
322-
public Set<String> getMatchingFieldNames(String pattern) {
323-
if (runtimeMappings.isEmpty()) {
324-
return mappingLookup.getMatchingFieldNames(pattern);
325-
}
326-
Set<String> matches = new HashSet<>(mappingLookup.getMatchingFieldNames(pattern));
327-
if ("*".equals(pattern)) {
328-
matches.addAll(runtimeMappings.keySet());
329-
} else if (Regex.isSimpleMatchPattern(pattern) == false) {
330-
// no wildcard
331-
if (runtimeMappings.containsKey(pattern)) {
332-
matches.add(pattern);
333-
}
334-
} else {
335-
for (String name : runtimeMappings.keySet()) {
336-
if (Regex.simpleMatch(pattern, name)) {
337-
matches.add(name);
338-
}
339-
}
340-
}
341-
return matches;
342-
}
343-
344313
/**
345314
* Returns true if the field identified by the provided name is mapped, false otherwise
346315
*/

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,11 +167,6 @@ protected void addExtraXContent(XContentBuilder builder, Params params) throws I
167167
}
168168
}
169169

170-
@Override
171-
protected QueryBuilder doSearchRewrite(SearchExecutionContext searchExecutionContext) throws IOException {
172-
return doIndexMetadataRewrite(searchExecutionContext);
173-
}
174-
175170
@Override
176171
protected QueryBuilder doIndexMetadataRewrite(QueryRewriteContext context) throws IOException {
177172
MappedFieldType fieldType = context.getFieldType(this.fieldName);

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import static org.hamcrest.CoreMatchers.equalTo;
2626
import static org.hamcrest.CoreMatchers.instanceOf;
27+
import static org.hamcrest.CoreMatchers.sameInstance;
2728

2829
public class ExistsQueryBuilderTests extends AbstractQueryTestCase<ExistsQueryBuilder> {
2930
@Override
@@ -121,4 +122,20 @@ public void testFromJson() throws IOException {
121122
assertEquals(json, 42.0, parsed.boost(), 0.0001);
122123
assertEquals(json, "user", parsed.fieldName());
123124
}
125+
126+
public void testRewriteIndexQueryToMatchNone() throws IOException {
127+
ExistsQueryBuilder query = QueryBuilders.existsQuery("does_not_exist");
128+
for (QueryRewriteContext context : new QueryRewriteContext[] { createSearchExecutionContext(), createQueryRewriteContext() }) {
129+
QueryBuilder rewritten = query.rewrite(context);
130+
assertThat(rewritten, instanceOf(MatchNoneQueryBuilder.class));
131+
}
132+
}
133+
134+
public void testRewriteIndexQueryToNotMatchNone() throws IOException {
135+
ExistsQueryBuilder query = QueryBuilders.existsQuery(KEYWORD_FIELD_NAME);
136+
for (QueryRewriteContext context : new QueryRewriteContext[] { createSearchExecutionContext(), createQueryRewriteContext() }) {
137+
QueryBuilder rewritten = query.rewrite(context);
138+
assertThat(rewritten, sameInstance(query));
139+
}
140+
}
124141
}

0 commit comments

Comments
 (0)