How to display Related Posts based on number of taxonomy terms matched

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().