Skip to content

fix(core): implement pagination for search() and searchCollection() (#1463)#1578

Open
marcusbellamyshaw-cell wants to merge 1 commit into
emdash-cms:mainfrom
Emdash-Bug-Testing:fix/1463-search-pagination
Open

fix(core): implement pagination for search() and searchCollection() (#1463)#1578
marcusbellamyshaw-cell wants to merge 1 commit into
emdash-cms:mainfrom
Emdash-Bug-Testing:fix/1463-search-pagination

Conversation

@marcusbellamyshaw-cell

Copy link
Copy Markdown

What does this PR do?

The search API advertised keyset pagination through its types — SearchOptions.cursor and SearchResponse.nextCursor — but searchWithDb never read the incoming cursor nor populated nextCursor. Results were silently capped at limit with no way to fetch a second page, so any "load more" wired against the documented shape got a button that never appears (and a cursor that's discarded).

This implements real pagination for both search() and searchCollection().

Why offset, not pure keyset: search results are merged from per-collection FTS queries and re-sorted by bm25 score, so there's no single stable keyset column to encode the way getEmDashCollection encodes (orderValue, id). Instead we page the merged, score-sorted set by offset, carried opaquely in the cursor's orderValue — reusing encodeCursor/decodeCursor so the cursor keeps the same base64-JSON shape and InvalidCursorError handling as the rest of the API.

Each collection fetches its top (offset + limit + 1) rows, which is exactly what's needed for the merged window [offset, offset + limit) to rank correctly even when the whole window comes from one collection, plus one row to detect a further page. A nextCursor is issued only when at least one result exists past the page.

  • search() / searchWithDb and searchCollection() both honour cursor and return nextCursor.
  • The /_emdash/api/search endpoint now accepts a cursor query param and returns nextCursor; a malformed cursor surfaces as 400 INVALID_CURSOR via the existing handleError mapping.

Closes #1463

Type of change

  • Bug fix

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation — n/a, no admin UI strings (server-side search API + docs)
  • I have added a changeset
  • New features link to an approved Discussion — n/a, makes an already-advertised API actually work

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.8 ultracode

Screenshots / test output

New tests/integration/search/pagination.test.ts covers: nextCursor presence past the limit, omission on the final page, a cursor walk over disjoint pages covering every match exactly once, malformed-cursor rejection, and single-collection pagination.

Test Files  8 passed (8)
     Tests  45 passed (45)

…mdash-cms#1463)

The search API advertised keyset pagination through its types
(SearchOptions.cursor, SearchResponse.nextCursor) but searchWithDb never
read the incoming cursor nor populated nextCursor, so results were silently
capped at `limit` with no way to fetch a second page — "load more" buttons
wired against the documented shape never appeared.

Results are merged from per-collection FTS queries and re-sorted by score, so
there is no single stable keyset column to encode the way getEmDashCollection
does. Page the merged, score-sorted set by offset instead, carried opaquely in
the cursor's orderValue (reusing encodeCursor/decodeCursor for the same
base64-JSON shape and InvalidCursorError handling). Each collection fetches its
top (offset + limit + 1) rows so the merged window ranks correctly and a
further page is detectable; a nextCursor is issued only when more results
exist past the page. searchCollection() gets the same treatment.

The /_emdash/api/search endpoint now accepts a `cursor` query param and returns
nextCursor; a malformed cursor surfaces as a 400 INVALID_CURSOR via the
existing handleError mapping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jun 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fb940ac

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
emdash Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Minor
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@emdashbot

emdashbot Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Could not push formatting changes to this fork. The contributor may have "Allow edits by maintainers" disabled.

Please run the formatter locally:

pnpm format

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

search() ignores the cursor option and never returns nextCursor — pagination is impossible

1 participant