Related Posts by Multiple Tags?

I was curious if was possible to display related posts by multiple tags.

The site I am working on has about 5 tags per post. Most posts have 1 or 2 tags in common. The related posts I’d like to show have 3-5 tags in common.

Read More

So, I’d like the related posts to function by looking for posts with the most number of tags in common and display them in descending order.

Let’s say I display 3 related posts: relatedpost1 would have 4 tags in common, relatedpost2 would have 3 in common and relatedpost3 would have 1 in common.

Is it even possible to do this?

Right now I am messing around with two ways of displaying the posts but they arent functioning as I’d like:

The first method (code) just shows posts with ANY tags in common.

<?php $orig_post = $post;
global $post;
$tags = wp_get_post_tags($post->ID);
if ($tags) {
$tag_ids = array();
foreach($tags as $individual_tag) $tag_ids[] = $individual_tag->term_id;
$args=array(
'tag__in' => $tag_ids,
'post__not_in' => array($post->ID),
'posts_per_page'=>3, // Number of related posts that will be shown.
'caller_get_posts'=>1
);
$my_query = new wp_query( $args );
if( $my_query->have_posts() ) {
echo '<div id="relatedposts"><h3>Related Posts</h3><div class="relatedbreak"></div><ul id="relatedul">';
while( $my_query->have_posts() ) {
$my_query->the_post(); ?>
<li><div class="relatedthumb"><a href="<? the_permalink()?>" rel="bookmark" title="<?php the_title(); ?>"><?php the_post_thumbnail(array(185, 185)); ?></a></div>
<div class="relatedcontent">
<center><a href="<? the_permalink()?>" rel="bookmark" title="<?php the_title(); ?>"><div class="comments_text"><?php the_title(); ?></div></center></a>
</div>
</li>
<? }
echo '</ul></div>';
}
}
$post = $orig_post;
wp_reset_query(); ?>`

The second method (code) just shows posts with the first tag in common.

<?php
//for use in the loop, list 5 post titles related to first tag on current post
$tags = wp_get_post_tags($post->ID);
if ($tags) {
echo '<div id="relatedposts"><h3>Related Posts</h3></div><div class="relatedbreak"></div>';
$first_tag = $tags[0]->term_id;
$args=array(
'tag__in' => array($first_tag),
'post__not_in' => array($post->ID),
'posts_per_page'=>3,
'caller_get_posts'=>1
);
$my_query = new WP_Query($args);
if( $my_query->have_posts() ) {
while ($my_query->have_posts()) : $my_query->the_post(); ?>
<ul id="relatedul">
<li><div class="relatedthumb"><a href="<? the_permalink()?>" rel="bookmark" title="<?php the_title(); ?>"><?php the_post_thumbnail(array(185, 185)); ?></a></div>
<a href="<?php the_permalink() ?>" rel="bookmark" title="Permanent Link to <?php the_title_attribute(); ?>"><div class="comments_text"><?php the_title(); ?></div></a></li>
</ul>
<?php
endwhile;
}
wp_reset_query();
}
?>

Both ways kind of suck; I’m either getting pretty random posts displaying (since most of my posts have at least 1 tag in common) or (for some posts) getting no related posts (since their common tags are tag 4 or 5).

Any help would be greatly appreciated.

Related posts

3 comments

  1. I had the same idea and wrote a small little plugin to help me do this.

    function get_pew_related_data($args, $post_id, $related_id) {
        global $post, $wpdb;
        $post_id = intval( $post_id );
        if( !$post_id && $post->ID ) {
            $post_id = $post->ID;
        }
    
        if( !$post_id ) {
            return false;
        }
    
        $defaults = array(
            'taxonomy' => 'topics',
            'post_type' => array('post'),
            'max' => 5
        );
        $options = wp_parse_args( $args, $defaults );
    
        $transient_name = 'pew-related-' . $options['taxonomy'] . '-' . $post_id;
    
        if( isset($_GET['flush-related-links']) && is_user_logged_in() ) {
            echo '<p>Related links flushed! (' . $transient_name . ')</p>';
            delete_transient( $transient_name );
        }
    
        $output = get_transient( $transient_name );
        if( $output !== false && !is_preview() ) {
            //echo $transient_name . ' read!';
            return $output;
        } 
    
        $args = array(
            'fields' => 'ids',
            'orderby' => 'count',
            'order' => 'ASC'
        );
        $orig_terms_set = wp_get_object_terms( $post_id, $options['taxonomy'], $args );
    
        //Make sure each returned term id to be an integer.
        $orig_terms_set = array_map('intval', $orig_terms_set);
    
        //Store a copy that we'll be reducing by one item for each iteration. 
        $terms_to_iterate = $orig_terms_set;
    
        $post_args = array(
            'fields' => 'ids',
            'post_type' => $options['post_type'],
            'post__not_in' => array($post_id),
            'posts_per_page' => 50
        );
        $output = array();
        while( count( $terms_to_iterate ) > 1 ) {
    
            $post_args['tax_query'] = array(
                array(
                    'taxonomy' => $options['taxonomy'],
                    'field' => 'id',
                    'terms' => $terms_to_iterate,
                    'operator' => 'AND'
                )
            );
    
            $posts = get_posts( $post_args );
    
            /*
            echo '<br>';
            echo '<br>';
            echo $wpdb->last_query;
            echo '<br>';
            echo 'Terms: ' . implode(', ', $terms_to_iterate);
            echo '<br>';
            echo 'Posts: ';
            echo '<br>';
            print_r( $posts );
            echo '<br>';
            echo '<br>';
            echo '<br>';
            */
    
            foreach( $posts as $id ) {
                $id = intval( $id );
                if( !in_array( $id, $output) ) {
                    $output[] = $id;
                }
            }
            array_pop( $terms_to_iterate );
        }
    
        $post_args['posts_per_page'] = 10;
        $post_args['tax_query'] = array(
            array(
                'taxonomy' => $options['taxonomy'],
                'field' => 'id',
                'terms' => $orig_terms_set
            )
        );
    
        $posts = get_posts( $post_args );
    
        foreach( $posts as $count => $id ) {
            $id = intval( $id );
            if( !in_array( $id, $output) ) {
                $output[] = $id;
            }
            if( count($output) > $options['max'] ) {
                //We have enough related post IDs now, stop the loop.
                break;
            }
        }
    
        if( !is_preview() ) {
            //echo $transient_name . ' set!';
            set_transient( $transient_name, $output, 24 * HOUR_IN_SECONDS );
        }
    
        return $output;
    }
    
    function pew_related( $args = array(), $post_id = '', $related_id = '' ) {
        $post_ids = get_pew_related_data( $args, $post_id, $related_id );
    
        if( !$post_ids ) {
            return false;
        }
    
        $defaults = array(
            'post__in' => $post_ids,
            'orderby' => 'post__in',
            'post_type' => array('post'),
            'posts_per_page' => min( array(count($post_ids), 10)),
            'related_title' => 'Related Posts'
        );
        $options = wp_parse_args( $args, $defaults );
    
        $related_posts = new WP_Query( $options );
        if( $related_posts->have_posts() ):
        ?>
        <h5><?=$options['related_title']?></h5>
        <div id="related-material" class="promo">
            <?php while ( $related_posts->have_posts() ):
                $related_posts->the_post();
            ?>
                <a class="post" href="<?=the_permalink();?>">
                    <div class="meta">
                        <?php
                        $post_project = wp_get_object_terms($related_posts->post->ID, 'projects');
                        $project = 'Pew Research Center';
                        $project_slug = '';
                        if( isset($post_project[0]) ) {
                            $project = $post_project[0]->name;
                            $project_slug =  $post_project[0]->slug;
                        } elseif( $related_posts->post->post_type == 'fact-tank' ) {
                            $project = 'Fact Tank';
                            $project_slug = 'fact-tank';
                        }
                        ?>
                        <span class="project <?=$project_slug;?> right-seperator"><?=$project;?></span>
                        <span class="date"><?php the_time('M j, Y'); ?></span>
                    </div>
                    <h2><?=the_title();?></h2>
                </a>
            <?php endwhile;
                wp_reset_postdata();
    
             ?> 
        </ol>
        </div>
        <?php
        endif;
    
    }
    

    It looks for posts that have common terms and the terms are sorted by frequency so the least used terms come first then the more popular terms. The first function fetches the data and stores it in a transient so the results aren’t run over and over and over again unnecessarily. The second function just renders the output. This is what powers our related posts on one of our sites at work http://www.pewresearch.org/fact-tank/2013/08/02/both-parties-underwater-heading-into-2014-elections/

    The algorithm works like this:

    1. Get all the terms from the post ordered by count in ascending order
      (least popular to more popular)
    2. Loop over this set of terms and look for posts that contain Term1
      AND Term2 AND Term3
    3. With each iteration remove the least popular term from the list
      broadening our results until we get the desired number of posts or
      we only have one term left to check.
    4. If we still don’t have enough posts to meet our needs, then look for
      posts that contain Term1 OR Term2 OR Term3
    5. Save the result to a transient so we don’t have to run these queries
      again for a while.

    Hope this helps you out.

  2. I don’t think query_posts() or new WP_Query() will do any good here. You need to query the database directly. Here goes the method I’ve written to achieve what you seem to need:

    /*
     * Returns related posts to a given post based on a specific taxonomy
     * By default, this method returns list of posts with the highest number of common tags
     *
     * var $post_id - the reference post for which we want to get the list of similar posts
     * var $number_posts - max how many related posts to return, 0 for unlimited
     * var $taxonomy - which taxonomy to use to determine related posts ( 'post_tag' or 'category' are the basic examples )
     * var $post_type - change to a custom post_type if you want to get related posts of another post type
     */
    function exe_get_related_posts_by_common_terms( $post_id, $number_posts = 0, $taxonomy = 'post_tag', $post_type = 'post' ) {
    
        global $wpdb;
    
        $post_id = (int) $post_id;
        $number_posts = (int) $number_posts;
    
        $limit = $number_posts > 0 ? ' LIMIT ' . $number_posts : '';
    
        $related_posts_records = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT tr.object_id, count( tr.term_taxonomy_id ) AS common_tax_count
                 FROM wp_term_relationships AS tr
                 INNER JOIN wp_term_relationships AS tr2 ON tr.term_taxonomy_id = tr2.term_taxonomy_id
                 INNER JOIN wp_term_taxonomy as tt ON tt.term_taxonomy_id = tr2.term_taxonomy_id
                 INNER JOIN wp_posts as p ON p.ID = tr.object_id
                 WHERE
                    tr2.object_id = %d
                    AND tt.taxonomy = %s
                    AND p.post_type = %s
                 GROUP BY tr.object_id
                 HAVING tr.object_id != %d
                 ORDER BY common_tax_count DESC" . $limit,
                 $post_id, $taxonomy, $post_type, $post_id
            )
        );
    
        if ( count( $related_posts_records ) === 0 )
            return false;
    
        $related_posts = array();
    
        foreach( $related_posts_records as $record )
            $related_posts[] = array(
                get_post( (int) $record->object_id ),
                'common_tax_count' => $record->common_tax_count
            );
    
        return $related_posts;
    
    }
    

    The mehtod is designed to fetch related posts based on a single taxonomy, by default on ‘post_tag’ taxonomy. Posts are sorted by the number of common terms from highest to lowest. To fetch related posts for multiple common taxonomies at once, you would need to tweak AND tt.taxonomy = %s to something like AND ( tt.taxonomy = 'post_tag' OR tt.taxonomy = 'category' ). The same applies if you wanted to fetch posts of different post types.

  3. Thanks @Dero. Please remember to set the database prefix correctly:

    function exe_get_related_posts_by_common_terms( $post_id, $number_posts = 0, $taxonomy = 'post_tag', $post_type = 'post' ) {
        global $wpdb;
    
        $post_id = (int) $post_id;
        $number_posts = (int) $number_posts;
    
        $limit = $number_posts > 0 ? ' LIMIT ' . $number_posts : '';
    
        $related_posts_records = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT tr.object_id, count( tr.term_taxonomy_id ) AS common_tax_count
                 FROM {$wpdb->term_relationships} AS tr
                 INNER JOIN {$wpdb->term_relationships} AS tr2 ON tr.term_taxonomy_id = tr2.term_taxonomy_id
                 INNER JOIN {$wpdb->term_taxonomy} as tt ON tt.term_taxonomy_id = tr2.term_taxonomy_id
                 INNER JOIN {$wpdb->posts} as p ON p.ID = tr.object_id
                 WHERE
                    tr2.object_id = %d
                    AND tt.taxonomy = %s
                    AND p.post_type = %s
                 GROUP BY tr.object_id
                 HAVING tr.object_id != %d
                 ORDER BY common_tax_count DESC" . $limit,
                $post_id, $taxonomy, $post_type, $post_id
            )
        );
    
        if ( count( $related_posts_records ) === 0 )
            return false;
    
        $related_posts = array();
    
        foreach( $related_posts_records as $record )
            $related_posts[] = array(
                'post_id' => (int) $record->object_id,
                'common_tax_count' => $record->common_tax_count
            );
    
        return $related_posts;
    }
    

    So it works great on all systems.

Comments are closed.