10

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.

Chad
  • 1,794
  • 1
  • 17
  • 30

6 Answers6

7

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
Etienne
  • 12,440
  • 5
  • 44
  • 50
  • Yeah I think this is the best way to do this. I'll probably end up doing it this way, except using itertools.chain as Francis suggested below, since that should be faster than constructing 2 lists and combining them. Thanks! – Chad Jul 10 '12 at 20:16
  • 1
    I'm glad that I learned the itertools chain trick. But in your case, if your limit is really 10, that should not change a thing. The queries will take most time and the list creation and concatenation time will be insignificant. – Etienne Jul 11 '12 at 02:02
3
# 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?

Community
  • 1
  • 1
Francis Yaconiello
  • 10,829
  • 2
  • 35
  • 54
  • Good solution, although I ended up accepting the other one because it factored the 10 item limit in. (Also I think there's a small typo in the exclude(), which I think should be exclude(pk__in=qs1)). – Chad Jul 10 '12 at 20:17
3

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')
Max Peterson
  • 633
  • 7
  • 6
1

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.

JeffS
  • 2,647
  • 2
  • 19
  • 24
  • Thanks for the suggestion. In this case I don't think full text search is needed since I have indexes on the names, so can just do a startswith in the django ORM call. – Chad Jul 10 '12 at 20:19
0

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.

Rohan
  • 52,392
  • 12
  • 90
  • 87
  • But wouldn't that sort the entire list by first name first? So I'd end up with Sammy Rogers and Samuel Baker switched, right? – Chad Jul 10 '12 at 05:51
  • @Chad Yes, it will, but shouldn't it be that way. IMHO I feel that is expected behavior. – Rohan Jul 10 '12 at 06:09
  • No in this case that isn't what we're looking for, unfortunately. Needs to be 2 groups, exact first name matches and then everything else, and each group needs to be sorted by last name. – Chad Jul 10 '12 at 06:17
0

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.

Genarito
  • 3,027
  • 5
  • 27
  • 53