Skip to content

fix: cursor pagination drops results for searched post connections (relevance-aware cursors)#3935

Merged
jasonbahl merged 4 commits into
mainfrom
fix/search-cursor-pagination
Jun 18, 2026
Merged

fix: cursor pagination drops results for searched post connections (relevance-aware cursors)#3935
jasonbahl merged 4 commits into
mainfrom
fix/search-cursor-pagination

Conversation

@jasonbahl

Copy link
Copy Markdown
Collaborator

What does this implement/fix? Explain your changes.

Cursor pagination silently dropped and duplicated results for searched post connections whenever search relevance order differed from date order. With 10 posts matching a search (6 matching in the title with old dates, 4 matching only in content with new dates), posts(first: 5, where: {search}) followed by the endCursor returned 1 result instead of 5 and reported hasNextPage: false, leaving 4 of 10 results unreachable.

Root cause

Two layers:

  1. A guard added in #897 - Cursor pagination does not preserve order with the "search" where arg #898 to force date ordering for searches checks $query_args['search'], but the GraphQL input is mapped to s before the check runs, so it has been dead code since it was added.
  2. With the guard inert, WP_Query applies its native relevance ordering (parse_search_order(), a CASE WHEN post_title LIKE ... THEN 1 ... expression prepended to the ORDER BY), but PostObjectCursor built its cutoff only from post_date/ID, so the WHERE clause and the ORDER BY disagreed.

Simply fixing the dead check (the approach in #1819) repairs pagination but forces searched results into date order, losing relevance ranking, which is why that PR was withdrawn in 2021. This PR keeps relevance ordering and makes the cursor understand it.

The fix

  • PostObjectCursor: when WP_Query's native search ordering is active (search set, title ordering clauses present, no other orderby), the cursor prepends a comparison against the same relevance expression parse_search_order() generates, built from the same prepared LIKE clauses WP_Query stores in the query vars during parse_search(). The cursor node's rank is computed in PHP by evaluating the same conditions in the same order, and the existing date/ID comparisons remain as tiebreakers, matching the actual ORDER BY exactly (relevance, then post_date, then ID from the existing stability filter).
  • Config: a new posts_search_orderby filter inverts the relevance expression for backward (last) pagination, mirroring how the date ordering is inverted, so backward queries read the tail of the relevance-ordered set.
  • PostObjectConnectionResolver: the dead guard is removed (preserving relevance ordering as the default for searched connections, matching plain WP_Query/wp-admin behavior), and the post_parent unset for searches now correctly checks s.

Explicitly supplied orderby input is unaffected: it is mapped after this logic and already overrides search ordering, and the cursor only enters relevance mode when no other orderby is set.

Out of scope

Plugins that replace the search SQL entirely (e.g. ElasticPress) bypass both WP's relevance ordering and this cursor logic, unchanged from current behavior.

Does this close any currently open issues?

Fixes #1818 (also closes the loop on the dead code introduced via #898 / withdrawn fix #1819; #2550 was already closed as a duplicate of #1818)

Any other comments?

Regression tests use a fixture where relevance and date order genuinely differ (title matches are older, content-only matches are newer). The pre-existing search pagination tests never caught this because all their fixture posts match in the title with sequential dates, making relevance order identical to date order.

Coverage: forward single-term, forward multi-term (exercising the CASE expression path), and backward (last/before) against the relevance-ordered set, plus the full existing pagination suite (14 tests, 557 assertions, all passing). PHPCS and PHPStan (level 8) clean.

Verified end-to-end over HTTP against a wp-env site: page 1 returns relevance-ordered results, page 2 returns the remaining results with accurate pageInfo, and last: 5 returns the exact tail of the display order.

… connections

When a post connection uses the search where arg and no explicit orderby,
WP_Query orders results by search relevance (parse_search_order), but the
cursor cutoff was built only from post_date/ID, so pages dropped and
duplicated results whenever relevance order differed from date order.

A guard added in #898 meant to force date ordering for searches checked
$query_args['search'], but the input is mapped to 's' before the check,
so it has been dead code since it was added, and removing it (rather than
fixing it) preserves relevance ordering for search results.

Instead, the cursor is now relevance-aware:

- PostObjectCursor detects when WP_Query's native search ordering is
  active and prepends a comparison against the same relevance expression
  parse_search_order generates, computing the cursor node's rank in PHP,
  with the existing date/ID comparisons as tiebreakers.
- Backward (last) pagination inverts the relevance expression via the
  posts_search_orderby filter, mirroring how date order is inverted.
- The dead search guard is removed; the post_parent unset now correctly
  checks 's'.

Fixes #1818
@jasonbahl jasonbahl added type: bug Issue that causes incorrect or unexpected behavior component: connections Relating to GraphQL Connections object type: post Relating to the Post Object Types component: pagination Relating to pagination labels Jun 12, 2026
@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
wpgraphql-com Skipped Skipped Jun 18, 2026 2:28pm

@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.77551% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.6%. Comparing base (0808257) to head (f9ad412).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...ns/wp-graphql/src/Data/Cursor/PostObjectCursor.php 90.4% 8 Missing ⚠️
plugins/wp-graphql/src/Data/Config.php 78.6% 3 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##              main   #3935      +/-   ##
==========================================
+ Coverage     77.5%   83.6%    +6.1%     
- Complexity     922    5330    +4408     
==========================================
  Files           65     286     +221     
  Lines         3307   22848   +19541     
==========================================
+ Hits          2562   19096   +16534     
- Misses         745    3752    +3007     
Flag Coverage Δ
wp-graphql-acf-wpunit-twentytwentyfive-single 77.5% <ø> (ø)
wp-graphql-wpunit-twentytwentyfive-multisite 84.6% <88.8%> (?)
wp-graphql-wpunit-twentytwentyfive-single 84.6% <88.8%> (?)
wp-graphql-wpunit-twentytwentyone-multisite 84.6% <88.8%> (?)
wp-graphql-wpunit-twentytwentyone-single 84.6% <88.8%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...c/Data/Connection/PostObjectConnectionResolver.php 97.1% <100.0%> (ø)
plugins/wp-graphql/src/Data/Config.php 67.1% <78.6%> (ø)
...ns/wp-graphql/src/Data/Cursor/PostObjectCursor.php 90.3% <90.4%> (ø)

... and 218 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jasonbahl jasonbahl merged commit b8f6405 into main Jun 18, 2026
63 of 64 checks passed
@jasonbahl jasonbahl deleted the fix/search-cursor-pagination branch June 18, 2026 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component: connections Relating to GraphQL Connections component: pagination Relating to pagination object type: post Relating to the Post Object Types type: bug Issue that causes incorrect or unexpected behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cursor pagination doesn't work with search queries

1 participant