You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

180 lines
4.2 KiB

  1. # frozen_string_literal: true
  2. class AccountSearchService < BaseService
  3. attr_reader :query, :limit, :offset, :options, :account
  4. def call(query, account = nil, options = {})
  5. @acct_hint = query.start_with?('@')
  6. @query = query.strip.gsub(/\A@/, '')
  7. @limit = options[:limit].to_i
  8. @offset = options[:offset].to_i
  9. @options = options
  10. @account = account
  11. search_service_results.compact.uniq
  12. end
  13. private
  14. def search_service_results
  15. return [] if query.blank? || limit < 1
  16. [exact_match] + search_results
  17. end
  18. def exact_match
  19. return unless offset.zero? && username_complete?
  20. return @exact_match if defined?(@exact_match)
  21. @exact_match = begin
  22. if options[:resolve]
  23. ResolveAccountService.new.call(query)
  24. elsif domain_is_local?
  25. Account.find_local(query_username)
  26. else
  27. Account.find_remote(query_username, query_domain)
  28. end
  29. end
  30. end
  31. def search_results
  32. return [] if limit_for_non_exact_results.zero?
  33. @search_results ||= begin
  34. results = from_elasticsearch if Chewy.enabled?
  35. results ||= from_database
  36. results
  37. end
  38. end
  39. def from_database
  40. if account
  41. advanced_search_results
  42. else
  43. simple_search_results
  44. end
  45. end
  46. def advanced_search_results
  47. Account.advanced_search_for(terms_for_query, account, limit_for_non_exact_results, options[:following], offset)
  48. end
  49. def simple_search_results
  50. Account.search_for(terms_for_query, limit_for_non_exact_results, offset)
  51. end
  52. def from_elasticsearch
  53. 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' } }]
  54. should_clauses = []
  55. if account
  56. return [] if options[:following] && following_ids.empty?
  57. if options[:following]
  58. must_clauses << { terms: { id: following_ids } }
  59. elsif following_ids.any?
  60. should_clauses << { terms: { id: following_ids, boost: 100 } }
  61. end
  62. end
  63. query = { bool: { must: must_clauses, should: should_clauses } }
  64. functions = [reputation_score_function, followers_score_function, time_distance_function]
  65. records = AccountsIndex.query(function_score: { query: query, functions: functions, boost_mode: 'multiply', score_mode: 'avg' })
  66. .limit(limit_for_non_exact_results)
  67. .offset(offset)
  68. .objects
  69. .compact
  70. ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
  71. records
  72. rescue Faraday::ConnectionFailed, Parslet::ParseFailed
  73. nil
  74. end
  75. def reputation_score_function
  76. {
  77. script_score: {
  78. script: {
  79. source: "(doc['followers_count'].value + 0.0) / (doc['followers_count'].value + doc['following_count'].value + 1)",
  80. },
  81. },
  82. }
  83. end
  84. def followers_score_function
  85. {
  86. field_value_factor: {
  87. field: 'followers_count',
  88. modifier: 'log2p',
  89. missing: 0,
  90. },
  91. }
  92. end
  93. def time_distance_function
  94. {
  95. gauss: {
  96. last_status_at: {
  97. scale: '30d',
  98. offset: '30d',
  99. decay: 0.3,
  100. },
  101. },
  102. }
  103. end
  104. def following_ids
  105. @following_ids ||= account.active_relationships.pluck(:target_account_id)
  106. end
  107. def limit_for_non_exact_results
  108. if exact_match?
  109. limit - 1
  110. else
  111. limit
  112. end
  113. end
  114. def terms_for_query
  115. if domain_is_local?
  116. query_username
  117. else
  118. query
  119. end
  120. end
  121. def split_query_string
  122. @split_query_string ||= query.split('@')
  123. end
  124. def query_username
  125. @query_username ||= split_query_string.first || ''
  126. end
  127. def query_domain
  128. @query_domain ||= query_without_split? ? nil : split_query_string.last
  129. end
  130. def query_without_split?
  131. split_query_string.size == 1
  132. end
  133. def domain_is_local?
  134. @domain_is_local ||= TagManager.instance.local_domain?(query_domain)
  135. end
  136. def exact_match?
  137. exact_match.present?
  138. end
  139. def username_complete?
  140. query.include?('@') && "@#{query}" =~ Account::MENTION_RE
  141. end
  142. def likely_acct?
  143. @acct_hint || username_complete?
  144. end
  145. end