Stan Reduta
Stan Reduta

Reputation: 3492

Filter multiple Django model fields with variable number of arguments

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.

Upvotes: 1

Views: 1917

Answers (2)

Stan Reduta
Stan Reduta

Reputation: 3492

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

Upvotes: 0

lukewarm
lukewarm

Reputation: 857

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.

Upvotes: 5

Related Questions