Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
FEATURE: dynamically update the topic heat settings monthly (#7670)
The site settings beginning with "topic views heat" and "topic post like heat" are set to defaults when installing Discourse, but there has not been a process or guidance for updating these values based on community activity. This feature will update them once a month. The low, medium, and high settings will be based on the minimums of the 45th, 25th, and 10th percentile topics respectively, so that 45% of topics will have some "heat". Disable automatic changes with the automatic_topic_heat_values setting.
- Loading branch information
Showing
5 changed files
with
206 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# frozen_string_literal: true | ||
|
||
module Jobs | ||
class UpdateHeatSettings < Jobs::Scheduled | ||
every 1.month | ||
|
||
def execute(args) | ||
HeatSettingsUpdater.update | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
# frozen_string_literal: true | ||
|
||
class HeatSettingsUpdater | ||
def self.update | ||
return unless SiteSetting.automatic_topic_heat_values | ||
|
||
views_by_percentile = views_thresholds | ||
update_setting(:topic_views_heat_high, views_by_percentile[10]) | ||
update_setting(:topic_views_heat_medium, views_by_percentile[25]) | ||
update_setting(:topic_views_heat_low, views_by_percentile[45]) | ||
|
||
like_ratios_by_percentile = like_ratio_thresholds | ||
update_setting(:topic_post_like_heat_high, like_ratios_by_percentile[10]) | ||
update_setting(:topic_post_like_heat_medium, like_ratios_by_percentile[25]) | ||
update_setting(:topic_post_like_heat_low, like_ratios_by_percentile[45]) | ||
end | ||
|
||
def self.views_thresholds | ||
results = DB.query(<<~SQL) | ||
SELECT ranked.bucket * 5 as percentile, MIN(ranked.views) as views | ||
FROM ( | ||
SELECT NTILE(20) OVER (ORDER BY t.views DESC) AS bucket, t.views | ||
FROM ( | ||
SELECT views | ||
FROM topics | ||
WHERE deleted_at IS NULL | ||
AND archetype <> 'private_message' | ||
AND visible = TRUE | ||
) t | ||
) ranked | ||
WHERE bucket <= 9 | ||
GROUP BY bucket | ||
SQL | ||
|
||
results.inject({}) do |h, row| | ||
h[row.percentile] = row.views | ||
h | ||
end | ||
end | ||
|
||
def self.like_ratio_thresholds | ||
results = DB.query(<<~SQL) | ||
SELECT ranked.bucket * 5 as percentile, MIN(ranked.ratio) as like_ratio | ||
FROM ( | ||
SELECT NTILE(20) OVER (ORDER BY t.ratio DESC) AS bucket, t.ratio | ||
FROM ( | ||
SELECT like_count::decimal / posts_count AS ratio | ||
FROM topics | ||
WHERE deleted_at IS NULL | ||
AND archetype <> 'private_message' | ||
AND visible = TRUE | ||
AND posts_count >= 10 | ||
AND like_count > 0 | ||
ORDER BY created_at DESC | ||
LIMIT 1000 | ||
) t | ||
) ranked | ||
WHERE bucket <= 9 | ||
GROUP BY bucket | ||
SQL | ||
|
||
results.inject({}) do |h, row| | ||
h[row.percentile] = row.like_ratio | ||
h | ||
end | ||
end | ||
|
||
def self.update_setting(name, new_value) | ||
if new_value.nil? || new_value <= SiteSetting.defaults[name] | ||
if SiteSetting.get(name) != SiteSetting.defaults[name] | ||
SiteSetting.set_and_log(name, SiteSetting.defaults[name]) | ||
end | ||
elsif SiteSetting.get(name) == 0 || | ||
(new_value.to_f / SiteSetting.get(name) - 1.0).abs >= 0.05 | ||
|
||
if SiteSetting.get(name) != new_value | ||
SiteSetting.set_and_log(name, new_value) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'rails_helper' | ||
|
||
describe HeatSettingsUpdater do | ||
describe '#update' do | ||
subject(:update_settings) { HeatSettingsUpdater.update } | ||
|
||
def expect_default_values | ||
[:topic_views_heat, :topic_post_like_heat].each do |prefix| | ||
[:low, :medium, :high].each do |level| | ||
setting_name = "#{prefix}_#{level}" | ||
expect(SiteSetting.get(setting_name)).to eq(SiteSetting.defaults[setting_name]) | ||
end | ||
end | ||
end | ||
|
||
it 'changes nothing on fresh install' do | ||
expect { | ||
update_settings | ||
}.to_not change { UserHistory.count } | ||
expect_default_values | ||
end | ||
|
||
context 'low activity' do | ||
let!(:hottest_topic1) { Fabricate(:topic, views: 3000, posts_count: 10, like_count: 2) } | ||
let!(:hottest_topic2) { Fabricate(:topic, views: 3000, posts_count: 10, like_count: 2) } | ||
let!(:warm_topic1) { Fabricate(:topic, views: 1500, posts_count: 10, like_count: 1) } | ||
let!(:warm_topic2) { Fabricate(:topic, views: 1500, posts_count: 10, like_count: 1) } | ||
let!(:warm_topic3) { Fabricate(:topic, views: 1500, posts_count: 10, like_count: 1) } | ||
let!(:lukewarm_topic1) { Fabricate(:topic, views: 800, posts_count: 10, like_count: 0) } | ||
let!(:lukewarm_topic2) { Fabricate(:topic, views: 800, posts_count: 10, like_count: 0) } | ||
let!(:lukewarm_topic3) { Fabricate(:topic, views: 800, posts_count: 10, like_count: 0) } | ||
let!(:lukewarm_topic4) { Fabricate(:topic, views: 800, posts_count: 10, like_count: 0) } | ||
let!(:cold_topic) { Fabricate(:topic, views: 100, posts_count: 10, like_count: 0) } | ||
|
||
it "doesn't make settings lower than defaults" do | ||
expect { | ||
update_settings | ||
}.to_not change { UserHistory.count } | ||
expect_default_values | ||
end | ||
|
||
it 'can set back down to minimum defaults' do | ||
[:low, :medium, :high].each do |level| | ||
SiteSetting.set("topic_views_heat_#{level}", 20_000) | ||
SiteSetting.set("topic_post_like_heat_#{level}", 5.0) | ||
end | ||
expect { | ||
update_settings | ||
}.to change { UserHistory.count }.by(6) | ||
expect_default_values | ||
end | ||
end | ||
|
||
context 'similar activity' do | ||
let!(:hottest_topic1) { Fabricate(:topic, views: 3530, posts_count: 100, like_count: 201) } | ||
let!(:hottest_topic2) { Fabricate(:topic, views: 3530, posts_count: 100, like_count: 201) } | ||
let!(:warm_topic1) { Fabricate(:topic, views: 2020, posts_count: 100, like_count: 99) } | ||
let!(:warm_topic2) { Fabricate(:topic, views: 2020, posts_count: 100, like_count: 99) } | ||
let!(:warm_topic3) { Fabricate(:topic, views: 2020, posts_count: 100, like_count: 99) } | ||
let!(:lukewarm_topic1) { Fabricate(:topic, views: 1010, posts_count: 100, like_count: 51) } | ||
let!(:lukewarm_topic2) { Fabricate(:topic, views: 1010, posts_count: 100, like_count: 51) } | ||
let!(:lukewarm_topic3) { Fabricate(:topic, views: 1010, posts_count: 100, like_count: 51) } | ||
let!(:lukewarm_topic4) { Fabricate(:topic, views: 1010, posts_count: 100, like_count: 51) } | ||
let!(:cold_topic) { Fabricate(:topic, views: 100, posts_count: 100, like_count: 1) } | ||
|
||
it "doesn't make small changes" do | ||
expect { | ||
update_settings | ||
}.to_not change { UserHistory.count } | ||
expect_default_values | ||
end | ||
end | ||
|
||
context 'increased activity' do | ||
let!(:hottest_topic1) { Fabricate(:topic, views: 10_100, posts_count: 100, like_count: 230) } | ||
let!(:hottest_topic2) { Fabricate(:topic, views: 10_000, posts_count: 100, like_count: 220) } | ||
let!(:warm_topic1) { Fabricate(:topic, views: 4020, posts_count: 100, like_count: 126) } | ||
let!(:warm_topic2) { Fabricate(:topic, views: 4010, posts_count: 100, like_count: 116) } | ||
let!(:warm_topic3) { Fabricate(:topic, views: 4000, posts_count: 100, like_count: 106) } | ||
let!(:lukewarm_topic1) { Fabricate(:topic, views: 2040, posts_count: 100, like_count: 84) } | ||
let!(:lukewarm_topic2) { Fabricate(:topic, views: 2030, posts_count: 100, like_count: 74) } | ||
let!(:lukewarm_topic3) { Fabricate(:topic, views: 2020, posts_count: 100, like_count: 64) } | ||
let!(:lukewarm_topic4) { Fabricate(:topic, views: 2000, posts_count: 100, like_count: 54) } | ||
let!(:cold_topic) { Fabricate(:topic, views: 100, posts_count: 100, like_count: 1) } | ||
|
||
it 'changes settings when difference is significant' do | ||
expect { | ||
update_settings | ||
}.to change { UserHistory.count }.by(6) | ||
expect(SiteSetting.topic_views_heat_high).to eq(10_000) | ||
expect(SiteSetting.topic_views_heat_medium).to eq(4000) | ||
expect(SiteSetting.topic_views_heat_low).to eq(2000) | ||
expect(SiteSetting.topic_post_like_heat_high).to eq(2.2) | ||
expect(SiteSetting.topic_post_like_heat_medium).to eq(1.06) | ||
expect(SiteSetting.topic_post_like_heat_low).to eq(0.54) | ||
end | ||
|
||
it "doesn't change settings when automatic_topic_heat_values is false" do | ||
SiteSetting.automatic_topic_heat_values = false | ||
expect { | ||
update_settings | ||
}.to_not change { UserHistory.count } | ||
expect_default_values | ||
end | ||
end | ||
end | ||
end |