Skip to content

Commit

Permalink
FEATURE: dynamically update the topic heat settings monthly (#7670)
Browse files Browse the repository at this point in the history
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
nlalonde committed Jun 4, 2019
1 parent e66024b commit ecc9c76
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 1 deletion.
11 changes: 11 additions & 0 deletions app/jobs/scheduled/update_heat_settings.rb
@@ -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
81 changes: 81 additions & 0 deletions app/services/heat_settings_updater.rb
@@ -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
2 changes: 2 additions & 0 deletions config/locales/server.en.yml
Expand Up @@ -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."
Expand Down
4 changes: 3 additions & 1 deletion config/site_settings.yml
Expand Up @@ -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
Expand All @@ -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:
Expand Down
109 changes: 109 additions & 0 deletions spec/services/heat_settings_updater_spec.rb
@@ -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

0 comments on commit ecc9c76

Please sign in to comment.