29

How can I query on the full name in Django?

To clarify, I essentially want to do create a temporary column, combining first_name and last_name to give a fullname, then do a LIKE on that, like so:

select [fields] from Users where CONCAT(first_name, ' ', last_name) LIKE '%John Smith%";

The above query would return all users named John Smith. If possible I'd like to avoid using a raw SQL call.

The model I'm talking about specifically is the stock django.contrib.auth.models User model. Making changes to the model directly isn't a problem.

For example, if a user was to search for 'John Paul Smith', it should match users with a first name of 'John Paul' and last name 'Smith', as well as users with first name 'John' and last name 'Paul Smith'.

  • Please include your Django models. If you want a "derived value" (like `CONCAT(first_name, ' ', last_name)` in SQL) you're going to have to add it to the model. Therefore, include the model in your question. – S.Lott Oct 06 '11 at 23:45
  • Are you aware, BTW, that `CONCAT(first_name, ' ', last_name) LIKE '%John Smith%"` is terribly inefficient? Using `first_name LIKE '%John' AND last_name LIKE 'Smith%'` can be more efficient? Why do you use the CONCAT when there are non-CONCAT ways to do this? – S.Lott Oct 06 '11 at 23:47
  • It's the standard django.contrib.auth.models User model. We're already adding/changing fields in this model, so modifications aren't a problem. –  Oct 06 '11 at 23:48
  • "adding/changing fields in this model" Why aren't you using the profile extension? https://docs.djangoproject.com/en/1.3/topics/auth/#storing-additional-information-about-users – S.Lott Oct 06 '11 at 23:49
  • @S.Lott I wasn't aware of the efficiency issue, thanks for pointing that out. For our application, querying users is a single field, so I can't just split on whitespace. –  Oct 06 '11 at 23:51
  • "querying users is a single field"? "so I can't just split on whitespace"? The `CONCAT` absolutely depends on whitespace. How can you suddenly no longer depend on whitespace? What part of the SQL is not an accurate portrayal of your requirements? – S.Lott Oct 06 '11 at 23:52
  • We're using MongoDB for our database, and the UserProfile creates two separate collections; one for Users, one for UserProfiles. So this creates situations involving potentially large joins, which we want to avoid. As for the CONCAT depending on whitespace, I agree completely. The problem is that users names can sometimes have spaces, so merging the two attributes into a single field is the closest approximation of a fullname. If I searched on "Jean Paul Smith", where the user's first name was Jean Paul, two LIKE statements wouldn't suffice. –  Oct 07 '11 at 00:01
  • "If I searched on "Jean Paul Smith"," The CONCAT wouldn't work, either. Please post the **full** requirements. – S.Lott Oct 07 '11 at 03:16
  • "potentially large joins"? What's the problem? Have you measured the performance? Why can't MongoDB do this? What is the measured impact does this join on your performance? – S.Lott Oct 07 '11 at 03:18
  • Why wouldn't CONCAT work for Jean Paul Smith work exactly? 'John' + ' ' + 'Paul Smith' or 'John Paul' + ' ' + 'Smith' will work just fine. –  Oct 07 '11 at 07:04
  • In MongoDB, joins are bad. The application I'm working with will have to process at least 10-20GB of user data a day to start with, so rather than allow the application to develop a large performance issue, we decided to come up with a solution to this before hand. I really don't see what that has to do with the question I'm asking. –  Oct 07 '11 at 07:06
  • I'll repeat my recommendation. Put **all** the requirements into the question. 'John' + ' ' + 'Paul Smith' or 'John Paul' + ' ' + 'Smith' will work just fine is a requirement that is **not** stated in the question. Please **update** the question. The rationale behind "avoid join" (probably just premature optimization, since there's no measurement) appears to be an important consideration but it is not in the question. Please **update** the question to include all the requirements and all the constraints. – S.Lott Oct 07 '11 at 09:51

7 Answers7

64

This question was posted long time ago, but I had the similar problem and find answers here pretty bad. The accepted answer only allows you to find exact match by first_name and last_name. The second answer is a little bit better but still bad because you hit database as much as there was words. Here's my solution that concatenates first_name and last_name annotates it and search in this field:

from django.db.models import Value as V
from django.db.models.functions import Concat   

users = User.objects.annotate(full_name=Concat('first_name', V(' '), 'last_name')).\
                filter(full_name__icontains=query)

For example if the name of the person is John Smith, you can find him by typing john smith, john, smith, hn smi and so on. It hits database only ones. And I think this will be the exact SQL that you wanted in the open post.

