Skip to content

Commit b46f245

Browse files
authored
Reject invalid reverse_nested aggs (#137047)
1 parent 4d9fc84 commit b46f245

File tree

6 files changed

+81
-3
lines changed

6 files changed

+81
-3
lines changed

docs/changelog/137047.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 137047
2+
summary: Reject invalid `reverse_nested` aggs
3+
area: Aggregations
4+
type: bug
5+
issues: []

modules/aggregations/src/yamlRestTest/resources/rest-api-spec/test/aggregations/nested.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,33 @@ setup:
194194
- match: { aggregations.courses.highpass_filter.unnest.department.buckets.0.doc_count: 1 }
195195
- match: { aggregations.courses.highpass_filter.unnest.department.buckets.1.key: math }
196196
- match: { aggregations.courses.highpass_filter.unnest.department.buckets.1.doc_count: 1 }
197+
---
198+
"Illegal reverse nested aggregation to a child nested object":
199+
- requires:
200+
capabilities:
201+
- method: POST
202+
path: /_search
203+
capabilities: [ reject_invalid_reverse_nesting ]
204+
test_runner_features: [ capabilities ]
205+
reason: "search does not yet reject invalid reverse nesting paths"
206+
- do:
207+
catch: /Reverse nested path \[courses.sessions\] is not a parent of the current nested scope \[courses\]/
208+
search:
209+
index: test
210+
body:
211+
{
212+
"aggs": {
213+
"parent_nested": {
214+
"nested": {
215+
"path": "courses"
216+
},
217+
"aggs": {
218+
"invalid_reverse_nested": {
219+
"reverse_nested": {
220+
"path": "courses.sessions"
221+
}
222+
}
223+
}
224+
}
225+
}
226+
}

server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/ReverseNestedIT.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -470,15 +470,19 @@ public void testReverseNestedAggWithoutNestedAgg() {
470470
public void testNonExistingNestedField() throws Exception {
471471
assertNoFailuresAndResponse(
472472
prepareSearch("idx2").setQuery(matchAllQuery())
473-
.addAggregation(nested("nested2", "nested1.nested2").subAggregation(reverseNested("incorrect").path("nested3"))),
473+
.addAggregation(nested("nested2", "nested1.nested2"))
474+
.addAggregation(nested("incorrect", "nested1.incorrect")),
474475
response -> {
475476

476477
SingleBucketAggregation nested = response.getAggregations().get("nested2");
477478
assertThat(nested, notNullValue());
478479
assertThat(nested.getName(), equalTo("nested2"));
480+
assertThat(nested.getDocCount(), is(27L));
479481

480-
SingleBucketAggregation reverseNested = nested.getAggregations().get("incorrect");
481-
assertThat(reverseNested.getDocCount(), is(0L));
482+
SingleBucketAggregation incorrect = response.getAggregations().get("incorrect");
483+
assertThat(incorrect, notNullValue());
484+
assertThat(incorrect.getName(), equalTo("incorrect"));
485+
assertThat(incorrect.getDocCount(), is(0L));
482486
}
483487
);
484488

server/src/main/java/org/elasticsearch/rest/action/search/SearchCapabilities.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ private SearchCapabilities() {}
6060
private static final String BUCKET_SCRIPT_PARENT_MULTI_BUCKET_ERROR = "bucket_script_parent_multi_bucket_error";
6161
private static final String EXCLUDE_SOURCE_VECTORS_SETTING = "exclude_source_vectors_setting";
6262
private static final String CLUSTER_STATS_EXTENDED_USAGE = "extended-search-usage-stats";
63+
private static final String REJECT_INVALID_REVERSE_NESTING = "reject_invalid_reverse_nesting";
6364

6465
public static final Set<String> CAPABILITIES;
6566
static {
@@ -90,6 +91,7 @@ private SearchCapabilities() {}
9091
capabilities.add(BUCKET_SCRIPT_PARENT_MULTI_BUCKET_ERROR);
9192
capabilities.add(EXCLUDE_SOURCE_VECTORS_SETTING);
9293
capabilities.add(CLUSTER_STATS_EXTENDED_USAGE);
94+
capabilities.add(REJECT_INVALID_REVERSE_NESTING);
9395
CAPABILITIES = Set.copyOf(capabilities);
9496
}
9597
}

server/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/ReverseNestedAggregationBuilder.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,18 @@ protected AggregatorFactory doBuild(AggregationContext context, AggregatorFactor
8484
throw new IllegalArgumentException("Reverse nested aggregation [" + name + "] can only be used inside a [nested] aggregation");
8585
}
8686

87+
if (path != null) {
88+
NestedObjectMapper currentParent = context.nestedScope().getObjectMapper();
89+
if (currentParent != null) {
90+
String parentPath = currentParent.fullPath();
91+
if (parentPath.equals(path) == false && parentPath.startsWith(path + ".") == false) {
92+
throw new IllegalArgumentException(
93+
"Reverse nested path [" + path + "] is not a parent of the current nested scope [" + parentPath + "]"
94+
);
95+
}
96+
}
97+
}
98+
8799
NestedObjectMapper nestedMapper = null;
88100
if (path != null) {
89101
nestedMapper = context.nestedLookup().getNestedMappers().get(path);

server/src/test/java/org/elasticsearch/search/aggregations/bucket/nested/ReverseNestedAggregatorTests.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,4 +247,29 @@ public void testNestedUnderTerms() throws IOException {
247247
protected List<ObjectMapper> objectMappers() {
248248
return NestedAggregatorTests.MOCK_OBJECT_MAPPERS;
249249
}
250+
251+
public void testErrorOnInvalidReverseNestedWithPath() throws IOException {
252+
AggregationBuilder aggregationBuilder = nested("n1", NESTED_OBJECT).subAggregation(
253+
nested("n2", NESTED_OBJECT + ".child").subAggregation(
254+
reverseNested("r1").path(NESTED_OBJECT).subAggregation(reverseNested("r2").path(NESTED_OBJECT + ".child"))
255+
)
256+
);
257+
258+
try (Directory directory = newDirectory()) {
259+
try (RandomIndexWriter iw = newRandomIndexWriterWithLogDocMergePolicy(directory)) {
260+
// No docs needed
261+
}
262+
try (DirectoryReader indexReader = wrapInMockESDirectoryReader(DirectoryReader.open(directory))) {
263+
IllegalArgumentException e = assertThrows(
264+
IllegalArgumentException.class,
265+
() -> searchAndReduce(indexReader, new AggTestConfig(aggregationBuilder))
266+
);
267+
assertThat(
268+
e.getMessage(),
269+
equalTo("Reverse nested path [nested_object.child] is not a parent of the current nested scope [nested_object]")
270+
);
271+
272+
}
273+
}
274+
}
250275
}

0 commit comments

Comments
 (0)