1

Is there a way to concatenate a unknown number of querysets into a list?

Here are my models:

class Item(models.Model):
    name = models.CharField(max_length=200)
    brand = models.ForeignKey(User, related_name='brand')
    tags = models.ManyToManyField(Tag, blank=True, null=True)
    def __unicode__(self):
        return self.name
    class Meta:
        ordering = ['-id']

class Tag(models.Model):
    name = models.CharField(max_length=64, unique=True)
    def __unicode__(self):
        return self.name

I have two types of queries that I'm working with:

  1. items = Item.objects.filter(brands__in=brands)

  2. items = Item.objects.filter(tags__name='80s').filter(tags__name='comedy')

With regards to the second type of query, users can save searches (for example "80s comedy"), and can save multiple searches at the same time, so I will need to create a query for each search that they have saved.

I originally wanted to try and construct a single query that will handle both cases (see Django Combining AND and OR Queries with ManyToMany Field ), but I now think the best way to do this would be to combine all queries into a list.

I like what @akaihola suggests here: How to combine 2 or more querysets in a Django view? but I can't figure out how to use itertools.chain with a variable number of queries.

Does anyone know the best way to accomplish that?

EDIT: Forgot to mention, what I'm looking for are items that have a certain brand OR have all of the required tags.

Community
  • 1
  • 1
rolling stone
  • 12,668
  • 9
  • 45
  • 63

1 Answers1

3

Slightly unorthodox, but you could use recursion. So in your example:

def recursive_search(tags, results_queryset):
    if len(tags) > 0:
        result_qs = result_queryset.filter(tags_name=tags[0])
        if result_queryset.exists():
            return filter_recursion(tags[1:],result_queryset)
        else:
            return None
    return result_queryset

tags = ["comedy", "80s", "action", "thriller"]  # This can be variable
result_queryset = Item.objects.filter(brands__in=brands) # Could be Item.objects.all()
print recursive_search(tags, result_queryset)

So you start off with a list of the tags you are searching for, and a queryset of ALL of your items that could possibly fit your criteria (in this case we start with the list of items of a particular brand)

You then recursively go through the list of tags one by one and cut the queryset down. For each level, you re-filter the entire queryset to only those items which have all the mentioned tags.

so:

  • the first call/level would be for all the items that have the tag favourite,
  • the second call/level would be for all the items that have the tags favourite and loudest,
  • etc.

If the queryset returned by the filter is None, it means there are no items that have all the required tags, and the method will quit and return None (i.e. it quits at the first possible instance of failure). Furthermore, there should only be a single hit to the database (I think!)

I've tested this out and it should work, so give it a shot

EDIT

To concatonate the queryset returned from the brands (q1) and the queryset created above using itertools (q2):

list = []
for item in itertools.chain(q1, q2):
    list.append(item)

EDIT 2

does this not accomplish what you need in one query?

# list of tags = ['comedy','80s']
qs = Item.objects.all( Q(brand__iexact="brand name") | Q(tags__name__in=[tag for tag in list_of_tags]) )
Timmy O'Mahony
  • 53,000
  • 18
  • 155
  • 177
  • thanks pastylegs, this is brilliant! unfortunately there's something I forgot to add in my question, which is that the query is for items that have a certain brand OR have all of the required tags. (added now as an edit). Your solution definitely works for the tags query - do you know how I could use that with itertools.chain to convert all of the result_querysets into a single list? I've pretty much given up on the single query option and am going to convert the QuerySets for each type of query into lists and join and dedupe them. – rolling stone Jun 04 '11 at 02:20
  • thanks for the edits, we're getting closer. Edit 2 doesn't seem to work, it retrieves all items that have either tag instead of both tags. Edit 1 works, but how would I make that work with a variable number of q2 queries? For example a user might have the following searches saved: "comedy 80s" and "action 90s", which results in 3 total queries: q1 = brand__iexact="brand", q2 = filter(tags__name='comedy').filter(tags__name='80s'), q3 = filter(tags__name='action').filter(tags__name='90s') Do you know how I can use itertools.chain with a variable number of queries? Thanks in advance for your help – rolling stone Jun 04 '11 at 06:37
  • I've shown you how. The recursive function will make a "variable number" of queries. The first edit shows you how to merge the two results using itertools – Timmy O'Mahony Jun 04 '11 at 10:15
  • if i understand it correctly the recursive function generates a single queryset for a variable number of tags. What i need is a variable number of querys. So to continue the example what im looking for are items that have brand = brand OR tags = 80s and comedy OR tags = action and 90s. There should be a variable number of results to merge using itertools.(Sorry probably should have used an example with several saved searches to make that more clear). – rolling stone Jun 04 '11 at 14:42