FEATURE: dynamically update the topic heat settings monthly ()

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

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

@ -1726,6 +1726,8 @@ uncategorized:
summary_percent_filter: 20
summary_max_results: 100
automatic_topic_heat_values: true
# View heat thresholds
topic_views_heat_low:
client: true
@ -1735,7 +1737,7 @@ uncategorized:
default: 2000
topic_views_heat_high:
client: true
default: 5000
default: 3500
# Post/Like heat thresholds
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