Pagination broken when merging search results with additional WP_Query

On the search results page (search.php) I would like to perform an additional WP_Query to keep a specific order of search result types e.g. search results from authors should always be shown before the other results –

The first few pages should hold the author results, the other results should be shown on all the following pages only (not mixed on each page).

global $wp_query; //holds the results of the initial search query

$authorposts = $wp_query->posts;    
$query = new WP_Query( 'year=2012&monthnum=12&day=12' );
$otherposts = $query->posts;

$wp_query->posts = array_merge( $authorposts, $otherposts  );
$wp_query->post_count = count( $wp_query->posts );

if ( have_posts() ) : while ( have_posts() ) : the_post(); ?>
  //show results loop...

The problems appear with the behaviour of pagination – while the $authorposts seem to paginate correctly i.e. correct page numbers for author posts and pagination works as only those where existent. But the $otherposts on the other hand are shown at the bottom of each page, right under the author posts. And they are the same on every page! Also the page count reflects the count of the first query only.

I have also tried to merge with the plus operator:

$wp_query->posts = $authorposts + $otherposts;

but in this scenario the $otherposts don’t show up at all and the page count stays the same (page count for author posts).

All the examples with similar goals using two separate queries I’ve seen looked quite the same –

So I’m wondering if the my way of extending the original search query results with that additional WP_Query is sufficient or if I’m missing some $wp_query fields to be populated (correctly)? What’s the correct way doing this?

Update: It should be noted that I’m using the WP-PageNavi plugin to paginate but this should not be relevant in the first place as the first page is already rendered wrong. And it’s also initialized with the query object: wp_pagenavi( array( 'query' => $wp_query ));

Solutions Collecting From Web of "Pagination broken when merging search results with additional WP_Query"

I think the only solution is to skip the sql pagination and handle it only via php.
My idea involves 2 functions, one hooked on pre_get_posts, the second to filter the_posts.

The first function does 2 things:

  1. unset the paged value for search, in this way all the posts are returned from the SQL query.
  2. save the queried paged value in global variable, in this way can be used in the second function that run later

Here the function:

function filter_my_search_query( $query ) {
  if ( is_search() && $query->is_main_query() && ! is_admin() ) {
    global $the_original_paged;
    $the_original_paged = $query->get('paged') ? $query->get('paged') : 1;
    $query->set('paged', NULL );
    $query->set('nopaging', TRUE );
  }
}
add_action('pre_get_posts', 'filter_my_search_query', 1);

Now the search query return all the posts, with no pagination, and the paged required is saved in the global variable $the_original_paged.

So we can filter the_posts merging the wanted additional posts, then get only the correct posts based on required page and posts per page setting, and finally reset the paged and other $wp_query properties to let pagination link works:

function add_posts_to_search_query( $posts ) {
  global $wp_query, $the_original_paged;
  if ( ! is_main_query() || is_admin() || ! is_search() || is_null($the_original_paged) )
      return $posts;
  // the wanted posts per page here setted on general settings
  $perpage = get_option('posts_per_page'); 
  remove_filter( 'the_posts', 'add_posts_to_search_query' );
  $new = new WP_Query( 'year=2012&monthnum=12&day=12&nopaging=1' );
  $merged = array_merge( $posts, $new->posts );
  $wp_query->found_posts += $new->found_posts;
  // getting the right posts based on current page and posts per page
  $wp_query->posts = array_slice($merged, ( $perpage * ($the_original_paged-1) ), $perpage );
  // set the paged and other wp_query properties to the right value, to make pagination work
  $wp_query->set('paged', $the_original_paged);
  $wp_query->post_count = count($wp_query->posts);
  $wp_query->max_num_pages = ceil( $wp_query->found_posts / $perpage ); 
  unset($the_original_paged); // clean up global variable
  return $wp_query->posts; 
}
add_filter('the_posts', 'add_posts_to_search_query');

Filter the main query to include the additional posts. You can add query params with pre_get_posts or modify the SQL with posts_request or posts_where.

Filter posts_results or the_posts to arrange the returned posts.

function filter_posts_where($where)
{
    $where .= 'OR …' /* post_date is 2012-12-12 */;

    return $where;
}

function filter_the_posts($posts)
{
    usort($posts, 'compare_posts');

    return $posts;
}

function compare_posts($a, $b)
{
    // compare "author" and dates?
    if     /* … */ return  1;
    elseif /* … */ return -1;
    else           return  0;
}

I could not completly solve all the issues involved in this question but for reference I post the current state here:

function SQ_the_posts($posts, $q = false) {
    if( is_search() && is_main_query() ){

            global $wp_query;

            $authorposts = $wp_query->posts;
            $paged = get_query_var('paged');

            remove_filter( 'the_posts', 'SQ_the_posts' );
            $query = new WP_Query( 'year=2012&monthnum=12&day=12' );
            add_filter( 'the_posts', 'SQ_the_posts' );

            $otherposts = $query->posts;
            $mergedPosts = array_merge( $authorposts, $otherposts  );

            $wp_query->found_posts += $query->found_posts;
            $initialMaxNumPages = $wp_query->max_num_pages;
            if ($paged > $initialMaxNumPages) {
                    $wp_query->posts = $otherposts;
                    $wp_query->post_count = $query->post_count;
                    $posts = $otherposts;
            }

            $wp_query->max_num_pages = ($wp_query->found_posts > 0) ? $wp_query->found_posts / 10 : 0;
            $wp_query->max_num_pages += ($wp_query->found_posts % 10) ? 1 : 0;          
    }
    return $posts;
}
add_filter( 'the_posts', 'SQ_the_posts' );

What it does:

  • Queries the author and other posts
  • Calculates the correct amount of pages for both queries
  • Renders the first ten items of the second query when the max_num_pages of the first query is exceeded

What doesn’t work:

  • If for example the first query (author search query) results in 9 pages and the second query returns 5 pages the pagination shows correctly 14 pages. But when I click on page 10 pagination shows “page 10 of 5” because the 9 pages of the first query are not available anymore on that page. Is there any way to fix this?
  • The second query needs to be enhanced that the current page gets translated to its correct paging equavalent e.g. page 10 for the total queries means a paged parameter of “1” for the second query.

Still I’m amazed that there is no easy approach. In case anybody is able to solve these issues based on my answer (copy+paste) I’d be happy to mark it as accepted!