Add cold-start follow recommendations (#15945)
This commit is contained in:
@ -110,6 +110,7 @@ class Account < ApplicationRecord
|
||||
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||
scope :searchable, -> { without_suspended.where(moved_to_account_id: nil) }
|
||||
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) }
|
||||
scope :followable_by, ->(account) { joins(arel_table.join(Follow.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(Follow.arel_table[:target_account_id]).and(Follow.arel_table[:account_id].eq(account.id))).join_sources).where(Follow.arel_table[:id].eq(nil)).joins(arel_table.join(FollowRequest.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(FollowRequest.arel_table[:target_account_id]).and(FollowRequest.arel_table[:account_id].eq(account.id))).join_sources).where(FollowRequest.arel_table[:id].eq(nil)) }
|
||||
scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) }
|
||||
scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) }
|
||||
scope :by_recent_sign_in, -> { order(Arel.sql('(case when users.current_sign_in_at is null then 1 else 0 end) asc, users.current_sign_in_at desc, accounts.id desc')) }
|
||||
@ -363,7 +364,7 @@ class Account < ApplicationRecord
|
||||
end
|
||||
|
||||
def excluded_from_timeline_account_ids
|
||||
Rails.cache.fetch("exclude_account_ids_for:#{id}") { blocking.pluck(:target_account_id) + blocked_by.pluck(:account_id) + muting.pluck(:target_account_id) }
|
||||
Rails.cache.fetch("exclude_account_ids_for:#{id}") { block_relationships.pluck(:target_account_id) + blocked_by_relationships.pluck(:account_id) + mute_relationships.pluck(:target_account_id) }
|
||||
end
|
||||
|
||||
def excluded_from_timeline_domains
|
||||
|
17
app/models/account_suggestions.rb
Normal file
17
app/models/account_suggestions.rb
Normal file
@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AccountSuggestions
|
||||
class Suggestion < ActiveModelSerializers::Model
|
||||
attributes :account, :source
|
||||
end
|
||||
|
||||
def self.get(account, limit)
|
||||
suggestions = PotentialFriendshipTracker.get(account, limit).map { |target_account| Suggestion.new(account: target_account, source: :past_interaction) }
|
||||
suggestions.concat(FollowRecommendation.get(account, limit - suggestions.size, suggestions.map { |suggestion| suggestion.account.id }).map { |target_account| Suggestion.new(account: target_account, source: :global) }) if suggestions.size < limit
|
||||
suggestions
|
||||
end
|
||||
|
||||
def self.remove(account, target_account_id)
|
||||
PotentialFriendshipTracker.remove(account.id, target_account_id)
|
||||
end
|
||||
end
|
25
app/models/account_summary.rb
Normal file
25
app/models/account_summary.rb
Normal file
@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: account_summaries
|
||||
#
|
||||
# account_id :bigint(8) primary key
|
||||
# language :string
|
||||
# sensitive :boolean
|
||||
#
|
||||
|
||||
class AccountSummary < ApplicationRecord
|
||||
self.primary_key = :account_id
|
||||
|
||||
scope :safe, -> { where(sensitive: false) }
|
||||
scope :localized, ->(locale) { where(language: locale) }
|
||||
scope :filtered, -> { joins(arel_table.join(FollowRecommendationSuppression.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:account_id].eq(FollowRecommendationSuppression.arel_table[:account_id])).join_sources).where(FollowRecommendationSuppression.arel_table[:id].eq(nil)) }
|
||||
|
||||
def self.refresh
|
||||
Scenic.database.refresh_materialized_view(table_name, concurrently: true, cascade: false)
|
||||
end
|
||||
|
||||
def readonly?
|
||||
true
|
||||
end
|
||||
end
|
@ -63,5 +63,8 @@ module AccountAssociations
|
||||
|
||||
# Account deletion requests
|
||||
has_one :deletion_request, class_name: 'AccountDeletionRequest', inverse_of: :account, dependent: :destroy
|
||||
|
||||
# Follow recommendations
|
||||
has_one :follow_recommendation_suppression, inverse_of: :account, dependent: :destroy
|
||||
end
|
||||
end
|
||||
|
39
app/models/follow_recommendation.rb
Normal file
39
app/models/follow_recommendation.rb
Normal file
@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: follow_recommendations
|
||||
#
|
||||
# account_id :bigint(8) primary key
|
||||
# rank :decimal(, )
|
||||
# reason :text is an Array
|
||||
#
|
||||
|
||||
class FollowRecommendation < ApplicationRecord
|
||||
self.primary_key = :account_id
|
||||
|
||||
belongs_to :account_summary, foreign_key: :account_id
|
||||
belongs_to :account, foreign_key: :account_id
|
||||
|
||||
scope :safe, -> { joins(:account_summary).merge(AccountSummary.safe) }
|
||||
scope :localized, ->(locale) { joins(:account_summary).merge(AccountSummary.localized(locale)) }
|
||||
scope :filtered, -> { joins(:account_summary).merge(AccountSummary.filtered) }
|
||||
|
||||
def readonly?
|
||||
true
|
||||
end
|
||||
|
||||
def self.get(account, limit, exclude_account_ids = [])
|
||||
account_ids = Redis.current.zrevrange("follow_recommendations:#{account.user_locale}", 0, -1).map(&:to_i) - exclude_account_ids - [account.id]
|
||||
|
||||
return [] if account_ids.empty? || limit < 1
|
||||
|
||||
accounts = Account.followable_by(account)
|
||||
.not_excluded_by_account(account)
|
||||
.not_domain_blocked_by_account(account)
|
||||
.where(id: account_ids)
|
||||
.limit(limit)
|
||||
.index_by(&:id)
|
||||
|
||||
account_ids.map { |id| accounts[id] }.compact
|
||||
end
|
||||
end
|
26
app/models/follow_recommendation_filter.rb
Normal file
26
app/models/follow_recommendation_filter.rb
Normal file
@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class FollowRecommendationFilter
|
||||
KEYS = %i(
|
||||
language
|
||||
status
|
||||
).freeze
|
||||
|
||||
attr_reader :params, :language
|
||||
|
||||
def initialize(params)
|
||||
@language = params.delete('language') || I18n.locale
|
||||
@params = params
|
||||
end
|
||||
|
||||
def results
|
||||
if params['status'] == 'suppressed'
|
||||
Account.joins(:follow_recommendation_suppression).order(FollowRecommendationSuppression.arel_table[:id].desc).to_a
|
||||
else
|
||||
account_ids = Redis.current.zrevrange("follow_recommendations:#{@language}", 0, -1).map(&:to_i)
|
||||
accounts = Account.where(id: account_ids).index_by(&:id)
|
||||
|
||||
account_ids.map { |id| accounts[id] }.compact
|
||||
end
|
||||
end
|
||||
end
|
28
app/models/follow_recommendation_suppression.rb
Normal file
28
app/models/follow_recommendation_suppression.rb
Normal file
@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: follow_recommendation_suppressions
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8) not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class FollowRecommendationSuppression < ApplicationRecord
|
||||
include Redisable
|
||||
|
||||
belongs_to :account
|
||||
|
||||
after_commit :remove_follow_recommendations, on: :create
|
||||
|
||||
private
|
||||
|
||||
def remove_follow_recommendations
|
||||
redis.pipelined do
|
||||
I18n.available_locales.each do |locale|
|
||||
redis.zrem("follow_recommendations:#{locale}", account_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -21,6 +21,10 @@ class Form::AccountBatch
|
||||
approve!
|
||||
when 'reject'
|
||||
reject!
|
||||
when 'suppress_follow_recommendation'
|
||||
suppress_follow_recommendation!
|
||||
when 'unsuppress_follow_recommendation'
|
||||
unsuppress_follow_recommendation!
|
||||
end
|
||||
end
|
||||
|
||||
@ -79,4 +83,18 @@ class Form::AccountBatch
|
||||
records.each { |account| authorize(account.user, :reject?) }
|
||||
.each { |account| DeleteAccountService.new.call(account, reserve_email: false, reserve_username: false) }
|
||||
end
|
||||
|
||||
def suppress_follow_recommendation!
|
||||
authorize(:follow_recommendation, :suppress?)
|
||||
|
||||
accounts.each do |account|
|
||||
FollowRecommendationSuppression.create(account: account)
|
||||
end
|
||||
end
|
||||
|
||||
def unsuppress_follow_recommendation!
|
||||
authorize(:follow_recommendation, :unsuppress?)
|
||||
|
||||
FollowRecommendationSuppression.where(account_id: account_ids).destroy_all
|
||||
end
|
||||
end
|
||||
|
Reference in New Issue
Block a user