fix(find): orderBy is the order on every path, not only the metadata-only one

`find({ where, orderBy })` answered in field order; `find({ query, where,
orderBy })` and `find({ vector, where, orderBy })` answered in SCORE order.
The vector/filter block ranks the fused candidates by score, cuts the page and
returns early — and the tail's orderBy sort sits below that early return, so on
those paths it never ran. Nothing threw and nothing warned: the ordering request
was dropped in silence, and the two paths disagreed about what "ordered by rank"
means.

Where `connected` or `fusion` kept the tail alive the defect changed shape
rather than disappearing. The block had already CUT the page by score, so the
tail ordered the rows relevance had chosen — a correctly sorted page of the
wrong rows.

The early cut fires only once the candidate set reaches offset+limit rows, which
is why it read green for so long: below that threshold the block falls through
and the tail's sort does apply. An ordering that is correct until there is
enough data to matter.

THE LAW: an explicit orderBy displaces score as the ordering key on every path.
The candidate set the path produced is ordered IN FULL and the page is cut from
that ordering — "page last", the graph-first law applied to ordering rather than
to filtering. Score-ranked early paging stays exactly as it was for the default
case, where score IS the requested order.

The pin is differential against the metadata-only path, the one path that always
honoured orderBy. It is sized so the hybrid legs (each bounded at limit*2)
provably cover the filter universe, and that covering is asserted from the leg's
own output rather than assumed — orderBy orders the candidate set, it does not
enlarge it, and the pin claims nothing about recall.
This commit is contained in:
David Snelling 2026-09-02 13:30:20 -07:00
parent 69bda5b7cb
commit 5e720d17ae
2 changed files with 261 additions and 1 deletions

View file

@ -8486,7 +8486,23 @@ export class Brainy<T = any> implements BrainyInterface<T> {
// Rank by score (top offset+limit), then drop the offset — identical ordering
// to a full `sort((a, b) => b.score - a.score)` + slice, but the native
// `sort:topK` provider can compute only the page instead of the full sort.
if (results.length >= offset + limit) {
//
// ONLY when score IS the requested order. An explicit `orderBy` names a
// different ordering key, and this block cannot serve it: it ranks by
// score and CUTS the page, so the tail's `orderBy` sort below either
// never runs at all (the early return, when there is no `connected` /
// `fusion` work left) or runs over a page that score already chose —
// ordering eight rows relevance picked instead of the eight the field
// ordering asks for. Both readings were silent: `find({ query, where,
// orderBy })` answered in score order while `find({ where, orderBy })`
// answered in field order, and nothing said the request had been dropped.
//
// With `orderBy` present the candidate set falls through UNCUT to the
// tail, which orders it in full and pages that ordering — "page last",
// the graph-first law applied to ordering rather than to filtering. The
// set is bounded by the legs (the text matches inside the universe plus
// the beam walk's `limit * 2`), not by the store.
if (!params.orderBy && results.length >= offset + limit) {
const k = offset + limit
const order = rankIndicesByScore(results.map(r => r.score), k, true)
results = reorderByIndices(results, order).slice(offset, k)