FEATURE: improvements to hot algorithm (#25295)

- Decrease gravity, we come in too hot prioritizing too many new topics
- Remove all muted topics / categories and tags from the hot list
- Punish topics with zero likes in algorithm
This commit is contained in:
Sam 2024-01-17 16:12:03 +11:00 committed by GitHub
parent 54e6c1d823
commit df8bb947b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 49 additions and 16 deletions

View File

@ -100,11 +100,11 @@ class TopicHotScore < ActiveRecord::Base
# we need an extra index for this
DB.exec(<<~SQL, args)
UPDATE topic_hot_scores ths
SET score = topics.like_count /
SET score = (topics.like_count - 1) /
(EXTRACT(EPOCH FROM (:now - topics.created_at)) / 3600 + 2) ^ :gravity
+
CASE WHEN ths.recent_first_bumped_at IS NULL THEN 0 ELSE
(ths.recent_likes + ths.recent_posters) /
(ths.recent_likes + ths.recent_posters - 1) /
(EXTRACT(EPOCH FROM (:now - recent_first_bumped_at)) / 3600 + 2) ^ :gravity
END
,

View File

@ -3116,7 +3116,7 @@ dashboard:
default: false
hot_topics_gravity:
hidden: true
default: 1.8
default: 1.2
hot_topics_recent_days:
hidden: true
default: 7

View File

@ -340,6 +340,9 @@ class TopicQuery
def list_hot
create_list(:hot, unordered: true) do |topics|
topics = remove_muted_topics(topics, user)
topics = remove_muted_categories(topics, user, exclude: options[:category])
TopicQuery.remove_muted_tags(topics, user, options)
topics.joins("JOIN topic_hot_scores on topics.id = topic_hot_scores.topic_id").order(
"topic_hot_scores.score DESC",
)

View File

@ -75,6 +75,36 @@ RSpec.describe TopicQuery do
end
end
describe "#list_hot" do
it "excludes muted categories and topics" do
muted_category = Fabricate(:category)
muted_topic = Fabricate(:topic, category: muted_category)
TopicHotScore.create!(topic_id: muted_topic.id, score: 1.0)
expect(TopicQuery.new(user).list_hot.topics.map(&:id)).to include(muted_topic.id)
tu =
TopicUser.create!(
user_id: user.id,
topic_id: muted_topic.id,
notification_level: TopicUser.notification_levels[:muted],
)
expect(TopicQuery.new(user).list_hot.topics.map(&:id)).not_to include(muted_topic.id)
tu.destroy!
CategoryUser.create!(
user_id: user.id,
category_id: muted_category.id,
notification_level: CategoryUser.notification_levels[:muted],
)
expect(TopicQuery.new(user).list_hot.topics.map(&:id)).not_to include(muted_topic.id)
end
end
describe "#prioritize_pinned_topics" do
it "does the pagination correctly" do
num_topics = 15
@ -245,7 +275,7 @@ RSpec.describe TopicQuery do
group = Fabricate(:group)
group.add(group_moderator)
category = Fabricate(:category, reviewable_by_group: group)
topic = Fabricate(:topic, category: category, deleted_at: 1.year.ago)
_topic = Fabricate(:topic, category: category, deleted_at: 1.year.ago)
expect(TopicQuery.new(admin, status: "deleted").list_latest.topics.size).to eq(1)
expect(TopicQuery.new(moderator, status: "deleted").list_latest.topics.size).to eq(1)
@ -265,7 +295,7 @@ RSpec.describe TopicQuery do
it "includes users own pms in regular topic lists" do
topic = Fabricate(:topic)
own_pm = Fabricate(:private_message_topic, user: user)
other_pm = Fabricate(:private_message_topic, user: Fabricate(:user))
_other_pm = Fabricate(:private_message_topic, user: Fabricate(:user))
expect(TopicQuery.new(user).list_latest.topics).to contain_exactly(topic)
expect(TopicQuery.new(admin).list_latest.topics).to contain_exactly(topic)
@ -1466,8 +1496,8 @@ RSpec.describe TopicQuery do
it "should return random topics excluding topics that are muted by user and not older than `suggested_topics_max_days_old` site setting" do
topic2 = Fabricate(:topic, user: user)
topic3 = Fabricate(:topic, user: user)
topic4 = Fabricate(:topic, user: user, created_at: 8.days.ago)
topic5 = Fabricate(:topic).tap { |t| TopicNotifier.new(t).mute!(user) }
_topic4 = Fabricate(:topic, user: user, created_at: 8.days.ago)
_topic5 = Fabricate(:topic).tap { |t| TopicNotifier.new(t).mute!(user) }
SiteSetting.suggested_topics_max_days_old = 7
@ -1483,7 +1513,7 @@ RSpec.describe TopicQuery do
fab!(:topic_in_category_that_user_created_and_has_partially_read) do
Fabricate(:topic, user: user, category:).tap do |t|
first_post = Fabricate(:post, topic: t)
_first_post = Fabricate(:post, topic: t)
second_post = Fabricate(:post, topic: t)
TopicUser.change(
@ -1498,7 +1528,7 @@ RSpec.describe TopicQuery do
fab!(:topic_in_category2_that_user_created_and_has_partially_read) do
Fabricate(:topic, user: user, category: category2).tap do |t|
first_post = Fabricate(:post, topic: t)
_first_post = Fabricate(:post, topic: t)
second_post = Fabricate(:post, topic: t)
TopicUser.change(
@ -1513,7 +1543,7 @@ RSpec.describe TopicQuery do
fab!(:topic_in_category_that_user_has_partially_read) do
Fabricate(:topic, category:).tap do |t|
first_post = Fabricate(:post, topic: t)
_first_post = Fabricate(:post, topic: t)
second_post = Fabricate(:post, topic: t)
TopicUser.change(
@ -1528,7 +1558,7 @@ RSpec.describe TopicQuery do
fab!(:topic_in_category2_that_user_has_partially_read) do
Fabricate(:topic, category: category2).tap do |t|
first_post = Fabricate(:post, topic: t)
_first_post = Fabricate(:post, topic: t)
second_post = Fabricate(:post, topic: t)
TopicUser.change(

View File

@ -31,7 +31,7 @@ RSpec.describe TopicHotScore do
expect(hot_scoring.recent_likes).to eq(2)
expect(hot_scoring.recent_posters).to eq(2)
expect(hot_scoring.recent_first_bumped_at).to eq_time(new_reply.created_at)
expect(hot_scoring.score).to be_within(0.001).of(1.020)
expect(hot_scoring.score).to be_within(0.001).of(1.219)
expect(TopicHotScore.find_by(topic_id: -1).recent_likes).to eq(0)
end
@ -44,15 +44,15 @@ RSpec.describe TopicHotScore do
TopicHotScore.update_scores
expect(TopicHotScore.find_by(topic_id: topic1.id).score).to be_within(0.001).of(0.415)
expect(TopicHotScore.find_by(topic_id: topic2.id).score).to be_within(0.001).of(0.551)
expect(TopicHotScore.find_by(topic_id: topic1.id).score).to be_within(0.001).of(0.535)
expect(TopicHotScore.find_by(topic_id: topic2.id).score).to be_within(0.001).of(1.304)
freeze_time(2.hours.from_now)
TopicHotScore.update_scores
expect(TopicHotScore.find_by(topic_id: topic1.id).score).to be_within(0.001).of(0.165)
expect(TopicHotScore.find_by(topic_id: topic2.id).score).to be_within(0.001).of(0.301)
expect(TopicHotScore.find_by(topic_id: topic1.id).score).to be_within(0.001).of(0.289)
expect(TopicHotScore.find_by(topic_id: topic2.id).score).to be_within(0.001).of(0.871)
end
end
end