Using the new full-text search in django.contrib.postgres
as a starting point, one can expand upon SearchQuery
to create a version that handles searches for a partial part of the final word:
from psycopg2.extensions import adapt
from django.contrib.postgres.search import SearchQuery
class PrefixedPhraseQuery(SearchQuery):
"""
Alter the tsquery executed by SearchQuery
"""
def as_sql(self, compiler, connection):
# Or <-> available in Postgres 9.6
value = adapt('%s:*' % ' & '.join(self.value.split()))
if self.config:
config_sql, config_params = compiler.compile(self.config)
template = 'to_tsquery({}::regconfig, {})'\
.format(config_sql, value)
params = config_params
else:
template = 'to_tsquery({})'\
.format(value)
params = []
if self.invert:
template = '!!({})'.format(template)
return template, params
Refer to the Postgres docs for the ts_query
syntax.
You can then use it in a query like so:
vector = SearchVector(
'first_name',
'last_name',
'email',
config='simple')
query = PrefixedPhraseQuery(query, config='simple')
queryset = queryset\
.annotate(vector=vector)\
.filter(vector=query)
You could also write a startswith
lookup, refer to the implementation of SearchVectorExact
.
Django 3+ Answer
This has become much simpler in more recent versions of Django. SearchQuery
now has a raw
mode that can be used to request a prefix query.
query = SearchQuery("search & term & prefix:*", search_type="raw")
results = Model.objects\
.filter(_search_vector=query)\
.annotate(
rank=SearchRank(
F("_search_vector"),
query,
cover_density=True,
)
)
.order_by("-rank")
Where _search_vector
is a SearchVectorField
, or can be annotated onto the model.