Question

I have a custom post-type 'News' which has multiple custom taxonomies 'news-category', 'news-tags'. Each post contains between 4-8 'news-tags' and 1 'news-category'.

For my current post, I'd like to display related posts based on common 'news-tags' terms added to my current post, while also matching the 'news-category' term.

So, I'd like the related posts to look for posts of the same 'news-category' with the most number of 'news-tags' terms in common and display them in descending order.

If I were to display 4 related posts:

  • the first post might have 5 'news-tags' terms in common,
  • the second post might have 3 'news-tags' terms in common,
  • the third post might have 2 'news-tags' terms in common,
  • and the last post might have 'news-tags' 1 term in common.

and they'd all belong to the same 'news-category'.

Would it be possible to do this? I've really been struggling with this so I'll appreciate any help.

Was it helpful?

Solution

Let's split the problem up in three bits: retrieving the related posts from the database, sorting them and displaying the result.

Post retrieval

This is possible using the taxonomy parameters offered in WP_Query.

Let's first retrieve the category of the current post and find their IDs:

$categories = get_the_terms( get_the_ID(), 'news-category' );

foreach ( $categories as $category ) {
                        $category_ids[] = $category->term_id;
                    }

We do the same for the tags:

$tags = get_the_terms( get_the_ID(), 'news-tags' );

foreach ( $tags as $tag) {
                        $tag_ids[] = $tag->term_id;
                    }

Then, we build a set of query arguments (we later feed to a WP_Query-call):

$related_args = array(
    'post_type'      => array(
        'news',
    ),
    'post_status'    => 'publish',
    'posts_per_page' => -1, // Get all posts
    'post__not_in'   => array( get_the_ID() ), // Hide current post in list of related content
    'tax_query'      => array(
        'relation' => 'AND', // Make sure to mach both category and term
        array(
            'taxonomy' => 'news-category',
            'field'    => 'term_id',
            'terms'    => $category_ids,
        ),
        array(
            'taxonomy' => 'news-tags',
            'field'    => 'term_id',
            'terms'    => $tag_ids,
        ),
    ),
);

Finally, we run the query that gives us an array of

$related_all = new WP_Query( $related_args );

Please note this query retrieves all posts that match the 'filter', as we're doing the ordering later on. If we'd now only query 4 posts, those might not be the most relevant.

Sidenote: the above query is quite heavy, in the sense that it (potentially) retrieves a large number of posts. When I've created a related posts section for a client project (also a news section), I ordered by date rather than relevance. That allows you to set a limit to the number of posts you retrieve from the database (change the -1 to 4 in your case, and add 'orderby' => array( 'date' => 'DESC' ) to the $related_args-array). If you want to stick with ordering on the overlap, I suggest you add a date filter in the query arguments, or limit the number of results to some finite value, from which set you then retrieve the most relevant posts.

Post ordering

Following the previous step, $related_all is a WP_Query-object. We access the actual posts as follows, and store them in $related_all_posts:

$related_all_posts = $related_all->posts;

That gives us an array we can more easily work with. We then loop through all the results in that array. Per the comments in the code, when looping through the results, we find the tags associated to the (related) post, find their IDs, compare that with the $tag_ids (of the main post) found earlier and see how much overlap there is using array_intersect():

foreach($related_all_posts as $related_post){
    // Find all tags of the related post under consideration
    $related_post_tags = get_the_terms( $related_post->ID, 'news-tags' );
        foreach ( $related_post_tags as $related_post_tag ) {
            $related_tag_ids[] = $related_post_tag->term_id;    // Save their IDs in a query
        }

    // Find overlap with tags of main post (in $tag_ids) using array_intersect, and save that number in
    // an array that has the ID of the related post as array key.
    $related_posts_commonality[$related_post->ID] = count(array_intersect($related_tag_ids, $tag_ids));
}

We then sort that latest array by value, from high to low, using arsort().

arsort($related_posts_commonality);

Finally, we limit it to only four posts using array_slice():

$related_posts_commonality = array_slice($related_posts_commonality, 0, 4);

You can find the IDs of the related posts using array_keys, e.g.:

$related_posts_IDs = array_keys($related_posts_commonality);

Post display

To actually display the posts, there's two routes you can take. You can either use the $related_posts_commonality array to loop through the results of WP_Query (i.e., $related_all), save the matching posts (in their right order) in a new array or object and loop through these once again for display. As this doesn't require additional queries, it's probably the most efficient one. However, it's also a pain.

As such, you can also use simply use the IDs we've just found ($related_posts_IDs) to run another query.

$related_sorted = WP_query(array('post__in' => $related_posts_IDs));

Then, you can use a regular loop (while ($related_sorted->have_posts()) and so on) to go through and display the results using functions as the_title() and the_content().

OTHER TIPS

Since a given post type shares the same taxonomies and a common scenario might be to find related posts based on the number of matching terms across all taxonomies (no matter how many or what they are called), I created a snippet for this. This approach doesn't require explicitly defining your custom taxonomies to check against (although you can if you want) and also gets a total count of all matching terms in order to sort the related posts from highest to lowest. You can modify the snippet in order to specify only certain taxonomies or change the limit of up to how many related posts should be returned.

The snippet has comments for explanation, but essentially in your template this function will take in the current post ID and return an array of post IDs for the related posts. From there, you can check the result and, assuming at least some other posts with matching terms are found, you can loop over the related post IDs returned and then use the ID to display whatever information about each post (title, link to the post etc).

Not necessarily a correct answer, but just something that based on the title of the OP's question might help others coming to this question no matter what custom taxonomies or number of different taxonomies they might be using in their project. It's worked pretty well for me at least. Hopefully someone may find it useful.

