PERF: avoid expensive order by random for suggested topics
This commit is contained in:
parent
7b6e85cb6c
commit
b760d22460
|
@ -0,0 +1,75 @@
|
|||
class RandomTopicSelector
|
||||
|
||||
BACKFILL_SIZE = 3000
|
||||
BACKFILL_LOW_WATER_MARK = 500
|
||||
|
||||
def self.backfill(category=nil)
|
||||
|
||||
exclude = category.try(:topic_id)
|
||||
|
||||
# don't leak private categories into the "everything" group
|
||||
user = category ? CategoryFeaturedTopic.fake_admin : nil
|
||||
|
||||
options = {
|
||||
per_page: SiteSetting.category_featured_topics,
|
||||
visible: true,
|
||||
no_definitions: true
|
||||
}
|
||||
|
||||
options[:except_topic_ids] = [category.topic_id] if exclude
|
||||
options[:category] = category.id if category
|
||||
|
||||
query = TopicQuery.new(user, options)
|
||||
results = query.latest_results.order('RANDOM()')
|
||||
.where(closed: false, archived: false)
|
||||
.limit(BACKFILL_SIZE)
|
||||
.reorder('RANDOM()')
|
||||
.pluck(:id)
|
||||
|
||||
key = cache_key(category)
|
||||
results.each do |id|
|
||||
$redis.rpush(key, id)
|
||||
end
|
||||
$redis.expire(key, 2.days)
|
||||
|
||||
results
|
||||
end
|
||||
|
||||
def self.next(count, category=nil)
|
||||
key = cache_key(category)
|
||||
|
||||
results = []
|
||||
|
||||
left = count
|
||||
|
||||
while left > 0
|
||||
id = $redis.lpop key
|
||||
break unless id
|
||||
|
||||
results << id.to_i
|
||||
left -= 1
|
||||
end
|
||||
|
||||
backfilled = false
|
||||
if left > 0
|
||||
ids = backfill(category)
|
||||
backfilled = true
|
||||
results += ids[0...count]
|
||||
results.uniq!
|
||||
results = results[0...count]
|
||||
end
|
||||
|
||||
if !backfilled && $redis.llen(key) < BACKFILL_LOW_WATER_MARK
|
||||
Scheduler::Defer.later("backfill") do
|
||||
backfill(category)
|
||||
end
|
||||
end
|
||||
|
||||
results
|
||||
end
|
||||
|
||||
def self.cache_key(category=nil)
|
||||
"random_topic_cache_#{category.try(:id)}"
|
||||
end
|
||||
|
||||
end
|
|
@ -414,7 +414,16 @@ class TopicQuery
|
|||
result = result.order("CASE WHEN topics.category_id = #{topic.category_id.to_i} THEN 0 ELSE 1 END")
|
||||
end
|
||||
|
||||
result.order("RANDOM()")
|
||||
# Best effort, it over selects, however if you have a high number
|
||||
# of muted categories there is tiny chance we will not select enough
|
||||
# in particular this can happen if current category is empty and tons
|
||||
# of muted, big edge case
|
||||
#
|
||||
# we over select in case cache is stale
|
||||
max = (count*1.3).to_i
|
||||
ids = RandomTopicSelector.next(max) + RandomTopicSelector.next(max, topic.category)
|
||||
|
||||
result.where(id: ids)
|
||||
end
|
||||
|
||||
def suggested_ordering(result, options)
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe RandomTopicSelector do
|
||||
|
||||
it 'can correctly use cache' do
|
||||
key = RandomTopicSelector.cache_key
|
||||
|
||||
$redis.del key
|
||||
|
||||
4.times do |t|
|
||||
$redis.rpush key, t
|
||||
end
|
||||
|
||||
RandomTopicSelector.next(2).should == [0,1]
|
||||
RandomTopicSelector.next(2).should == [2,3]
|
||||
end
|
||||
|
||||
it 'can correctly backfill' do
|
||||
category = Fabricate(:category)
|
||||
t1 = Fabricate(:topic, category_id: category.id)
|
||||
_t2 = Fabricate(:topic, category_id: category.id, visible: false)
|
||||
_t3 = Fabricate(:topic, category_id: category.id, deleted_at: 1.minute.ago)
|
||||
t4 = Fabricate(:topic, category_id: category.id)
|
||||
|
||||
RandomTopicSelector.next(5, category).sort.should == [t1.id,t4.id].sort
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue