Reputation: 1842
I want to find products matching as many optional tags as possible which are tagged by all conditional tags, ordered by the amount of matching optional tags.
I came up with this for counting the optional tags:
optional_tags = [1,2,3]
conditional_tags = [4]
products = Product.select('COUNT(*) AS count_all, products.*')
.joins(:tags).where('tags.id IN (?)', optional_tags)
.group('products.id')
.order('count_all DESC')
I successfully get the product ids and the number of matching optional tags:
products: {30=>4, 26=>3, 29=>3, 27=>2, 28=>1}
When I try to add the conditional tags I only get an empty hash. I tried it this way:
products = Product.where('tags.id IN (?)', conditional_tags)
.select('COUNT(*) AS count_all, products.*')
.joins(:tags).where('tags.id IN (?)', optional_tags)
.group('products.id')
.order('count_all DESC')
Rails optimizes the query and combines all where parts...
How could I add the conditional tags to my construct? Thanks for any help!
Example:
The type of a product is important, but people don't care about the details.
conditional_tags = [1 car]
optional_tags = [2 red, 3 mercedes, 4 fast]
A red car would be ok, but a red fast motorcycle by mercedes should not be in the list of results even though there are more matching tags.
Upvotes: 0
Views: 278
Reputation: 1842
Thank you for your help, Jason. I think I figured it out... I perform a sub-query for the conditional tags within the main query for the optional tags.
My Product scopes:
scope :tagged, lambda { |tag_ids|
joins(:tags)
.where('tags.id IN (?)', tag_ids)
.group('products.id')
}
scope :matched, lambda { |product_ids, tag_ids|
select('COUNT(*) AS count_all, products.*')
.joins(:tags)
.where('products.id IN (?) AND tags.id IN (?)', product_ids, tag_ids)
.group('products.id')
.order('count_all DESC')
}
My query construct:
result = Product.matched(Product.tagged(branch_ids).map(&:id), tag_ids)
I am a little concerned about the performance, because Rails has to perform two separate queries. The nice thing is, that I can add a .limit() to the "main" query.
If someone has an idea how to solve this within one query, please let me know! Thanks!
Upvotes: 0
Reputation: 45094
Okay, I think I finally figured it out:
class Product < ActiveRecord::Base
scope :having_tags, -> { |tags|
joins(:tags).where('tags.id IN (?)', tags)
}
scope :ordered_by_tag_count, -> { |tags|
joins(:tags)
.where('tags.id IN (?)', tags)
.group('products.id')
.order('count_all DESC')
}
end
So if you have these tags:
conditional_tags = [1 car]
optional_tags = [2 red, 3 mercedes, 4 fast]
You might do this:
Product.having_tags([1]).ordered_by_tag_count([2, 3, 4])
Upvotes: 1
Reputation: 45094
I don't know if you're aware, but I think your second query is equivalent to this:
products = Product.select('COUNT(*) AS count_all, products.*')
.joins(:tags)
.where('tags.id IN (?) AND tags.id IN (?)', optional_tags, conditional_tags)
.group('products.id')
.order('count_all DESC')
Maybe you're after something closer to this?
products = Product.select('COUNT(*) AS count_all, products.*')
.joins(:tags)
.where('tags.id IN (?)', optional_tags | conditional_tags)
.group('products.id')
.order('count_all DESC')
My guess is probably not right, since I don't completely understand what you're trying to do. Maybe that gets you a little further down the road, though.
Upvotes: 1