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")
Fuzzy search
Fuzzy search can help autocorrect misspelled queries:
from modelsearch.query import Fuzzy
james_joyce.books.search(Fuzzy("Ulises"))
Note
The PostgreSQL backend requires extensions depending on which fuzzy features you use:
# Trigram similarity (required for the default trigram algorithm)
python manage.py enable_trigram
# Accent-insensitive matching (required for Fuzzy(unaccent=True))
python manage.py enable_unaccent
# Levenshtein distance (required for FUZZY_ALGORITHM = "levenshtein")
python manage.py enable_fuzzystrmatch
See the PostgreSQL backend configuration for tuning the similarity threshold.
Phrase search
Search queries usually allow the terms to be a different order and not next to each other in the document. You can use a phrase search if you need the words to appear in their exact order:
from modelsearch.query import Phrase
Book.objects.search(Phrase("Peace and War"))
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 termsOR- 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.
Autocomplete search
Autocomplete searches are a bit special because they require different indexing behaviour. This is for performance but also to switch off stemming which can cause strange results with autocomplete.
Also, generally, you only want to autocomplete names and titles of things and not the contents.
For these reasons, Django Modelsearch indexes them separately to search fields. Any field that you want to autocomplete on must also be indexed with index.AutocompleteField.
To run an autocomplete query, use the .autocomplete() method:
Person.objects.autocomplete('stef')
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 oneBoost(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")