3

I am trying to get post from a category ordered random, but making sure that the author does not repeat itself.

So I have this query which works as intended, but there is the chance that the author will repeat in those 4 posts. So any ideas how I could make sure it doesn't?

I was thinking of using $wpdb but I was hoping for a more straight forward solution.

$args = array (
'post_type'              => 'post',
'post_status'            => 'publish',
'category_name'          => 'premium',
'orderby'                => 'rand',
'posts_per_page'         => 4,
'post__not_in'           => get_option( 'sticky_posts' ),
'date_query'             => array(
    array(
         array(
                'before' => 'this week'
        ) 
    ),
),
);
$query = new WP_Query( $args );

edit:

I've been trying this inside the loop. But for some reason only one post get's displayed and the $temp array still get filled with all the duplicates.

$temp=array();

if ($query->have_posts()) : while ($query->have_posts()) : $query->the_post();

 $author_id = the_author_meta( 'ID' );
 if( !in_array($author_id, $temp) ):
  array_push($temp, $author_id);
  //loop stuff
 endif;
endwhile;endif;wp_reset_query();
2
  • There is no native way to achieve this. The only two ways I can think of is getting a random array of 4 authors and then get a random post from each, or maybe a custom SQL query Feb 5, 2015 at 11:19
  • Answer updated, please see my update Feb 6, 2015 at 4:42

3 Answers 3

1

With what you are asking, it is really difficult to come up with some performant easy way that is also reliable. As @birgire already stated, his solution is not reliable, but from tests, it seem to be the fastest clocking in at 2 db queries in about 0.015s average.

From a quick discussion between @birgire and me in comments to his answer, I've decided to try and do it with what Wordpress offers. It is the most reliable way by far, but does come at some price, clocking in at 20 db queries in about 0.05s average.

The real big problem is that you cannot use any kind of cache due to the request for random results, so I had to look at other ways to cut down on time and db calls.

For the first section, I used this solution by @birgire (also mentioned by him in comments) where we make use of pre_user_query to to alter the SQL query of WP_User_Query to get only the amount of authors needed randomly. This section of the code is really fast. To increase performance, I have also set the WP_User_Query to only get the author ID's as this is all we need. This section clock in at 2 db queries in 0.002s average.

To get a single post from each author, we need to do a single WP_Query for each, which amount to a total of 4 in your case. This is the rather expensive side of the function. Contributing factors that really slows the queries down is the fact that we need random order and we are doing a tax_query as well which uses join clauses.

The only way I could speed this section up was to also just get the post ID's. Overall, this came in quicker than getting the complete posts and displaying them than just taking the post ID's and running another WP_Query afterwards. I got 7 queries less running an extra instance of WP_Query

OK, enough talk, here is the function: (Can maybe use some fine tuning)

function wpse177162_random_author_posts($number = 4, $args = [])
{

    function my_pre_user_query( $q )
    {

        $limit = preg_replace( '/[^\d]/', '', $q->query_limit );

        $from   = 'WHERE 1=1';
        $to     = sprintf( 'WHERE RAND()<(SELECT ((%d/COUNT(*))*10) FROM %susers)', 
                            $limit, 
                            $GLOBALS['wpdb']->prefix 
                 );

        $q->query_where   = str_replace( $from, $to, $q->query_where );
        $q->query_orderby = ' ORDER BY RAND() ';

        // remove the hook    
        remove_action( current_filter() , __FUNCTION__ );
    }

    $user_query_args = [
        'who'    => 'authors',
        'fields' => 'ID',
        'number' => $number,
    ];

    add_action( 'pre_user_query', 'my_pre_user_query' );
    $user_query = new WP_User_Query($user_query_args);
    remove_action( 'pre_user_query', 'my_pre_user_query' );

    $users = $user_query->results;
    $post_ids = '';
    if ($users) {

        foreach ($users as $user) {

            $user_args = [
                'author' => $user,
                'fields' => 'ids',
             'no_found_rows' => true
        ];
            $combined_args = wp_parse_args($args, $user_args);
            $q = new WP_Query($combined_args);

            $q_posts[] = $q->posts;

        }

        foreach ($q_posts as $q_post) {

            foreach ($q_post as $v ) {

                $post_ids[] = $v;

            }

        }

    }
    return (array) $post_ids;

}

Just a few notes on the function

  • The first parameter $number is the amount of authors to get

  • The second parameter is $args which is the same as the parameters used by WP_Query and is also directly fed into WP_Query as query arguments. You can use this in the same exact way, with one exception, don't set the author parameters, this will break the function

To come to how it is used, you would then use it in your template like this

