account_search_service.rb 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. # frozen_string_literal: true
  2. class AccountSearchService < BaseService
  3. attr_reader :query, :limit, :offset, :options, :account
  4. MENTION_ONLY_RE = /\A#{Account::MENTION_RE}\z/i
  5. # Min. number of characters to look for non-exact matches
  6. MIN_QUERY_LENGTH = 5
  7. class QueryBuilder
  8. def initialize(query, account, options = {})
  9. @query = query
  10. @account = account
  11. @options = options
  12. end
  13. def build
  14. AccountsIndex.query(
  15. bool: {
  16. must: {
  17. function_score: {
  18. query: {
  19. bool: {
  20. must: must_clauses,
  21. },
  22. },
  23. functions: [
  24. reputation_score_function,
  25. followers_score_function,
  26. time_distance_function,
  27. ],
  28. },
  29. },
  30. should: should_clauses,
  31. }
  32. )
  33. end
  34. private
  35. def must_clauses
  36. if @account && @options[:following]
  37. [core_query, only_following_query]
  38. else
  39. [core_query]
  40. end
  41. end
  42. def should_clauses
  43. if @account && !@options[:following]
  44. [boost_following_query]
  45. else
  46. []
  47. end
  48. end
  49. # This function limits results to only the accounts the user is following
  50. def only_following_query
  51. {
  52. terms: {
  53. id: following_ids,
  54. },
  55. }
  56. end
  57. # This function promotes accounts the user is following
  58. def boost_following_query
  59. {
  60. terms: {
  61. id: following_ids,
  62. boost: 100,
  63. },
  64. }
  65. end
  66. # This function deranks accounts that follow more people than follow them
  67. def reputation_score_function
  68. {
  69. script_score: {
  70. script: {
  71. source: "(Math.max(doc['followers_count'].value, 0) + 0.0) / (Math.max(doc['followers_count'].value, 0) + Math.max(doc['following_count'].value, 0) + 1)",
  72. },
  73. },
  74. }
  75. end
  76. # This function promotes accounts that have more followers
  77. def followers_score_function
  78. {
  79. script_score: {
  80. script: {
  81. source: "(Math.max(doc['followers_count'].value, 0) / (Math.max(doc['followers_count'].value, 0) + 1))",
  82. },
  83. },
  84. }
  85. end
  86. # This function deranks accounts that haven't posted in a long time
  87. def time_distance_function
  88. {
  89. gauss: {
  90. last_status_at: {
  91. scale: '30d',
  92. offset: '30d',
  93. decay: 0.3,
  94. },
  95. },
  96. }
  97. end
  98. def following_ids
  99. @following_ids ||= @account.active_relationships.pluck(:target_account_id) + [@account.id]
  100. end
  101. end
  102. class AutocompleteQueryBuilder < QueryBuilder
  103. private
  104. def core_query
  105. {
  106. multi_match: {
  107. query: @query,
  108. type: 'bool_prefix',
  109. fields: %w(username^2 username.*^2 display_name display_name.*),
  110. },
  111. }
  112. end
  113. end
  114. class FullQueryBuilder < QueryBuilder
  115. private
  116. def core_query
  117. {
  118. multi_match: {
  119. query: @query,
  120. type: 'most_fields',
  121. fields: %w(username^2 display_name^2 text text.*),
  122. operator: 'and',
  123. },
  124. }
  125. end
  126. end
  127. def call(query, account = nil, options = {})
  128. @query = query&.strip&.gsub(/\A@/, '')
  129. @limit = options[:limit].to_i
  130. @offset = options[:offset].to_i
  131. @options = options
  132. @account = account
  133. search_service_results.compact.uniq
  134. end
  135. private
  136. def search_service_results
  137. return [] if query.blank? || limit < 1
  138. [exact_match] + search_results
  139. end
  140. def exact_match
  141. return unless offset.zero? && username_complete?
  142. return @exact_match if defined?(@exact_match)
  143. match = if options[:resolve]
  144. ResolveAccountService.new.call(query)
  145. elsif domain_is_local?
  146. Account.find_local(query_username)
  147. else
  148. Account.find_remote(query_username, query_domain)
  149. end
  150. match = nil if !match.nil? && !account.nil? && options[:following] && !account.following?(match)
  151. @exact_match = match
  152. end
  153. def search_results
  154. return [] if limit_for_non_exact_results.zero?
  155. @search_results ||= begin
  156. results = from_elasticsearch if Chewy.enabled?
  157. results ||= from_database
  158. results
  159. end
  160. end
  161. def from_database
  162. if account
  163. advanced_search_results
  164. else
  165. simple_search_results
  166. end
  167. end
  168. def advanced_search_results
  169. Account.advanced_search_for(terms_for_query, account, limit: limit_for_non_exact_results, following: options[:following], offset: offset)
  170. end
  171. def simple_search_results
  172. Account.search_for(terms_for_query, limit: limit_for_non_exact_results, offset: offset)
  173. end
  174. def from_elasticsearch
  175. query_builder = begin
  176. if options[:use_searchable_text]
  177. FullQueryBuilder.new(terms_for_query, account, options.slice(:following))
  178. else
  179. AutocompleteQueryBuilder.new(terms_for_query, account, options.slice(:following))
  180. end
  181. end
  182. records = query_builder.build.limit(limit_for_non_exact_results).offset(offset).objects.compact
  183. ActiveRecord::Associations::Preloader.new(records: records, associations: [:account_stat, { user: :role }]).call
  184. records
  185. rescue Faraday::ConnectionFailed, Parslet::ParseFailed
  186. nil
  187. end
  188. def limit_for_non_exact_results
  189. return 0 if @account.nil? && query.size < MIN_QUERY_LENGTH
  190. if exact_match?
  191. limit - 1
  192. else
  193. limit
  194. end
  195. end
  196. def terms_for_query
  197. if domain_is_local?
  198. query_username
  199. else
  200. query
  201. end
  202. end
  203. def split_query_string
  204. @split_query_string ||= query.split('@')
  205. end
  206. def query_username
  207. @query_username ||= split_query_string.first || ''
  208. end
  209. def query_domain
  210. @query_domain ||= query_without_split? ? nil : split_query_string.last
  211. end
  212. def query_without_split?
  213. split_query_string.size == 1
  214. end
  215. def domain_is_local?
  216. @domain_is_local ||= TagManager.instance.local_domain?(query_domain)
  217. end
  218. def exact_match?
  219. exact_match.present?
  220. end
  221. def username_complete?
  222. query.include?('@') && "@#{query}".match?(MENTION_ONLY_RE)
  223. end
  224. end