mirror of
https://github.com/discourse/discourse.git
synced 2024-12-24 01:43:45 +08:00
fc2093fc7e
We're expecting the period param to be something that neatly coerces into a symbol. If we receive something like a nested parameter, this will blow up. This commit raises an InvalidParameters exception in the case of a non-stringy period parameter.
310 lines
11 KiB
Ruby
310 lines
11 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class TopTopic < ActiveRecord::Base
|
|
belongs_to :topic
|
|
|
|
# The top topics we want to refresh often
|
|
def self.refresh_daily!
|
|
DistributedMutex.synchronize("update_top_topics", validity: 5.minutes) do
|
|
transaction do
|
|
remove_invisible_topics
|
|
add_new_visible_topics
|
|
|
|
update_counts_and_compute_scores_for(:daily)
|
|
end
|
|
end
|
|
end
|
|
|
|
# We don't have to refresh these as often
|
|
def self.refresh_older!
|
|
DistributedMutex.synchronize("update_top_topics", validity: 5.minutes) do
|
|
older_periods = periods - %i[daily all]
|
|
|
|
transaction { older_periods.each { |period| update_counts_and_compute_scores_for(period) } }
|
|
|
|
compute_top_score_for(:all)
|
|
end
|
|
end
|
|
|
|
def self.refresh!
|
|
refresh_daily!
|
|
refresh_older!
|
|
end
|
|
|
|
def self.periods
|
|
@@periods ||= %i[all yearly quarterly monthly weekly daily].freeze
|
|
end
|
|
|
|
def self.sorted_periods
|
|
ascending_periods ||= Enum.new(daily: 1, weekly: 2, monthly: 3, quarterly: 4, yearly: 5, all: 6)
|
|
end
|
|
|
|
def self.score_column_for_period(period)
|
|
TopTopic.validate_period(period)
|
|
"#{period}_score"
|
|
end
|
|
|
|
def self.validate_period(period)
|
|
@invalid_period_error ||=
|
|
Discourse::InvalidParameters.new("Invalid period. Valid periods are #{periods.join(", ")}")
|
|
|
|
raise @invalid_period_error if period.blank? || !periods.include?(period.to_sym)
|
|
rescue NoMethodError
|
|
raise @invalid_period_error
|
|
end
|
|
|
|
private
|
|
|
|
def self.sort_orders
|
|
@@sort_orders ||= %i[posts views likes op_likes].freeze
|
|
end
|
|
|
|
def self.update_counts_and_compute_scores_for(period)
|
|
sort_orders.each { |sort| TopTopic.public_send("update_#{sort}_count_for", period) }
|
|
compute_top_score_for(period)
|
|
end
|
|
|
|
def self.remove_invisible_topics
|
|
DB.exec(
|
|
"WITH category_definition_topic_ids AS (
|
|
SELECT COALESCE(topic_id, 0) AS id FROM categories
|
|
), invisible_topic_ids AS (
|
|
SELECT id
|
|
FROM topics
|
|
WHERE deleted_at IS NOT NULL
|
|
OR NOT visible
|
|
OR archetype = :private_message
|
|
OR archived
|
|
OR id IN (SELECT id FROM category_definition_topic_ids)
|
|
)
|
|
DELETE FROM top_topics
|
|
WHERE topic_id IN (SELECT id FROM invisible_topic_ids)",
|
|
private_message: Archetype.private_message,
|
|
)
|
|
end
|
|
|
|
def self.add_new_visible_topics
|
|
DB.exec(
|
|
"WITH category_definition_topic_ids AS (
|
|
SELECT COALESCE(topic_id, 0) AS id FROM categories
|
|
), visible_topics AS (
|
|
SELECT t.id
|
|
FROM topics t
|
|
LEFT JOIN top_topics tt ON t.id = tt.topic_id
|
|
WHERE t.deleted_at IS NULL
|
|
AND t.visible
|
|
AND t.archetype <> :private_message
|
|
AND NOT t.archived
|
|
AND t.id NOT IN (SELECT id FROM category_definition_topic_ids)
|
|
AND tt.topic_id IS NULL
|
|
)
|
|
INSERT INTO top_topics (topic_id)
|
|
SELECT id FROM visible_topics",
|
|
private_message: Archetype.private_message,
|
|
)
|
|
end
|
|
|
|
def self.update_posts_count_for(period)
|
|
sql =
|
|
"SELECT topic_id, GREATEST(COUNT(*), 1) AS count
|
|
FROM posts
|
|
WHERE created_at >= :from
|
|
AND deleted_at IS NULL
|
|
AND NOT hidden
|
|
AND post_type = #{Post.types[:regular]}
|
|
AND user_id <> #{Discourse.system_user.id}
|
|
GROUP BY topic_id"
|
|
|
|
update_top_topics(period, "posts", sql)
|
|
end
|
|
|
|
def self.update_views_count_for(period)
|
|
sql =
|
|
"SELECT topic_id, COUNT(*) AS count
|
|
FROM topic_views
|
|
WHERE viewed_at >= :from
|
|
GROUP BY topic_id"
|
|
|
|
update_top_topics(period, "views", sql)
|
|
end
|
|
|
|
def self.update_likes_count_for(period)
|
|
sql =
|
|
"SELECT topic_id, SUM(like_count) AS count
|
|
FROM posts
|
|
WHERE created_at >= :from
|
|
AND deleted_at IS NULL
|
|
AND NOT hidden
|
|
AND post_type = #{Post.types[:regular]}
|
|
GROUP BY topic_id"
|
|
|
|
update_top_topics(period, "likes", sql)
|
|
end
|
|
|
|
def self.update_op_likes_count_for(period)
|
|
sql =
|
|
"SELECT topic_id, like_count AS count
|
|
FROM posts
|
|
WHERE created_at >= :from
|
|
AND post_number = 1
|
|
AND deleted_at IS NULL
|
|
AND NOT hidden
|
|
AND post_type = #{Post.types[:regular]}"
|
|
|
|
update_top_topics(period, "op_likes", sql)
|
|
end
|
|
|
|
def self.compute_top_score_for(period)
|
|
log_views_multiplier = SiteSetting.top_topics_formula_log_views_multiplier.to_f
|
|
log_views_multiplier = 2 if log_views_multiplier == 0
|
|
|
|
first_post_likes_multiplier = SiteSetting.top_topics_formula_first_post_likes_multiplier.to_f
|
|
first_post_likes_multiplier = 0.5 if first_post_likes_multiplier == 0
|
|
|
|
least_likes_per_post_multiplier =
|
|
SiteSetting.top_topics_formula_least_likes_per_post_multiplier.to_f
|
|
least_likes_per_post_multiplier = 3 if least_likes_per_post_multiplier == 0
|
|
|
|
if period == :all
|
|
top_topics =
|
|
"(
|
|
SELECT t.like_count all_likes_count,
|
|
t.id topic_id,
|
|
t.posts_count all_posts_count,
|
|
p.like_count all_op_likes_count,
|
|
t.views all_views_count
|
|
FROM topics t
|
|
JOIN posts p ON p.topic_id = t.id AND p.post_number = 1
|
|
) as top_topics"
|
|
time_filter = "false"
|
|
else
|
|
top_topics = "top_topics"
|
|
time_filter = "topics.created_at < :from"
|
|
end
|
|
|
|
sql = <<~SQL
|
|
WITH top AS (
|
|
SELECT CASE
|
|
WHEN #{time_filter} THEN 0
|
|
ELSE log(GREATEST(#{period}_views_count, 1)) * #{log_views_multiplier} +
|
|
#{period}_op_likes_count * #{first_post_likes_multiplier} +
|
|
CASE WHEN #{period}_likes_count > 0 AND #{period}_posts_count > 0
|
|
THEN
|
|
LEAST(#{period}_likes_count / #{period}_posts_count, #{least_likes_per_post_multiplier})
|
|
ELSE 0
|
|
END +
|
|
CASE WHEN topics.posts_count < 10 THEN
|
|
0 - ((10 - topics.posts_count) / 20) * #{period}_op_likes_count
|
|
ELSE
|
|
10
|
|
END +
|
|
log(GREATEST(#{period}_posts_count, 1))
|
|
END AS score,
|
|
topic_id
|
|
FROM #{top_topics}
|
|
LEFT JOIN topics ON topics.id = top_topics.topic_id AND
|
|
topics.deleted_at IS NULL
|
|
)
|
|
UPDATE top_topics
|
|
SET #{period}_score = top.score
|
|
FROM top
|
|
WHERE top_topics.topic_id = top.topic_id
|
|
AND #{period}_score <> top.score
|
|
SQL
|
|
|
|
DB.exec(sql, from: start_of(period))
|
|
|
|
DiscourseEvent.trigger(:top_score_computed, period: period)
|
|
end
|
|
|
|
def self.start_of(period)
|
|
case period
|
|
when :yearly
|
|
1.year.ago
|
|
when :monthly
|
|
1.month.ago
|
|
when :quarterly
|
|
3.months.ago
|
|
when :weekly
|
|
1.week.ago
|
|
when :daily
|
|
1.day.ago
|
|
end
|
|
end
|
|
|
|
def self.update_top_topics(period, sort, inner_join)
|
|
DB.exec(
|
|
"UPDATE top_topics
|
|
SET #{period}_#{sort}_count = c.count
|
|
FROM top_topics tt
|
|
INNER JOIN (#{inner_join}) c ON tt.topic_id = c.topic_id
|
|
WHERE tt.topic_id = top_topics.topic_id
|
|
AND tt.#{period}_#{sort}_count <> c.count",
|
|
from: start_of(period),
|
|
)
|
|
end
|
|
end
|
|
|
|
# == Schema Information
|
|
#
|
|
# Table name: top_topics
|
|
#
|
|
# id :integer not null, primary key
|
|
# topic_id :integer
|
|
# yearly_posts_count :integer default(0), not null
|
|
# yearly_views_count :integer default(0), not null
|
|
# yearly_likes_count :integer default(0), not null
|
|
# monthly_posts_count :integer default(0), not null
|
|
# monthly_views_count :integer default(0), not null
|
|
# monthly_likes_count :integer default(0), not null
|
|
# weekly_posts_count :integer default(0), not null
|
|
# weekly_views_count :integer default(0), not null
|
|
# weekly_likes_count :integer default(0), not null
|
|
# daily_posts_count :integer default(0), not null
|
|
# daily_views_count :integer default(0), not null
|
|
# daily_likes_count :integer default(0), not null
|
|
# daily_score :float default(0.0)
|
|
# weekly_score :float default(0.0)
|
|
# monthly_score :float default(0.0)
|
|
# yearly_score :float default(0.0)
|
|
# all_score :float default(0.0)
|
|
# daily_op_likes_count :integer default(0), not null
|
|
# weekly_op_likes_count :integer default(0), not null
|
|
# monthly_op_likes_count :integer default(0), not null
|
|
# yearly_op_likes_count :integer default(0), not null
|
|
# quarterly_posts_count :integer default(0), not null
|
|
# quarterly_views_count :integer default(0), not null
|
|
# quarterly_likes_count :integer default(0), not null
|
|
# quarterly_score :float default(0.0)
|
|
# quarterly_op_likes_count :integer default(0), not null
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_top_topics_on_all_score (all_score)
|
|
# index_top_topics_on_daily_likes_count (daily_likes_count)
|
|
# index_top_topics_on_daily_op_likes_count (daily_op_likes_count)
|
|
# index_top_topics_on_daily_posts_count (daily_posts_count)
|
|
# index_top_topics_on_daily_score (daily_score)
|
|
# index_top_topics_on_daily_views_count (daily_views_count)
|
|
# index_top_topics_on_monthly_likes_count (monthly_likes_count)
|
|
# index_top_topics_on_monthly_op_likes_count (monthly_op_likes_count)
|
|
# index_top_topics_on_monthly_posts_count (monthly_posts_count)
|
|
# index_top_topics_on_monthly_score (monthly_score)
|
|
# index_top_topics_on_monthly_views_count (monthly_views_count)
|
|
# index_top_topics_on_quarterly_likes_count (quarterly_likes_count)
|
|
# index_top_topics_on_quarterly_op_likes_count (quarterly_op_likes_count)
|
|
# index_top_topics_on_quarterly_posts_count (quarterly_posts_count)
|
|
# index_top_topics_on_quarterly_views_count (quarterly_views_count)
|
|
# index_top_topics_on_topic_id (topic_id) UNIQUE
|
|
# index_top_topics_on_weekly_likes_count (weekly_likes_count)
|
|
# index_top_topics_on_weekly_op_likes_count (weekly_op_likes_count)
|
|
# index_top_topics_on_weekly_posts_count (weekly_posts_count)
|
|
# index_top_topics_on_weekly_score (weekly_score)
|
|
# index_top_topics_on_weekly_views_count (weekly_views_count)
|
|
# index_top_topics_on_yearly_likes_count (yearly_likes_count)
|
|
# index_top_topics_on_yearly_op_likes_count (yearly_op_likes_count)
|
|
# index_top_topics_on_yearly_posts_count (yearly_posts_count)
|
|
# index_top_topics_on_yearly_score (yearly_score)
|
|
# index_top_topics_on_yearly_views_count (yearly_views_count)
|
|
#
|