vette982
vette982

Reputation: 4870

Custom ordering in Rails query

I have a Post model with some has_many associations.

class Post < ActiveRecord::Base
 ...
 has_many :votes
 has_many :comments
 has_many :ratings
end

I want a query that orders the posts by (votes.count + comments.count + ratings.count).

For example, if I a post had 3 votes, 2 comments, and 1 rating, its ordering "metric" would have a value of 6. How would I do this?

I also want a second query that orders it with the same 3 parameters (votes,comments,ratings), but also adds a 4th parameter that is inverse proportional to created_at, so newer posts would be ranked highly and older posts would be ranked lower. In summary the ordering metric would be something like:

(F*(1/created_at) + votes.count + comments.count + ratings.count), where F is a scaling factor. How would I do this?

Upvotes: 1

Views: 1829

Answers (3)

tomciopp
tomciopp

Reputation: 2742

Is there a reason why this needs to be done in the database? If not I would suggest you use the sort_by ruby method, after finding all of the records and their included associations. Something like:

# In the post model 
class Post < ActiveRecord::Base
  def custom_metric
    votes.size + comments.size + ratings.size
  end
end

# In post controller
@posts = Post.where(id: ..).includes(:votes, :comments, :ratings).sort_by(&:custom_metric)

You can follow the same type of logic for the other way you want to sort your objects. This method will be comparably fast to the others suggested and will have the bonus of not causing any data denormalization. The query will always return the desired result regardless of the state of your database.

Upvotes: 1

Billy Chan
Billy Chan

Reputation: 24815

This is about algorithm.

Query is okay for very simple algorithm. When your ideas growing, more complex methods are required, and query will no long fit.

I suggest you to build one more field named "score" to store the calculated result. It has an initial value when you create the record. Then, every time you update one of the factors - votes, comments, ratings, you trigger a hook to calculate the "score" again.

When your algorithm changed, you arrange a worker to calculate "score" for all of the records again.

For ordering, just simply order them by "score".

Upvotes: 3

mu is too short
mu is too short

Reputation: 434635

I'd recommend that you use an AR counter cache here:

4.1.2.4 :counter_cache

The :counter_cache option can be used to make finding the number of belonging objects more efficient.
[...]
Although the :counter_cache option is specified on the model that includes the belongs_to declaration, the actual column must be added to the associated model.

So you'd modify the corresponding belongs_to declarations to include the :counter_cache option:

class Vote < ActiveRecord::Base
  belongs_to :post, :counter_cache => true
end
# Similarly for the other two...

and then add counter columns to your posts table in a migration:

def change
  change_table :posts do |t|
    t.integer :votes_count
    #...
  end
end

You'll also want a migration to initialize the counters for your existing Posts.

Then you'll have the counters as properties of your models and you can say things like:

Post.where(...).order('posts.votes_count + posts.comments_count + posts.ratings_count')

If you want to include created_at then you could use extract(epoch from created_at) to get the timestamp as a convenient double precision value that you can use in arithmetic expressions.


The downside to this is that the counters can get out of sync if you stray but a hair from The One True Path To Rails Nirvana (or where ever it is really going ;) so you'll need to be careful not to touch the database yourself and always go through the associations to create and destroy things. I'd also recommend that you build a quick'n'dirty sanity checker that you can run every now and then to make sure the counters are correct.

If you're happy to be PostgreSQL-specific then you could ditch the :counter_cache => true nonsense and all the brittleness that comes with it and use triggers in the database to maintain the cached counter values.

Upvotes: 4

Related Questions