So we wanted to be able to find a few, random products to put on the homepage of one of our client's sites. I went looking for solutions, and the most commonly spouted one is to do a find with "order by 'rand()'"...
Now, this does the trick, for sure, but the problem is that it will order the entire table by the random amount... and we have a table with many thousand products, and this leads us towards a less-than-optimal query-time - every time somebody hits the homepage. :P
Given we only wanted a handful of products each time - I figured there just had to be a way of pulling out a small set of randomly-selected products without killing the load-time.
then I stumbled upon this article on Random records in rails. It provides a quick-trick fix that will pull out a single random record without all the fuss of ordering the entire table...
...but it doesn't provide a complete, extensible solution. We need it to pull out more than one - and I don't want to have to hit the database multiple times, if I can find a way around it.
Also - we don't just want *any* random product. Some of them are archived or out of stock, and so we need to be able to pass in other finder-option or use our nifty named scopes (eg "in_stock" or "best_sellers").
So here's my new solution. It lets you choose the number of random records to return, and pass in options, and plays nice with named scopes.
# pull out a unique set of random active record objects without killing
# the db by using "order by rand()"
# Note: not true-random, but good enough for rough-and-ready use
#
# The first param specifies how many you want.
# You can pass in find-options in the second param
# examples:
# Product.random => one random product
# Product.random(3) => three random products in random order
#
# Note - this method works fine with scopes too! eg:
# Product.in_stock.random => one random product that fits the "in_stock" scope
# Product.in_stock.random(3) => three random products that fit the "in_stock" scope
# Product.best_seller.in_stock.random => one random product that fits both scopes
#
def find_random(num = 1, opts = {})
# skip out if we don't have any
return nil if (max = self.count(opts)) == 0
# don't request more than we have
num = [max,num].min
# build up a set of random offsets to go find
find_ids = [] # this is here for scoping
# get rid of the trivial cases
if 1 == num # we only want one - pick one at random
find_ids = [rand(max)]
else
# just randomise the set of possible ids
find_ids = (0..max-1).to_a.sort_by { rand }
# then grab out the number that we need
find_ids = find_ids.slice(0..num-1) if num != max
end
# we've got a random set of ids - now go pull out the records
find_ids.map {|the_id| first(opts.merge(:offset => the_id)) }
end
2 comments:
Thank you! That saved me a lot of time :)
You're welcome :)
Post a Comment