fix: cursor pagination drops results for searched post connections (relevance-aware cursors)#3935
Merged
Merged
Conversation
… 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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ 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
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 theendCursorreturned 1 result instead of 5 and reportedhasNextPage: false, leaving 4 of 10 results unreachable.Root cause
Two layers:
$query_args['search'], but the GraphQL input is mapped tosbefore the check runs, so it has been dead code since it was added.parse_search_order(), aCASE WHEN post_title LIKE ... THEN 1 ...expression prepended to the ORDER BY), butPostObjectCursorbuilt its cutoff only frompost_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 expressionparse_search_order()generates, built from the same prepared LIKE clauses WP_Query stores in the query vars duringparse_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, thenpost_date, thenIDfrom the existing stability filter).Config: a newposts_search_orderbyfilter 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 thepost_parentunset for searches now correctly checkss.Explicitly supplied
orderbyinput 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, andlast: 5returns the exact tail of the display order.