Using Redis sets (unique lists) to track relationships between users and their friends, and making friend suggestions

In this blog post I'll share some code I was tinkering with that uses Redis sets (unique lists) to track relationships between users and their friends. My intention was to create a bunch of users, add a number of friends for each user, and then make a friend suggestion based on a user's friends.. friends.

I started by creating a Redis module that when included would add its methods to the class and its instances. file: lib/base.rb

moduleFriendFinderclassUserincludeBaseincludeFrienddefinitialize(options={})@data=options||={}fail'Options must be a Hash'unlessoptions.is_a?(Hash)# dynamically add a few methods# IE: makes user object instance var @data[:id] accessible as user.id[:id,:first_name,:last_name].eachdo|meth|unlessself.class.method_defined?(meth)self.class.send(:define_method,meth)do@data[meth]endendendend# create a new user and store to redis hash# ex key: FriendFinder::User:1defcreate!options=default_options.merge(@data)redis.hmset(to_key,options.flatten)redis.sadd(self.class.id_key,id)enddefdefault_optionsrandom_nameenddefrandom_name@@names||=load_random_names{first_name:@@names[:first].sample,last_name:@@names[:last].sample}end# load random names from a file,# formatted: FIRST LAST\ndefload_random_namesret={first:[],last:[]}File.readlines('names.txt').eachdo|line|first_name,last_name=line.strip.split(/\s/)ret[:first]<<first_nameret[:last]<<last_nameendretend# define redis key for user# ex key: FriendFinder::User:1defto_keyfail'Id required'ifid.nil?"#{self.class.name}:#{id}"end# load a user's data from redis hashdefload!data=redis.hgetall(to_key)data=Hash[data.map{|(k,v)|[k.to_sym,v]}]@data.merge!dataselfenddefputs_outputputs"ID:\t\t#{id}"puts"first name:\t#{first_name}"puts"last name:\t#{last_name}"putsend## Class methods## define key to track set of redis user idsdefself.id_key"#{name}:ID"end# select random user iddefself.random_user_idredis.srandmemberid_keyend# create a bunch of usersdefself.create_users!(how_many=1_000)fail'How many must be an Integer'unlesshow_many.respond_to?(:times)how_many.timesdo|i|new(id:i+1).create!endend# create a bunch of friends for each userdefself.create_friends!(how_many_each=100)fail'How many each must be an Integer'unlesshow_many_each.respond_to?(:times)all_ids.eachdo|id|user=new(id:id).load!whileuser.friends_size<how_many_eachuser.friends_create!(random_user_id)endendend# get a list of all user idsdefself.all_idsredis.smembersid_keyendendend

I created another module for Friend methods, which is included by the User module. file: lib/friend.rb

moduleFriendFindermoduleFrienddefself.included(klass)klass.extend(ClassMethods)klass.send(:include,InstanceMethods)endmoduleInstanceMethodsdeffriends_keyfail'Id required'ifid.nil?"#{self.class.name}:#{id}:Friends"enddeffriends_sizeredis.scardfriends_keyenddefhas_friend?(friend_id)redis.sismemberfriends_key,friend_idenddeffriends_create!(friend_id)returnfalseifhas_friend?(friend_id)redis.saddfriends_key,friend_idenddeffriend_idsredis.smembersfriends_keyendendmoduleClassMethodsdefcreate_friends!(how_many_each=100)fail'How many each must be an Integer'unlesshow_many_each.respond_to?(:times)all_ids.eachdo|id|user=new(id:id).load!whileuser.friends_size<how_many_eachuser.friends_create!(random_user_id)endendenddefsuggest_friend(user_id=nil)user_id||=random_user_idfail'User id required'ifuser_id.nil?user=new(id:user_id).load!# outputputs'Randomly selected user:'user.puts_output# get friends of friendsfriend_ids=user.friend_idsfriends_of_friends=[]friend_ids.eachdo|friend_id|friends_of_friends.push(*new(id:friend_id).load!.friend_ids)endfail'No friends of friends found'iffriends_of_friends.empty?# group/countfriend_counts=friends_of_friends.inject(Hash.new(0)){|h,i|h[i]+=1;h}# sortfriend_counts=Hash[friend_counts.sort_by{|_k,v|-v}]# remove existingfriend_counts.reject!{|k,_v|friend_ids.include?(k)}# get first friend suggestionsuggested_friend_id,suggested_friend_count=friend_counts.firstsuggested_friend=new(id:suggested_friend_id).load!# outputputs'Suggested friend:'suggested_friend.puts_outputputs"Number of friends with #{suggested_friend.first_name}: #{suggested_friend_count}"endendendend

I created the next two modules to parse command line flags and interact with user objects.