$author_posts = wpse177162_random_author_posts(4, array('posts_per_page' => 1, 'cat' => 1, 'orderby' => 'rand'));
$q = new WP_Query(array('post__in' => $author_posts));

As already said, the reason for the extra WP_Query is for performance, as doing it this way gave better numbers in testing

EDIT

On recommendation from @birgire, I have updated the code above with no_found_rows to save on db calls. I did get a slight performance enhancement, on test I saved 4 db calls although the time basically stayed the same.

update_post_meta_cache and update_post_term_cache actually doubled the time taken to complete the queries, and the queries stayed fixed at 20 throughout. So not a way to go :-)

10
  • 1
    Thanks for your comment :-). Noddy badge for me for forgetting about this. I did quickly run some test, and no_found_rows did bring down the amount of db calls. update_post_meta_cache and update_post_term_cache actually doubled execution time, I think because of posts being ordered randomly :-) Between your answer and mine, the OP should be able to get a somewhat reliable way to solve his problem @birgire Feb 6, 2015 at 4:42
  • 1
    I think the OP should consider your answer instead of the hardcoded SQL, at least when dealing with few selected authors. Thanks for sharing the tests runs. I will probably get back to this problem again in the future ;-)
    – birgire
    Feb 6, 2015 at 11:18
  • 1
    This is definitely one of those questions that should be revisited in the near future when we get wiser ;-). Two hours left till weekend.... Enjoy Feb 6, 2015 at 11:22
  • 1
    Same thank you. Enjoy this one Feb 6, 2015 at 11:56
  • 1
    You most probably have authors that has no posts, and the query pulls in such authors. This means that, when you get two posts back, the query has randomly picked two authors which does not have posts Feb 23, 2015 at 14:01
2

Out of curiosity, I played around with static SQL queries and this one seemed to work:

SELECT r.post_author, r.ID, r.post_title FROM (
    SELECT  p.post_author, p.ID, p.post_title
    FROM wp_posts p 
    INNER JOIN wp_term_relationships tr ON ( p.ID = tr.object_id ) 
    WHERE    p.post_date < '2015-02-05 00:00:00'
         AND p.ID NOT IN (10,20) 
         AND tr.term_taxonomy_id IN (1)
         AND p.post_type = 'post' 
         AND p.post_status = 'publish' 
    ORDER BY RAND() 
) as r 
GROUP BY r.post_author
LIMIT 0,4

First I tried a direct group by on the post_author field and order by RAND() but then the un-aggregated fields didn't play along with it.

But of course this isn't very flexible ;-)

4
  • I'm thinking about a reliable, flexible way to do this, but what I have in mind is a bit costly using WP_User_Query and WP_Query. The problem is, you cannot really cache random ordering. This will simple defeat the purpose of random ordering :-). Must say though, I've learned something from your answer again. Wish I had more time to really sit down and learn proper php and SQL :-) Feb 5, 2015 at 12:34
  • Yes, this problem seems to be harder than it looks like at first ;-) I'm still learning SQL, so there might be some interesting solutions around this. One of the challenges here is that we could have many posts written by one author, and only few by others. It's attempting to put an extra LIMIT into the sub query, but then we might be skipping the few posts by written by the occasional authors. Also ordering by RAND() can be slow for large tables. I tried to implement a funny trick here around that problem. @PieterGoosen
    – birgire
    Feb 5, 2015 at 13:14
  • I will play around with that and some performance testing on that idea. This is a tricky one which tickled me now. Can wait to get to my pc for testing. Will post something if I come up with something viable :-) Feb 5, 2015 at 13:21
  • Look forward to hear about your experiments ;-)
    – birgire
    Feb 5, 2015 at 13:30
1

Another solution that seams to work is explained:here

Adding the filter before the query:

function filter_authors($groupby) {
 global $wpdb;
 $groupby = " {$wpdb->posts}.post_author";
 return $groupby;
}

add_filter('posts_groupby','filter_authors');

And not forgetting to remove it afterwards:

remove_filter('posts_groupby','filter_authors');

This solution seams to work quite nicely.

4
  • 1
    Do you really get random posts with this setup? I already tried this approach but always got the same posts returned.
    – birgire
    Feb 5, 2015 at 12:25
  • Yes, i get random posts with using the $args from the initial post here.
    – CiprianD
    Feb 5, 2015 at 13:28
  • How strange - I always get the earliest post by each author when using that approach in SQL. I also tried your query with that filter but it didn't change at all. I'm on MySQL 5.5.
    – birgire
    Feb 5, 2015 at 13:54
  • Actually you're right. The posts shown are random, but if I have an author with more than one post then I only get the oldest post each time.
    – CiprianD
    Feb 5, 2015 at 14:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.