2024-01-16 21:01:04 -05:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
class TopicHotScore < ActiveRecord::Base
|
|
|
|
belongs_to :topic
|
|
|
|
|
|
|
|
DEFAULT_BATCH_SIZE = 1000
|
|
|
|
|
|
|
|
def self.update_scores(max = DEFAULT_BATCH_SIZE)
|
|
|
|
# score is
|
|
|
|
# (total likes - 1) / (age in hours + 2) ^ gravity
|
|
|
|
|
|
|
|
# 1. insert a new record if one does not exist (up to batch size)
|
|
|
|
# 2. update recently created (up to batch size)
|
|
|
|
# 3. update all top scoring topics (up to batch size)
|
|
|
|
|
|
|
|
now = Time.zone.now
|
|
|
|
|
|
|
|
args = {
|
|
|
|
now: now,
|
|
|
|
gravity: SiteSetting.hot_topics_gravity,
|
|
|
|
max: max,
|
|
|
|
private_message: Archetype.private_message,
|
|
|
|
recent_cutoff: now - SiteSetting.hot_topics_recent_days.days,
|
2024-02-01 18:53:27 -05:00
|
|
|
regular: Post.types[:regular],
|
2024-01-16 21:01:04 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
# insert up to BATCH_SIZE records that are missing from table
|
|
|
|
DB.exec(<<~SQL, args)
|
|
|
|
INSERT INTO topic_hot_scores (
|
|
|
|
topic_id,
|
|
|
|
score,
|
|
|
|
recent_likes,
|
|
|
|
recent_posters,
|
|
|
|
created_at,
|
|
|
|
updated_at
|
|
|
|
)
|
|
|
|
SELECT
|
|
|
|
topics.id,
|
|
|
|
0.0,
|
|
|
|
0,
|
|
|
|
0,
|
|
|
|
:now,
|
|
|
|
:now
|
|
|
|
|
|
|
|
FROM topics
|
|
|
|
LEFT OUTER JOIN topic_hot_scores ON topic_hot_scores.topic_id = topics.id
|
|
|
|
WHERE topic_hot_scores.topic_id IS NULL
|
|
|
|
AND topics.deleted_at IS NULL
|
|
|
|
AND topics.archetype <> :private_message
|
|
|
|
AND topics.created_at <= :now
|
2024-02-15 02:27:54 -05:00
|
|
|
ORDER BY
|
|
|
|
CASE WHEN topics.pinned_at IS NOT NULL THEN 0 ELSE 1 END ASC,
|
|
|
|
topics.bumped_at desc
|
2024-01-16 21:01:04 -05:00
|
|
|
LIMIT :max
|
|
|
|
SQL
|
|
|
|
|
|
|
|
# update recent counts for batch
|
|
|
|
DB.exec(<<~SQL, args)
|
|
|
|
UPDATE topic_hot_scores thsOrig
|
|
|
|
SET
|
|
|
|
recent_likes = COALESCE(new_values.likes_count, 0),
|
|
|
|
recent_posters = COALESCE(new_values.unique_participants, 0),
|
|
|
|
recent_first_bumped_at = COALESCE(new_values.first_bumped_at, ths.recent_first_bumped_at)
|
|
|
|
FROM
|
|
|
|
topic_hot_scores ths
|
|
|
|
LEFT OUTER JOIN
|
|
|
|
(
|
|
|
|
SELECT
|
|
|
|
t.id AS topic_id,
|
|
|
|
COUNT(DISTINCT p.user_id) AS unique_participants,
|
|
|
|
(
|
2024-02-01 01:11:40 -05:00
|
|
|
SELECT COUNT(distinct pa.user_id)
|
2024-01-16 21:01:04 -05:00
|
|
|
FROM post_actions pa
|
|
|
|
JOIN posts p2 ON p2.id = pa.post_id
|
|
|
|
WHERE p2.topic_id = t.id
|
2024-02-01 18:53:27 -05:00
|
|
|
AND p2.post_type = :regular
|
|
|
|
AND p2.deleted_at IS NULL
|
|
|
|
AND p2.user_deleted = false
|
2024-01-16 21:01:04 -05:00
|
|
|
AND pa.post_action_type_id = 2 -- action_type for 'like'
|
|
|
|
AND pa.created_at >= :recent_cutoff
|
|
|
|
AND pa.deleted_at IS NULL
|
|
|
|
) AS likes_count,
|
|
|
|
MIN(p.created_at) AS first_bumped_at
|
|
|
|
FROM
|
|
|
|
topics t
|
|
|
|
JOIN
|
|
|
|
posts p ON t.id = p.topic_id
|
|
|
|
WHERE
|
|
|
|
p.created_at >= :recent_cutoff
|
|
|
|
AND t.archetype <> 'private_message'
|
|
|
|
AND t.deleted_at IS NULL
|
|
|
|
AND p.deleted_at IS NULL
|
2024-02-01 18:53:27 -05:00
|
|
|
AND p.user_deleted = false
|
2024-01-16 21:01:04 -05:00
|
|
|
AND t.created_at <= :now
|
|
|
|
AND t.bumped_at >= :recent_cutoff
|
|
|
|
AND p.created_at < :now
|
|
|
|
AND p.created_at >= :recent_cutoff
|
2024-02-01 18:53:27 -05:00
|
|
|
AND p.post_type = :regular
|
2024-01-16 21:01:04 -05:00
|
|
|
GROUP BY
|
|
|
|
t.id
|
|
|
|
) AS new_values
|
|
|
|
ON ths.topic_id = new_values.topic_id
|
|
|
|
|
|
|
|
WHERE thsOrig.topic_id = ths.topic_id
|
|
|
|
SQL
|
|
|
|
|
2024-02-08 15:45:47 -05:00
|
|
|
# we may end up update 2x batch size, this is ok
|
|
|
|
# we need to update 1 batch of high scoring topics
|
|
|
|
# we need to update a second batch of recently bumped topics
|
|
|
|
sql = <<~SQL
|
|
|
|
WITH topic_ids AS (
|
|
|
|
SELECT topic_id FROM (
|
|
|
|
SELECT th3.topic_id FROM topic_hot_scores th3
|
|
|
|
JOIN topics t3 on t3.id = th3.topic_id
|
|
|
|
ORDER BY t3.bumped_at DESC
|
|
|
|
LIMIT :max
|
|
|
|
) Y
|
|
|
|
|
|
|
|
UNION ALL
|
|
|
|
|
|
|
|
SELECT topic_id FROM (
|
|
|
|
SELECT th2.topic_id FROM topic_hot_scores th2
|
|
|
|
ORDER BY th2.score DESC, th2.recent_first_bumped_at DESC NULLS LAST
|
|
|
|
LIMIT :max
|
|
|
|
) X
|
|
|
|
)
|
2024-01-16 21:01:04 -05:00
|
|
|
UPDATE topic_hot_scores ths
|
2024-02-01 01:11:40 -05:00
|
|
|
SET score = (
|
|
|
|
CASE WHEN topics.created_at > :recent_cutoff
|
|
|
|
THEN ths.recent_likes ELSE topics.like_count END
|
|
|
|
) /
|
2024-01-16 21:01:04 -05:00
|
|
|
(EXTRACT(EPOCH FROM (:now - topics.created_at)) / 3600 + 2) ^ :gravity
|
|
|
|
+
|
|
|
|
CASE WHEN ths.recent_first_bumped_at IS NULL THEN 0 ELSE
|
2024-01-17 00:12:03 -05:00
|
|
|
(ths.recent_likes + ths.recent_posters - 1) /
|
2024-01-16 21:01:04 -05:00
|
|
|
(EXTRACT(EPOCH FROM (:now - recent_first_bumped_at)) / 3600 + 2) ^ :gravity
|
|
|
|
END
|
|
|
|
,
|
|
|
|
updated_at = :now
|
|
|
|
|
|
|
|
FROM topics
|
|
|
|
WHERE topics.id IN (
|
2024-02-08 15:45:47 -05:00
|
|
|
SELECT topic_id FROM topic_ids
|
2024-08-26 08:07:31 -04:00
|
|
|
) AND ths.topic_id = topics.id AND topics.created_at <= :now
|
2024-01-16 21:01:04 -05:00
|
|
|
SQL
|
2024-02-08 15:45:47 -05:00
|
|
|
|
|
|
|
DB.exec(sql, args)
|
2024-10-18 10:08:48 -04:00
|
|
|
|
|
|
|
DiscourseEvent.trigger(:topic_hot_scores_updated)
|
2024-01-16 21:01:04 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# == Schema Information
|
|
|
|
#
|
|
|
|
# Table name: topic_hot_scores
|
|
|
|
#
|
|
|
|
# id :bigint not null, primary key
|
|
|
|
# topic_id :integer not null
|
|
|
|
# score :float default(0.0), not null
|
|
|
|
# recent_likes :integer default(0), not null
|
|
|
|
# recent_posters :integer default(0), not null
|
|
|
|
# recent_first_bumped_at :datetime
|
|
|
|
# created_at :datetime not null
|
|
|
|
# updated_at :datetime not null
|
|
|
|
#
|
|
|
|
# Indexes
|
|
|
|
#
|
|
|
|
# index_topic_hot_scores_on_score_and_topic_id (score,topic_id) UNIQUE
|
|
|
|
# index_topic_hot_scores_on_topic_id (topic_id) UNIQUE
|
|
|
|
#
|