Skip to content

Commit e60676b

Browse files
committed
Refactor InnerHitsRewriteContext into a more generic PerDocumentQueryRewriteContext
The DFS and highlight phases require rewriting the Lucene query outside of the query phase. However, if the query contains a k-NN query, this triggers a nearest neighbor search on the entire shard, which is unnecessary in these phases since computing top-N results is not required. This change builds upon #104006, applying the same transformation used for nested inner hits. As a result, DFS and highlight phases avoid wasting time and resources on costly nearest neighbor searches. Note: The explain and matched query phases are also affected but still require the nearest neighbor search for accurate results, so they remain unchanged for now.
1 parent 2e84950 commit e60676b

File tree

24 files changed

+166
-74
lines changed

24 files changed

+166
-74
lines changed

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ protected void doBuild(SearchContext context, InnerHitsContext innerHitsContext)
7070
String name = innerHitBuilder.getName() != null ? innerHitBuilder.getName() : typeName;
7171
JoinFieldInnerHitSubContext joinFieldInnerHits = new JoinFieldInnerHitSubContext(
7272
name,
73+
query,
7374
context,
7475
typeName,
7576
fetchChildInnerHits,
@@ -89,8 +90,15 @@ static final class JoinFieldInnerHitSubContext extends InnerHitsContext.InnerHit
8990
private final boolean fetchChildInnerHits;
9091
private final Joiner joiner;
9192

92-
JoinFieldInnerHitSubContext(String name, SearchContext context, String typeName, boolean fetchChildInnerHits, Joiner joiner) {
93-
super(name, context);
93+
JoinFieldInnerHitSubContext(
94+
String name,
95+
QueryBuilder query,
96+
SearchContext context,
97+
String typeName,
98+
boolean fetchChildInnerHits,
99+
Joiner joiner
100+
) {
101+
super(name, query, context);
94102
this.typeName = typeName;
95103
this.fetchChildInnerHits = fetchChildInnerHits;
96104
this.joiner = joiner;

server/src/internalClusterTest/java/org/elasticsearch/search/retriever/RetrieverRewriteIT.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ public QueryBuilder topDocsQuery() {
151151
@Override
152152
public RetrieverBuilder rewrite(QueryRewriteContext ctx) throws IOException {
153153
assertNull(ctx.getPointInTimeBuilder());
154-
assertNull(ctx.convertToInnerHitsRewriteContext());
154+
assertNull(ctx.convertToPerDocumentQueryRewriteContext());
155155
assertNull(ctx.convertToCoordinatorRewriteContext());
156156
assertNull(ctx.convertToIndexMetadataContext());
157157
assertNull(ctx.convertToSearchExecutionContext());
@@ -215,7 +215,7 @@ public QueryBuilder topDocsQuery() {
215215
@Override
216216
public RetrieverBuilder rewrite(QueryRewriteContext ctx) throws IOException {
217217
assertNotNull(ctx.getPointInTimeBuilder());
218-
assertNull(ctx.convertToInnerHitsRewriteContext());
218+
assertNull(ctx.convertToPerDocumentQueryRewriteContext());
219219
assertNull(ctx.convertToCoordinatorRewriteContext());
220220
assertNull(ctx.convertToIndexMetadataContext());
221221
assertNull(ctx.convertToSearchExecutionContext());

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws
306306
if (queryRewriteContext == null) {
307307
return this;
308308
}
309-
final InnerHitsRewriteContext ihrc = queryRewriteContext.convertToInnerHitsRewriteContext();
309+
final PerDocumentQueryRewriteContext ihrc = queryRewriteContext.convertToPerDocumentQueryRewriteContext();
310310
if (ihrc != null) {
311311
return doInnerHitsRewrite(ihrc);
312312
}
@@ -358,11 +358,11 @@ protected QueryBuilder doIndexMetadataRewrite(final QueryRewriteContext context)
358358

359359
/**
360360
* Optional rewrite logic that allows for optimization for extracting inner hits
361-
* @param context an {@link InnerHitsRewriteContext} instance
361+
* @param context an {@link PerDocumentQueryRewriteContext} instance
362362
* @return A {@link QueryBuilder} representing the rewritten query optimized for inner hit extraction
363363
* @throws IOException if an error occurs while rewriting the query
364364
*/
365-
protected QueryBuilder doInnerHitsRewrite(final InnerHitsRewriteContext context) throws IOException {
365+
protected QueryBuilder doInnerHitsRewrite(final PerDocumentQueryRewriteContext context) throws IOException {
366366
return this;
367367
}
368368

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

Lines changed: 0 additions & 35 deletions
This file was deleted.

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,8 +380,16 @@ protected void doBuild(SearchContext parentSearchContext, InnerHitsContext inner
380380
}
381381
String name = innerHitBuilder.getName() != null ? innerHitBuilder.getName() : nestedMapper.fullPath();
382382
NestedObjectMapper parentObjectMapper = searchExecutionContext.nestedScope().nextLevel(nestedMapper);
383+
384+
// We rewrite the query to ensure that child documents can receive scores from approximate nearest neighbor queries.
385+
PerDocumentQueryRewriteContext innerHitsRewriteContext = new PerDocumentQueryRewriteContext(
386+
searchExecutionContext.getParserConfig(),
387+
searchExecutionContext.nowInMillis
388+
);
389+
var childQuery = Rewriteable.rewrite(query, innerHitsRewriteContext, true);
383390
NestedInnerHitSubContext nestedInnerHits = new NestedInnerHitSubContext(
384391
name,
392+
childQuery,
385393
parentSearchContext,
386394
parentObjectMapper,
387395
nestedMapper
@@ -399,11 +407,12 @@ static final class NestedInnerHitSubContext extends InnerHitsContext.InnerHitSub
399407

400408
NestedInnerHitSubContext(
401409
String name,
410+
QueryBuilder query,
402411
SearchContext context,
403412
NestedObjectMapper parentObjectMapper,
404413
NestedObjectMapper childObjectMapper
405414
) {
406-
super(name, context);
415+
super(name, query, context);
407416
this.parentObjectMapper = parentObjectMapper;
408417
this.childObjectMapper = childObjectMapper;
409418
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
package org.elasticsearch.index.query;
10+
11+
import org.elasticsearch.action.ActionListener;
12+
import org.elasticsearch.xcontent.XContentParserConfiguration;
13+
14+
import java.util.function.LongSupplier;
15+
16+
/**
17+
* A context object for rewriting {@link QueryBuilder} instances to optimize query execution
18+
* by reducing the global operations required for processing Lucene queries on a selection of top documents.
19+
*
20+
* This rewrite context ensures that expensive operations, such as approximate nearest neighbor (ANN) searches,
21+
* are avoided when evaluating documents in isolation. Instead, ANN queries are rewritten into exact k-NN queries,
22+
* which execute efficiently when processing only a small subset of documents (or none at all).
23+
*
24+
* Additionally, this context is beneficial for nested inner hits, allowing child documents to receive
25+
* independent similarity scores regardless of whether they matched the approximate nearest neighbor query.
26+
*/
27+
public final class PerDocumentQueryRewriteContext extends QueryRewriteContext {
28+
public PerDocumentQueryRewriteContext(final XContentParserConfiguration parserConfiguration, final LongSupplier nowInMillis) {
29+
super(parserConfiguration, null, nowInMillis);
30+
}
31+
32+
@Override
33+
public PerDocumentQueryRewriteContext convertToPerDocumentQueryRewriteContext() {
34+
return this;
35+
}
36+
37+
@Override
38+
public void executeAsyncActions(ActionListener<Void> listener) {
39+
// PerDocumentQueryRewriteContext does not support async actions at all, and doesn't supply a valid `client` object
40+
throw new UnsupportedOperationException("PerDocumentQueryRewriteContext does not support async actions");
41+
}
42+
43+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ public DataRewriteContext convertToDataRewriteContext() {
223223
return null;
224224
}
225225

226-
public InnerHitsRewriteContext convertToInnerHitsRewriteContext() {
226+
public PerDocumentQueryRewriteContext convertToPerDocumentQueryRewriteContext() {
227227
return null;
228228
}
229229

server/src/main/java/org/elasticsearch/search/DefaultSearchContext.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.elasticsearch.index.mapper.NestedLookup;
4646
import org.elasticsearch.index.mapper.SourceLoader;
4747
import org.elasticsearch.index.query.AbstractQueryBuilder;
48+
import org.elasticsearch.index.query.MatchAllQueryBuilder;
4849
import org.elasticsearch.index.query.ParsedQuery;
4950
import org.elasticsearch.index.query.QueryBuilder;
5051
import org.elasticsearch.index.query.SearchExecutionContext;
@@ -486,6 +487,11 @@ public String source() {
486487
return "search";
487488
}
488489

490+
@Override
491+
public QueryBuilder userQueryBuilder() {
492+
return request.source() != null ? request.source().query() : new MatchAllQueryBuilder();
493+
}
494+
489495
@Override
490496
public ShardSearchRequest request() {
491497
return this.request;

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

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@
6060
import org.elasticsearch.index.engine.Engine;
6161
import org.elasticsearch.index.query.CoordinatorRewriteContextProvider;
6262
import org.elasticsearch.index.query.InnerHitContextBuilder;
63-
import org.elasticsearch.index.query.InnerHitsRewriteContext;
6463
import org.elasticsearch.index.query.MatchAllQueryBuilder;
6564
import org.elasticsearch.index.query.MatchNoneQueryBuilder;
6665
import org.elasticsearch.index.query.QueryBuilder;
@@ -1341,22 +1340,16 @@ private void parseSource(DefaultSearchContext context, SearchSourceBuilder sourc
13411340
context.size(source.size());
13421341
Map<String, InnerHitContextBuilder> innerHitBuilders = new HashMap<>();
13431342
QueryBuilder query = source.query();
1344-
InnerHitsRewriteContext innerHitsRewriteContext = new InnerHitsRewriteContext(
1345-
context.getSearchExecutionContext().getParserConfig(),
1346-
context::getRelativeTimeInMillis
1347-
);
13481343
if (query != null) {
1349-
QueryBuilder rewrittenForInnerHits = Rewriteable.rewrite(query, innerHitsRewriteContext, true);
13501344
if (false == source.skipInnerHits()) {
1351-
InnerHitContextBuilder.extractInnerHits(rewrittenForInnerHits, innerHitBuilders);
1345+
InnerHitContextBuilder.extractInnerHits(query, innerHitBuilders);
13521346
}
13531347
searchExecutionContext.setAliasFilter(context.request().getAliasFilter().getQueryBuilder());
13541348
context.parsedQuery(searchExecutionContext.toQuery(query));
13551349
}
13561350
if (source.postFilter() != null) {
1357-
QueryBuilder rewrittenForInnerHits = Rewriteable.rewrite(source.postFilter(), innerHitsRewriteContext, true);
13581351
if (false == source.skipInnerHits()) {
1359-
InnerHitContextBuilder.extractInnerHits(rewrittenForInnerHits, innerHitBuilders);
1352+
InnerHitContextBuilder.extractInnerHits(query, innerHitBuilders);
13601353
}
13611354
context.parsedPostFilter(searchExecutionContext.toQuery(source.postFilter()));
13621355
}

server/src/main/java/org/elasticsearch/search/dfs/DfsPhase.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import org.apache.lucene.search.TopDocs;
2121
import org.apache.lucene.search.TopScoreDocCollectorManager;
2222
import org.elasticsearch.index.query.ParsedQuery;
23+
import org.elasticsearch.index.query.PerDocumentQueryRewriteContext;
24+
import org.elasticsearch.index.query.Rewriteable;
2325
import org.elasticsearch.index.query.SearchExecutionContext;
2426
import org.elasticsearch.search.builder.SearchSourceBuilder;
2527
import org.elasticsearch.search.internal.ContextIndexSearcher;
@@ -119,9 +121,19 @@ public CollectionStatistics collectionStatistics(String field) throws IOExceptio
119121
}
120122

121123
try {
124+
// We apply the per-document rewrite context to prevent costly nearest neighbor searches
125+
// during query rewriting. Since we are only extracting term statistics and not executing
126+
// the query, these expensive operations are unnecessary.
127+
PerDocumentQueryRewriteContext rewriteContext = new PerDocumentQueryRewriteContext(
128+
context.getSearchExecutionContext().getParserConfig(),
129+
context.getSearchExecutionContext()::nowInMillis
130+
);
131+
Query query = Rewriteable.rewrite(context.userQueryBuilder(), rewriteContext, true)
132+
.toQuery(context.getSearchExecutionContext());
133+
122134
Timer timer = maybeStartTimer(profiler, DfsTimingType.CREATE_WEIGHT);
123135
try {
124-
searcher.createWeight(context.rewrittenQuery(), ScoreMode.COMPLETE, 1);
136+
searcher.createWeight(searcher.rewrite(query), ScoreMode.COMPLETE, 1);
125137
} finally {
126138
if (timer != null) {
127139
timer.stop();

0 commit comments

Comments
 (0)