[Django]-Django icontains with __in lookup

52πŸ‘

βœ…

You can create querysets with the Q constructor and combine them with the | operator to get their union:

from django.db.models import Q

def companies_matching(merchants):
    """
    Return a queryset for companies whose names contain case-insensitive
    matches for any of the `merchants`.
    """
    q = Q()
    for merchant in merchants:
        q |= Q(name__icontains = merchant)
    return Companies.objects.filter(q)

(And similarly with iexact instead of icontains.)

5πŸ‘

I find it a cleaner approach using reduce and or_ operator:

from django.db.models import Q
from functools import reduce
from operator import or_

def get_companies_from_merchants(merchant_list):
    q_object = reduce(or_, (Q(name__icontains=merchant) for merchant in merchant_list))
    return Companies.objects.filter(q_object)

This would create a list of Q objects querying the name to contain a single element in merchant list. This would happpen for all the elements in merchant_list and all these Q objects would be reduced to a single Q object having mutliple ORs which can be directly applied to the filter query.

4πŸ‘

This is the approach that I adopted:

class MyManager(models.Manager):
    def exclusive_in(self, lookup, value_list):
        return self.filter(reduce(or_, (Q(**{lookup:_}) for _ in value_list)))

Here is now to use it:

Companies.objects.exclusive_in('name__icontains', possible_merchants])

It was inspired by other answers in this thread, as well as Django filter queryset __in for *every* item in list.

0πŸ‘

Another approach would be to simulate the actions that Django normally does for iexact queries (it converts both parts of the comparison statement to the upper case via SQL Upper function.

This way, the query will look like this:

Companies.objects.annotate(
    upper_name=models.Upper("name")
).filter(
    upper_name__in=[rchant.upper() for merchant in possible_merchants]
)

0πŸ‘

The answer from Gareth Rees helped me a lot with a similar issue while using the django-filter package.

To use this with django_filters in generic way one could create a MultipleInputs filter like this:

    class MultipleInputs(filters.BaseInFilter, filters.CharFilter):
        pass

Then use that in the filterset and use a custom filtering method:

    from django.db.models import Q
    
    class MyFilter(FilterSet):
        search = MultipleInputs(field_name='longname', label='Text search',
            method='multiplesearch', 
            help_text="Free text search. May be a comma separated list of strings.")
 
        def multiplesearch(self, queryset, field_name, value):
            q = Q()
            for searchstring in value:
                arguments = {field_name+'__icontains' : searchstring}

                q |= Q(**arguments)
            return queryset.filter(q)

No MultiInputs fields can have comma separated inputs.
Also works if you inherit MyFilter but overwrite search with a Filter with a different field_name.

0πŸ‘

using iregex would be:

regex = '|'.join(['match 1', 'match 2', 'match 3'])
Companies.objects.filter(name__iregex=f'({regex})')

You can even add lookaround so that matches are guaranteed to be individual words, rather than parts of other words with different meaning ie:

options = ['match 1', 'match 2', 'match 3']
regex_options = [f'(?<!\w){option}(?!\w)' for option in options]  # Adds lookaround
regex = '|'.join(regex_options)
Companies.objects.filter(name__iregex=f'({regex})')

lookaround (ie lookbehind + lookahead) will not incur in character consumption therefore it’ll match with substrings starting or ending strings, that would’t be possible with [^\w].

Leave a comment