Skip to content

feat: rank $search results by relevance - #1725

Open
patricebender wants to merge 14 commits into
mainfrom
feat/hana-search-orderby-ranking-correlation
Open

feat: rank $search results by relevance#1725
patricebender wants to merge 14 commits into
mainfrom
feat/hana-search-orderby-ranking-correlation

Conversation

@patricebender

@patricebender patricebender commented Sep 4, 2026

Copy link
Copy Markdown
Member

Ranks $search results by fuzzy relevance on HANA, and correctly correlates the ranking ORDER BY to the outer row. Standalone against main (supersedes the stacked PR #1717 / includes #1564's search-order groundwork).

What

  • Rank by relevance: inject ORDER BY <score> DESC for $search. Deep (path) search ranks each outer row by its best-matching child via a correlated (SELECT MAX(SCORE(...)) ... WHERE innerKey = outerKey) sub-select — resolving the earlier key IN (key) tautology (correlation applied post-infer(), mirroring expand's _correlate).
  • Gated to scoring backends: only when cds.db.kind === 'hana' and fuzzy is on — sqlite/postgres (no score) get no ranking.
  • Opt-out: cds.env.hana.fuzzy.ranked_search = false; fuzzy also accepts { score, ranked_search }.
  • Order-by precedence: user ordering → search rank → runtime implicit key ordering (implicit: true).

Tests

  • cqn4sql: rank shape, precedence, opt-out, non-HANA gating.
  • HANA e2e (live-verified): deep to-many ranking + dedup; user order-by precedence; opt-out contrast; OData $search via the bookshop service showing rank beats implicit key ordering; fuzzy object config.

BobdenOs and others added 14 commits September 4, 2026 11:56
The deep-search ranking ORDER BY reused the search expression without binding
it to the outer query row, so the sub-select produced a `key IN (key)`
tautology instead of a correlation (the TODO in cqn4sql). Defer the ranking
ORDER BY to after infer(), where the outer alias is known, and correlate the
score sub-select to the outer row post-transform (mirroring expand's
_correlate). A deep search fans one outer row out to many joined child rows,
so wrap the score in MAX() to keep the scalar sub-select single-valued and
rank by the best-matching child.

Also emit the numeric flag as the proper CQN literal `{ val: true }`, and make
the HANA fuzzy `search` renderer idempotent: it rewrites annotated columns
in place (ref -> xpr), which crashed when the same search() args are rendered
twice (WHERE predicate + injected ranking ORDER BY).

Verified end-to-end against a real HANA: deep to-many $search ranks by best
match, de-duplicated; fuzzy suite green.
The inherited `$self` composition (SearchAuthors : Authors) made the backlink
resolve to the base entity, which cds-compiler 7.0.1 (CI) rejects during the
relational SQL transform. Make the model self-contained: SearchAuthors owns its
books composition and Books.author points to SearchAuthors directly, so the
$self backlink is unambiguous. Verified compile+to.sql on 7.0.1 and the e2e
ranking still passes on a real HANA.
Relevance ranking is a HANA fuzzy-search feature: with cds.env.hana.fuzzy=false
(and on other DBs) search() yields no score, so the injected ORDER BY sorted by
a constant boolean — useless and a wasted duplicate of the search expression.
Skip the ranking injection unless fuzzy scoring is active. Reverts the fuzzy
fallback like-count assertions accordingly (no ranking ORDER BY, so no doubling).
cds.env.hana.fuzzy is undefined (not false) on every dialect and cds.env.hana
is an always-present config block, so the previous guard injected the ranking
ORDER BY on sqlite/postgres too — where search() is a boolean and the sort is
meaningless. Gate on the active db instead: cds.db?.kind === 'hana' && fuzzy
!== false. Reference tests stub cds.db = { kind: 'hana' } and add negative
tests asserting no ranking on non-HANA and on HANA with fuzzy=false.
- opt out of $search relevance ranking via cds.env.hana.fuzzy.ranked_search = false
- order-by precedence: user-provided ordering first, then the search rank, then
  the runtime's implicit key ordering (entries flagged `implicit: true`, added
  for stable pagination). The rank is inserted before the first implicit entry
  instead of always prepended; the deep-case correlation locates the rank entry
  by its sub-select rather than assuming position 0.

Tests: cqn4sql precedence (rank first / after user / before implicit / mixed)
and opt-out; e2e on HANA that a user `order by` wins over the rank.
With the opt-out cds.env.hana.fuzzy = { ranked_search: false }, `fuzzy` is an
object; the renderer's `fuzzy || 0.7` then rendered `MINIMAL SCORE [object
Object]`. Take the value as the minimal score only when it is a number. Adds an
e2e test that opting out skips the ranking while search still works.
Allow the minimal score to be configured alongside ranked_search via an object
`cds.env.hana.fuzzy = { score, ranked_search }`, in addition to the plain
`fuzzy: <score>`. The search renderer reads the score from `.score` when fuzzy
is an object.

Adds an OData e2e (bookshop admin service) proving the ranked search takes
precedence over the runtime's implicit key ordering: for the same
`$search=Jane&$top=5` request the two matching books flip order when ranking is
turned off via ranked_search: false.
@patricebender patricebender changed the title feat: rank $search results by relevance (HANA fuzzy) feat: rank $search results by relevance Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants