Add Full Text Search to Wagtail

Michael Yin

Last updated on December 10 2021

Table of Contents

Wagtail Tutorial Series:

  1. Create Wagtail Project
  2. Dockerizing Wagtail App
  3. Add Blog Models to Wagtail
  4. How to write Wagtail page template
  5. Add Bootstrap Theme to Wagtail
  6. How to use StreamField in Wagtail
  7. Wagtail Routable Page
  8. Add pagination component to Wagtail
  9. Customize Wagtail Page URL
  10. Add Full Text Search to Wagtail
  11. Add Markdown Support to Wagtail
  12. Add LaTeX Support & Code Highlight In Wagtail
  13. How to Build Form Page in Wagtail
  14. How to Create and Manage Menus in Wagtail
  15. Wagtail SEO Guide
  16. Source code: https://github.com/AccordBox/wagtail-bootstrap-blog

Wagtail Tips:

  1. Wagtail Tip #1: How to replace ParentalManyToManyField with InlinePanel
  2. Wagtail Tip #2: How to Export & Restore Wagtail Site

Write style in Wagtail:

  1. How to use SCSS/SASS in your Django project (Python Way)
  2. How to use SCSS/SASS in your Django project (NPM Way)

Other Wagtail Topics:

  1. How to make Wagtail project have good coding style
  2. How to do A/B Testing in Wagtail CMS 
  3. How to build a landing page using Wagtail CMS 
  4. How to support multi-language in Wagtail CMS 

More Wagtail articles and eBooks written by me

Objective

By the end of this chapter, you should be able to:

  1. Understand how Wagtail full text search works.
  2. Display search keywords on the search result page.

Prerequisites

Update wagtail_bootstrap_blog/urls.py

  1. Remove path('search/', search_views.search, name='search'),
  2. Remove from search import views as search_views

Backend

Update wagtail_bootstrap_blog/settings/base.py

INSTALLED_APPS = [
    # code omitted for brevity

    'wagtail.contrib.postgres_search',
]

WAGTAILSEARCH_BACKENDS = {
    'default': {
        'BACKEND': 'wagtail.contrib.postgres_search.backend',
    },
}

Notes:

  1. We added wagtail.contrib.postgres_search to the INSTALLED_APPS
  2. We config WAGTAILSEARCH_BACKENDS to use the postgres_search.backend
$ docker-compose exec web python manage.py migrate

Notes:

  1. This would create a db table for postgres search backend to store the index data

Model

Update blog/models.py

from wagtail.search import index


class PostPage(Page):

    search_fields = Page.search_fields + [
        index.SearchField('title'),
        index.SearchField('body'),
    ]

Notes:

  1. Here we defined search_fields so Wagtail would know which fields need to be processed when indexing.
  2. Do not forget to put from wagtail.search import index at the top of the file.

After this is done, let's run command to build the index.

$ docker-compose up -d
$ docker-compose logs -f

$ docker-compose exec web python manage.py update_index

Notes:

  1. We need to do this manually when we first setup postgres search backend
  2. The command would extract text from the search_fields, and write index to the index entry table.
  3. By default, when we do db operation (create page, edit page), the search index would also be updated. That is why the search function can work with latest data even we do not run update_index command.

Let's test in the Django shell.

$ docker-compose exec web python manage.py shell
>>> from blog.models import PostPage

# run search method to do full text search
>>> PostPage.objects.search('wagtail')
<SearchResults [<PostPage: PostPage4>, <PostPage: PostPage3>, <PostPage: PostPage2>, <PostPage: PostPage1>]>

Notes:

  1. The postgres search backend would transform the search method to search the Postgres index entry table.
  2. Different search backends would generate different query code here.

Route

Update blog/models.py

class BlogPage(RoutablePageMixin, Page):
    # code omitted for brevity

    @route(r"^search/$")
    def post_search(self, request, *args, **kwargs):
        search_query = request.GET.get("q", None)
        self.posts = self.get_posts()
        if search_query:
            self.posts = self.posts.search(search_query)
        return self.render(request)

Notes:

  1. We added a route to the BlogPage, which handle the search request.
  2. We get the keywords from the querystring q

Update wagtail_bootstrap_blog/templates/blog/components/sidebar.html

{% load blogapp_tags wagtailroutablepage_tags %}

<div class="col-md-4">
  {% if blog_page %}

  <div class="card my-4">
    <h5 class="card-header">Search</h5>
    <div class="card-body">
      <form role="search" method="get" class="form-search" action="{% routablepageurl blog_page "post_search" %}">

        <div class="input-group">
          <input type="text" class="form-control search-query" name="q" placeholder="Search&hellip;" title="Search for:" />
          <span class="input-group-btn">
            <button class="btn btn-secondary" type="submit">Go!</button>
          </span>
        </div>

      </form>
    </div>
  </div>

  {% categories_list %}

  {% tags_list %}

  {% endif %}
</div>

