Fix inner hits + aggregations concurrency bug (#128036)

Fork InnerHitSubContext instances before source is fetched in 
aggregations to prevent inter-segment race conditions.

Relates to #122419
This commit is contained in:
Ben Chaplin 2025-06-02 16:44:53 -04:00 committed by GitHub
parent 4762111b44
commit 13bce60be9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 219 additions and 1 deletions

View file

@ -26,6 +26,8 @@ import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.metrics.TopHits;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.FieldSortBuilder;
@ -51,6 +53,7 @@ import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_PRIMARY_T
import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_SEQ_NO;
import static org.elasticsearch.join.query.JoinQueryBuilders.hasChildQuery;
import static org.elasticsearch.join.query.JoinQueryBuilders.hasParentQuery;
import static org.elasticsearch.search.aggregations.AggregationBuilders.topHits;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCountAndNoFailures;
@ -64,6 +67,7 @@ import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
@ -698,4 +702,68 @@ public class InnerHitsIT extends ParentChildTestCase {
)
);
}
public void testTopHitsOnParentChild() throws Exception {
assertAcked(
prepareCreate("idx").setMapping(
jsonBuilder().startObject()
.startObject("_doc")
.startObject("properties")
.startObject("id")
.field("type", "keyword")
.endObject()
.startObject("join_field")
.field("type", "join")
.startObject("relations")
.field("parent", new String[] { "child1", "child2" })
.endObject()
.endObject()
.endObject()
.endObject()
.endObject()
)
);
ensureGreen("idx");
List<IndexRequestBuilder> requestBuilders = new ArrayList<>();
int numDocs = scaledRandomIntBetween(10, 100);
int child1 = 0;
int child2 = 0;
int[] child1InnerObjects = new int[numDocs];
int[] child2InnerObjects = new int[numDocs];
for (int parent = 0; parent < numDocs; parent++) {
String parentId = String.format(Locale.ENGLISH, "p_%03d", parent);
requestBuilders.add(createIndexRequest("idx", "parent", parentId, null));
int numChildDocs = child1InnerObjects[parent] = scaledRandomIntBetween(1, numDocs);
int limit = child1 + numChildDocs;
for (; child1 < limit; child1++) {
requestBuilders.add(createIndexRequest("idx", "child1", String.format(Locale.ENGLISH, "c1_%04d", child1), parentId));
}
numChildDocs = child2InnerObjects[parent] = scaledRandomIntBetween(1, numDocs);
limit = child2 + numChildDocs;
for (; child2 < limit; child2++) {
requestBuilders.add(createIndexRequest("idx", "child2", String.format(Locale.ENGLISH, "c2_%04d", child2), parentId));
}
}
indexRandom(true, requestBuilders);
ensureSearchable();
QueryBuilder hasChildQuery = hasChildQuery("child2", matchAllQuery(), ScoreMode.None).innerHit(new InnerHitBuilder().setSize(2));
AggregationBuilder topHitsAgg = topHits("top-children").size(3);
assertNoFailuresAndResponse(prepareSearch("idx").setQuery(hasChildQuery).addAggregation(topHitsAgg), response -> {
assertHitCount(response, numDocs);
TopHits topHits = response.getAggregations().get("top-children");
SearchHits hits = topHits.getHits();
assertThat(hits.getHits().length, equalTo(3));
for (SearchHit hit : hits) {
SearchHits innerHits = hit.getInnerHits().get("child2");
assertThat(innerHits.getHits().length, lessThanOrEqualTo(2));
}
});
}
}

View file

@ -88,12 +88,35 @@ class ParentChildInnerHitContextBuilder extends InnerHitContextBuilder {
private final String typeName;
private final boolean fetchChildInnerHits;
private final Joiner joiner;
private final SearchExecutionContext searchExecutionContext;
JoinFieldInnerHitSubContext(String name, SearchContext context, String typeName, boolean fetchChildInnerHits, Joiner joiner) {
super(name, context);
this.typeName = typeName;
this.fetchChildInnerHits = fetchChildInnerHits;
this.joiner = joiner;
this.searchExecutionContext = null;
}
JoinFieldInnerHitSubContext(
JoinFieldInnerHitSubContext joinFieldInnerHitSubContext,
SearchExecutionContext searchExecutionContext
) {
super(joinFieldInnerHitSubContext);
this.typeName = joinFieldInnerHitSubContext.typeName;
this.fetchChildInnerHits = joinFieldInnerHitSubContext.fetchChildInnerHits;
this.joiner = joinFieldInnerHitSubContext.joiner;
this.searchExecutionContext = searchExecutionContext;
}
@Override
public JoinFieldInnerHitSubContext copyWithSearchExecutionContext(SearchExecutionContext searchExecutionContext) {
return new JoinFieldInnerHitSubContext(this, searchExecutionContext);
}
@Override
public SearchExecutionContext getSearchExecutionContext() {
return searchExecutionContext != null ? searchExecutionContext : super.getSearchExecutionContext();
}
@Override