tags:

views:

33

answers:

1

I have three inputs coming in from a form. They are name, neighborhoods and tags. Neighborhoods and tags are multi-select box string lists. Here is my current query:

q = Restaurant.objects.filter(name__icontains=name)
q = q.filter(neighborhoods__name__in=neighborhoods)
for tag in tags:
    q = q.filter(tags__name=tag)
q = q.order_by('name').distinct()

Which currently fetches all restaurants that have ALL of the tags and ALL of the neighborhoods. I'm having a little trouble making this a weighted search. Basically, for each tag and neighborhood that matches, I want to add a point to a weight column. Then I will order by weight and even if a restaurant only matches two out of three tags, it will still be shown (its weight would be 2). This is to prevent 0 results from happening and show the closest it can. Additionally, I want to require that at least 1 point is required to select a restaurant.

I guess in SQL it would be something like:

SELECT *, 
    (SELECT COUNT(1) 
     FROM tags t 
     WHERE t.name IN (%s)
    ) AS weight 
FROM restaurants 
WHERE weight > 0 
ORDER BY weight DESC
+2  A: 

You want to use annotate()

from django.db.models import Count
q = Restaurant.objects.filter(name__icontains=name)
q = q.filter(neighborhoods__name__in=neighborhoods)
for tag in tags:
    q = q.filter(tags__name=tag)
q = q.order_by('name').annotate(num_tags=Count('tags__name')).filter(num_tags__gte=2)

update

Looking at the code again I see that unfortunately it's filtering out so that only matches with all tags work. I think just this change should work:

Get rid of:

for tag in tags:
    q = q.filter(tags__name=tag)

Replace with:

q = q.filter(tags__name__in=tags)

That way it matches all queries where a Restaurant is tagged with at least one of the requested tags. The annotate and filter later on takes care of making sure it matches at least 2.

Jordan Reiter
Wait a minute here... this is just counting the number of tags for the restaurant, not the # of tags that match the tag list. I need something like `.annotate(num_tags=Count('tags__name__in'=tags))`
Matt Williamson
No, the `q = q.filter(tags__name__in=tags)` code limits the rows to only include the tags that match. `.annotate(Count('tags__name'))` groups the records by everything other than the tag record. If you search for 5 tags, have 3 restaurants, A, B, and C; and A matches 1 tag, B 3, and C 4, then after annotate rows are (A: num_tags 1, B: num_tags 3, C: num_tags 4) and `filter()` removes A, leaving B and C. I can tell you that `.annotate(num_tags=Count('tags__name__in'=tags))` would throw an error. The argument for `Count` is just the field name, not a clause as in filter.
Jordan Reiter
ok, my bad. Thank you so much!
Matt Williamson