My dear reader, how are you? السلام عليكم

Be yourself; everyone else is already taken— Oscar Wilde

This is the third part of DjangoBlog tutorial series. This post extends our application with a tagging functionality and full-text search using PostgreSQL search features.

Few useful links to practically follow the project:

  1. DjangoBlog GitHub repository — DirectMe
  2. All other tutorials on Django-Blog — DirectMe
  3. Set your GitHub repo to DjangoBlog Part 2 using the following command:
git fetch origin 952756d3c3573aca30c14c34ca94782c474d0115

Assigning Category to Posts using tagging functionality

For this functionality, we will use a third-party Django module called django-taggit

(blogenv)Django-Blog-Application/Django-Blog$ pip3 install django-taggit

# open config/ and add the installed app


# now add tag instance to Post model as shown below
# open blog/ and add

from taggit.managers import TaggableManager

class Post(models.Model):
    tags = TaggableManager()

# finally add the migrations

(blogenv)Django-Blog-Application/Django-Blog$ python3 makemigrations blog

(blogenv)Django-Blog-Application/Django-Blog$ python3 migrate

# you can manually first add a few tags from admin view at and then add them to view

# open blog/templates/post/list.html and add 

<p class="tags">Tags: {{ post.tags.all|join:", " }}</p>

Add the tags in the list view as shown below

# open blog/ and update post_list view as below

from taggit.models import Tag

def post_list(request, tag_slug=None):
    posts = Post.published.all()

    tag = None

    if tag_slug:
        tag = get_object_or_404(Tag, slug=tag_slug)
        object_list = object_list.filter(tags__in=[tag])

     paginator = Paginator(object_list, 3)
     page = request.GET.get('page')
         posts =
     except PageNotAnInteger:
         posts =
     except EmptyPage:
         posts =

     return render(request,
                    {'page': page,
                    'posts': posts,
                    'tag': tag})

# note that we are adding the pagination in list view as well. See the functional pagination by running the source code of DjangoBlog on GitHub.

Let us now add a route to tags as shown below

# open blog/ and add 

urlpatterns = [
            views.post_list, name='post_list_by_tag'),

Now, update the templates as shown below

# open templates/post/list.html and extend it by adding the following program 

{% if tag %}
    <h2>Posts tagged with "{{ }}"</h2>
{% endif %}

<p class="tags">
        {% for tag in post.tags.all %}
                <a href="{% url "blog:post_list_by_tag" tag.slug %}">
                {{ }}

        {% if not forloop.last %}, {% endif %}
        {% endfor %}

Voila! This completes adding our tagging feature for this blog application.

Extracting posts by similarity

Using the number of tags that posts share with each other, we can now implement functionality to display similar posts. We will add the list of similar posts in the detailed view. Follow the steps given below for implementation.

# open blog/ and add the following in post_detail function

def post_detail(request, year, month, day, post):
    # add it under comments program
    # get similar posts
    post_tags_ids = post.tags.values_list('id', flat=True)
    similar_posts = Post.published.filter(tags__in=post_tags_ids)\
    similar_posts = similar_posts.annotate(same_tags=Count('tags'))\

    return render(request,
                  {'post': post,
                   'comments': comments,
                   'new_comment': new_comment,
                   'comment_form': comment_form
                   'similar_posts': similar_posts})

Add the template code as below

# open blog/templates/blog/post/detail.html and update with the code below

<h2>Similar posts</h2>
{% for post in similar_posts %}
<a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
{% empty %}
There are no similar posts yet.
{% endfor %}

full-text search using POSTGRESQL

Since our DjangoBlog is already configured with PostgreSQL, we will now use its search functionality as shown below.

# add the following in config/ 


Add a search form

# open blog/ and add 

class SearchForm(forms.Form):
    query = forms.CharField()

We will now add a search view as shown below

# open blog/ and add the following code 

from import SearchVector
from .forms import EmailPostForm, CommentForm, SearchForm

def post_search(request):
    form = SearchForm()
    query = None
    results = []

    if 'query' in request.GET:
        form = SearchForm(request.GET)
    if form.is_valid():
        query = form.cleaned_data['query']
        results = Post.objects.annotate(
        similarity=TrigramSimilarity('title', query),

    return render(request,
            {'form': form,
            'query': query,
            'results': results})

Let us now add a route to search view

# open blog/ and apend it with following 

urlpatterns = [
        path('', views.post_list, name='post_list'),
            views.post_share, name='post_share'),
            views.post_list, name='post_list_by_tag'),
        path('search/', views.post_search, name='post_search'),

Finally, we need to add a template for the search view as shown below

# create blog/templates/blog/post/search.html and add the following program 

{% extends "base.html" %}

{% block title %}Search{% endblock %}

{% block content %}
  {% if query %}
    <h1>Posts containing "{{ query }}"</h1>
      {% with results.count as total_results %}
          Found {{ total_results }} result{{ total_results|pluralize }}
      {% endwith %}
    {% for post in results %}
        <h4><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h4>
        {{ post.body|truncatewords:5 }}
    {% empty %}
      <p>There are no results for your query.</p>
    {% endfor %}
    <p><a href="{% url "blog:post_search" %}">Search again</a></p>
  {% else %}
    <h1>Search for posts</h1>
    <form action="." method="get">
      {{ form.as_p }}
      <input type="submit" value="Search">
  {% endif %}
{% endblock %}

If you run the development server at this point in time you should be able to see the added functionalities at

I hope you find this tutorial useful. If you find any errors or feel any need for improvement, let me know in your comments below.

Signing off for today. Stay tuned and I will see you next week! Happy learning.


Please enter your comment!
Please enter your name here