I'm building a recommendation method for users in my project. Users generate interest records whenever they view, create, comment or interact with objects (weighted depending on the action).
I've written a find method that looks at a user's interests, and then finds users who are also interested in those items. However, it is horrendously inefficient, making as many db calls as the user has interests (up to 50).
Here's a chopped down version of what's going on:
#User.rb
...
has_many :interests, :as => :interestable, :dependent => :destroy
def recommendations
recommendations = []
Interest.for(self).limit(50).each do |item|
recommendations << Interest.other_fans_of(item)
end
user_ids = recommendations.flatten.map(&:user_id).uniq
end
...
#interest.rb
...
belongs_to :user
belongs_to :interestable, :polymorphic => true
named_scope :for, lambda { |user| { :conditions => { :user_id => user.id } } }
named_scope :limit, lambda { |num| { :limit => num } }
named_scope :other_fans_of, lambda { |interest| { :conditions => { :interestable_type => interest.interestable_type, :interestable_id => interest.interestable_id } } }
default_scope :order => "weight DESC"
...
Are there any sql geniuses out there who can turn that into one nice clean db call?
Something like this should do the job. There might be prettier ways…
class User < ActiveRecord::Base
#...
def recommendations
# get a list of the relevant interests
the_interests = Interest.for(self).limit(50).map{|x| [x.interestable_type, x.interestable_id]}
# make some sql
conditions = the_interests.map{|x| "(`interestable_type`=? AND `interestable_id`=?)"}.join(" OR ")
# use ruby magic to make a valid finder and get the other user_ids
user_ids = Interest.all(:select => '`user_id`', :conditions => [conditions, *(the_interests.flatten)]).map(&:user_id).uniq
end
#...
end
Related
class Answer < ActiveRecord::Base
attr_accessible :question_id, :result, :user_id
end
class Question < ActiveRecord::Base
attr_accessible :prompt, :topic
end
I have the following 2 models above in my Rails app. I'd like to run a query that selects questions that have a certain topic (Math) but that are unanswered for a given user_id (result = "Unanswered").
I'm having trouble coming up with the query that would get this.
first_question = Question.where(:topic => "Math")
But I'm not sure how to incorporate the :result => "Unanswered" from the Answer table. Any advice?
To get all unanswered questions for user #1 under the MATH topic use:
Question.includes(:answers).where(:topic => 'MATH', 'answers.result' => 'Unanswered', 'answers.user_id' => 1)
If you want to just get the first unanswered question:
Question.includes(:answers).where(:topic => 'MATH', 'answers.result' => 'Unanswered', 'answers.user_id' => 1).first
I might create some scopes to make these easier to reuse:
class Question < ActiveRecord::Base
has_many :answers
scope :topic, lambda {|topic|
where(:topic => topic)
}
scope :unanswered_by, lambda { |user|
includes(:answers).where('answers.result' => 'Unanswered', 'answers.user_id' => user.id)
}
end
So you can then query like this:
user = User.last
Question.topic('MATH').unanswered_by(user)
You could use a joins to do an inner join between the questions and answers table, then merge a query for :result => "Unanswered":
Question.joins(:answer).where(:topic => "Math", :user_id => <user_id>).merge(Answer.where(:result => "Unanswered"))
I have post model
class Post < ActiveRecord::Base
acts_as_voteable
end
and Vote model
class Vote < ActiveRecord::Base
scope :for_voter, lambda { |*args| where(["voter_id = ? AND voter_type = ?", args.first.id, args.first.class.name]) }
scope :for_voteable, lambda { |*args| where(["voteable_id = ? AND voteable_type = ?", args.first.id, args.first.class.name]) }
scope :recent, lambda { |*args| where(["created_at > ?", (args.first || 2.weeks.ago)]) }
scope :descending, order("created_at DESC")
belongs_to :voteable, :counter_cache=>true,:polymorphic => true,:touch=>true
belongs_to :voter, :polymorphic => true
attr_accessible :vote, :voter, :voteable
# Comment out the line below to allow multiple votes per user.
validates_uniqueness_of :voteable_id, :scope => [:voteable_type, :voter_type, :voter_id]
end
when I get the post voters with these method
<% #post.voters_who_voted.each do |voter|%>
<%= voter.name %>
<% end %>
I load my database
how can I select only the user name and user id from these array?
update I changed my code I am using thumbs_up gem I pasted less code first to simplify the question
What do you mean by "load database"? If you want to select only id and name columns, then use #post.users.select([:id, :name]).each ...
Or is it about this problem (according to code that you provided)?
UPD.
voters_who_voted loads all voters and returns array https://github.com/bouchard/thumbs_up/blob/master/lib/acts_as_voteable.rb#L113. You have to add own association to Post model:
has_many :voters, :through => :votes, :source => :voter, :source_type => 'User'
It's just example, perhaps voters will clash with already existing method, if any.
Then use it here instead of voters_who_voted
did you try collect method ??
names = #post.users.collect(&:name)
ids = #post.user.collect(&:id)
If you want it to be related you can make a HASH with it. Id's mapped to the names.
I'm trying to connect the values of two join tables that I have and show the results based on a conditional relationship...and i'm having some problems
I have a Users Model(:name, :password, :email), and Events model(:name, :etc) and Interests model (:name)
I created about 5 records in each model.
Then I created two join tables -> UsersInterests and EventsInterests; each not containing a primary key and only comprised of the user_id/interest_id and event_id/interest_id respectively.
Then I added to the model files the HABTM Relationship
users => has_and_belongs_to_many :interests
events => has_and_belongs_to_many :interests
interests => has_and_belongs_to_many :users
has_and_belongs_to_many :events
Now I wanted to create a controller that finds only the events where the users interests correspond with the events interests
From working on this for a while I've figured that I need something in the area of
#Events = Event.User.find([condition])
[condition] = where users.interest == event.interest
or something like that... I'm kind of lost..How do you state the find condition?...I know how to do the inner join in sql but I'm looking for the elegant Rails way to do this... any tips guys?
The elegant ruby way to do this is with named scopes. However because you've decided to use has_and_belongs_to_many relationships instead of has_many :through relationships, you're going to need to define the join with raw SQL, which isn't very elegant. And because of the way Rails handles SQL generation, you will have to make a scope for use with a single user, and a second named scope for use with many users.
Class Event < ActiveRecord::Base
...
#find events that share an interest with a single user
named_scope :shares_interest_with_user, lambda {|user|
{ :joins => "LEFT JOIN events_interests ei ON ei.event_id = events.id " +
"LEFT JOIN users_intersets ui ON ui.interest_id = ei.interest_id",
:conditions => ["ui.user_id = ?", user], :group_by => "events.id"
}
#find events that share an interest with a list of users
named_scope :shares_interest_with_users, lambda {|users|
{ :joins => "LEFT JOIN events_interests ei ON ei.event_id = events.id " +
"LEFT JOIN users_intersets ui ON ui.interest_id = ei.interest_id",
:conditions => ["ui.user_id IN ?", users], :group_by => "events.id"
}
}
#find events that share an interest with any user
named_scope :shares_interest_with_any_user, lambda {
{ :joins => "LEFT JOIN events_interests ei ON ei.event_id = events.id " +
"JOIN users_intersets ui ON ui.interest_id = ei.interest_id",
:conditions => "ui.user_id IS NOT NULL", :group_by => "events.id"
}
}
end
Now you can do this to get all the events a user might be interested in:
#events = Event.shares_interest_with_user(#user)
Or this to get all the events a list of users might be interested in:
#events = Event.shares_interest_with_users(#users)
But as I warned, that's not really elegant.
You can greatly simplify the joins if you redefine your relationships to be has_many through relationships with proper join models instead of HABTM relationships. Your case would require the nested has many through plugin for this to work. N.B. You'll have to add corresponding has_many/belongs_to statements in all of the other models. Even the join models.
Class Event < ActiveRecord::Base
has_many :event_interests
has_many :interests, :through => :event_interests
has_many :user_interests, :through => :interests
has_many :users, :through => :user_interests
...
#find events that share an interest with a list of users
named_scope :shares_interest_with_users, lambda {|user|
{ :joins => :user_interests, :group_by => "events.id",
:conditions => {:user_interests => {:user_id => user}}
}
}
#find events that share an interest with any user
named_scope :shares_interest_with_any_user, lambda {
{ :joins => :user_interests, :group_by => "events.id",
:conditions => "user_interests.user_id IS NOT NULL"
}
end
Now, the following will work.
#user = User.first; #users = User.find(1,2,3)
# #events = all events a single user would be interested in
#events = Event.shares_interest_with_users(#user)
# #events = all events any of the listed users would be interested in.
#events = Event.shares_interest_with_users(#user)
You could even define a named scope to select events that haven't happened yet and chain the two:
named_scope :future_events, lambda {
{ :conditions => ["start_time > ?", Time.now]}
}
Events.future_events #=> Events that haven't started yet.
# Events that a user would be interested in but only choose those
# that haven't started yet.
Events.future_events.shares_interest_with_user(#user)
A user has many employments.
What do you think?
Is this a valid and clear way to fetch all siblings (belonging to the same user) of a given employment object?
class Employment < ActiveRecord::Base
belongs_to :user
has_many :silblings,
:primary_key => :user_id,
:foreign_key => :user_id,
:class_name => 'Employment'
end
This can be extended with the following named scope:
named_scope :except, lambda {|id| {:conditions => ["id != ?", id]} if id}
Now I can do stuff like:
self.silblings.except(self.id).each do |silbling|
puts silbling
end
The resulting SQL statement looks like:
SELECT * FROM `employments`
WHERE (`employments`.user_id = 49)
AND ((id != 46) AND (`employments`.user_id = 49))
Comments like 'no, you abuse XY, rather use this XZ' are very welcome!
Reto
Looks fine. Except that the SQL doubles ('employments'.user_id = 49) in the query. Which is nothing major. If it's something you really don't want, you could go about defining siblings like this:
class Employment < ActiveRecord::Base
belongs_to :user
named_scope :for_user, lambda { |user|
{ :conditions => {:user_id => user} }
}
named_scope :except, lambda {|employment|
{:conditions => ["id != ?", employment}
}
def siblings
Employment.for_user(user_id).except(id)
end
end
Believe it or not you can still call named scopes on #employment.siblings. Although doing things this way means you can't assign to siblings. The siblings call comes out a little cleaner. There may be a performance improvement, but it probably won't be significant to make a difference.
How do I turn this into a has_one association?
(Possibly has_one + a named scope for size.)
class User < ActiveRecord::Base
has_many :assets, :foreign_key => 'creator_id'
def avatar_asset size = :thumb
# The LIKE is because it might be a .jpg, .png, or .gif.
# More efficient methods that can handle that are OK. ;)
self.assets.find :first, :conditions =>
["thumbnail = '#{size}' and filename LIKE ?", self.login + "_#{size}.%"]
end
end
EDIT: Cuing from AnalogHole on Freenode #rubyonrails, we can do this:
has_many :assets, :foreign_key => 'creator_id' do
def avatar size = :thumb
find :first, :conditions => ["thumbnail = ? and filename LIKE ?",
size.to_s, proxy_owner.login + "_#{size}.%"]
end
end
... which is fairly cool, and makes syntax a bit better at least.
However, this still doesn't behave as well as I would like. Particularly, it doesn't allow for further nice find chaining (such that it doesn't execute this find until it's gotten all its conditions).
More importantly, it doesn't allow for use in an :include. Ideally I want to do something like this:
PostsController
def show
post = Post.get_cache(params[:id]) {
Post.find(params[:id],
:include => {:comments => {:users => {:avatar_asset => :thumb}} }
...
end
... so that I can cache the assets together with the post. Or cache them at all, really - e.g. get_cache(user_id){User.find(user_id, :include => :avatar_assets)} would be a good first pass.
This doesn't actually work (self == User), but is correct in spirit:
has_many :avatar_assets, :foreign_key => 'creator_id',
:class_name => 'Asset', :conditions => ["filename LIKE ?", self.login + "_%"]
(Also posted on Refactor My Code.)
Since there are actually multiple avatar_assets ( one for each size ), you have to keep it as a has_many association.
class User < AR::B
has_many :avatar_assets, :conditions => ['filename like ?' '%avatar%'], :class_name => 'Asset'
named_scope :avatar_size, lambda { |size|
{ :conditions => [ "thumbnail = ?", size ] }
}
end
An alternative would be to put all the work in the named scope:
class User < AR::B
named_scope :avatar_for, lambda { |user, options|
if options[:size]
{ :conditions => [ "filename like ? AND thumbnail = ?", user.login, options[:size] ] }
else
{ :conditions => [ "filename like ?", user.login ] }
end
}
end
this allows you to say
Asset.avatar_for(current_user, :size => :medium)
but is less cool when you find yourself saying
current_user.avatar_for( current_user, :size => :medium )
you could add some :avatar, :avatar?, etc methods to User to clean this up.
Personally I advise you to check out the Paperclip plugin and avoid these issues entirely.
EDIT:
Per your comment, to create a condition like "show me comments by avatar-having users", I'm not sure that will do it. You'd could make a relationship like so:
class Comment
named_scope :with_avatars, :include => { :user => :avatar_assets }, :conditions => [ 'assets.thumbnail = ?', :thumb ]
end
EDIT:
Since you're only interested in caching, rather than conditions, we can drop the condition array:
named_scope :with_avatars, :include => { :user => :avatar_assets }
I revised the code above to be more workable. The key difference is to make the 'avatar'-ness of the assets easily queryable. If you can update your existing avatar_assets to have a filename including the pattern 'avatar-[login]', you can make the condition set static which is much cleaner than always having to search for the avatar based on the user login. Association extensions are another way to resolve this, however I don't think you'll be able to chain them or combine them with named scopes.