Searching

Searching QuerySets

Django Modelsearch is built on Django’s QuerySet API. You should be able to search any Django QuerySet provided the model and the fields being filtered have been added to the search index.

Making a model searchable

To make an indexed model searchable, you need to create a QuerySet that inherits from modelsearch.queryset.SearchableQuerySetMixin and use it on the model’s objects attribute. This will add the .search() method to all QuerySets for the model:

class PersonQuerySet(SearchableQuerySetMixin, QuerySet):
    pass


class Person(index.Indexed, models.Model):
    # ...

    objects = PersonQuerySet.as_manager()

Simple searches

To search the model by all searchable fields:

Person.objects.search("Stefan Zweig")

Pick individual fields to search on with the fields argument:

Person.objects.search("Stefan", fields=["first_name"])

Filter results by using Django’s .filter() and .exclude() methods before .search()

Person.objects.filter(birth_date__year=1881).search(...)

Note that any fields you filter on must be indexed with index.FilterField.

If you’ve indexed a ForeignKey or OneToOneField, you can search using the related name (assuming the Book model is also indexed in this example):

stefan_zweig.books.search("The World of Yesterday")

Custom ordering

Pass an order_by argument to search() to order by that field. This will disable ranking and just do a basic match search:

Book.objects.search("The Hobbit", order_by="release_date")

Note the field being ordered by must be indexed with index.FilterField.

Alternatively, passing order_by_relevance=False when calling search will preserve any ordering already defined on the queryset:

Book.objects.order_by('release_date').search("The Hobbit", order_by_relevance=False)

Changing the search operator

The search operator determines whether we need to match all terms in the query or just one:

  • AND - Match all terms

  • OR - Match one or more terms

By default, Django Modelsearch searches with the OR operator. This helps if the user misspells a word in the query. Search ranking will ensure the best match always gets to the top.

But if you don’t want records that don’t match the whole query to appear, you can switch to the AND operator instead:

Book.objects.get("The tale of two cities", operator="and")

We won’t get random books about tales and cities in the results, just the Charles Dickens classic.

Structured Queries

In the above section, we saw a couple of query objects: Fuzzy and Phrase.

There are a couple more:

  • PlainText(query, operator='or') - This is the default one

  • Boost(query, boost) - Boosts the wrapped query

Query objects can be combined with | and & operators. parentheses can be used as well to build complex structured queries:

Book.objects.search(Boost(Phrase("War and Peace"), 2.0) | PlainText("War and Peace"))

This will perform both a phrase and a plain search and give an extra boost to results that match the phrase too.

How does .search() work?

When you call .search() on a QuerySet, it is converted to a SearchResults object. Any filters or ordering that was applied on the QuerySet are translated and applied to the new SearchResults.

Like with QuerySets, the search is not actually performed until you try to iterate the results or fetch an individual result.

SearchResults methods

The SearchResults class has a couple of useful methods:

facet(field_name)

Performs a faceted search on the results. It returns a dictionary containing each value of the given field as keys, and the counts of records as values.

For example, say we are searching for products, and we want to see the categories faceted:

>>> Product.objects.search("The Hobbit").facet("category")
{
    "Books": 3,
    "Films": 1,
    "Games": 5,
}

annotate_score(field_name)

Search engines work by calculating a score for each result and ordering the results by that score.

This method allows you to see the score for each result by annotating it on each returned object. This is useful for debugging:

>>> results = Product.objects.search("The Hobbit").annotate_score("score")
>>> results[0].score
123.4

Query string parser

Modelsearch provides a little helper for parsing a well known syntax for phrase queries ("double quotes") and filters (field:value) into a query object and a QueryDict of filters (the same type Django uses for request.GET):

from modelsearch.utils import parse_query_string

filters, query = parse_query_string('my query string "this is a phrase" this_is_a:filter key:value1 key:value2')

filters == {
    'this_is_a': ['filter'],
    'key': ['value1', 'value2']
}

query == PlainText("my query string") & Phrase("this is a phrase")