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:
Neil Lalonde 2019-06-04 10:34:07 -04:00 committed by GitHub
parent e66024bd3b
commit ecc9c76692
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 206 additions and 1 deletions

View File

@ -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

View File

@ -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

View File

@ -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."

View File

@ -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:

View File

@ -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