Files
discourse/app/models/reviewable_score.rb
Alan Guo Xiang Tan 684569c30e PERF: Avoid slow preloading in SiteSerializer (#33283)
`SiteSerializer#post_action_types` and `SiteSerializer#topic_flag_types`
both call the `Flag.used_flag_ids` method which executes queries that
becomes very slow on sites with alot of records in either the
`post_actions` or `reviewable_scores` table.

On the `post_actions` table, we execute `SELECT DISTINCT
post_action_type_id FROM post_actions`. On the `reviewable_scores`
table, we execute `SELECT DISTINCT reviewable_scope_type FROM
reviewable_scopes`. The problem with both queries is that it requires
the PG planner to scan through every single row in those tables.

For our use case, all we actually need is to check if a flag is being
referenced by a record in either the `post_actions` or
`reviewable_scores` tables. This commit updates the `Flag.used_flag_ids`
to accept a `flag_ids`
argument and use the argument to check whether the flag ids are
referenced in the `post_action_type_id` or `reviewable_scope_type`
foreign keys.
2025-06-25 15:03:34 +08:00

128 lines
3.9 KiB
Ruby

# frozen_string_literal: true
class ReviewableScore < ActiveRecord::Base
belongs_to :reviewable
belongs_to :user
belongs_to :reviewed_by, class_name: "User"
belongs_to :meta_topic, class_name: "Topic"
enum :status, { pending: 0, agreed: 1, disagreed: 2, ignored: 3 }
# To keep things simple the types correspond to `PostActionType` for backwards
# compatibility, but we can add extra reasons for scores.
def self.types
PostActionType.flag_types.merge(PostActionType.score_types).merge(@api_types || {})
end
def self.type_title(type)
I18n.t("post_action_types.#{type}.title", default: nil) ||
I18n.t("reviewable_score_types.#{type}.title", default: nil) ||
PostActionType.names[types[type]]
end
# When extending post action flags, we need to call this method in order to
# get the latests flags.
def self.reload_types
@api_types = nil
types
end
def self.add_new_types(type_names)
@api_types ||= {}
next_id = types.values.max + 1
type_names.each_with_index { |name, idx| @api_types[name] = next_id + idx }
end
def self.score_transitions
{ approved: statuses[:agreed], rejected: statuses[:disagreed], ignored: statuses[:ignored] }
end
def score_type
Reviewable::Collection::Item.new(reviewable_score_type)
end
def took_action?
take_action_bonus > 0
end
def self.calculate_score(user, type_bonus, take_action_bonus)
score = user_flag_score(user) + type_bonus + take_action_bonus
score > 0 ? score : 0
end
# A user's flag score is:
# 1.0 + trust_level + user_accuracy_bonus
# (trust_level is 5 for staff)
def self.user_flag_score(user)
1.0 + (user.staff? ? 5.0 : user.trust_level.to_f) + user_accuracy_bonus(user)
end
# A user's accuracy bonus is:
# if 5 or less flags => 0.0
# if > 5 flags => (agreed flags / total flags) * 5.0
def self.user_accuracy_bonus(user)
user_stat = user&.user_stat
return 0.0 if user_stat.blank? || user.bot?
calc_user_accuracy_bonus(user_stat.flags_agreed, user_stat.flags_disagreed)
end
def self.calc_user_accuracy_bonus(agreed, disagreed)
agreed ||= 0
disagreed ||= 0
total = (agreed + disagreed).to_f
return 0.0 if total <= 5
accuracy_axis = 0.7
percent_correct = agreed / total
positive_accuracy = percent_correct >= accuracy_axis
bottom = positive_accuracy ? accuracy_axis : 0.0
top = positive_accuracy ? 1.0 : accuracy_axis
absolute_distance = positive_accuracy ? percent_correct - bottom : top - percent_correct
axis_distance_multiplier = 1.0 / (top - bottom)
positivity_multiplier = positive_accuracy ? 1.0 : -1.0
(
absolute_distance * axis_distance_multiplier * positivity_multiplier *
(Math.log(total, 4) * 5.0)
).round(2)
end
def reviewable_conversation
return if meta_topic.blank?
Reviewable::Conversation.new(meta_topic)
end
end
# == Schema Information
#
# Table name: reviewable_scores
#
# id :bigint not null, primary key
# reviewable_id :integer not null
# user_id :integer not null
# reviewable_score_type :integer not null
# status :integer not null
# score :float default(0.0), not null
# take_action_bonus :float default(0.0), not null
# reviewed_by_id :integer
# reviewed_at :datetime
# meta_topic_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# reason :string
# user_accuracy_bonus :float default(0.0), not null
# context :string
#
# Indexes
#
# index_reviewable_scores_on_reviewable_id (reviewable_id)
# index_reviewable_scores_on_reviewable_score_type (reviewable_score_type)
# index_reviewable_scores_on_user_id (user_id)
#