statuses.rb 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. # frozen_string_literal: true
  2. class Trends::Statuses < Trends::Base
  3. PREFIX = 'trending_statuses'
  4. self.default_options = {
  5. threshold: 5,
  6. review_threshold: 3,
  7. score_halflife: 2.hours.freeze,
  8. decay_threshold: 0.3,
  9. }
  10. class Query < Trends::Query
  11. def filtered_for!(account)
  12. @account = account
  13. self
  14. end
  15. def filtered_for(account)
  16. clone.filtered_for!(account)
  17. end
  18. def to_arel
  19. scope = Status.joins(:trend).reorder(score: :desc)
  20. scope = scope.reorder(language_order_clause.desc, score: :desc) if preferred_languages.present?
  21. scope = scope.merge(StatusTrend.allowed) if @allowed
  22. scope = scope.not_excluded_by_account(@account).not_domain_blocked_by_account(@account) if @account.present?
  23. scope = scope.offset(@offset) if @offset.present?
  24. scope = scope.limit(@limit) if @limit.present?
  25. scope
  26. end
  27. private
  28. def language_order_clause
  29. Arel::Nodes::Case.new.when(StatusTrend.arel_table[:language].in(preferred_languages)).then(1).else(0)
  30. end
  31. def preferred_languages
  32. if @account&.chosen_languages.present?
  33. @account.chosen_languages
  34. else
  35. @locale
  36. end
  37. end
  38. end
  39. def register(status, at_time = Time.now.utc)
  40. add(status.proper, status.account_id, at_time) if eligible?(status.proper)
  41. end
  42. def add(status, _account_id, at_time = Time.now.utc)
  43. record_used_id(status.id, at_time)
  44. end
  45. def query
  46. Query.new(key_prefix, klass)
  47. end
  48. def refresh(at_time = Time.now.utc)
  49. statuses = Status.where(id: (recently_used_ids(at_time) + StatusTrend.pluck(:status_id)).uniq).includes(:status_stat, :account)
  50. calculate_scores(statuses, at_time)
  51. end
  52. def request_review
  53. StatusTrend.pluck('distinct language').flat_map do |language|
  54. score_at_threshold = StatusTrend.where(language: language, allowed: true).order(rank: :desc).where('rank <= ?', options[:review_threshold]).first&.score || 0
  55. status_trends = StatusTrend.where(language: language, allowed: false).joins(:status).includes(status: :account)
  56. status_trends.filter_map do |trend|
  57. status = trend.status
  58. if trend.score > score_at_threshold && !status.trendable? && status.requires_review_notification?
  59. status.account.touch(:requested_review_at)
  60. status
  61. end
  62. end
  63. end
  64. end
  65. protected
  66. def key_prefix
  67. PREFIX
  68. end
  69. def klass
  70. Status
  71. end
  72. private
  73. def eligible?(status)
  74. status.public_visibility? && status.account.discoverable? && !status.account.silenced? && status.spoiler_text.blank? && !status.sensitive? && !status.reply? && valid_locale?(status.language)
  75. end
  76. def calculate_scores(statuses, at_time)
  77. items = statuses.map do |status|
  78. expected = 1.0
  79. observed = (status.reblogs_count + status.favourites_count).to_f
  80. score = begin
  81. if expected > observed || observed < options[:threshold]
  82. 0
  83. else
  84. ((observed - expected)**2) / expected
  85. end
  86. end
  87. decaying_score = begin
  88. if score.zero? || !eligible?(status)
  89. 0
  90. else
  91. score * (0.5**((at_time.to_f - status.created_at.to_f) / options[:score_halflife].to_f))
  92. end
  93. end
  94. [decaying_score, status]
  95. end
  96. to_insert = items.filter { |(score, _)| score >= options[:decay_threshold] }
  97. to_delete = items.filter { |(score, _)| score < options[:decay_threshold] }
  98. StatusTrend.transaction do
  99. StatusTrend.upsert_all(to_insert.map { |(score, status)| { status_id: status.id, account_id: status.account_id, score: score, language: status.language, allowed: status.trendable? || false } }, unique_by: :status_id) if to_insert.any?
  100. StatusTrend.where(status_id: to_delete.map { |(_, status)| status.id }).delete_all if to_delete.any?
  101. StatusTrend.connection.exec_update('UPDATE status_trends SET rank = t0.calculated_rank FROM (SELECT id, row_number() OVER w AS calculated_rank FROM status_trends WINDOW w AS (PARTITION BY language ORDER BY score DESC)) t0 WHERE status_trends.id = t0.id')
  102. end
  103. end
  104. end