@@ -38,6 +38,13 @@ class Tests_Query_DeterministicOrdering extends WP_UnitTestCase {
3838 */
3939 protected static $ menu_order_post_ids = array ();
4040
41+ /**
42+ * Post IDs for search relevance tests.
43+ *
44+ * @var array
45+ */
46+ protected static $ search_relevance_post_ids = array ();
47+
4148 /**
4249 * Set up shared fixtures for all tests.
4350 */
@@ -106,6 +113,20 @@ public static function set_up_before_class() {
106113 )
107114 );
108115 }
116+
117+ // Create posts for search relevance tests.
118+ // All posts will have the same content to ensure same relevance scores.
119+ $ identical_content = 'This is a search test post with identical content ' ;
120+ for ( $ i = 1 ; $ i <= 20 ; $ i ++ ) {
121+ self ::$ search_relevance_post_ids [] = self ::factory ()->post ->create (
122+ array (
123+ 'post_type ' => 'wptests_time_ident ' ,
124+ 'post_title ' => "Search Post $ i " ,
125+ 'post_content ' => $ identical_content ,
126+ 'post_excerpt ' => $ identical_content ,
127+ )
128+ );
129+ }
109130 }
110131
111132 /**
@@ -115,10 +136,11 @@ public static function tear_down_after_class() {
115136 _unregister_post_type ( 'wptests_time_ident ' );
116137 _unregister_post_type ( 'wptests_title_ident ' );
117138
118- self ::$ date_identical_post_ids = array ();
119- self ::$ title_identical_post_ids = array ();
120- self ::$ search_post_ids = array ();
121- self ::$ menu_order_post_ids = array ();
139+ self ::$ date_identical_post_ids = array ();
140+ self ::$ title_identical_post_ids = array ();
141+ self ::$ search_post_ids = array ();
142+ self ::$ menu_order_post_ids = array ();
143+ self ::$ search_relevance_post_ids = array ();
122144
123145 parent ::tear_down_after_class ();
124146 }
@@ -497,4 +519,118 @@ public function test_deterministic_ordering_with_metadata() {
497519 $ this ->assertEquals ( 10 , count ( $ page1_ids ), 'First page should have 10 posts ' );
498520 $ this ->assertEquals ( 10 , count ( $ page2_ids ), 'Second page should have 10 posts ' );
499521 }
522+
523+ /**
524+ * Test that deterministic ordering works with search relevance ordering.
525+ *
526+ * When ordering by search relevance, multiple posts can have the same relevance score,
527+ * causing duplicate records across pages without deterministic ordering.
528+ *
529+ * @ticket xxxxx
530+ */
531+ public function test_deterministic_ordering_with_search_relevance () {
532+ // Use shared fixtures with identical content (same relevance scores)
533+ // Get first page ordering by relevance
534+ $ query1 = new WP_Query (
535+ array (
536+ 'post_type ' => 'wptests_time_ident ' ,
537+ 'post__in ' => self ::$ search_relevance_post_ids ,
538+ 's ' => 'search test ' ,
539+ 'orderby ' => 'relevance ' ,
540+ 'order ' => 'DESC ' ,
541+ 'posts_per_page ' => 10 ,
542+ 'paged ' => 1 ,
543+ )
544+ );
545+
546+ // Get second page ordering by relevance
547+ $ query2 = new WP_Query (
548+ array (
549+ 'post_type ' => 'wptests_time_ident ' ,
550+ 'post__in ' => self ::$ search_relevance_post_ids ,
551+ 's ' => 'search test ' ,
552+ 'orderby ' => 'relevance ' ,
553+ 'order ' => 'DESC ' ,
554+ 'posts_per_page ' => 10 ,
555+ 'paged ' => 2 ,
556+ )
557+ );
558+
559+ $ page1_ids = wp_list_pluck ( $ query1 ->posts , 'ID ' );
560+ $ page2_ids = wp_list_pluck ( $ query2 ->posts , 'ID ' );
561+
562+ // Verify no overlap between pages (no duplicates)
563+ $ overlap = array_intersect ( $ page1_ids , $ page2_ids );
564+ $ this ->assertEmpty ( $ overlap , 'Pages should not contain duplicate posts when ordering by search relevance ' );
565+
566+ // Verify total count is correct
567+ $ this ->assertEquals ( 20 , $ query1 ->found_posts , 'Total posts should be 20 ' );
568+ $ this ->assertEquals ( 10 , count ( $ page1_ids ), 'First page should have 10 posts ' );
569+ $ this ->assertEquals ( 10 , count ( $ page2_ids ), 'Second page should have 10 posts ' );
570+
571+ // Verify deterministic ordering: same query should return same results
572+ $ query1_repeat = new WP_Query (
573+ array (
574+ 'post_type ' => 'wptests_time_ident ' ,
575+ 'post__in ' => self ::$ search_relevance_post_ids ,
576+ 's ' => 'search test ' ,
577+ 'orderby ' => 'relevance ' ,
578+ 'order ' => 'DESC ' ,
579+ 'posts_per_page ' => 10 ,
580+ 'paged ' => 1 ,
581+ )
582+ );
583+ $ page1_repeat_ids = wp_list_pluck ( $ query1_repeat ->posts , 'ID ' );
584+
585+ $ this ->assertEquals ( $ page1_ids , $ page1_repeat_ids , 'Same query should return same results when ordering by search relevance ' );
586+ }
587+
588+ /**
589+ * Test that deterministic ordering works with search when orderby is empty (defaults to relevance).
590+ *
591+ * When orderby is empty and search is present, WordPress orders by relevance.
592+ * Multiple posts can have the same relevance score, causing duplicate records across pages.
593+ *
594+ * @ticket xxxxx
595+ */
596+ public function test_deterministic_ordering_with_search_empty_orderby () {
597+ // Use shared fixtures with identical content (same relevance scores)
598+ // Get first page with empty orderby (defaults to relevance)
599+ $ query1 = new WP_Query (
600+ array (
601+ 'post_type ' => 'wptests_time_ident ' ,
602+ 'post__in ' => self ::$ search_relevance_post_ids ,
603+ 's ' => 'search test ' ,
604+ 'orderby ' => '' , // Empty orderby with search defaults to relevance
605+ 'order ' => 'DESC ' ,
606+ 'posts_per_page ' => 10 ,
607+ 'paged ' => 1 ,
608+ )
609+ );
610+
611+ // Get second page with empty orderby
612+ $ query2 = new WP_Query (
613+ array (
614+ 'post_type ' => 'wptests_time_ident ' ,
615+ 'post__in ' => self ::$ search_relevance_post_ids ,
616+ 's ' => 'search test ' ,
617+ 'orderby ' => '' , // Empty orderby with search defaults to relevance
618+ 'order ' => 'DESC ' ,
619+ 'posts_per_page ' => 10 ,
620+ 'paged ' => 2 ,
621+ )
622+ );
623+
624+ $ page1_ids = wp_list_pluck ( $ query1 ->posts , 'ID ' );
625+ $ page2_ids = wp_list_pluck ( $ query2 ->posts , 'ID ' );
626+
627+ // Verify no overlap between pages (no duplicates)
628+ $ overlap = array_intersect ( $ page1_ids , $ page2_ids );
629+ $ this ->assertEmpty ( $ overlap , 'Pages should not contain duplicate posts when ordering by search relevance (empty orderby) ' );
630+
631+ // Verify total count is correct
632+ $ this ->assertEquals ( 20 , $ query1 ->found_posts , 'Total posts should be 20 ' );
633+ $ this ->assertEquals ( 10 , count ( $ page1_ids ), 'First page should have 10 posts ' );
634+ $ this ->assertEquals ( 10 , count ( $ page2_ids ), 'Second page should have 10 posts ' );
635+ }
500636}
0 commit comments