Skip to content

Commit 13e2f2d

Browse files
andrevandevenAndre van de Ven
andauthored
Expand fetch phase profiling to support inner hits and top hits aggregation phases (opensearch-project#18936)
--------- Signed-off-by: Andre van de Ven <[email protected]> Signed-off-by: Andre van de Ven <[email protected]> Signed-off-by: Andre van de Ven <[email protected]> Co-authored-by: Andre van de Ven <[email protected]>
1 parent d9f4b11 commit 13e2f2d

File tree

10 files changed

+454
-68
lines changed

10 files changed

+454
-68
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55

66
## [Unreleased 3.x]
77
### Added
8+
- Expand fetch phase profiling to support inner hits and top hits aggregation phases ([##18936](https://github.com/opensearch-project/OpenSearch/pull/18936))
89
- Add temporal routing processors for time-based document routing ([#18920](https://github.com/opensearch-project/OpenSearch/issues/18920))
910

11+
1012
### Changed
1113
- Add CompletionStage variants to methods in the Client Interface and default to ActionListener impl ([#18998](https://github.com/opensearch-project/OpenSearch/pull/18998))
1214

rest-api-spec/src/main/resources/rest-api-spec/test/search.profile/10_fetch_phase.yml

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ setup:
4242
---
4343
"Combined fetch sub-phases profiling":
4444
- skip:
45-
version: " - 3.1.99"
46-
reason: "Fetch phase profiling was introduced in 3.2.0"
45+
version: " - 3.2.99"
46+
reason: "Inner hits fetch phase profiling was introduced in 3.3.0"
4747
features: "contains"
4848

4949
- do:
@@ -80,12 +80,19 @@ setup:
8080
fields:
8181
"object_field.nested_field": {}
8282

83-
# 1. Verify basic fetch profile structure
84-
- is_true: profile.shards.0.fetch.0
83+
# 1. Verify fetch profile structure - should have main fetch + inner hits fetch
84+
- length: { profile.shards.0.fetch: 2 }
85+
86+
# Main fetch profile
8587
- match: { profile.shards.0.fetch.0.type: "fetch" }
8688
- match: { profile.shards.0.fetch.0.description: "fetch" }
8789
- is_true: profile.shards.0.fetch.0.time_in_nanos
8890

91+
# Inner hits fetch profile
92+
- match: { profile.shards.0.fetch.1.type: "fetch_inner_hits[object_field]" }
93+
- match: { profile.shards.0.fetch.1.description: "fetch_inner_hits[object_field]" }
94+
- is_true: profile.shards.0.fetch.1.time_in_nanos
95+
8996
# 2. Verify detailed breakdown of the main fetch operation
9097
- is_true: profile.shards.0.fetch.0.breakdown
9198
- is_true: profile.shards.0.fetch.0.breakdown.load_stored_fields
@@ -99,7 +106,20 @@ setup:
99106
- is_true: profile.shards.0.fetch.0.breakdown.create_stored_fields_visitor
100107
- match: { profile.shards.0.fetch.0.breakdown.create_stored_fields_visitor_count: 1}
101108

102-
# 3. Verify all expected fetch sub-phases are present as children
109+
# 3. Verify inner hits fetch breakdown has all required fields (some may be 0)
110+
- is_true: profile.shards.0.fetch.1.breakdown
111+
- gte: { profile.shards.0.fetch.1.breakdown.load_stored_fields: 0 }
112+
- gte: { profile.shards.0.fetch.1.breakdown.load_stored_fields_count: 0 }
113+
- gte: { profile.shards.0.fetch.1.breakdown.load_source: 0 }
114+
- gte: { profile.shards.0.fetch.1.breakdown.load_source_count: 0 }
115+
- gte: { profile.shards.0.fetch.1.breakdown.get_next_reader: 0 }
116+
- match: { profile.shards.0.fetch.1.breakdown.get_next_reader_count: 1 }
117+
- gte: { profile.shards.0.fetch.1.breakdown.build_sub_phase_processors: 0 }
118+
- match: { profile.shards.0.fetch.1.breakdown.build_sub_phase_processors_count: 1 }
119+
- gte: { profile.shards.0.fetch.1.breakdown.create_stored_fields_visitor: 0 }
120+
- match: { profile.shards.0.fetch.1.breakdown.create_stored_fields_visitor_count: 1 }
121+
122+
# 4. Verify all expected fetch sub-phases are present as children in main fetch
103123
- length: { profile.shards.0.fetch.0.children: 9 }
104124
- contains:
105125
profile.shards.0.fetch.0.children:
@@ -129,6 +149,16 @@ setup:
129149
profile.shards.0.fetch.0.children:
130150
type: "FetchScorePhase"
131151

152+
# 5. Verify inner hits fetch has exactly 1 sub-phase (FetchSourcePhase)
153+
- length: { profile.shards.0.fetch.1.children: 1 }
154+
- match: { profile.shards.0.fetch.1.children.0.type: "FetchSourcePhase" }
155+
- is_true: profile.shards.0.fetch.1.children.0.time_in_nanos
156+
- is_true: profile.shards.0.fetch.1.children.0.breakdown
157+
- is_true: profile.shards.0.fetch.1.children.0.breakdown.process
158+
- gte: { profile.shards.0.fetch.1.children.0.breakdown.process_count: 1 }
159+
- is_true: profile.shards.0.fetch.1.children.0.breakdown.set_next_reader
160+
- match: { profile.shards.0.fetch.1.children.0.breakdown.set_next_reader_count: 1 }
161+
132162
---
133163
"No source or empty fetch profiling":
134164
- skip:
@@ -169,8 +199,9 @@ setup:
169199
---
170200
"Top-hits aggregation profiling":
171201
- skip:
172-
version: " - 3.1.99"
173-
reason: "Fetch phase profiling was introduced in 3.2.0"
202+
version: " - 3.2.99"
203+
reason: "Top-hits aggregation profiling was introduced in 3.3.0"
204+
features: "contains"
174205

175206
- do:
176207
search:
@@ -181,13 +212,42 @@ setup:
181212
match:
182213
text_field: "document"
183214
aggs:
184-
top_hits_agg:
215+
top_hits_agg1:
216+
top_hits:
217+
size: 1
218+
top_hits_agg2:
185219
top_hits:
186220
size: 1
221+
sort:
222+
- numeric_field: { order: desc }
223+
224+
- length: { profile.shards.0.fetch: 3 }
225+
226+
- contains:
227+
profile.shards.0.fetch:
228+
type: "fetch"
229+
description: "fetch"
230+
231+
- contains:
232+
profile.shards.0.fetch:
233+
type: "fetch_top_hits_aggregation[top_hits_agg1]"
234+
description: "fetch_top_hits_aggregation[top_hits_agg1]"
235+
236+
- contains:
237+
profile.shards.0.fetch:
238+
type: "fetch_top_hits_aggregation[top_hits_agg2]"
239+
description: "fetch_top_hits_aggregation[top_hits_agg2]"
240+
241+
- is_true: profile.shards.0.fetch.0.time_in_nanos
242+
- is_true: profile.shards.0.fetch.0.breakdown
243+
- is_true: profile.shards.0.fetch.1.time_in_nanos
244+
- is_true: profile.shards.0.fetch.1.breakdown
245+
- is_true: profile.shards.0.fetch.2.time_in_nanos
246+
- is_true: profile.shards.0.fetch.2.breakdown
187247

188-
# Verify that the profile contains a single fetch operation for the query
189-
- length: { profile.shards.0.fetch: 1 }
190-
- match: { profile.shards.0.fetch.0.type: "fetch" }
191-
- match: { profile.shards.0.fetch.0.description: "fetch" }
192248
- length: { profile.shards.0.fetch.0.children: 1 }
193249
- match: { profile.shards.0.fetch.0.children.0.type: "FetchSourcePhase" }
250+
- length: { profile.shards.0.fetch.1.children: 1 }
251+
- match: { profile.shards.0.fetch.1.children.0.type: "FetchSourcePhase" }
252+
- length: { profile.shards.0.fetch.2.children: 1 }
253+
- match: { profile.shards.0.fetch.2.children.0.type: "FetchSourcePhase" }

server/src/internalClusterTest/java/org/opensearch/search/profile/aggregation/AggregationProfilerIT.java

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.opensearch.action.index.IndexRequestBuilder;
3838
import org.opensearch.action.search.SearchResponse;
3939
import org.opensearch.common.settings.Settings;
40+
import org.opensearch.index.query.QueryBuilders;
4041
import org.opensearch.search.aggregations.Aggregator.SubAggCollectionMode;
4142
import org.opensearch.search.aggregations.BucketOrder;
4243
import org.opensearch.search.aggregations.InternalAggregation;
@@ -46,8 +47,11 @@
4647
import org.opensearch.search.aggregations.metrics.Stats;
4748
import org.opensearch.search.profile.ProfileResult;
4849
import org.opensearch.search.profile.ProfileShardResult;
50+
import org.opensearch.search.profile.fetch.FetchProfileShardResult;
4951
import org.opensearch.search.profile.query.CollectorResult;
5052
import org.opensearch.search.profile.query.QueryProfileShardResult;
53+
import org.opensearch.search.sort.SortBuilders;
54+
import org.opensearch.search.sort.SortOrder;
5155
import org.opensearch.test.OpenSearchIntegTestCase;
5256
import org.opensearch.test.ParameterizedStaticSettingsOpenSearchIntegTestCase;
5357
import org.hamcrest.core.IsNull;
@@ -69,6 +73,7 @@
6973
import static org.opensearch.search.aggregations.AggregationBuilders.max;
7074
import static org.opensearch.search.aggregations.AggregationBuilders.stats;
7175
import static org.opensearch.search.aggregations.AggregationBuilders.terms;
76+
import static org.opensearch.search.aggregations.AggregationBuilders.topHits;
7277
import static org.opensearch.test.hamcrest.OpenSearchAssertions.assertAcked;
7378
import static org.opensearch.test.hamcrest.OpenSearchAssertions.assertSearchResponse;
7479
import static org.hamcrest.Matchers.containsString;
@@ -1000,4 +1005,93 @@ private void assertCollectorResultWithConcurrentSearchEnabled(QueryProfileShardR
10001005
assertThat(collectorResult.getCollectorResult().getProfiledChildren().get(1).getReason(), equalTo(REASON_AGGREGATION));
10011006
}
10021007
}
1008+
1009+
public void testTopHitsAggregationFetchProfiling() throws Exception {
1010+
SearchResponse response = client().prepareSearch("idx")
1011+
.setProfile(true)
1012+
.setQuery(QueryBuilders.matchAllQuery())
1013+
.addAggregation(topHits("top_hits_agg1").size(1))
1014+
.addAggregation(topHits("top_hits_agg2").size(1).sort(SortBuilders.fieldSort(NUMBER_FIELD).order(SortOrder.DESC)))
1015+
.get();
1016+
1017+
assertSearchResponse(response);
1018+
Map<String, ProfileShardResult> profileResults = response.getProfileResults();
1019+
assertNotNull("Profile results should not be null", profileResults);
1020+
assertFalse("Profile results should not be empty", profileResults.isEmpty());
1021+
1022+
int shardsWithDocuments = 0;
1023+
int shardsWithCorrectProfile = 0;
1024+
1025+
for (ProfileShardResult shardResult : profileResults.values()) {
1026+
FetchProfileShardResult fetchProfileResult = shardResult.getFetchProfileResult();
1027+
if (fetchProfileResult != null && !fetchProfileResult.getFetchProfileResults().isEmpty()) {
1028+
shardsWithDocuments++;
1029+
List<ProfileResult> fetchProfileResults = fetchProfileResult.getFetchProfileResults();
1030+
1031+
// Count different types of fetch operations dynamically
1032+
int mainFetchCount = 0;
1033+
int topHitsAgg1Count = 0;
1034+
int topHitsAgg2Count = 0;
1035+
ProfileResult topHitsFetch1 = null;
1036+
ProfileResult topHitsFetch2 = null;
1037+
1038+
for (ProfileResult result : fetchProfileResults) {
1039+
if ("fetch".equals(result.getQueryName())) {
1040+
mainFetchCount++;
1041+
} else if (result.getQueryName().contains("top_hits_agg1")) {
1042+
if (topHitsFetch1 == null) {
1043+
topHitsFetch1 = result; // Keep first instance for validation
1044+
}
1045+
topHitsAgg1Count++;
1046+
} else if (result.getQueryName().contains("top_hits_agg2")) {
1047+
if (topHitsFetch2 == null) {
1048+
topHitsFetch2 = result; // Keep first instance for validation
1049+
}
1050+
topHitsAgg2Count++;
1051+
}
1052+
}
1053+
1054+
// Verify we have the expected aggregations (concurrent search may create multiple instances)
1055+
assertTrue("Should have at least 1 top_hits_agg1 fetch operation", topHitsAgg1Count >= 1);
1056+
assertTrue("Should have at least 1 top_hits_agg2 fetch operation", topHitsAgg2Count >= 1);
1057+
assertTrue("Should have at least one main fetch operation", mainFetchCount >= 1);
1058+
assertTrue("Should have at least 3 total fetch operations", fetchProfileResults.size() >= 3);
1059+
1060+
assertNotNull("Should have top_hits_agg1 fetch operation", topHitsFetch1);
1061+
assertTrue("Should be top_hits aggregation fetch", topHitsFetch1.getQueryName().startsWith("fetch_top_hits_aggregation"));
1062+
assertTrue("Should contain aggregation name", topHitsFetch1.getQueryName().contains("top_hits_agg1"));
1063+
assertNotNull(topHitsFetch1.getTimeBreakdown());
1064+
assertEquals("Top hits fetch should have 1 child (FetchSourcePhase)", 1, topHitsFetch1.getProfiledChildren().size());
1065+
assertEquals("FetchSourcePhase", topHitsFetch1.getProfiledChildren().get(0).getQueryName());
1066+
1067+
assertNotNull("Should have top_hits_agg2 fetch operation", topHitsFetch2);
1068+
assertTrue("Should be top_hits aggregation fetch", topHitsFetch2.getQueryName().startsWith("fetch_top_hits_aggregation"));
1069+
assertTrue("Should contain aggregation name", topHitsFetch2.getQueryName().contains("top_hits_agg2"));
1070+
assertNotNull(topHitsFetch2.getTimeBreakdown());
1071+
assertEquals("Top hits fetch should have 1 child (FetchSourcePhase)", 1, topHitsFetch2.getProfiledChildren().size());
1072+
assertEquals("FetchSourcePhase", topHitsFetch2.getProfiledChildren().get(0).getQueryName());
1073+
1074+
for (ProfileResult fetchResult : fetchProfileResults) {
1075+
Map<String, Long> breakdown = fetchResult.getTimeBreakdown();
1076+
assertTrue(
1077+
"CREATE_STORED_FIELDS_VISITOR timing should be present",
1078+
breakdown.containsKey("create_stored_fields_visitor")
1079+
);
1080+
assertTrue("BUILD_SUB_PHASE_PROCESSORS timing should be present", breakdown.containsKey("build_sub_phase_processors"));
1081+
assertTrue("GET_NEXT_READER timing should be present", breakdown.containsKey("get_next_reader"));
1082+
assertTrue("LOAD_STORED_FIELDS timing should be present", breakdown.containsKey("load_stored_fields"));
1083+
assertTrue("LOAD_SOURCE timing should be present", breakdown.containsKey("load_source"));
1084+
}
1085+
1086+
shardsWithCorrectProfile++;
1087+
}
1088+
}
1089+
1090+
assertTrue("Should have at least one shard with documents", shardsWithDocuments > 0);
1091+
assertEquals(
1092+
"All shards with documents should have correct fetch profile structure",
1093+
shardsWithDocuments,
1094+
shardsWithCorrectProfile
1095+
);
1096+
}
10031097
}

0 commit comments

Comments
 (0)