Hassek
Hassek

Reputation: 8995

django icontains with __in lookup

So I want to find any kind of matching given some fields, so for example, this is what I would like to do:

possible_merchants = ["amazon", "web", "services"]
# Possible name --> "Amazon Service"
Companies.objects.filter(name__icontains__in=possible_merchants)

sadly it is not possible to mix icontains and the __in lookup.

It seems to be a pretty complex query so if at least I could ignore case the name that would be enough, for example:

Companies.objects.filter(name__ignorecase__in=possible_merchants)

Any ideas?

P.D.: The queries I posted don't work, it's just a way to express what I need (just in case heh)

Upvotes: 30

Views: 34872

Answers (6)

juan Isaza
juan Isaza

Reputation: 3987

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].

Upvotes: 0

Tycho
Tycho

Reputation: 83

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.

Upvotes: 0

Slava Riazanov
Slava Riazanov

Reputation: 1

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]
)

Upvotes: 0

caram
caram

Reputation: 1719

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.

Upvotes: 4

Sanyam Khurana
Sanyam Khurana

Reputation: 1421

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.

Upvotes: 5

Gareth Rees
Gareth Rees

Reputation: 65884

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.)

Upvotes: 55

Related Questions