Reputation: 4870
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
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
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
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 thebelongs_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 Post
s.
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