Notes:

  1. In the sidebar, we added a search form above the Category widget.
  2. We can even put the code in a new template file to make the sidebar.html cleaner.
  3. The form action is the url of the post_search route.
  4. When we submit the form, it would send GET request which has url like http://127.0.0.1:8000/search/?q=wagtail

Display search keywords

To display search keywords on the search result page.

Update blog/models.py

class BlogPage(RoutablePageMixin, Page):

    @route(r"^(\d{4})/$")
    @route(r"^(\d{4})/(\d{2})/$")
    @route(r"^(\d{4})/(\d{2})/(\d{2})/$")
    def post_by_date(self, request, year, month=None, day=None, *args, **kwargs):
        self.search_type = 'date'
        self.search_term = year
        self.posts = self.get_posts().filter(post_date__year=year)
        if month:
            df = DateFormat(datetime.date(int(year), int(month), 1))
            self.search_term = df.format('F Y')
            self.posts = self.posts.filter(post_date__month=month)
        if day:
            self.search_term = date_format(datetime.date(int(year), int(month), int(day)))
            self.posts = self.posts.filter(post_date__day=day)
        return self.render(request)

    @route(r'^tag/(?P<tag>[-\w]+)/$')
    def post_by_tag(self, request, tag, *args, **kwargs):
        self.search_type = 'tag'
        self.search_term = tag
        self.posts = self.get_posts().filter(tags__slug=tag)
        return self.render(request)

    @route(r'^category/(?P<category>[-\w]+)/$')
    def post_by_category(self, request, category, *args, **kwargs):
        self.search_type = 'category'
        self.search_term = category
        self.posts = self.get_posts().filter(categories__blog_category__slug=category)
        return self.render(request)

    @route(r"^search/$")
    def post_search(self, request, *args, **kwargs):
        search_query = request.GET.get("q", None)
        self.posts = self.get_posts()
        if search_query:
            self.search_term = search_query
            self.search_type = 'search'
            self.posts = self.posts.search(search_query)
        return self.render(request)

Notes:

  1. We added search_term to make it represent the search keywords, category or tag
  2. We added search_type to make it represent the filter type.

Update wagtail_bootstrap_blog/templates/blog/blog_page.html

{% extends "base.html" %}

{% load wagtailcore_tags wagtailimages_tags blogapp_tags %}

{% block content %}

    {% if page.search_term %}
      <div class="alert alert-success">
        Posts for <span>{{ page.search_type }}: {{ page.search_term }}</span>
      </div>
    {% endif %}

    ......

{% endblock %}

Notes:

  1. If the page.search_term is not None, display the filter messages.

Performance Notes:

From Wagtail doc, there are multiple search backends for us to use

  1. If the indexed data is huge, please check Elasticsearch backend
  2. The default database backend have performance issue when dealing with big data.
  3. If you use Postgres and the indexed data is not huge, then postgres search backend is the best option.
  4. If you use Mysql, you can check wagtail-whoosh, I am the maintainer of this repo. If it can not work for you, then you might need to use Elasticsearch backend

Wagtail Tutorial Series:

  1. Create Wagtail Project
  2. Dockerizing Wagtail App
  3. Add Blog Models to Wagtail
  4. How to write Wagtail page template
  5. Add Bootstrap Theme to Wagtail
  6. How to use StreamField in Wagtail
  7. Wagtail Routable Page
  8. Add pagination component to Wagtail
  9. Customize Wagtail Page URL
  10. Add Full Text Search to Wagtail
  11. Add Markdown Support to Wagtail
  12. Add LaTeX Support & Code Highlight In Wagtail
  13. How to Build Form Page in Wagtail
  14. How to Create and Manage Menus in Wagtail
  15. Wagtail SEO Guide
  16. Source code: https://github.com/AccordBox/wagtail-bootstrap-blog

Wagtail Tips:

  1. Wagtail Tip #1: How to replace ParentalManyToManyField with InlinePanel
  2. Wagtail Tip #2: How to Export & Restore Wagtail Site

Write style in Wagtail:

  1. How to use SCSS/SASS in your Django project (Python Way)
  2. How to use SCSS/SASS in your Django project (NPM Way)

Other Wagtail Topics:

  1. How to make Wagtail project have good coding style
  2. How to do A/B Testing in Wagtail CMS 
  3. How to build a landing page using Wagtail CMS 
  4. How to support multi-language in Wagtail CMS 

More Wagtail articles and eBooks written by me


Michael Yin

Michael is a Full Stack Developer from China who loves writing code, tutorials about Django, and modern frontend tech.

He has published some ebooks on leanpub and tech course on testdriven.io.

He is also the founder of the AccordBox which provides the web development services.


Table of Contents

Frontend Guide For Python Dev

This FREE guide help Python developers to learn the Modern frontend tech

Learn More

This book will teach you how to build a SPA (single-page application) with React and Wagtail CMS

Read More

Subscribe

Get notified about new great Web Development Tutorial