# frozen_string_literal: true
class AccountSearchService < BaseService
- attr_reader :query, :limit, :resolve, :account
+ attr_reader :query, :limit, :offset, :options, :account
- def call(query, limit, resolve = false, account = nil)
- @query = query
- @limit = limit
- @resolve = resolve
- @account = account
+ def call(query, account = nil, options = {})
+ @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?
- [FollowRemoteAccountService.new.call("#{query_username}@#{query_domain}")]
- else
- search_results_and_exact_match.compact.uniq.slice(0, limit)
- end
+ [exact_match] + search_results
end
- def resolving_non_matching_remote_account?
- 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
+ return @exact_match if defined?(@exact_match)
+
+ @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 query_blank_or_hashtag?
- query.blank? || query.start_with?('#')
+ def search_results
+ return [] if limit_for_non_exact_results.zero?
+
+ @search_results ||= begin
+ results = from_elasticsearch if Chewy.enabled?
+ results ||= from_database
+ results
+ end
end
- def split_query_string
- @_split_query_string ||= query.gsub(/\A@/, '').split('@')
+ def from_database
+ if account
+ advanced_search_results
+ else
+ simple_search_results
+ end
end
- def query_username
- @_query_username ||= split_query_string.first || ''
+ def advanced_search_results
+ Account.advanced_search_for(terms_for_query, account, limit_for_non_exact_results, options[:following], offset)
end
- def query_domain
- @_query_domain ||= query_without_split? ? nil : split_query_string.last
+ def simple_search_results
+ Account.search_for(terms_for_query, limit_for_non_exact_results, offset)
end
- def query_without_split?
- split_query_string.size == 1
+ 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 = []
+
+ 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 domain_is_local?
- @_domain_is_local ||= TagManager.instance.local_domain?(query_domain)
+ 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 exact_match
- @_exact_match ||= Account.find_remote(query_username, query_domain)
+ def followers_score_function
+ {
+ field_value_factor: {
+ field: 'followers_count',
+ modifier: 'log2p',
+ missing: 0,
+ },
+ }
end
- def search_results
- @_search_results ||= begin
- if account
- advanced_search_results
- else
- simple_search_results
- end
- end
+ def time_distance_function
+ {
+ gauss: {
+ last_status_at: {
+ scale: '30d',
+ offset: '30d',
+ decay: 0.3,
+ },
+ },
+ }
end
- def advanced_search_results
- Account.advanced_search_for(terms_for_query, account, limit)
+ def following_ids
+ @following_ids ||= account.active_relationships.pluck(:target_account_id)
end
- def simple_search_results
- Account.search_for(terms_for_query, limit)
+ 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