From 571ae9a3d60fb94f6fed4ffa668c2211359c3eda Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Fri, 1 May 2015 13:22:34 -0500 Subject: [PATCH] VRFS-3007 : Fix another musician search issue and a few tests. --- ruby/lib/jam_ruby/models/search.rb | 568 +++++++++++++++-------------- 1 file changed, 290 insertions(+), 278 deletions(-) diff --git a/ruby/lib/jam_ruby/models/search.rb b/ruby/lib/jam_ruby/models/search.rb index ae0ba396e..20830befe 100644 --- a/ruby/lib/jam_ruby/models/search.rb +++ b/ruby/lib/jam_ruby/models/search.rb @@ -2,7 +2,7 @@ module JamRuby # not a active_record model; just a search result container class Search - attr_accessor :results, :search_type + attr_accessor :results, :search_type, :query attr_accessor :user_counters, :page_num, :page_count LIMIT = 10 @@ -10,82 +10,6 @@ module JamRuby SEARCH_TEXT_TYPES = [:musicians, :bands, :fans] SEARCH_TEXT_TYPE_ID = :search_text_type - def self.band_search(txt, user = nil) - self.text_search({ SEARCH_TEXT_TYPE_ID => :bands, :query => txt }, user) - end - - def self.fan_search(txt, user = nil) - self.text_search({ SEARCH_TEXT_TYPE_ID => :fans, :query => txt }, user) - end - - def self.musician_search(txt, user = nil) - self.text_search({ SEARCH_TEXT_TYPE_ID => :musicians, :query => txt }, user) - end - - def self.session_invite_search(query, user) - srch = Search.new - srch.search_type = :session_invite - like_str = "%#{query.downcase}%" - rel = User - .musicians - .where(["users.id IN (SELECT friend_id FROM friendships WHERE user_id = '#{user.id}')"]) - .where(["first_name ILIKE ? OR last_name ILIKE ?", like_str, like_str]) - .limit(10) - .order([:last_name, :first_name]) - srch.results = rel.all - srch - end - - def self.text_search(params, user = nil) - srch = Search.new - unless (params.blank? || params[:query].blank? || 2 > params[:query].length) - srch.text_search(params, user) - end - srch - end - - def text_search(params, user = nil) - tsquery = Search.create_tsquery(params[:query]) - return [] if tsquery.blank? - - rel = case params[SEARCH_TEXT_TYPE_ID].to_s - when 'bands' - @search_type = :bands - Band.scoped - when 'fans' - @search_type = :fans - User.fans - else - @search_type = :musicians - User.musicians - end - @results = rel.where("(name_tsv @@ to_tsquery('jamenglish', ?))", tsquery).limit(10) - @results - end - - def initialize(search_results=nil) - @results = [] - self - end - - def self.create_tsquery(query) - return nil if query.blank? - - search_terms = query.split - return nil if search_terms.length == 0 - - args = nil - search_terms.each do |search_term| - if args == nil - args = search_term - else - args = args + " & " + search_term - end - end - args = args + ":*" - args - end - PARAM_SESSION_INVITE = :srch_sessinv PARAM_MUSICIAN = :srch_m PARAM_BAND = :srch_b @@ -133,164 +57,322 @@ module JamRuby DATE_OPTS = [['Today', 'today'], ['This Week', 'week'], ['This Month', 'month'], ['All Time', 'all']] - def self.order_param(params, keys=M_ORDERING_KEYS) - ordering = params[:orderby] - ordering.blank? ? keys[0] : keys.detect { |oo| oo.to_s == ordering } + + def initialize(search_results=nil) + @results = [] + self + end + + def is_blank? + !!@query && @query.empty? end - # produce a list of musicians (users where musician is true) - # params: - # instrument - instrument to search for or blank - # score_limit - a range specification for score, see M_SCORE_OPTS above. - # handled by relation_pagination: - # page - page number to fetch (origin 1) - # per_page - number of entries per page - # handled by order_param: - # orderby - what sort of search, also defines order (followed, plays, playing) - # previously handled by where_latlng: - # distance - defunct! - # city - defunct! - # remote_ip - defunct! - def self.musician_filter(params={}, user=nil) + def text_search(params, user = nil) + @query = params[:query] + tsquery = Search.create_tsquery(params[:query]) + return [] if tsquery.blank? - rel = User.musicians # not musicians_geocoded on purpose; we allow 'unknowns' to surface in the search page - rel = rel.select('users.*') - rel = rel.group('users.id') + rel = case params[SEARCH_TEXT_TYPE_ID].to_s + when 'bands' + @search_type = :bands + Band.scoped + when 'fans' + @search_type = :fans + User.fans + else + @search_type = :musicians + User.musicians + end + @results = rel.where("(name_tsv @@ to_tsquery('jamenglish', ?))", tsquery).limit(10) + @results + end - unless (instrument = params[:instrument]).blank? - rel = rel.joins("inner JOIN musicians_instruments AS minst ON minst.user_id = users.id") - .where(['minst.instrument_id = ?', instrument]) + class << self + def band_search(txt, user = nil) + self.text_search({ SEARCH_TEXT_TYPE_ID => :bands, :query => txt }, user) end - # to find appropriate musicians we need to join users with scores to get to those with no scores or bad scores - # weeded out - - # filter on scores using selections from params - # see M_SCORE_OPTS - score_limit = ANY_SCORE - l = params[:score_limit] - unless l.nil? - score_limit = l + def fan_search(txt, user = nil) + self.text_search({ SEARCH_TEXT_TYPE_ID => :fans, :query => txt }, user) end - locidispid = user.nil? ? 0 : (user.last_jam_locidispid || 0) - - # user can override their location with these 3 values - country = params[:country] - region = params[:region] - city = params[:city] - - my_locid = nil # this is used for distance searches only - - if country && region && city - geoiplocation = GeoIpLocations.where(countrycode: country, region: region, city: city).first - my_locid = geoiplocation.locid + def musician_search(txt, user = nil) + self.text_search({ SEARCH_TEXT_TYPE_ID => :musicians, :query => txt }, user) end - unless my_locid - my_locid = locidispid/1000000 # if the user didn't specify a location to search on, user their account locidispid + def session_invite_search(query, user) + srch = Search.new + srch.search_type = :session_invite + like_str = "%#{query.downcase}%" + rel = User + .musicians + .where(["users.id IN (SELECT friend_id FROM friendships WHERE user_id = '#{user.id}')"]) + .where(["first_name ILIKE ? OR last_name ILIKE ?", like_str, like_str]) + .limit(10) + .order([:last_name, :first_name]) + srch.results = rel.all + srch end - if !locidispid.nil? && !user.nil? - # score_join of left allows for null scores, whereas score_join of inner requires a score however good or bad - # this is ANY_SCORE: - score_join = 'left outer' # or 'inner' - score_min = nil - score_max = nil - # these score_min, score_max come from here (doubled): https://jamkazam.atlassian.net/browse/VRFS-1962 - case score_limit - when GOOD_SCORE - score_join = 'inner' - score_min = nil - score_max = 40 - when MODERATE_SCORE - score_join = 'inner' - score_min = 40 - score_max = 70 - when POOR_SCORE - score_join = 'inner' - score_min = 80 - score_max = 100 - when UNACCEPTABLE_SCORE - score_join = 'inner' - score_min = 100 - score_max = nil - when SCORED_SCORE - score_join = 'inner' - score_min = nil - score_max = nil - when TEST_SCORE - score_join = 'inner' - score_min = nil - score_max = 60 - when ANY_SCORE - # the default of ANY setup above applies + def text_search(params, user = nil) + srch = Search.new + unless (params.blank? || params[:query].blank? || 2 > params[:query].length) + srch.text_search(params, user) + end + srch + end + + def create_tsquery(query) + return nil if query.blank? + + search_terms = query.split + return nil if search_terms.length == 0 + + args = nil + search_terms.each do |search_term| + if args == nil + args = search_term else - # the default of ANY setup above applies + args = args + " & " + search_term + end + end + args = args + ":*" + args + end + def order_param(params, keys=M_ORDERING_KEYS) + ordering = params[:orderby] + ordering.blank? ? keys[0] : keys.detect { |oo| oo.to_s == ordering } + end + + # produce a list of musicians (users where musician is true) + # params: + # instrument - instrument to search for or blank + # score_limit - a range specification for score, see M_SCORE_OPTS above. + # handled by relation_pagination: + # page - page number to fetch (origin 1) + # per_page - number of entries per page + # handled by order_param: + # orderby - what sort of search, also defines order (followed, plays, playing) + # previously handled by where_latlng: + # distance - defunct! + # city - defunct! + # remote_ip - defunct! + def musician_filter(params={}, user=nil) + + rel = User.musicians # not musicians_geocoded on purpose; we allow 'unknowns' to surface in the search page + rel = rel.select('users.*') + rel = rel.group('users.id') + + unless (instrument = params[:instrument]).blank? + rel = rel.joins("inner JOIN musicians_instruments AS minst ON minst.user_id = users.id") + .where(['minst.instrument_id = ?', instrument]) end - rel = rel.joins("LEFT JOIN current_scores ON current_scores.a_userid = users.id AND current_scores.b_userid = '#{user.id}'") + # to find appropriate musicians we need to join users with scores to get to those with no scores or bad scores + # weeded out - rel = rel.joins('LEFT JOIN regions ON regions.countrycode = users.country AND regions.region = users.state') + # filter on scores using selections from params + # see M_SCORE_OPTS + score_limit = ANY_SCORE + l = params[:score_limit] + unless l.nil? + score_limit = l + end - rel = rel.where(['current_scores.full_score > ?', score_min]) unless score_min.nil? - rel = rel.where(['current_scores.full_score <= ?', score_max]) unless score_max.nil? + locidispid = user.nil? ? 0 : (user.last_jam_locidispid || 0) - rel = rel.select('current_scores.full_score, current_scores.score, regions.regionname') - rel = rel.group('current_scores.full_score, current_scores.score, regions.regionname') + # user can override their location with these 3 values + country = params[:country] + region = params[:region] + city = params[:city] + + my_locid = nil # this is used for distance searches only + + if country && region && city + geoiplocation = GeoIpLocations.where(countrycode: country, region: region, city: city).first + my_locid = geoiplocation.locid + end + + unless my_locid + my_locid = locidispid/1000000 # if the user didn't specify a location to search on, user their account locidispid + end + + if !locidispid.nil? && !user.nil? + # score_join of left allows for null scores, whereas score_join of inner requires a score however good or bad + # this is ANY_SCORE: + score_join = 'left outer' # or 'inner' + score_min = nil + score_max = nil + # these score_min, score_max come from here (doubled): https://jamkazam.atlassian.net/browse/VRFS-1962 + case score_limit + when GOOD_SCORE + score_join = 'inner' + score_min = nil + score_max = 40 + when MODERATE_SCORE + score_join = 'inner' + score_min = 40 + score_max = 70 + when POOR_SCORE + score_join = 'inner' + score_min = 80 + score_max = 100 + when UNACCEPTABLE_SCORE + score_join = 'inner' + score_min = 100 + score_max = nil + when SCORED_SCORE + score_join = 'inner' + score_min = nil + score_max = nil + when TEST_SCORE + score_join = 'inner' + score_min = nil + score_max = 60 + when ANY_SCORE + # the default of ANY setup above applies + else + # the default of ANY setup above applies + end + + rel = rel.joins("LEFT JOIN current_scores ON current_scores.a_userid = users.id AND current_scores.b_userid = '#{user.id}'") + + rel = rel.joins('LEFT JOIN regions ON regions.countrycode = users.country AND regions.region = users.state') + + rel = rel.where(['current_scores.full_score > ?', score_min]) unless score_min.nil? + rel = rel.where(['current_scores.full_score <= ?', score_max]) unless score_max.nil? + + rel = rel.select('current_scores.full_score, current_scores.score, regions.regionname') + rel = rel.group('current_scores.full_score, current_scores.score, regions.regionname') + end + + ordering = self.order_param(params) + case ordering + when :latency + # nothing to do. the sort added below 'current_scores.score ASC NULLS LAST' handles this + when :distance + # convert miles to meters for PostGIS functions + miles = params[:distance].blank? ? 500 : params[:distance].to_i + meters = miles * 1609.34 + rel = rel.joins("INNER JOIN geoiplocations AS my_geo ON #{my_locid} = my_geo.locid") + rel = rel.joins("INNER JOIN geoiplocations AS other_geo ON users.last_jam_locidispid/1000000 = other_geo.locid") + rel = rel.where("users.last_jam_locidispid/1000000 IN (SELECT locid FROM geoiplocations WHERE geog && st_buffer((SELECT geog FROM geoiplocations WHERE locid = #{my_locid}), #{meters}))") + rel = rel.group("my_geo.geog, other_geo.geog") + rel = rel.order('st_distance(my_geo.geog, other_geo.geog)') + when :plays # FIXME: double counting? + # sel_str = "COUNT(records)+COUNT(sessions) AS play_count, #{sel_str}" + rel = rel.select('COUNT(records.id)+COUNT(sessions.id) AS search_play_count') + rel = rel.joins("LEFT JOIN music_sessions AS sessions ON sessions.user_id = users.id") + rel = rel.joins("LEFT JOIN recordings AS records ON records.owner_id = users.id") + rel = rel.order("search_play_count DESC") + when :followed + rel = rel.joins('left outer join follows on follows.followable_id = users.id') + rel = rel.select('count(follows.user_id) as search_follow_count') + rel = rel.order('search_follow_count DESC') + when :playing + rel = rel.joins("inner JOIN connections ON connections.user_id = users.id") + rel = rel.where(['connections.aasm_state != ?', 'expired']) + end + + if !locidispid.nil? && !user.nil? + rel = rel.order('current_scores.full_score ASC NULLS LAST') + end + + rel = rel.order('users.created_at DESC') + + rel, page = self.relation_pagination(rel, params) + rel = rel.includes([:instruments, :followings, :friends]) + + # XXX: DOES THIS MEAN ALL MATCHING USERS ARE RETURNED? + objs = rel.all + + srch = Search.new + srch.search_type = :musicians_filter + srch.page_num, srch.page_count = page, objs.total_pages + srch.musician_results_for_user(objs, user) end - ordering = self.order_param(params) - case ordering - when :latency - # nothing to do. the sort added below 'current_scores.score ASC NULLS LAST' handles this - when :distance - # convert miles to meters for PostGIS functions - miles = params[:distance].blank? ? 500 : params[:distance].to_i - meters = miles * 1609.34 - rel = rel.joins("INNER JOIN geoiplocations AS my_geo ON #{my_locid} = my_geo.locid") - rel = rel.joins("INNER JOIN geoiplocations AS other_geo ON users.last_jam_locidispid/1000000 = other_geo.locid") - rel = rel.where("users.last_jam_locidispid/1000000 IN (SELECT locid FROM geoiplocations WHERE geog && st_buffer((SELECT geog FROM geoiplocations WHERE locid = #{my_locid}), #{meters}))") - rel = rel.group("my_geo.geog, other_geo.geog") - rel = rel.order('st_distance(my_geo.geog, other_geo.geog)') + def relation_pagination(rel, params) + perpage = [(params[:per_page] || M_PER_PAGE).to_i, 100].min + page = [params[:page].to_i, 1].max + [rel.paginate(:page => page, :per_page => perpage), page] + end + + def new_musicians(usr, since_date) + # this attempts to find interesting musicians to tell another musician about where interesting + # is "has a good score and was created recently" + # we're sort of depending upon usr being a musicians_geocoded as well... + # this appears to only be called from EmailBatchNewMusician#deliver_batch_sets! which is + # an offline process and thus uses the last jam location as "home base" + + locidispid = usr.last_jam_locidispid + score_limit = 70 + limit = 50 + + rel = User.musicians_geocoded + .where(['users.created_at >= ? AND users.id != ?', since_date, usr.id]) + .joins('inner join current_scores on users.id = current_scores.a_userid') + .where(['current_scores.b_userid = ?', usr.id]) + .where(['current_scores.full_score <= ?', score_limit]) + .order('current_scores.full_score') # best scores first + .order('users.created_at DESC') # then most recent + .limit(limit) + + objs = rel.all.to_a + + if block_given? + yield(objs) if 0 < objs.count + else + return objs + end + end + + def band_filter(params={}, current_user=nil) + rel = Band.scoped + + unless (genre = params[:genre]).blank? + rel = Band.joins("RIGHT JOIN genre_players AS bgenres ON bgenres.player_id = bands.id AND bgenres.player_type = 'JamRuby::Band'") + .where(['bgenres.genre_id = ? AND bands.id IS NOT NULL', genre]) + end + + rel = GeoIpLocations.where_latlng(rel, params, current_user) + + sel_str = 'bands.*' + case ordering = self.order_param(params, B_ORDERING_KEYS) when :plays # FIXME: double counting? - # sel_str = "COUNT(records)+COUNT(sessions) AS play_count, #{sel_str}" - rel = rel.select('COUNT(records.id)+COUNT(sessions.id) AS search_play_count') - rel = rel.joins("LEFT JOIN music_sessions AS sessions ON sessions.user_id = users.id") - rel = rel.joins("LEFT JOIN recordings AS records ON records.owner_id = users.id") - rel = rel.order("search_play_count DESC") + sel_str = "COUNT(records)+COUNT(msh) AS play_count, #{sel_str}" + rel = rel.joins("LEFT JOIN music_sessions AS msh ON msh.band_id = bands.id") + .joins("LEFT JOIN recordings AS records ON records.band_id = bands.id") + .group("bands.id") + .order("play_count DESC, bands.created_at DESC") when :followed - rel = rel.joins('left outer join follows on follows.followable_id = users.id') - rel = rel.select('count(follows.user_id) as search_follow_count') - rel = rel.order('search_follow_count DESC') + sel_str = "COUNT(follows) AS search_follow_count, #{sel_str}" + rel = rel.joins("LEFT JOIN follows ON follows.followable_id = bands.id") + .group("bands.id") + .order("COUNT(follows) DESC, bands.created_at DESC") when :playing - rel = rel.joins("inner JOIN connections ON connections.user_id = users.id") - rel = rel.where(['connections.aasm_state != ?', 'expired']) + rel = rel.joins("LEFT JOIN music_sessions AS msh ON msh.band_id = bands.id") + .where('msh.music_session_id IS NOT NULL AND msh.session_removed_at IS NULL') + .order("bands.created_at DESC") + end + + rel = rel.select(sel_str) + rel, page = self.relation_pagination(rel, params) + rel = rel.includes([{ :users => :instruments }, :genres ]) + + objs = rel.all + srch = Search.new + srch.search_type = :band_filter + srch.page_num, srch.page_count = page, objs.total_pages + if 1 == page && current_user.bands.present? + current_user.bands.order('created_at DESC').each { |bb| objs.unshift(bb) } + end if current_user && current_user.is_a?(User) + srch.band_results_for_user(objs, current_user) end - if !locidispid.nil? && !user.nil? - rel = rel.order('current_scores.full_score ASC NULLS LAST') - end - - rel = rel.order('users.created_at DESC') - - rel, page = self.relation_pagination(rel, params) - rel = rel.includes([:instruments, :followings, :friends]) - - # XXX: DOES THIS MEAN ALL MATCHING USERS ARE RETURNED? - objs = rel.all - - srch = Search.new - srch.search_type = :musicians_filter - srch.page_num, srch.page_count = page, objs.total_pages - srch.musician_results_for_user(objs, user) end - def self.relation_pagination(rel, params) - perpage = [(params[:per_page] || M_PER_PAGE).to_i, 100].min - page = [params[:page].to_i, 1].max - [rel.paginate(:page => page, :per_page => perpage), page] - end + RESULT_FOLLOW = :follows RESULT_FRIEND = :friends @@ -399,77 +481,7 @@ module JamRuby false end - def self.new_musicians(usr, since_date) - # this attempts to find interesting musicians to tell another musician about where interesting - # is "has a good score and was created recently" - # we're sort of depending upon usr being a musicians_geocoded as well... - # this appears to only be called from EmailBatchNewMusician#deliver_batch_sets! which is - # an offline process and thus uses the last jam location as "home base" - - locidispid = usr.last_jam_locidispid - score_limit = 70 - limit = 50 - - rel = User.musicians_geocoded - .where(['users.created_at >= ? AND users.id != ?', since_date, usr.id]) - .joins('inner join current_scores on users.id = current_scores.a_userid') - .where(['current_scores.b_userid = ?', usr.id]) - .where(['current_scores.full_score <= ?', score_limit]) - .order('current_scores.full_score') # best scores first - .order('users.created_at DESC') # then most recent - .limit(limit) - - objs = rel.all.to_a - if block_given? - yield(objs) if 0 < objs.count - else - return objs - end - end - - def self.band_filter(params={}, current_user=nil) - rel = Band.scoped - - unless (genre = params[:genre]).blank? - rel = Band.joins("RIGHT JOIN genre_players AS bgenres ON bgenres.player_id = bands.id AND bgenres.player_type = 'JamRuby::Band'") - .where(['bgenres.genre_id = ? AND bands.id IS NOT NULL', genre]) - end - - rel = GeoIpLocations.where_latlng(rel, params, current_user) - - sel_str = 'bands.*' - case ordering = self.order_param(params, B_ORDERING_KEYS) - when :plays # FIXME: double counting? - sel_str = "COUNT(records)+COUNT(msh) AS play_count, #{sel_str}" - rel = rel.joins("LEFT JOIN music_sessions AS msh ON msh.band_id = bands.id") - .joins("LEFT JOIN recordings AS records ON records.band_id = bands.id") - .group("bands.id") - .order("play_count DESC, bands.created_at DESC") - when :followed - sel_str = "COUNT(follows) AS search_follow_count, #{sel_str}" - rel = rel.joins("LEFT JOIN follows ON follows.followable_id = bands.id") - .group("bands.id") - .order("COUNT(follows) DESC, bands.created_at DESC") - when :playing - rel = rel.joins("LEFT JOIN music_sessions AS msh ON msh.band_id = bands.id") - .where('msh.music_session_id IS NOT NULL AND msh.session_removed_at IS NULL') - .order("bands.created_at DESC") - end - - rel = rel.select(sel_str) - rel, page = self.relation_pagination(rel, params) - rel = rel.includes([{ :users => :instruments }, :genres ]) - - objs = rel.all - srch = Search.new - srch.search_type = :band_filter - srch.page_num, srch.page_count = page, objs.total_pages - if 1 == page && current_user.bands.present? - current_user.bands.order('created_at DESC').each { |bb| objs.unshift(bb) } - end if current_user && current_user.is_a?(User) - srch.band_results_for_user(objs, current_user) - end def band_results_for_user(_results, user) @results = _results