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