Return exact matches at top of Django Queryset

Question:

I have a django model, called “User” which stores some basic information about people, namely first and last name. I currently have a simple search across my django model where, if a user types in a first name, The django queryset returns the first 10 matches, ordered by last name.

For example, currently, if you search for “Sam” you might get the following results:

  1. Sam Abbott
  2. Samuel Baker
  3. Sammy Rogers
  4. Sam Simmons

The code for this is simple:

User.objects.filter(Q(first__istartswith=token)).order_by('last')

However, I want to alter this so that any exact first name matches are returned first, followed by the rest of the results. So if someone types in “Sam”, the results should instead be:

  1. Sam Abbott
  2. Sam Simmons
  3. Samuel Baker
  4. Sammy Rogers

(Exact first name matches first, sorted by last name, followed by the rest of the matches sorted by last name).

I thought about turning this into 2 querysets and then just combining the lists, but I was wondering if it was possible to do this in 1 query, ideally sticking with the basic django queryset API (rather than writing a one-off query). Does anyone know a way to do that?

Thanks in advance.

Asked By: Chad

||

Answers:

You can order by multiple attributes.

User.objects.filter(Q(first__istartswith=token)).order_by('first', 'last')

So you order first by first so your objects get filters according to exact match and subsequent matches. And then you order again on last to sort according to last name.

Answered By: Rohan

I think you might actually need full text search to accomplish that. Check out djang-sphinx.

If your example is as complicated as your full use case, you could just handle the sorting and ordering in your user code.

Answered By: JeffS

I don’t think it’s really possible to do that with only one query (at least with Django ORM).

So your 2 queries should look like this:

limit = 10
q1 = User.objects.filter(first__iexact=token).order_by('last')[:limit]
limit -= len(q1)
if limit:
    q2 = User.objects.exclude(pk__in=q1).filter(first__istartswith=token).order_by('last')[:limit]
else:
    q2 = []
users = list(q1) + list(q2)

Another way to do it is to filter the query in python but you will have to get all the results, not only the last 10:

query = User.objects.filter(first__istartswith=token).order_by('last')
exacts = [user for user in query if user.first == token]
others = [user for user in query if user.first != token]
users = exacts + others
Answered By: Etienne
# Get exact matches first
qs1 = User.objects.filter(first__iexact=token).order_by('last')

# get secondary results second
qs2 = User.objects.filter(first__istartswith=token).exclude(qs1).order_by('last')

result = itertools.chain(qs1, qs2)

also take a look at this question which goes inot more depth than you probably need: How to combine 2 or more querysets in a Django view?

Answered By: Francis Yaconiello

It is not pretty and personally I would recommend using a search engine such as django-haystack but, if you know what database you are using you could use QuerySet.extra to add a field to order the records:

extra = {'is_exact': "%s.first LIKE '%s'" % (User._meta.db_table, token)}
User.objects.filter(Q(first__istartswith=token)).extra(select=extra).order_by('-is_exact', 'last')
Answered By: Max Peterson

I think this is the simplest solution as It’s computed in only one SQL query:

from django.db.models import Case, When, Value

User.objects.annotate(
    match_order=Case(
        When(first__iexact=token, then=Value(0)),
        default=Value(1)
    )
).order_by('match_order', 'last')

This generates a new field match_order with the lowest value in case the matching is exact. The orders are by that value and then by the last name.

Answered By: Genarito
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.