diff options
Diffstat (limited to 'app/services/account_search_service.rb')
-rw-r--r-- | app/services/account_search_service.rb | 189 |
1 files changed, 134 insertions, 55 deletions
diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb index 7bdffbbd2..d217dabb3 100644 --- a/app/services/account_search_service.rb +++ b/app/services/account_search_service.rb @@ -4,98 +4,177 @@ class AccountSearchService < BaseService attr_reader :query, :limit, :offset, :options, :account def call(query, account = nil, options = {}) - @query = query.strip - @limit = options[:limit].to_i - @offset = options[:offset].to_i - @options = options - @account = account + @acct_hint = query&.start_with?('@') + @query = query&.strip&.gsub(/\A@/, '') + @limit = options[:limit].to_i + @offset = options[:offset].to_i + @options = options + @account = account - search_service_results + search_service_results.compact.uniq end private def search_service_results - return [] if query_blank_or_hashtag? || limit < 1 + return [] if query.blank? || limit < 1 - if resolving_non_matching_remote_account? - [ResolveAccountService.new.call("#{query_username}@#{query_domain}")].compact - else - search_results_and_exact_match.compact.uniq.slice(0, limit) - end + [exact_match] + search_results end - def resolving_non_matching_remote_account? - options[:resolve] && !exact_match && !domain_is_local? - end + def exact_match + return unless offset.zero? && username_complete? - def search_results_and_exact_match - exact = [exact_match] - return exact if !exact[0].nil? && limit == 1 - exact + search_results.to_a - end + return @exact_match if defined?(@exact_match) - def query_blank_or_hashtag? - query.blank? || query.start_with?('#') + @exact_match = begin + if options[:resolve] + ResolveAccountService.new.call(query) + elsif domain_is_local? + Account.find_local(query_username) + else + Account.find_remote(query_username, query_domain) + end + end end - def split_query_string - @_split_query_string ||= query.gsub(/\A@/, '').split('@') - end + def search_results + return [] if limit_for_non_exact_results.zero? - def query_username - @_query_username ||= split_query_string.first || '' + @search_results ||= begin + results = from_elasticsearch if Chewy.enabled? + results ||= from_database + results + end end - def query_domain - @_query_domain ||= query_without_split? ? nil : split_query_string.last + def from_database + if account + advanced_search_results + else + simple_search_results + end end - def query_without_split? - split_query_string.size == 1 + def advanced_search_results + Account.advanced_search_for(terms_for_query, account, limit_for_non_exact_results, options[:following], offset) end - def domain_is_local? - @_domain_is_local ||= TagManager.instance.local_domain?(query_domain) + def simple_search_results + Account.search_for(terms_for_query, limit_for_non_exact_results, offset) end - def search_from - options[:following] && account ? account.following : Account - end + def from_elasticsearch + must_clauses = [{ multi_match: { query: terms_for_query, fields: likely_acct? ? %w(acct.edge_ngram acct) : %w(acct.edge_ngram acct display_name.edge_ngram display_name), type: 'most_fields', operator: 'and' } }] + should_clauses = [] - def exact_match - @_exact_match ||= begin - if domain_is_local? - search_from.without_suspended.find_local(query_username) - else - search_from.without_suspended.find_remote(query_username, query_domain) + if account + return [] if options[:following] && following_ids.empty? + + if options[:following] + must_clauses << { terms: { id: following_ids } } + elsif following_ids.any? + should_clauses << { terms: { id: following_ids, boost: 100 } } end end + + query = { bool: { must: must_clauses, should: should_clauses } } + functions = [reputation_score_function, followers_score_function, time_distance_function] + + records = AccountsIndex.query(function_score: { query: query, functions: functions, boost_mode: 'multiply', score_mode: 'avg' }) + .limit(limit_for_non_exact_results) + .offset(offset) + .objects + .compact + + ActiveRecord::Associations::Preloader.new.preload(records, :account_stat) + + records + rescue Faraday::ConnectionFailed, Parslet::ParseFailed + nil end - def search_results - @_search_results ||= begin - if account - advanced_search_results - else - simple_search_results - end - end + def reputation_score_function + { + script_score: { + script: { + source: "(doc['followers_count'].value + 0.0) / (doc['followers_count'].value + doc['following_count'].value + 1)", + }, + }, + } end - def advanced_search_results - Account.advanced_search_for(terms_for_query, account, limit, options[:following], offset) + def followers_score_function + { + field_value_factor: { + field: 'followers_count', + modifier: 'log2p', + missing: 0, + }, + } end - def simple_search_results - Account.search_for(terms_for_query, limit, offset) + def time_distance_function + { + gauss: { + last_status_at: { + scale: '30d', + offset: '30d', + decay: 0.3, + }, + }, + } + end + + def following_ids + @following_ids ||= account.active_relationships.pluck(:target_account_id) + [account.id] + end + + def limit_for_non_exact_results + if exact_match? + limit - 1 + else + limit + end end def terms_for_query if domain_is_local? query_username else - "#{query_username} #{query_domain}" + query end end + + def split_query_string + @split_query_string ||= query.split('@') + end + + def query_username + @query_username ||= split_query_string.first || '' + end + + def query_domain + @query_domain ||= query_without_split? ? nil : split_query_string.last + end + + def query_without_split? + split_query_string.size == 1 + end + + def domain_is_local? + @domain_is_local ||= TagManager.instance.local_domain?(query_domain) + end + + def exact_match? + exact_match.present? + end + + def username_complete? + query.include?('@') && "@#{query}" =~ Account::MENTION_RE + end + + def likely_acct? + @acct_hint || username_complete? + end end |