Here is a link to the snippet.

Get Recent Posts

Here is the snippet itself copied below as well.

/**
 * A WordPress helper function that takes the ID of the current post
 * and returns the top three related posts ranked by highest number of taxonomy
 * terms in common with the current post. Alternately, you can modify lines 26-33
 * to exclude certain taxonomies so that we only check for terms in specific taxonomies 
 * to determine the related posts. To include up to the top X related posts instead of
 * up to three, you can modify lines 148-149.
 *
 * In your template to make use of this function you would do something like...
 *
 * $current_post_id = get_the_id();
 * $related_post_ids = get_related_posts($current_post_id);
 * 
 */

function get_related_posts($current_post_id)
    {   
        // Get the post type we're dealing with based on the current post ID.
        $post_type = get_post_type($current_post_id);

        // Get all taxonomies of the specified post type of the current post.
        $taxonomies = [];
        $taxonomy_objects = get_object_taxonomies( $post_type, 'objects' );
        foreach($taxonomy_objects as $taxonomy) {
            // If you want to only check against certain taxonomies, modify this section as needed
            // to set conditions for which taxonomies should be excluded or included. Below is just an example.
            // if ($taxonomy->name !== 'post_format' && $taxonomy->name !== 'post_tag') {
            //     array_push($taxonomies, $taxonomy);
            // }
          
            // By default, we will check against all taxonomies.
            array_push($taxonomies, $taxonomy);
        }

        // Get all the posts of the specified post type,
        // excluding the current post, so that we can compare these
        // against the current post.
        $other_posts_args = array(
            'post_type'      => $post_type,
            'post__not_in'   => array($current_post_id),
        );
        $other_posts = new WP_Query( $other_posts_args );

        wp_reset_postdata();

        // We will create an object for each matching post that will include
        // the ID and count of the number of times it matches any taxonomy term with the current post.
        // Later, when we create those, they will get pushed to this $matching_posts array.
        $matching_posts = array();

        // If we have other posts, loop through them and
        // count matches for any taxonomy terms in common.
        if($other_posts->have_posts()) {

            foreach($taxonomies as $taxonomy) {

                // Get the term IDs of terms for the current post
                // (the post presumably displaying as a single post
                // back in our template, for which were finding related posts).
                $current_post_terms = get_the_terms($current_post_id, $taxonomy->name);


                // Only continue if the current post actually has some terms for this taxonomy.
                if($current_post_terms !== false) {

                    foreach($other_posts->posts as $post) {

                        // Get the term IDs of terms for this taxonomy
                        // for the other post we are currently looping over.
                        $other_post_terms = get_the_terms($post->ID, $taxonomy->name);

                        // Check that other post has terms and only continue if there
                        // are terms to compare.
                        if($other_post_terms !== false) {

                            $other_post_term_IDs = array();
                            $current_post_term_IDs = array();

                            // Get term IDs from each term in the current post.
                            foreach($current_post_terms as $term) {
                                array_push($current_post_term_IDs, $term->term_id);
                            }

                            // Get term IDs from each term in the other post.
                            foreach($other_post_terms as $term) {
                                array_push($other_post_term_IDs, $term->term_id);
                            }

                            if( !empty($other_post_term_IDs) && !empty($current_post_term_IDs) ) {
                                
                                // Collect the matching term IDs for the terms the posts have in common.
                                $match_count = sizeof(array_intersect($other_post_term_IDs, $current_post_term_IDs));
                                
                                // Get the ID of the other post to use to identify and store this post
                                // in our results.
                                $post_ID = $post->ID;

                                if ($match_count > 0) {

                                    // Assume post not added previously.
                                    $post_already_added = false;

                                    // If posts have already been added to our matches
                                    // then check to see if we already added this post.
                                    if(!empty($matching_posts)) {
        
                                        foreach($matching_posts as $post) {
                                            // If this post was added previously then let's increment the count
                                            // for our new matching terms.
                                            if (isset($post->ID) && $post->ID == $post_ID) {
                                                $post->count += $match_count;
                                                // Switch this to true for the check we perform below.
                                                $post_already_added = true;
                                            }
                                        }
                                        
                                        // If never found a post with same ID in our $matching_posts
                                        // list then create a new entry associated with this post and add it.
                                        if ($post_already_added === false) {
                                            $new_matching_post = new stdClass();
                                            $new_matching_post->ID = $post_ID;
                                            $new_matching_post->count = $match_count;
                                            array_push($matching_posts, $new_matching_post);
                                        }
                                    } else {
                                        // If no posts have been added yet to $matching_posts then this will be the first.
                                        $new_matching_post = new stdClass();
                                        $new_matching_post->ID = $post_ID;
                                        $new_matching_post->count += $match_count;
                                        array_push($matching_posts, $new_matching_post);
                                    }
                                }
                            }
                        }
                    }
                }
            }

            
            if(!empty($matching_posts)) {
                // Sort the array in order of highest count for total terms in common
                // (most related to least).
                usort($matching_posts, function($a, $b) {
                    return strcmp($b->count, $a->count);
                });

                // Just take the top 3 most related
                $most_related = array_slice($matching_posts, 0, 3);

                $related_posts = array_map(function($obj) {
                    return $obj->ID;
                }, $most_related);

                // Return an array of post IDs for the related posts
                return $related_posts;
    
            }
   
        } 

        // Else we never found any related posts.
        return false;


    }
Licensed under: CC-BY-SA with attribution
Not affiliated with wordpress.stackexchange
scroll top