Add cold-start follow recommendations (#15945)

This commit is contained in:
Eugen Rochko
2021-04-12 12:37:14 +02:00
committed by GitHub
parent ad61265268
commit f7117646af
32 changed files with 560 additions and 26 deletions

View File

@ -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

View 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

View 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

View File

@ -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

View 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

View 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

View 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

View File

@ -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