Retrieving posts by similarity
Now that we have implemented tagging for our blog posts, we can do many interesting things with them. Using tags, we can classify our blog posts very well. 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, we can suggest to them that they read other related posts.
In order to retrieve similar posts for a specific post, we need to perform the following steps:
- Retrieve all tags for the current post
- Get all posts that are tagged with any of those tags
- Exclude the current post from that list to avoid recommending the same post
- Order the results by the number of tags shared with the current post
- In case of two or more posts with the same number of tags, recommend the most recent post
- Limit the query to the number of posts we want to recommend
These steps are translated into a complex QuerySet that we will include in our 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 us to perform aggregated counts of tags. django.db.models includes the following aggregation functions:
- Avg: The value average
- Max: The maximum value
- Min: The minimum value
- Count: The objects count
You can learn about aggregation at https://docs.djangoproject.com/en/2.0/topics/db/aggregation/.
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')[:4]
The preceding code is as follows:
- 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 a flat list like [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. We slice the result to retrieve only the first four posts.
Add the similar_posts object to the context dictionary for the render() function, as follows:
return render(request,
'blog/post/detail.html',
{'post': post,
'comments': comments,
'new_comment': new_comment,
'comment_form': comment_form,
'similar_posts': similar_posts})
Now, edit the blog/post/detail.html template and add the following code before the post comments list:
<h2>Similar posts</h2>
{% for post in similar_posts %}
<p>
<a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
</p>
{% empty %}
There are no similar posts yet.
{% endfor %}
Now, your post detail page should look like this:
You are now able to successfully recommend similar posts to your users. django-taggit also includes a similar_objects() manager that you can use to retrieve objects by shared tags. You can take a look at all django-taggit managers at https://django-taggit.readthedocs.io/en/latest/api.html.
You can also add the list of tags to your post detail template the same way we did in the blog/post/list.html template.