Skip to content

Commit 7b64410

Browse files
authored
Avoid reloading _source for every inner hit. (#60494)
Previously if an inner_hits block required _ source, we would reload and parse the root document's source for every hit. This PR adds a shared SourceLookup to the inner hits context that allows inner hits to reuse parsed source if it's already available. This matches our approach for sharing the root document ID. Relates to #32818.
1 parent ae01606 commit 7b64410

File tree

6 files changed

+209
-175
lines changed

6 files changed

+209
-175
lines changed

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

Lines changed: 60 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -97,83 +97,75 @@ static final class JoinFieldInnerHitSubContext extends InnerHitsContext.InnerHit
9797
}
9898

9999
@Override
100-
public TopDocsAndMaxScore[] topDocs(SearchHit[] hits) throws IOException {
101-
Weight innerHitQueryWeight = createInnerHitQueryWeight();
102-
TopDocsAndMaxScore[] result = new TopDocsAndMaxScore[hits.length];
103-
for (int i = 0; i < hits.length; i++) {
104-
SearchHit hit = hits[i];
105-
String joinName = getSortedDocValue(joinFieldMapper.name(), context, hit.docId());
106-
if (joinName == null) {
107-
result[i] = new TopDocsAndMaxScore(Lucene.EMPTY_TOP_DOCS, Float.NaN);
108-
continue;
109-
}
100+
public TopDocsAndMaxScore topDocs(SearchHit hit) throws IOException {
101+
Weight innerHitQueryWeight = getInnerHitQueryWeight();
102+
String joinName = getSortedDocValue(joinFieldMapper.name(), context, hit.docId());
103+
if (joinName == null) {
104+
return new TopDocsAndMaxScore(Lucene.EMPTY_TOP_DOCS, Float.NaN);
105+
}
110106

111-
QueryShardContext qsc = context.getQueryShardContext();
112-
ParentIdFieldMapper parentIdFieldMapper =
113-
joinFieldMapper.getParentIdFieldMapper(typeName, fetchChildInnerHits == false);
114-
if (parentIdFieldMapper == null) {
115-
result[i] = new TopDocsAndMaxScore(Lucene.EMPTY_TOP_DOCS, Float.NaN);
116-
continue;
117-
}
107+
QueryShardContext qsc = context.getQueryShardContext();
108+
ParentIdFieldMapper parentIdFieldMapper =
109+
joinFieldMapper.getParentIdFieldMapper(typeName, fetchChildInnerHits == false);
110+
if (parentIdFieldMapper == null) {
111+
return new TopDocsAndMaxScore(Lucene.EMPTY_TOP_DOCS, Float.NaN);
112+
}
118113

119-
Query q;
120-
if (fetchChildInnerHits) {
121-
Query hitQuery = parentIdFieldMapper.fieldType().termQuery(hit.getId(), qsc);
122-
q = new BooleanQuery.Builder()
123-
// Only include child documents that have the current hit as parent:
124-
.add(hitQuery, BooleanClause.Occur.FILTER)
125-
// and only include child documents of a single relation:
126-
.add(joinFieldMapper.fieldType().termQuery(typeName, qsc), BooleanClause.Occur.FILTER)
127-
.build();
128-
} else {
129-
String parentId = getSortedDocValue(parentIdFieldMapper.name(), context, hit.docId());
130-
if (parentId == null) {
131-
result[i] = new TopDocsAndMaxScore(Lucene.EMPTY_TOP_DOCS, Float.NaN);
132-
continue;
133-
}
134-
q = context.mapperService().fieldType(IdFieldMapper.NAME).termQuery(parentId, qsc);
114+
Query q;
115+
if (fetchChildInnerHits) {
116+
Query hitQuery = parentIdFieldMapper.fieldType().termQuery(hit.getId(), qsc);
117+
q = new BooleanQuery.Builder()
118+
// Only include child documents that have the current hit as parent:
119+
.add(hitQuery, BooleanClause.Occur.FILTER)
120+
// and only include child documents of a single relation:
121+
.add(joinFieldMapper.fieldType().termQuery(typeName, qsc), BooleanClause.Occur.FILTER)
122+
.build();
123+
} else {
124+
String parentId = getSortedDocValue(parentIdFieldMapper.name(), context, hit.docId());
125+
if (parentId == null) {
126+
return new TopDocsAndMaxScore(Lucene.EMPTY_TOP_DOCS, Float.NaN);
135127
}
128+
q = context.mapperService().fieldType(IdFieldMapper.NAME).termQuery(parentId, qsc);
129+
}
136130

137-
Weight weight = context.searcher().createWeight(context.searcher().rewrite(q), ScoreMode.COMPLETE_NO_SCORES, 1f);
138-
if (size() == 0) {
139-
TotalHitCountCollector totalHitCountCollector = new TotalHitCountCollector();
140-
for (LeafReaderContext ctx : context.searcher().getIndexReader().leaves()) {
141-
intersect(weight, innerHitQueryWeight, totalHitCountCollector, ctx);
142-
}
143-
result[i] = new TopDocsAndMaxScore(
144-
new TopDocs(
145-
new TotalHits(totalHitCountCollector.getTotalHits(), TotalHits.Relation.EQUAL_TO),
146-
Lucene.EMPTY_SCORE_DOCS
147-
), Float.NaN);
148-
} else {
149-
int topN = Math.min(from() + size(), context.searcher().getIndexReader().maxDoc());
150-
TopDocsCollector<?> topDocsCollector;
151-
MaxScoreCollector maxScoreCollector = null;
152-
if (sort() != null) {
153-
topDocsCollector = TopFieldCollector.create(sort().sort, topN, Integer.MAX_VALUE);
154-
if (trackScores()) {
155-
maxScoreCollector = new MaxScoreCollector();
156-
}
157-
} else {
158-
topDocsCollector = TopScoreDocCollector.create(topN, Integer.MAX_VALUE);
131+
Weight weight = context.searcher().createWeight(context.searcher().rewrite(q), ScoreMode.COMPLETE_NO_SCORES, 1f);
132+
if (size() == 0) {
133+
TotalHitCountCollector totalHitCountCollector = new TotalHitCountCollector();
134+
for (LeafReaderContext ctx : context.searcher().getIndexReader().leaves()) {
135+
intersect(weight, innerHitQueryWeight, totalHitCountCollector, ctx);
136+
}
137+
return new TopDocsAndMaxScore(
138+
new TopDocs(
139+
new TotalHits(totalHitCountCollector.getTotalHits(), TotalHits.Relation.EQUAL_TO),
140+
Lucene.EMPTY_SCORE_DOCS
141+
), Float.NaN);
142+
} else {
143+
int topN = Math.min(from() + size(), context.searcher().getIndexReader().maxDoc());
144+
TopDocsCollector<?> topDocsCollector;
145+
MaxScoreCollector maxScoreCollector = null;
146+
if (sort() != null) {
147+
topDocsCollector = TopFieldCollector.create(sort().sort, topN, Integer.MAX_VALUE);
148+
if (trackScores()) {
159149
maxScoreCollector = new MaxScoreCollector();
160150
}
161-
try {
162-
for (LeafReaderContext ctx : context.searcher().getIndexReader().leaves()) {
163-
intersect(weight, innerHitQueryWeight, MultiCollector.wrap(topDocsCollector, maxScoreCollector), ctx);
164-
}
165-
} finally {
166-
clearReleasables(Lifetime.COLLECTION);
167-
}
168-
TopDocs topDocs = topDocsCollector.topDocs(from(), size());
169-
float maxScore = Float.NaN;
170-
if (maxScoreCollector != null) {
171-
maxScore = maxScoreCollector.getMaxScore();
151+
} else {
152+
topDocsCollector = TopScoreDocCollector.create(topN, Integer.MAX_VALUE);
153+
maxScoreCollector = new MaxScoreCollector();
154+
}
155+
try {
156+
for (LeafReaderContext ctx : context.searcher().getIndexReader().leaves()) {
157+
intersect(weight, innerHitQueryWeight, MultiCollector.wrap(topDocsCollector, maxScoreCollector), ctx);
172158
}
173-
result[i] = new TopDocsAndMaxScore(topDocs, maxScore);
159+
} finally {
160+
clearReleasables(Lifetime.COLLECTION);
161+
}
162+
TopDocs topDocs = topDocsCollector.topDocs(from(), size());
163+
float maxScore = Float.NaN;
164+
if (maxScoreCollector != null) {
165+
maxScore = maxScoreCollector.getMaxScore();
174166
}
167+
return new TopDocsAndMaxScore(topDocs, maxScore);
175168
}
176-
return result;
177169
}
178170

179171
private String getSortedDocValue(String field, SearchContext context, int docId) {

server/src/internalClusterTest/java/org/elasticsearch/search/fetch/subphase/InnerHitsIT.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,17 @@ public void testNestedSource() throws Exception {
644644
assertHitCount(response, 1);
645645
assertThat(response.getHits().getAt(0).getInnerHits().get("comments").getTotalHits().value, equalTo(1L));
646646
assertThat(response.getHits().getAt(0).getInnerHits().get("comments").getAt(0).getSourceAsMap().size(), equalTo(0));
647+
648+
// Check that inner hits contain _source even when it's disabled on the root request.
649+
response = client().prepareSearch()
650+
.setFetchSource(false)
651+
.setQuery(nestedQuery("comments", matchQuery("comments.message", "fox"), ScoreMode.None)
652+
.innerHit(new InnerHitBuilder()))
653+
.get();
654+
assertNoFailures(response);
655+
assertHitCount(response, 1);
656+
assertThat(response.getHits().getAt(0).getInnerHits().get("comments").getTotalHits().value, equalTo(2L));
657+
assertFalse(response.getHits().getAt(0).getInnerHits().get("comments").getAt(0).getSourceAsMap().isEmpty());
647658
}
648659

649660
public void testInnerHitsWithIgnoreUnmapped() throws Exception {

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

Lines changed: 45 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -388,61 +388,57 @@ public void seqNoAndPrimaryTerm(boolean seqNoAndPrimaryTerm) {
388388
}
389389

390390
@Override
391-
public TopDocsAndMaxScore[] topDocs(SearchHit[] hits) throws IOException {
392-
Weight innerHitQueryWeight = createInnerHitQueryWeight();
393-
TopDocsAndMaxScore[] result = new TopDocsAndMaxScore[hits.length];
394-
for (int i = 0; i < hits.length; i++) {
395-
SearchHit hit = hits[i];
396-
Query rawParentFilter;
397-
if (parentObjectMapper == null) {
398-
rawParentFilter = Queries.newNonNestedFilter();
399-
} else {
400-
rawParentFilter = parentObjectMapper.nestedTypeFilter();
401-
}
391+
public TopDocsAndMaxScore topDocs(SearchHit hit) throws IOException {
392+
Weight innerHitQueryWeight = getInnerHitQueryWeight();
402393

403-
int parentDocId = hit.docId();
404-
final int readerIndex = ReaderUtil.subIndex(parentDocId, searcher().getIndexReader().leaves());
405-
// With nested inner hits the nested docs are always in the same segement, so need to use the other segments
406-
LeafReaderContext ctx = searcher().getIndexReader().leaves().get(readerIndex);
407-
408-
Query childFilter = childObjectMapper.nestedTypeFilter();
409-
BitSetProducer parentFilter = context.bitsetFilterCache().getBitSetProducer(rawParentFilter);
410-
Query q = new ParentChildrenBlockJoinQuery(parentFilter, childFilter, parentDocId);
411-
Weight weight = context.searcher().createWeight(context.searcher().rewrite(q),
412-
org.apache.lucene.search.ScoreMode.COMPLETE_NO_SCORES, 1f);
413-
if (size() == 0) {
414-
TotalHitCountCollector totalHitCountCollector = new TotalHitCountCollector();
415-
intersect(weight, innerHitQueryWeight, totalHitCountCollector, ctx);
416-
result[i] = new TopDocsAndMaxScore(new TopDocs(new TotalHits(totalHitCountCollector.getTotalHits(),
417-
TotalHits.Relation.EQUAL_TO), Lucene.EMPTY_SCORE_DOCS), Float.NaN);
418-
} else {
419-
int topN = Math.min(from() + size(), context.searcher().getIndexReader().maxDoc());
420-
TopDocsCollector<?> topDocsCollector;
421-
MaxScoreCollector maxScoreCollector = null;
422-
if (sort() != null) {
423-
topDocsCollector = TopFieldCollector.create(sort().sort, topN, Integer.MAX_VALUE);
424-
if (trackScores()) {
425-
maxScoreCollector = new MaxScoreCollector();
426-
}
427-
} else {
428-
topDocsCollector = TopScoreDocCollector.create(topN, Integer.MAX_VALUE);
394+
Query rawParentFilter;
395+
if (parentObjectMapper == null) {
396+
rawParentFilter = Queries.newNonNestedFilter();
397+
} else {
398+
rawParentFilter = parentObjectMapper.nestedTypeFilter();
399+
}
400+
401+
int parentDocId = hit.docId();
402+
final int readerIndex = ReaderUtil.subIndex(parentDocId, searcher().getIndexReader().leaves());
403+
// With nested inner hits the nested docs are always in the same segement, so need to use the other segments
404+
LeafReaderContext ctx = searcher().getIndexReader().leaves().get(readerIndex);
405+
406+
Query childFilter = childObjectMapper.nestedTypeFilter();
407+
BitSetProducer parentFilter = context.bitsetFilterCache().getBitSetProducer(rawParentFilter);
408+
Query q = new ParentChildrenBlockJoinQuery(parentFilter, childFilter, parentDocId);
409+
Weight weight = context.searcher().createWeight(context.searcher().rewrite(q),
410+
org.apache.lucene.search.ScoreMode.COMPLETE_NO_SCORES, 1f);
411+
if (size() == 0) {
412+
TotalHitCountCollector totalHitCountCollector = new TotalHitCountCollector();
413+
intersect(weight, innerHitQueryWeight, totalHitCountCollector, ctx);
414+
return new TopDocsAndMaxScore(new TopDocs(new TotalHits(totalHitCountCollector.getTotalHits(),
415+
TotalHits.Relation.EQUAL_TO), Lucene.EMPTY_SCORE_DOCS), Float.NaN);
416+
} else {
417+
int topN = Math.min(from() + size(), context.searcher().getIndexReader().maxDoc());
418+
TopDocsCollector<?> topDocsCollector;
419+
MaxScoreCollector maxScoreCollector = null;
420+
if (sort() != null) {
421+
topDocsCollector = TopFieldCollector.create(sort().sort, topN, Integer.MAX_VALUE);
422+
if (trackScores()) {
429423
maxScoreCollector = new MaxScoreCollector();
430424
}
431-
try {
432-
intersect(weight, innerHitQueryWeight, MultiCollector.wrap(topDocsCollector, maxScoreCollector), ctx);
433-
} finally {
434-
clearReleasables(Lifetime.COLLECTION);
435-
}
425+
} else {
426+
topDocsCollector = TopScoreDocCollector.create(topN, Integer.MAX_VALUE);
427+
maxScoreCollector = new MaxScoreCollector();
428+
}
429+
try {
430+
intersect(weight, innerHitQueryWeight, MultiCollector.wrap(topDocsCollector, maxScoreCollector), ctx);
431+
} finally {
432+
clearReleasables(Lifetime.COLLECTION);
433+
}
436434

437-
TopDocs td = topDocsCollector.topDocs(from(), size());
438-
float maxScore = Float.NaN;
439-
if (maxScoreCollector != null) {
440-
maxScore = maxScoreCollector.getMaxScore();
441-
}
442-
result[i] = new TopDocsAndMaxScore(td, maxScore);
435+
TopDocs td = topDocsCollector.topDocs(from(), size());
436+
float maxScore = Float.NaN;
437+
if (maxScoreCollector != null) {
438+
maxScore = maxScoreCollector.getMaxScore();
443439
}
440+
return new TopDocsAndMaxScore(td, maxScore);
444441
}
445-
return result;
446442
}
447443
}
448444
}

0 commit comments

Comments
 (0)