diff --git a/app/models/articles/feeds.rb b/app/models/articles/feeds.rb index 14cf9104d..ed295a091 100644 --- a/app/models/articles/feeds.rb +++ b/app/models/articles/feeds.rb @@ -87,7 +87,8 @@ module Articles order_by_lever(:final_order_by_random_weighted_to_score, label: "Order by conflating a random number and the score (see forem/forem#16128)", - order_by_fragment: "RANDOM() ^ (1.0 / greatest(articles.score, 0.1)) DESC") + order_by_fragment: "article_relevancies.randomized_value " \ + "^ (1.0 / greatest(articles.score, 0.1)) DESC") relevancy_lever(:comments_count_by_those_followed, label: "Weight to give for the number of comments on the article from other users" \ diff --git a/app/services/articles/feeds/variant_query.rb b/app/services/articles/feeds/variant_query.rb index ba877b54f..82e97ad01 100644 --- a/app/services/articles/feeds/variant_query.rb +++ b/app/services/articles/feeds/variant_query.rb @@ -42,12 +42,14 @@ module Articles # @param number_of_articles [Integer, #to_i] # @param page [Integer, #to_i] # @param tag [NilClass] not used - def initialize(config:, user: nil, number_of_articles: 50, page: 1, tag: nil) + # @param seed [Number] used in the `setseed` Postgresql function to set the randomization seed. + def initialize(config:, user: nil, number_of_articles: 50, page: 1, tag: nil, seed: nil) @user = user @number_of_articles = number_of_articles @page = page @tag = tag @config = config + @seed = Float(seed || rand) oldest_published_at = Articles::Feeds.oldest_published_at_to_consider_for( user: @user, days_since_published: config.max_days_since_published, @@ -56,7 +58,7 @@ module Articles configure! end - attr_reader :config, :query_parameters + attr_reader :config, :query_parameters, :seed # Query for articles relevant to the user's interest. # @@ -108,8 +110,17 @@ module Articles # articles and then sort on the attributes on either the article or the result set # (e.g. sort on the relevancy score). join_fragment = Arel.sql( - "INNER JOIN (#{Article.sanitize_sql(unsanitized_sql_sub_query)}) " \ - "AS article_relevancies ON articles.id = article_relevancies.id", + "INNER JOIN (" \ + "\n--- The setseed needs to be called independently; later we reference seeder \n" \ + "WITH seeder AS (SELECT setseed(#{Float(@seed)})) "\ + "\n--- We are using the inner logic to build relevancy score" \ + "\n--- The outer part, with seeder, is to create a stable randomized number \n" \ + "SELECT inner_article_relevancies.id, "\ + "inner_article_relevancies.relevancy_score, " \ + "RANDOM() AS randomized_value " \ + "FROM seeder, " \ + "(#{Article.sanitize_sql(unsanitized_sql_sub_query)}) AS inner_article_relevancies" \ + ") AS article_relevancies ON articles.id = article_relevancies.id", ) # This sub-query allows us to take the hard work of the hand-coded unsanitized sql and