fix(es-search): restore per-hit sort values in /api/es/search (#36581) - #36582
Conversation
The phase-aware SearchAPI cutover (#36398) rebuilds the legacy ES-wire response manually and dropped the per-hit `sort` array (e.g. the `_geo_distance` value clients read to display each result's distance). #36480 only coerced the non-finite `_score`; it did not restore `sort`. Fix is two-layer, symmetric ES/OS: - SearchHit: add a `sortValues` component, populated from esSearchHit.getSortValues() and osHit.sort() (FieldValue unwrapped). - ESContentResourcePortlet.hitsToLegacyJson: emit `sort` per hit when present (non-finite entries coerced to null like `_score`); omit it for relevance-only queries to match the native ES/OS wire format. Adds integration tests for a _geo_distance-sorted query (sort present, ascending, nearest ~0km) and a relevance-only query (no sort key). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @fabrizzio-dotCMS's task in 9m 12s —— View job Rollback-Safety Analysis — PR #36582
Verdict: Unsafe to rollback — 🟠 HIGH, category H-8 (VTL Viewtool Contract Change) The fix adds a new Full finding with code references posted here: #36582 (comment) Label Other categories checked and ruled out: no DB migration/ |
…#36581) Addresses PR review feedback: - SearchHit.from(osHit): guard against a raw null element in Hit.sort() so fieldValue.isNull() cannot NPE (defensive; client is not known to emit nulls). - Add SearchHitTest unit test exercising the OpenSearch sort-unwrap branch (double/string FieldValue, empty sort, null element) that the ES-only integration environment does not cover. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the review 🙏 Addressed the two actionable items in 7c3f53f:
Re: the |
|
No critical or warn findings. This is a tight, well-scoped fix: null-safe defaults on the new sortValues record component, correct unwrap of the OpenSearch FieldValue tagged union, sort key omitted for relevance-only queries (matching native wire format), and solid test coverage (unit tests for the OS branch, integration test for the ES geo-distance path). |
…ool (#36581) Complements the REST (ESContentResourcePortletTest) and OS-conversion (SearchHitTest) coverage with the Velocity path: a field-sorted $estool.search(...) whose template reads $hit.sortValues and $hit.getSortValues().get(0). Guards the hop-2 (reflection/bean accessor) side — the neutral SearchHit must expose the sort value under a get-named accessor Velocity can resolve, not silently yield null. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pull Request Unsafe to Rollback!!!
|
|
Tick the box to add this pull request to the merge queue (same as
|
Proposed Changes
Fixes #36581.
/api/es/search(and/api/es/raw) silently dropped the per-hitsortarray from the response. When a query sorts by a field — e.g. a_geo_distancesort — Elasticsearch/OpenSearch return the computed sort value on each hit underhits.hits[i].sort; for a geo sort that value is the distance clients display. Since the phase-aware SearchAPI cutover (#36398, first releasedv26.07.04-01) the legacy ES-wire response is rebuilt manually and thesortelement was lost. #36480 (the non-finite_scorefix) only touched_score; it did not restoresort, and thetrack_scores: trueworkaround does not bring it back.Root cause (two layers)
com.dotcms.content.index.domain.SearchHithad no component for per-hit sort values, soSearchHit.from(esSearchHit)/from(osHit)discardedgetSortValues()/sort()on ingestion.ESContentResourcePortlet.hitsToLegacyJson()emitted only_id/_index/_score/_source.Fix (symmetric ES/OS)
SearchHit: add asortValuescomponent, populated fromesSearchHit.getSortValues()(ESObject[]) andosHit.sort()(OpenSearchList<FieldValue>, unwrapped via_get()). Defaults to an empty list, so JSON (de)serialization of cached hits stays backward-compatible.hitsToLegacyJson: emit asortarray per hit only when sort values are present — relevance-only queries get nosortkey, matching the native engine wire format. Non-finite entries are coerced tonull(newfiniteOrNull(Object)overload), consistent with the_scorehandling.Both
/api/es/search(GET + POST) and/api/es/rawsharetoLegacyEsJson, so all are covered.Testing
Integration tests added to
ESContentResourcePortletTest(registered inMainSuite2a):test_search_geoDistanceSort_emitsPerHitSortValues— a_geo_distance-sorted query returns each hit with asortarray carrying the finite distance, in ascending order (nearest ~0 km).test_search_relevanceOnlyQuery_omitsPerHitSort— a relevance-only query returns hits with nosortkey.Verified end-to-end on a local build in migration Phase 0: the customer's exact
_geo_distancequery now returnssort: [0.0003, 11.82, 35.03, 71.57]km per hit — identical to the native Elasticsearch response — with_scorestillnull.sort[0.0003, 11.82, 35.03, 71.57]🤖 Generated with Claude Code
This PR fixes: #36581