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.
This commit is contained in:
parent
e66024bd3b
commit
ecc9c76692
|
@ -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
|
|
@ -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
|
|
@ -1715,6 +1715,8 @@ en:
|
||||||
title_prettify: "Prevent common title typos and errors, including all caps, lowercase first character, multiple ! and ?, extra . at end, etc."
|
title_prettify: "Prevent common title typos and errors, including all caps, lowercase first character, multiple ! and ?, extra . at end, etc."
|
||||||
title_remove_extraneous_space: "Remove leading whitespaces in front of the end punctuation."
|
title_remove_extraneous_space: "Remove leading whitespaces in front of the end punctuation."
|
||||||
|
|
||||||
|
automatic_topic_heat_values: 'Automatically update the "topic views heat" and "topic post like heat" settings based on site activity.'
|
||||||
|
|
||||||
topic_views_heat_low: "After this many views, the views field is slightly highlighted."
|
topic_views_heat_low: "After this many views, the views field is slightly highlighted."
|
||||||
topic_views_heat_medium: "After this many views, the views field is moderately highlighted."
|
topic_views_heat_medium: "After this many views, the views field is moderately highlighted."
|
||||||
topic_views_heat_high: "After this many views, the views field is strongly highlighted."
|
topic_views_heat_high: "After this many views, the views field is strongly highlighted."
|
||||||
|
|
|
@ -1726,6 +1726,8 @@ uncategorized:
|
||||||
summary_percent_filter: 20
|
summary_percent_filter: 20
|
||||||
summary_max_results: 100
|
summary_max_results: 100
|
||||||
|
|
||||||
|
automatic_topic_heat_values: true
|
||||||
|
|
||||||
# View heat thresholds
|
# View heat thresholds
|
||||||
topic_views_heat_low:
|
topic_views_heat_low:
|
||||||
client: true
|
client: true
|
||||||
|
@ -1735,7 +1737,7 @@ uncategorized:
|
||||||
default: 2000
|
default: 2000
|
||||||
topic_views_heat_high:
|
topic_views_heat_high:
|
||||||
client: true
|
client: true
|
||||||
default: 5000
|
default: 3500
|
||||||
|
|
||||||
# Post/Like heat thresholds
|
# Post/Like heat thresholds
|
||||||
topic_post_like_heat_low:
|
topic_post_like_heat_low:
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue