I am using the standard User model of django.
I would like to search users by first name and last name. So I have a free text search and using icontains I am searching for the that text.
However, I would like that a user can search for "John A" and retrieve all users which their First name + Last name contains "John A" however, because I am using icontains over first name and last name separately , it doesn't work. "John" alone will work. but as long as I have involved both fields, it doesn't (which make sense)
Any simple solution for this ?
Use Q objects to create your queries, and use | for OR and & for AND.
>>> from django.db.models import Q
>>> qs = Q(first_name__icontains='John A') | Q(last_name__icontains='John A')
>>> User.objects.filter(qs)
read more about it here
p.s. You can also use | and & to join regular querysets
User.objects.filter(...) | User.objects.filter(...)
but I think using Q objects is just neater when going over multiple fields
edit
as par your comment, all you want to do is this:
>>> query = 'John A'
>>> fn, ln = query.split()
>>> User.objects.filter(first_name=fn, last_name=ln)
For complex situations you probably want check the first name and last name. The second one usually is a middle name or part of the others, so icontain will catch it. So something like this:
>>> query = 'Jon Van Mendrik'
>>> fn, ln = [i[0], i[-1] for i in query.split()] #take the first and last
>>> print fn, ln
"Jon Mendrik"
>>> qs = Q(first_name__icontains=fn, last_name__icontains=ln)
>>> qs = qs | Q(first_name__icontains=ln, last_name__icontains=fn)
>>> User.objects.filter(qs).distinct()
It's not very pretty, but will probably work for most cases, including where the last name was entered first and so on. Just make sure you understand what I did here and not just copy it blindly to your project -
I took the first and last parts of the string and assigned them to fn and ln respectively
I create a queryset for the firtcase
I then joined it to another queryset for the reverse option using |
lastly, I filtered on that queryset and used distinct to get rid of possible duplicates
You can probably add a lot more logic. For example, 'John A' - the last name is obviously an acronym and not the entire name while the first is a full name. You can implement some logic that takes care of that and build the queryset accordingly (contains is more costly then startswith or then simple comparison). Using Q objects will make that easy. Play around with it, you'll get the hang of it
I suggest you use natural_key. In your User model, add something like:
def natural_key(self):
return (self.first_name, self.last_name)
result:
users = User.objects.filter()
for user in users:
print(user.natural_key)
You can then filter on natural keys.
Hope it helps.
Related
Lets suppose the following model:
class Person(models.Model):
name = models.CharField(null=False)
...
I know that I can filter Persons with a name that contains the letter a using Person.objects.filter(name__contains='a'). And I know that I can filter Persons with name is in a list using Person.objects.filter(name__in=['John Doe', 'Mary Jane']).
Its possible ( or, whats the performative way ) to do the two things using just one filter ?
I know that I can do 2 queries (maybe more) and fetch the data. But in my current case, I have a method on my view called get_filters that returns a Q object that will be used in get_queryset method. So I need to implement this inside a Q object and fetching only 1 query. Its possible ?
Exactly as you figured, you can build up a Q() filter object.
Q(name__contains='a') & Q(name__in=['John Doe', 'Mary Jane'])
However, that will only ever match an object Mary Jane since John Doe doesn't contain an a. (The equivalent SQL is name LIKE '%a%' AND name IN ('John Doe', 'Mary Jane').
If you mean "find any object containing any of these substrings", that's possible too:
q = Q()
for substring in ['John', 'Mary', 'Jane']:
q |= Q(name__contains=substring)
This will be the equivalent of name LIKE '%John%' OR name LIKE '%Mary%' OR name LIKE '%Jane%'.
I'm implementing search functionality with an option of looking for a record by matching multiple tables and multiple fields in these tables.
Say I want to find a Customer by his/her first or last name, or by ID of placed Order which is stored in different model than Customer.
The easy scenario which I already implemented is that a user only types single word into search field, I then use Django Q to query Order model using direct field reference or related_query_name reference like:
result = Order.objects.filter(
Q(customer__first_name__icontains=user_input)
|Q(customer__last_name__icontains=user_input)
|Q(order_id__icontains=user_input)
).distinct()
Piece of a cake, no problems at all.
But what if user wants to narrow the search and types multiple words into search field.
Example: user has typed Bruce and got a whole lot of records back as a result of search.
Now he/she wants to be more specific and adds customer's last name to search.So the search becomes Bruce Wayne, after splitting this into separate parts I'm having Bruce and Wayne. Obviously I don't want to search Orders model because order_id is a single-word instance and it's sufficient to find customer at once so for this case I'm dropping it out of query at all.
Now I'm trying to match customer by both first AND last name, I also want to handle the scenario where the order of provided data is random, to properly handle Bruce Wayne and Wayne Bruce, meaning I still have customers full name but the position of first and last name aren't fixed.
And this is the question I'm looking answer for: how to build query that will search multiple fields of model not knowing which of search words belongs to which table.
I'm guessing the solution is trivial and there's for sure an elegant way to create such a dynamic query, but I can't think of a way how.
You can dynamically OR a variable number of Q objects together to achieve your desired search. The approach below makes it trivial to add or remove fields you want to include in the search.
from functools import reduce
from operator import or_
fields = (
'customer__first_name__icontains',
'customer__last_name__icontains',
'order_id__icontains'
)
parts = []
terms = ["Bruce", "Wayne"] # produce this from your search input field
for term in terms:
for field in fields:
parts.append(Q(**{field: term}))
query = reduce(or_, parts)
result = Order.objects.filter(query).distinct()
The use of reduce combines the Q objects by ORing them together. Credit to that part of the answer goes to this answer.
The solution I came up with is rather complex, but it works exactly the way I wanted to handle this problem:
search_keys = user_input.split()
if len(search_keys) > 1:
first_name_set = set()
last_name_set = set()
for key in search_keys:
first_name_set.add(Q(customer__first_name__icontains=key))
last_name_set.add(Q(customer__last_name__icontains=key))
query = reduce(and_, [reduce(or_, first_name_set), reduce(or_, last_name_set)])
else:
search_fields = [
Q(customer__first_name__icontains=user_input),
Q(customer__last_name__icontains=user_input),
Q(order_id__icontains=user_input),
]
query = reduce(or_, search_fields)
result = Order.objects.filter(query).distinct()
I need to do something like this in order to match some objects.
This is the best way to do it?
Is possible to do it in a faster way?
if User.objects.filter(first_name=name, age=age).exists():
user = User.objects.filter(first_name=name, age=age)
function_x(user)
elif User.objects.filter(full_name__icontains=name, age=age).exists():
user = User.objects.filter(full_name__icontains=name, age=age)
function_x(user)
If you want use one of that condition, just use Q object, it allows you use logic or operator in your query.
from django.db.models import Q
User.objects.filter(Q(first_name=name) |
Q(full_name__icontains=name),
age=age)
In that case | means or and , means and, so age is required in both conditions.
Given the variable name, I guess you are expecting the above query to return a single user. Thus, you can eliminate one more db hit using .first():
.first() basically returns the first object matched by the queryset, or None if there isn't one. By this way, you don't have to perform .exists().
user = User.objects.filter(Q(first_name=name) | Q(full_name__icontains=name), age=age).first()
i have a django app that retrieve all subjects from a single table of users. i've also implemented an input search form,
this is the query performed:
all_soggs = Entity.objects.filter(lastname__istartswith=request.GET['query_term']).order_by('lastname')
if(all_soggs.count()==0):
all_soggs = Entity.objects.filter(firstname__istartswith=request.GET['query_term']).order_by('firstname')
as you can see the query first search for matching items by lastname, and then by firstname. this works until i insert the complete name 'firstaname lastname' or 'lastname firstname', in this case there's no results. how can i modify the query to make a better search?
thanks - luke
Copy/paste from: https://stackoverflow.com/a/17361729/1297812
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
You need Q objects and you also need to split your query into separate terms (since no first name will match the full string "Firstname Lastname").
Here's an idea to match any first or last name starting with either "Firstname" or "Lastname" in the search "Firstname Lastname".
This is a generic search - adjust the query to suit your specific needs!
Edit: oops, I really don't like using reduce since it looks confusing, but these need to be ORed together and we can't do a more verbose version because the number of terms is unknown.
import operator
from django.db.models import Q
search_args = []
for term in request.GET['query_term'].split():
for query in ('first_name__istartswith', 'last_name__istartswith'):
search_args.append(Q(**{query: term}))
all_soggs = Entity.objects.filter(reduce(operator.or_, search_args))
To clarify how to use Q objects, given the search "Firstname Lastname" the previous query is equal to:
Entity.objects.filter(
Q(first_name__istartswith="Firstname") | Q(last_name__istartswith="Firstname") |
Q(first_name__istartswith="Lastname") | Q(last_name__istartswith="Lastname")
)
This is a fairly old question but I just ran into the same problem and I thought I would share a more elegant solution.
from django.db.models import Value as V
from django.db.models.functions import Concat
from ..models import User
def find_user_by_name(search_str) -> QuerySey[User]:
q = User.objects.annotate(full_name=Concat('first_name', V(' '), 'last_name'))
q = q.filter(full_name__icontains=search_str)
return q
This is the only solution that I have found which lets gives the behaviour I wanted, IE seaching with a full name string with a space ("John Doe") and with a partial string ("John Do").
Similar question:Querying full name in Django
query = request.GET.get('query')
entities = []
try:
firstname = query.split(' ')[0]
lastname = query.split(' ')[1]
entities += Entity.objects.filter(firstname__icontains=firstname,lastname__icontains=lastname)
entities += Entity.objects.filter(firstname__icontains=lastname,lastname__icontains=firstname)
entities = set(entities)
I have a list of names that I want to match case insensitive, is there a way to do it without using a loop like below?
a = ['name1', 'name2', 'name3']
result = any([Name.objects.filter(name__iexact=name) for name in a])
Unfortunatley, there are no __iin field lookup. But there is a iregex that might be useful, like so:
result = Name.objects.filter(name__iregex=r'(name1|name2|name3)')
or even:
a = ['name1', 'name2', 'name3']
result = Name.objects.filter(name__iregex=r'(' + '|'.join(a) + ')')
Note that if a can contain characters that are special in a regex, you need to escape them properly.
NEWS: In Django 1.7+ it is possible to create your own lookups, so you can actually use filter(name__iin=['name1', 'name2', 'name3']) after proper initialization. See documentation reference for details.
In Postgresql you could try creating a case insensitive index as described here:
https://stackoverflow.com/a/4124225/110274
Then run a query:
from django.db.models import Q
name_filter = Q()
for name in names:
name_filter |= Q(name__iexact=name)
result = Name.objects.filter(name_filter)
Index search will run faster than the regex matching query.
Another way to this using django query functions and annotation
from django.db.models.functions import Lower
Record.objects.annotate(name_lower=Lower('name')).filter(name_lower__in=['two', 'one']
Adding onto what Rasmuj said, escape any user-input like so
import re
result = Name.objects.filter(name__iregex=r'(' + '|'.join([re.escape(n) for n in a]) + ')')
Keep in mind that at least in MySQL you have to set utf8_bin collation in your tables to actually make them case sensitive. Otherwise they are case preserving but case insensitive. E.g.
>>> models.Person.objects.filter(first__in=['John', 'Ringo'])
[<Person: John Lennon>, <Person: Ringo Starr>]
>>> models.Person.objects.filter(first__in=['joHn', 'RiNgO'])
[<Person: John Lennon>, <Person: Ringo Starr>]
So, if portability is not crucial and you use MySQL you may choose to ignore the issue altogether.
I am expanding Exgeny idea into an two liner.
import functools
Name.objects.filter(functools.reduce(lambda acc,x: acc | Q(name_iexact=x)), names, Q()))
After trying many methods, including annotate, which resulted in duplicate objects, I discovered transformers (https://docs.djangoproject.com/en/4.1/howto/custom-lookups/#a-transformer-example) which allow for a simple solution.
Add the following to models.py before model declarations:
class LowerCase(models.Transform):
lookup_name = "lower"
function = "LOWER"
models.CharField.register_lookup(LowerCase)
models.TextField.register_lookup(LowerCase)
You can now use the __lower transformer alongside any lookup, in this case: field__lower__in. You can also add bilateral = True to the transformer class for it to apply to both the field and the list items, which should be functionally equivalent to __iin.
Here is an example of custom User model classmethod to filter users by email case-insensitive
from django.db.models import Q
#classmethod
def get_users_by_email_query(cls, emails):
q = Q()
for email in [email.strip() for email in emails]:
q = q | Q(email__iexact=email)
return cls.objects.filter(q)
If this is a common use case for anyone, you can implement this by adapting the code from Django's In and IExact transformers.
Make sure the following code is imported before all model declarations:
from django.db.models import Field
from django.db.models.lookups import In
#Field.register_lookup
class IIn(In):
lookup_name = 'iin'
def process_lhs(self, *args, **kwargs):
sql, params = super().process_lhs(*args, **kwargs)
# Convert LHS to lowercase
sql = f'LOWER({sql})'
return sql, params
def process_rhs(self, qn, connection):
rhs, params = super().process_rhs(qn, connection)
# Convert RHS to lowercase
params = tuple(p.lower() for p in params)
return rhs, params
Example usage:
result = Name.objects.filter(name__iin=['name1', 'name2', 'name3'])