6

I've got a wagtail site powered by Postgres and would like to implement a fuzzy search on all documents. However, according to wagtail docs "SearchField(partial_match=True) is not handled." Does anyone know of a way I can implement my own partial matching search?

I'm leaving this question intentionally open-ended because I'm open to pretty much any solution that works well and is fairly scalable.

fina
  • 309
  • 2
  • 10
  • 1
    You mention “fuzzy search”, but it’s actually something else. Fuzzy search is not for autocomplete, it’s for spelling suggestions. Of course, a combination of the two can be done, like on Google, where autocomplete can change the spelling of the query. But that’s another topic. – Bertrand Bordage Aug 25 '17 at 09:06

2 Answers2

4

We’re currently rebuilding the Wagtail search API in order to make autocomplete usable roughly the same way across backends.

For now, you can use directly the IndexEntry model that stores search data. Unfortunately, django.contrib.postgres.search does not contain a way to do an autocomplete query, so we have to do it ourselves for now. Here is how to do that:

from django.contrib.postgres.search import SearchQuery
from wagtail.contrib.postgres_search.models import IndexEntry

class SearchAutocomplete(SearchQuery):
    def as_sql(self, compiler, connection):
        return "to_tsquery(''%s':*')", [self.value]

query = SearchAutocomplete('postg')
print(IndexEntry.objects.filter(body_search=query).rank(query))
# All results containing words starting with “postg”
# should be displayed, sorted by relevance.
Bertrand Bordage
  • 921
  • 9
  • 16
  • Could it be that this answer is outdated? `IndexEntry` object has no `body_search` field or `rank` method. – tutuca Nov 16 '18 at 22:18
  • 2
    Yes, in more recent versions of Wagtail, Karl Hobley and I created an `autocomplete` method on all search backends, as shown in Mark Chackerian’s answer. – Bertrand Bordage Nov 17 '18 at 03:18
2

It doesn't seem to be documented yet, but the gist of autocomplete filtering with Postgres, using a request object, is something like

from django.conf import settings
from wagtail.search.backends import get_search_backend
from wagtail.search.backends.base import FilterFieldError, OrderByFieldError

def filter_queryset(queryset, request):
    search_query = request.GET.get("search", "").strip()
    search_enabled = getattr(settings, 'WAGTAILAPI_SEARCH_ENABLED', True)

    if 'search' in request.GET and search_query:
        if not search_enabled:
            raise BadRequestError("search is disabled")

        search_operator = request.GET.get('search_operator', None)
        order_by_relevance = 'order' not in request.GET

        sb = get_search_backend()
        try:
            queryset = sb.autocomplete(search_query, queryset, operator=search_operator, order_by_relevance=order_by_relevance)
        except FilterFieldError as e:
            raise BadRequestError("cannot filter by '{}' while searching (field is not indexed)".format(e.field_name))
        except OrderByFieldError as e:
            raise BadRequestError("cannot order by '{}' while searching (field is not indexed)".format(e.field_name))

The line to note is the call to sb.autocomplete.

If you want to use custom fields with autocomplete, you'll also need to add them into search_fields as an AutocompleteField in addition to a SearchField -- for example

search_fields = Page.search_fields + [
    index.SearchField("field_to_index", partial_match=True)
    index.AutocompleteField("field_to_index", partial_match=True),
    ...

This solution is working for Wagtail 2.3. If you using an older version, it is unlikely to work, and if you are using a future version, hopefully the details will be incorporated into the official documents, which currently state that autocomplete with Postgres is NOT possible. Thankfully, that has turned out to not be true, due to the work of Bertrand Bordage in the time since he wrote the other answer.

Mark Chackerian
  • 21,866
  • 6
  • 108
  • 99