Retrieving Posts by Similarity - Django Blog #8

Hello Internet Programmer, today we retrieving posts by similarity. So, activate your virtual environment and let’s code.

We have implemented tagging for your blog posts, you can do many interesting things with tags.

Tags allow us to categorize posts in a non-hierarchical manner. Posts about similar topics will have several tags in common.

Posts about similar topics will have several tags in common. We will build a functionality to display similar posts by the number of tags they share.

In this way, when a user reads a post, you can suggest to them that they read other related posts.

What we do

In order to retrieve similar posts for a specific post, you need to perform the following steps:

  1. Retrieve all tags for the current post
  2. Get all posts that are tagged with any of those tags
  3. Exclude the current post from that list to avoid recommending the same post
  4. Order the results by the number of tags shared with the current post
  5. In the case of two or more posts with the same number of tags, recommend the most recent post
  6. Limit the query to the number of posts you want to recommend

Change in View

These steps are translated into a complex QuerySet that you will include in your post_detail view.

Open the views.py file of your blog application and add the following import at the top of it,

from django.db.models import Count

This is the Count aggregation function of the Django ORM. This function will allow you to perform aggregated counts of tags. django.db.models includes the following aggregation functions,

  • Avg: The mean value
  • Max: The maximum value
  • Min: The minimum value
  • Count: The total number of objects

Add the following lines inside the post_detail view before the render() function, with the same indentation level,

    # List of similar posts
    post_tags_ids = post.tags.values_list('id', flat=True)
    similar_posts = Post.published.filter(tags__in=post_tags_ids).exclude(id=post.id)
    similar_posts = similar_posts.annotate(same_tags=Count('tags')).order_by('-same_tags','-publish')[:6]

    return render(request, 'post_detail.html',{'post':post,'comments': comments,'comment_form':comment_form,'similar_posts':similar_posts})

We retrieve a Python list of IDs for the tags of the current post. The values_list() QuerySet returns tuples with the values for the given fields.

We pass flat=True to it to get single values such as [1, 2, 3, …] instead
of one-tuples such as [(1,), (2,), (3,) …].

We get all posts that contain any of these tags, excluding the current post itself.

We use the Count aggregation function to generate a calculated field same_tags that contains the number of tags shared with all the tags queried.

We order the result by the number of shared tags (descending order) and by publish to display recent posts first for the posts with the same number of shared tags. You slice the result to retrieve only the first four posts.

At last, we added the similar_post object to the context dictionary for the render() function.

Displaying on Template

Open post_detail.html of template folder and add following code to display similar post,

<hr />

<div class="my-3">
    <h3>Similar Posts</h3>
</div>
<div class="row">
    {% for post in similar_posts %}
    <div class="col-md-6 col-sm-6 col-lg-4 mb-3">
        <div class="card border-0">
            <div class="ratio ratio-16x9">
                <img src="{{ post.image.url }}" class="rounded featured-image-list" alt="{{post.title}}">
            </div>
            <div class="card-body p-0 m-0 mt-2">
                <h2 class="card-title h6 my-0 py-0">
                    <a href="{{post.get_absolute_url}}">{{post.title}}</a>
                </h2>
                <p class="card-text">
                    <small class="text-muted">
                        <span>{{post.publish.date}} by
                            <b>{{post.author}}</b></span>

                        <!-- <a href="" class="badge badge-light text-muted">{{post.tags.all|join:" "}}</a> -->


                    </small>
                </p>
                <!-- <p>{{post.body|truncatechars:100|linebreaks}}</p> -->
            </div>
        </div>
    </div>
    {% empty %}
    <p class="text-muted">There is no similar post yet</p>
    {% endfor %}
</div>
             

Now the post detail page should look like this,

Similar posts

That’s it for this tutorial. Hope you are enjoying this series. Share this with your friends and happy coding.

GitHub Link: https://github.com/SoniArpit/awwblog

Previous: Adding the Tagging Functionality in the Blog – Django Blog #7

Next: Building a Search Functionality – Django Blog #9