fix(viewtools): restore aggregation tree for VTL $estool.search (#36026)#36027
fix(viewtools): restore aggregation tree for VTL $estool.search (#36026)#36027fabrizzio-dotCMS wants to merge 5 commits into
Conversation
…6026) The ES → OS migration changed the type returned by $results.aggregations from org.elasticsearch.search.aggregations.Aggregations to Map<String, List<AggregationBucket>>. Existing VTL templates that walked aggregations.<name>.buckets / bucket.getKeyAsNumber() / getDocCount() / nested top_hits now silently produce no output. ContentSearchToolTest renders the customer template through the dotCMS Velocity engine and: - verbatimCustomerVtl_reproducesRegression: locks the broken behaviour (data present, but .buckets is null so the bucket loop emits nothing). - migratedVtl_emitsBuckets: documents the neutral-API migration path for the flat terms aggregation (key()/docCount(), no .buckets). - raw_aggregationDataIsPresent: proves the buckets are in the response, so the regression is purely a VTL access/typing problem. This is the entry point for the real fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @fabrizzio-dotCMS's task in 3m 5s —— View job PR Review
The fix is structurally sound and the tests are well-written. A few issues worth addressing: Issues1.
2. Unknown OS aggregation types are silently dropped
3. The ES factory calls 4.
5.
6. Broken javadoc Line 24 of 7.
Reviewer request not yet implemented @wezell requested converting |
The ES→OS migration flattened the aggregations exposed to Velocity from an ES Aggregations object tree to a Map<String,List<AggregationBucket>>, silently breaking templates that walked aggregations.<name>.buckets, getKeyAsNumber()/ getDocCount(), nested getAggregations(), and the top_hits metric aggregation. Restore a vendor-neutral aggregation tree that mirrors the legacy ES shape so existing templates keep working unchanged: - New Aggregation type (getBuckets for terms, getHits for top_hits, Iterable over buckets); the container is a plain Map<String,Aggregation> (Velocity resolves $aggs.name via Map#get) — no separate container class. - AggregationBucket gains getKey/getKeyAsString/getKeyAsNumber/getDocCount and nested getAggregations(); ES and OS factories capture sub-aggregations. - SearchHit/SearchHits accessors switched to bean-style (getId/getHits/...) so $topHits.getHits().getHits() and $hit.id resolve without alias methods. - ContentSearchResponse keeps the flat aggregations() map intact (now derived from the tree) so ContentTypeAPIImpl/WorkflowHelper are unaffected; adds aggregationTree(). ContentSearchResults.getAggregations() returns the tree. Verified: AggregationDomainTest (unit) and ContentSearchToolTest (integration, against real OpenSearch) — the verbatim customer template renders buckets and nested top_hits hits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…p body Cosmetic readability only — pretty-print the aggregation JSON inside the Velocity string literal and indent the #foreach body. Re-verified against real OpenSearch (neutralFluentVtl_emitsBuckets passes). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cosmetic readability only — pretty-print the JSON query inside the verbatim customer template's Velocity string literal (same query, unchanged semantics). Re-verified against real OpenSearch (verbatimCustomerVtl_rendersAfterFix passes: buckets + nested top_hits hits render). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Make SearchHit a record, not immutable :)
There was a problem hiding this comment.
Maybe a record too? Let me know if that is too much. work but it would be good to start flexing this muscle.
The flat ContentSearchResponse.aggregations() map dropped terms aggregations whose bucket list was empty (the flatten() guard filtered on !getBuckets().isEmpty()), breaking the contract that a declared terms aggregation key is present even with no matching documents. This regressed OSSearchAPIImplIntegrationTest.test_searchRaw_withTermsAgg_shouldReturnAggregationKey in the OpenSearch Upgrade Suite. Discriminate bucket vs metric aggregations by getHits()==null instead: terms aggregations (hits null) are always included — even with empty buckets — while top_hits (hits non-null) is skipped. Adds a unit regression guard. Verified: AggregationDomainTest 5/5 (unit) and OSSearchAPIImplIntegrationTest 11/11 (integration, real OpenSearch upgrade suite). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix for #36026 — VTL aggregation return-type regression
The Elasticsearch → OpenSearch migration made the search aggregation API vendor-neutral and, in doing so, changed the type exposed to Velocity as
$results.aggregations($estool.search(...)), silently breaking existing VTL templates (Velocity runs withruntime.references.strict=false, so broken accessors render empty instead of erroring).$results.aggregations→org.elasticsearch.search.aggregations.Aggregations(object tree)$results.aggregations→Map<String, List<AggregationBucket>>, whereAggregationBucketexposed onlykey()/docCount()and nested sub-aggregations were dropped.Three regressions
aggregations.<name>.buckets→null(the map value was already the bucket list).bucket.getKeyAsNumber()/getDocCount()gone; the fluentkey()/docCount()are not reachable as Velocity properties (noget/isprefix).top_hitsper bucket unrecoverable —ContentSearchResponse.from(...)mapped only first-levelterms.The fix — restore a vendor-neutral aggregation tree
A neutral tree that mirrors the legacy ES API shape, so existing templates keep working unchanged (honoring the migration's "no visible behavior change" goal).
Aggregationtype —getBuckets()(terms),getHits()(top_hits),Iterableover buckets. The container is a plainMap<String, Aggregation>(Velocity resolves$aggs.nameviaMap#get) — no separate container class.AggregationBucketgainsgetKey()/getKeyAsString()/getKeyAsNumber()(lenient:nullon non-numeric) /getDocCount()and nestedgetAggregations(); ES and OS factories now capture sub-aggregations.SearchHit/SearchHitsaccessors switched to bean-style (getId(),getHits(), …) so$topHits.getHits().getHits()→$hit.idresolve without alias methods.ContentSearchResponsekeeps the flataggregations()Mapintact (now@Value.Derivedfrom the tree) →ContentTypeAPIImpl/WorkflowHelperand existing tests are unaffected; addsaggregationTree().ContentSearchResults.getAggregations()(the VTL entry point) returns the tree.The flat map is always derived from the tree, so the two views can never disagree.
Tests
AggregationDomainTest(unit) — Jackson round-trip with no duplicateidproperty, lenientgetKeyAsNumber(), flat-map derivation excludes bucket-less aggs, nested sub-aggs reachable.ContentSearchToolTest(integration, run against real OpenSearch):verbatimCustomerVtl_rendersAfterFixkey:/docCount:) and nestedtop_hitshits (hit id:).neutralFluentVtl_emitsBuckets$group.key()/docCount(), iterating theAggregation).aggregationTreePreservesTopHitsaggregationTree()preserves first-level buckets and nestedtop_hitswith its hits.raw_flatAggregationsMapStillPopulatedMapused by Java callers stays populated.✅ Unit 4/4 · ✅ Integration 4/4 against OpenSearch.
Closes #36026
🤖 Generated with Claude Code