54

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])
Kevin Brown-Silva
  • 40,873
  • 40
  • 203
  • 237
dragoon
  • 5,601
  • 5
  • 37
  • 55

9 Answers9

65

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.

0buz
  • 3,443
  • 2
  • 8
  • 29
Rasmus Kaj
  • 4,224
  • 1
  • 20
  • 23
  • 2
    Postgres supports case-insensitive indexes, so for that case it may be faster to run separate "iexact" queries for each item than an iregex match. In django's postgres backend "iexact" search uses an UPPER() transform, so with a custom index on UPPER() for that row it is possible to get a speedup. – Evgeny Sep 24 '12 at 01:56
  • 18
    I wish they'd implement __iin – JREAM Jun 19 '13 at 20:02
  • @Evgeny I wish if you could add an answer, or give us a link. Thanks! – Grijesh Chauhan Jan 29 '14 at 09:32
  • @GrijeshChauhan sure, have a look at my post below. – Evgeny Jan 30 '14 at 22:21
  • 2
    FOR ALL which simply copy this answer: There are two more things to consider: (1) Regex Escaping as mentioned by @Martin Smith further below, and (2) if you want to have an __in operator, make sure, you are using start and end delimiters `result = Name.objects.filter(name__iregex=r'^(' + '|'.join([re.escape(b) for b in a]) + ')$')` – blacklwhite May 25 '18 at 09:28
32

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']
TobiMcNamobi
  • 4,687
  • 3
  • 33
  • 52
Noortheen Raja
  • 791
  • 11
  • 15
  • If I'm not mistaken, bear in mind that this won't be able to use any indexes in the ''name'` field, unless the index is already created in a `Lower('name')` manner: https://stackoverflow.com/a/7005656/1236843 – Chesco Igual Oct 16 '19 at 13:54
29

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.

Community
  • 1
  • 1
Evgeny
  • 10,698
  • 9
  • 60
  • 70
5

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.

m000
  • 5,932
  • 3
  • 31
  • 28
5

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]) + ')')
Martin Smith
  • 51
  • 1
  • 1
2

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()))
user1462442
  • 7,672
  • 1
  • 24
  • 27
2

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.

abmyii
  • 31
  • 1
  • 4
1

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)
pymen
  • 5,737
  • 44
  • 35
0

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'])
jpnauta
  • 31
  • 1
  • 3