svalemento
  • 702
  • 6
  • 11
  • "but still bad because you hit database as much as there was words" -> django queries are [lazy](https://docs.djangoproject.com/en/dev/topics/db/queries/#querysets-are-lazy), it hits db only once. The `Concat` annotation produces a very convoluted sql statement. Functionally the only difference will be your answer allows for contains within the concatenation (as you noted, "hn smi" f.e.) while my answer would allow for contains in either columns (e.g. searching "John Smith" or "Smith John" both would match a row with `first_name` "John Fitzgerald" and `last_name` "Smith"). – laffuste Jun 23 '19 at 10:38
  • This should be the accepted answer. The other answers don't answer the question properly. – I_am_learning_now Dec 06 '19 at 22:57
  • 2
    Bless you. Posting a way better solution 7 years after the initial question deserves respect. – Lewis Menelaws Apr 29 '20 at 18:13
  • This is solution is pretty elegant Not sure what's the relevance of the abrasiveness though. – Siphiwe Gwebu May 17 '21 at 15:25
  • 1
    Perfect, thanks! Just in case this is helpful for others: I was initially getting a "can't set attribute error" and found it to be because I already had a full_name @property on my user model already. So changing the above code to use the_full_name instead sorted it out. – Ffion Jun 29 '21 at 11:00
13

Easier:

from django.db.models import Q 

def find_user_by_name(query_name):
   qs = User.objects.all()
   for term in query_name.split():
     qs = qs.filter( Q(first_name__icontains = term) | Q(last_name__icontains = term))
   return qs

Where query_name could be "John Smith" (but would also retrieve user Smith John if any).

Rich Tier
  • 9,021
  • 10
  • 48
  • 71
laffuste
  • 16,287
  • 8
  • 84
  • 91
  • I have started using this and as soon as there is more than one search term the http response time goes from 100ms to 5seconds. Any ideas why this would be? – abarax Jul 15 '16 at 23:29
  • You have to pinpoint where is the problem first. Database? ORM? Template? Execute the query (`print qs.query`) in an sql client to see if the problem is in db. If not, by exclusion (removing pieces of code), try to guess if its in the template. If not, it might be on the ORM (try queries returning less rows). You be facing an ORM N+1 problem (fix it with `select_related` and `prefetch_related`). Can you guess anything wrong by using EXPLAIN (mysql/postgres) with the query (from an sql client)? Is you table massive? If so, does indexing first_name and last_name ease the problem? – laffuste Jul 18 '16 at 04:49
  • Thanks for the tips – abarax Jul 19 '16 at 01:37
5
class User( models.Model ):
    first_name = models.CharField( max_length=64 )
    last_name = models.CharField( max_length=64 )
    full_name = models.CharField( max_length=128 )
    def save( self, *args, **kw ):
        self.full_name = '{0} {1}'.format( first_name, last_name )
        super( User, self ).save( *args, **kw )
S.Lott
  • 384,516
  • 81
  • 508
  • 779
3

I used this Query to search firstname, lastname, also the fullname.

It solved my problem.

from django.db.models import Q, F
from django.db.models import Value as V
from django.db.models.functions import Concat 

user_list = models.User.objects.annotate(
                        full_name=Concat('first_name', V(' '), 'last_name')
                    ).filter(   
                        Q(full_name__icontains=keyword) | 
                        Q(first_name__icontains=keyword) | 
                        Q(last_name__icontains=keyword)
                    )
Chandan Sharma
  • 2,321
  • 22
  • 22
3

Unless I'm missing something, you can use python to query your DB like so:

from django.contrib.auth.models import User
x = User.objects.filter(first_name='John', last_name='Smith') 

Edit: To answer your question:

If you need to return 'John Paul Smith' when the user searches for 'John Smith' then you can use 'contains' which translates into a SQL LIKE. If you just need the capacity to store the name 'John Paul' put both names in the first_name column.

User.objects.filter(first_name__contains='John', last_name__contains='Smith') 

This translates to:

SELECT * FROM USERS
WHERE first_name LIKE'%John%' 
AND last_name LIKE'%Smith%'
garnertb
  • 9,454
  • 36
  • 38
  • 2
    What happens if the query is "John Paul Smith"? –  Oct 07 '11 at 00:05
  • If first_name == "John Paul" or last_name == "Paul Smith", it will still match this way. – jdi Oct 07 '11 at 00:36
  • 1
    What I'm saying, is, what if the user typed in John Paul Smith? –  Oct 07 '11 at 07:33
  • `(first_name like '%John' or first_name like '%John Paul') and (last_name like 'Paul Smith%' or last_name like 'Smith%')` It trivially extends to *n* words by doing `words[:-1]` in the first_name likes and `words[1:]` in the last_name likes. It avoids the expensive and unindexable CONCAT. – S.Lott Oct 07 '11 at 09:54
0

What about this:

query = request.GET.get('query')
users = []

try:
    firstname = query.split(' ')[0]
    lastname  = query.split(' ')[1]
    users += Users.objects.filter(firstname__icontains=firstname,lastname__icontains=lastname)
    users += Users.objects.filter(firstname__icontains=lastname,lastname__icontains=firstname)

users = set(users)

Tried and tested!

atx
  • 73
  • 1
  • 8
0

I would use the Django method get_full_name() on User model. There is no need to create a column on the Database.

See here for more info: https://docs.djangoproject.com/en/4.1/ref/contrib/auth/#django.contrib.auth.models.User.get_full_name

Then you could do something like this in your templates:

{{ myUser.get_full_name|default:myUser.username }}

As first_name and last_name are not required on the User model, it could return empty. Then this way you can have a default of username if they are.

Wavesailor
  • 662
  • 10
  • 19