0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-04 10:39:43 +08:00
discourse/lib/nested_replies/sort.rb
Mark VanLandingham dc2d170a9f
FEATURE: Hot algorithm for nested replies (simple) (#41742)
Previously, nested replies offered only `top`, `new`, and `old`
ordering, so active discussions could not surface recently engaged
branches while keeping tree loading bounded.

This change adds default-off `hot` sorting backed by demand-driven
PostgreSQL snapshots and bounded branch preloading, preserves the
resolved fallback across pagination, and skips hot-cache work for small
topics or topics inactive for 30 days.
2026-07-20 10:56:05 -05:00

76 lines
2.4 KiB
Ruby
Vendored

# frozen_string_literal: true
module NestedReplies
module Sort
ALGORITHMS = %w[top hot new old].freeze
def self.sql_order_expression(algorithm, posts_table: "posts")
raise ArgumentError, "Invalid sort algorithm: #{algorithm}" unless valid?(algorithm)
case algorithm
when "top"
"#{posts_table}.like_count DESC, #{posts_table}.post_number ASC"
when "hot"
"#{hot_score_expression(posts_table, :thread_hot_score)} DESC, " \
"#{hot_score_expression(posts_table, :hot_score)} DESC, " \
"#{posts_table}.post_number ASC"
when "new"
"#{posts_table}.created_at DESC"
when "old"
"#{posts_table}.post_number ASC"
end
end
def self.apply(scope, algorithm)
scope = scope.joins(hot_score_join_sql) if algorithm == "hot"
scope.order(Arel.sql(sql_order_expression(algorithm)))
end
def self.hot_score_join_sql(posts_table: "posts")
<<~SQL.squish
LEFT JOIN nested_hot_post_scores
ON nested_hot_post_scores.post_id = #{posts_table}.id
SQL
end
def self.hot_score_expression(posts_table, column)
fallback = HotScoreCalculator.hot_score_sql(posts_table)
"COALESCE(nested_hot_post_scores.#{column}, #{fallback})"
end
def self.sort_in_memory(posts, algorithm, hot_scores: nil)
raise ArgumentError, "Invalid sort algorithm: #{algorithm}" unless valid?(algorithm)
case algorithm
when "top"
posts.sort_by { |post| [-post.like_count, post.post_number] }
when "hot"
hot_scores ||= {}
posts.sort_by do |post|
thread_hot_score, hot_score = hot_score_values(post, hot_scores[post.id])
[-thread_hot_score, -hot_score, post.post_number]
end
when "new"
posts.sort_by { |post| -post.created_at.to_i }
when "old"
posts.sort_by(&:post_number)
end
end
def self.hot_score_values(post, scores)
if scores.present?
thread_hot_score, hot_score = scores
hot_score = (hot_score || thread_hot_score || 0.0).to_f
thread_hot_score = (thread_hot_score || hot_score).to_f
[thread_hot_score, hot_score]
else
hot_score = HotScoreCalculator.score_for(post).to_f
[hot_score, hot_score]
end
end
def self.valid?(algorithm)
ALGORITHMS.include?(algorithm)
end
end
end